@react-perfscope/core 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -88,23 +88,25 @@ function isSelfRequest(url) {
88
88
  // src/sourcemap.ts
89
89
  var CHROME_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/;
90
90
  var FIREFOX_FRAME = /^(.*?)@(.+?):(\d+):(\d+)$/;
91
+ function lookupFrame(tracer, frame) {
92
+ const pos = originalPositionFor(tracer, { line: frame.line, column: frame.col - 1 });
93
+ if (pos.source == null || pos.line == null || pos.column == null) {
94
+ return frame;
95
+ }
96
+ const resolved = {
97
+ file: pos.source,
98
+ line: pos.line,
99
+ col: pos.column + 1
100
+ };
101
+ if (pos.name) resolved.fnName = pos.name;
102
+ else if (frame.fnName) resolved.fnName = frame.fnName;
103
+ return resolved;
104
+ }
91
105
  async function resolveFrame(frame, fetchMap) {
92
106
  try {
93
107
  const map = await fetchMap(frame.file);
94
108
  if (!map) return frame;
95
- const tracer = new TraceMap(map);
96
- const pos = originalPositionFor(tracer, { line: frame.line, column: frame.col });
97
- if (pos.source == null || pos.line == null || pos.column == null) {
98
- return frame;
99
- }
100
- const resolved = {
101
- file: pos.source,
102
- line: pos.line,
103
- col: pos.column
104
- };
105
- if (pos.name) resolved.fnName = pos.name;
106
- else if (frame.fnName) resolved.fnName = frame.fnName;
107
- return resolved;
109
+ return lookupFrame(new TraceMap(map), frame);
108
110
  } catch (err) {
109
111
  console.warn("[react-perfscope] resolveFrame failed:", err);
110
112
  return frame;
@@ -127,7 +129,7 @@ function attachLazyStack(target, raw, skipTopFrames = 0) {
127
129
  function createSourceMapResolver(opts = {}) {
128
130
  const f = opts.fetch ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
129
131
  const cache = /* @__PURE__ */ new Map();
130
- function fetchMapFor(sourceUrl) {
132
+ function fetchTracerFor(sourceUrl) {
131
133
  if (!f) return Promise.resolve(null);
132
134
  const existing = cache.get(sourceUrl);
133
135
  if (existing) return existing;
@@ -146,13 +148,13 @@ function createSourceMapResolver(opts = {}) {
146
148
  const header = ref.slice(0, commaIdx);
147
149
  const payload = ref.slice(commaIdx + 1);
148
150
  const decoded = header.includes("base64") ? typeof atob === "function" ? atob(payload) : globalThis.Buffer.from(payload, "base64").toString("utf8") : decodeURIComponent(payload);
149
- return JSON.parse(decoded);
151
+ return new TraceMap(JSON.parse(decoded));
150
152
  }
151
153
  const mapUrl = new URL(ref, sourceUrl).href;
152
154
  markSelfRequest(mapUrl);
153
155
  const mapRes = await f(mapUrl);
154
156
  if (!mapRes || !mapRes.ok) return null;
155
- return await mapRes.json();
157
+ return new TraceMap(await mapRes.json());
156
158
  } catch {
157
159
  return null;
158
160
  }
@@ -163,7 +165,9 @@ function createSourceMapResolver(opts = {}) {
163
165
  return {
164
166
  async resolve(frame) {
165
167
  try {
166
- return await resolveFrame(frame, fetchMapFor);
168
+ const tracer = await fetchTracerFor(frame.file);
169
+ if (!tracer) return frame;
170
+ return lookupFrame(tracer, frame);
167
171
  } catch {
168
172
  return frame;
169
173
  }
@@ -227,6 +231,48 @@ function unsupportedKinds() {
227
231
  return Object.keys(caps).filter((k) => !caps[k]);
228
232
  }
229
233
 
234
+ // src/leak-trend.ts
235
+ var MIN_SAMPLES = 4;
236
+ var FLOOR_SEGMENTS = 8;
237
+ var MIN_NET_GROWTH = 3;
238
+ function slope(points) {
239
+ const n = points.length;
240
+ if (n < 2) return 0;
241
+ let sx = 0;
242
+ let sy = 0;
243
+ let sxy = 0;
244
+ let sxx = 0;
245
+ for (const { x, y } of points) {
246
+ sx += x;
247
+ sy += y;
248
+ sxy += x * y;
249
+ sxx += x * x;
250
+ }
251
+ const denom = n * sxx - sx * sx;
252
+ if (denom === 0) return 0;
253
+ return (n * sxy - sx * sy) / denom;
254
+ }
255
+ function analyzeLeakTrend(samples) {
256
+ if (samples.length < MIN_SAMPLES) return null;
257
+ const t0 = samples[0].at;
258
+ const segSize = Math.max(1, Math.ceil(samples.length / FLOOR_SEGMENTS));
259
+ const floorPoints = [];
260
+ for (let i = 0; i < samples.length; i += segSize) {
261
+ const seg = samples.slice(i, i + segSize);
262
+ let min = Infinity;
263
+ for (const s of seg) min = Math.min(min, s.retained);
264
+ const mid = seg[Math.floor(seg.length / 2)];
265
+ floorPoints.push({ x: (mid.at - t0) / 6e4, y: min });
266
+ }
267
+ const slopePerMin = slope(floorPoints);
268
+ const spanMin = (samples[samples.length - 1].at - t0) / 6e4;
269
+ const projectedGrowth = slopePerMin * spanMin;
270
+ const recent = floorPoints.slice(Math.floor(floorPoints.length / 2));
271
+ const recentSlope = recent.length >= 2 ? slope(recent) : slopePerMin;
272
+ const leaking = projectedGrowth >= MIN_NET_GROWTH && recentSlope > 0;
273
+ return { leaking, slopePerMin };
274
+ }
275
+
230
276
  // src/correlate.ts
231
277
  var FRAME_MS = 16;
232
278
  function isAnchor(signal) {
@@ -678,37 +724,40 @@ function createNetworkCollector() {
678
724
 
679
725
  // src/collectors/web-vitals.ts
680
726
  import { onLCP, onINP, onCLS, onFCP, onTTFB } from "web-vitals";
727
+ var subscribed = false;
728
+ var sinks = /* @__PURE__ */ new Set();
729
+ function ensureSubscribed() {
730
+ if (subscribed) return true;
731
+ try {
732
+ const fan = (name) => (metric) => {
733
+ for (const sink of sinks) sink(name, metric.value);
734
+ };
735
+ onLCP(fan("LCP"));
736
+ onINP(fan("INP"));
737
+ onCLS(fan("CLS"));
738
+ onFCP(fan("FCP"));
739
+ onTTFB(fan("TTFB"));
740
+ subscribed = true;
741
+ } catch (err) {
742
+ console.warn("[react-perfscope] web-vitals collector failed to subscribe:", err);
743
+ }
744
+ return subscribed;
745
+ }
681
746
  function createWebVitalsCollector() {
682
- let active = false;
683
- let subscribed = false;
684
747
  let emit = () => {
685
748
  };
686
- function makeHandler(name) {
687
- return (metric) => {
688
- if (!active) return;
689
- emit({ kind: "web-vital", name, value: metric.value });
690
- };
691
- }
749
+ const sink = (name, value) => {
750
+ emit({ kind: "web-vital", name, value });
751
+ };
692
752
  return {
693
753
  kind: "web-vital",
694
754
  activate(emitFn) {
755
+ if (!ensureSubscribed()) return;
695
756
  emit = emitFn;
696
- active = true;
697
- if (subscribed) return;
698
- try {
699
- onLCP(makeHandler("LCP"));
700
- onINP(makeHandler("INP"));
701
- onCLS(makeHandler("CLS"));
702
- onFCP(makeHandler("FCP"));
703
- onTTFB(makeHandler("TTFB"));
704
- subscribed = true;
705
- } catch (err) {
706
- console.warn("[react-perfscope] web-vitals collector failed to subscribe:", err);
707
- active = false;
708
- }
757
+ sinks.add(sink);
709
758
  },
710
759
  deactivate() {
711
- active = false;
760
+ sinks.delete(sink);
712
761
  }
713
762
  };
714
763
  }
@@ -716,19 +765,19 @@ function createWebVitalsCollector() {
716
765
  // src/collectors/heap.ts
717
766
  var SAMPLE_INTERVAL_MS = 250;
718
767
  var MAX_SAMPLES = 2e4;
719
- var MIN_SAMPLES = 4;
720
- var FLOOR_SEGMENTS = 8;
768
+ var MIN_SAMPLES2 = 4;
769
+ var FLOOR_SEGMENTS2 = 8;
721
770
  var MB = 1024 * 1024;
722
771
  var GROWING_SLOPE = 1 * MB;
723
772
  var LEAK_SLOPE = 5 * MB;
724
- var MIN_NET_GROWTH = 2 * MB;
773
+ var MIN_NET_GROWTH2 = 2 * MB;
725
774
  function readMemory() {
726
775
  const perf = globalThis.performance;
727
776
  const mem = perf?.memory;
728
777
  if (!mem || typeof mem.usedJSHeapSize !== "number") return null;
729
778
  return mem;
730
779
  }
731
- function slope(points) {
780
+ function slope2(points) {
732
781
  const n = points.length;
733
782
  if (n < 2) return 0;
734
783
  let sx = 0;
@@ -746,9 +795,9 @@ function slope(points) {
746
795
  return (n * sxy - sx * sy) / denom;
747
796
  }
748
797
  function analyzeHeapTrend(samples) {
749
- if (samples.length < MIN_SAMPLES) return null;
798
+ if (samples.length < MIN_SAMPLES2) return null;
750
799
  const t0 = samples[0].at;
751
- const segSize = Math.max(1, Math.ceil(samples.length / FLOOR_SEGMENTS));
800
+ const segSize = Math.max(1, Math.ceil(samples.length / FLOOR_SEGMENTS2));
752
801
  const floorPoints = [];
753
802
  for (let i = 0; i < samples.length; i += segSize) {
754
803
  const seg = samples.slice(i, i + segSize);
@@ -757,13 +806,13 @@ function analyzeHeapTrend(samples) {
757
806
  const mid = seg[Math.floor(seg.length / 2)];
758
807
  floorPoints.push({ x: (mid.at - t0) / 6e4, y: min });
759
808
  }
760
- const slopeBytesPerMin = slope(floorPoints);
809
+ const slopeBytesPerMin = slope2(floorPoints);
761
810
  const spanMin = (samples[samples.length - 1].at - t0) / 6e4;
762
811
  const projectedGrowth = slopeBytesPerMin * spanMin;
763
812
  const recent = floorPoints.slice(Math.floor(floorPoints.length / 2));
764
- const recentSlope = recent.length >= 2 ? slope(recent) : slopeBytesPerMin;
813
+ const recentSlope = recent.length >= 2 ? slope2(recent) : slopeBytesPerMin;
765
814
  let classification = "stable";
766
- const sustained = projectedGrowth >= MIN_NET_GROWTH && recentSlope >= GROWING_SLOPE;
815
+ const sustained = projectedGrowth >= MIN_NET_GROWTH2 && recentSlope >= GROWING_SLOPE;
767
816
  if (sustained) {
768
817
  classification = slopeBytesPerMin >= LEAK_SLOPE && recentSlope >= LEAK_SLOPE ? "leak-suspected" : "growing";
769
818
  }
@@ -801,6 +850,7 @@ function createHeapCollector() {
801
850
 
802
851
  // src/collectors/interaction.ts
803
852
  var DURATION_THRESHOLD = 40;
853
+ var MAX_ENTRIES = 5e3;
804
854
  function selectorFor(target) {
805
855
  const el = target;
806
856
  if (!el || typeof el.tagName !== "string") return void 0;
@@ -829,7 +879,10 @@ function createInteractionCollector() {
829
879
  try {
830
880
  observer = new PerformanceObserver((list) => {
831
881
  if (!active) return;
832
- for (const e of list.getEntries()) entries.push(e);
882
+ for (const e of list.getEntries()) {
883
+ if (entries.length >= MAX_ENTRIES) break;
884
+ entries.push(e);
885
+ }
833
886
  });
834
887
  observer.observe({ type: "event", buffered: false, durationThreshold: DURATION_THRESHOLD });
835
888
  try {
@@ -853,8 +906,10 @@ function createInteractionCollector() {
853
906
  }
854
907
  },
855
908
  finalize(result) {
909
+ const buffered = entries;
910
+ entries = [];
856
911
  const byId = /* @__PURE__ */ new Map();
857
- for (const e of entries) {
912
+ for (const e of buffered) {
858
913
  const id = e.interactionId;
859
914
  if (!id) continue;
860
915
  const group = byId.get(id);
@@ -1095,6 +1150,7 @@ export {
1095
1150
  BUFFER_CAP,
1096
1151
  analyzeFrames,
1097
1152
  analyzeHeapTrend,
1153
+ analyzeLeakTrend,
1098
1154
  attachLazyStack,
1099
1155
  attributeLongTaskSignals,
1100
1156
  attributeWindow,