@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.cjs CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  BUFFER_CAP: () => BUFFER_CAP,
24
24
  analyzeFrames: () => analyzeFrames,
25
25
  analyzeHeapTrend: () => analyzeHeapTrend,
26
+ analyzeLeakTrend: () => analyzeLeakTrend,
26
27
  attachLazyStack: () => attachLazyStack,
27
28
  attributeLongTaskSignals: () => attributeLongTaskSignals,
28
29
  attributeWindow: () => attributeWindow,
@@ -138,23 +139,25 @@ function isSelfRequest(url) {
138
139
  // src/sourcemap.ts
139
140
  var CHROME_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/;
140
141
  var FIREFOX_FRAME = /^(.*?)@(.+?):(\d+):(\d+)$/;
142
+ function lookupFrame(tracer, frame) {
143
+ const pos = (0, import_trace_mapping.originalPositionFor)(tracer, { line: frame.line, column: frame.col - 1 });
144
+ if (pos.source == null || pos.line == null || pos.column == null) {
145
+ return frame;
146
+ }
147
+ const resolved = {
148
+ file: pos.source,
149
+ line: pos.line,
150
+ col: pos.column + 1
151
+ };
152
+ if (pos.name) resolved.fnName = pos.name;
153
+ else if (frame.fnName) resolved.fnName = frame.fnName;
154
+ return resolved;
155
+ }
141
156
  async function resolveFrame(frame, fetchMap) {
142
157
  try {
143
158
  const map = await fetchMap(frame.file);
144
159
  if (!map) return frame;
145
- const tracer = new import_trace_mapping.TraceMap(map);
146
- const pos = (0, import_trace_mapping.originalPositionFor)(tracer, { line: frame.line, column: frame.col });
147
- if (pos.source == null || pos.line == null || pos.column == null) {
148
- return frame;
149
- }
150
- const resolved = {
151
- file: pos.source,
152
- line: pos.line,
153
- col: pos.column
154
- };
155
- if (pos.name) resolved.fnName = pos.name;
156
- else if (frame.fnName) resolved.fnName = frame.fnName;
157
- return resolved;
160
+ return lookupFrame(new import_trace_mapping.TraceMap(map), frame);
158
161
  } catch (err) {
159
162
  console.warn("[react-perfscope] resolveFrame failed:", err);
160
163
  return frame;
@@ -177,7 +180,7 @@ function attachLazyStack(target, raw, skipTopFrames = 0) {
177
180
  function createSourceMapResolver(opts = {}) {
178
181
  const f = opts.fetch ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
179
182
  const cache = /* @__PURE__ */ new Map();
180
- function fetchMapFor(sourceUrl) {
183
+ function fetchTracerFor(sourceUrl) {
181
184
  if (!f) return Promise.resolve(null);
182
185
  const existing = cache.get(sourceUrl);
183
186
  if (existing) return existing;
@@ -196,13 +199,13 @@ function createSourceMapResolver(opts = {}) {
196
199
  const header = ref.slice(0, commaIdx);
197
200
  const payload = ref.slice(commaIdx + 1);
198
201
  const decoded = header.includes("base64") ? typeof atob === "function" ? atob(payload) : globalThis.Buffer.from(payload, "base64").toString("utf8") : decodeURIComponent(payload);
199
- return JSON.parse(decoded);
202
+ return new import_trace_mapping.TraceMap(JSON.parse(decoded));
200
203
  }
201
204
  const mapUrl = new URL(ref, sourceUrl).href;
202
205
  markSelfRequest(mapUrl);
203
206
  const mapRes = await f(mapUrl);
204
207
  if (!mapRes || !mapRes.ok) return null;
205
- return await mapRes.json();
208
+ return new import_trace_mapping.TraceMap(await mapRes.json());
206
209
  } catch {
207
210
  return null;
208
211
  }
@@ -213,7 +216,9 @@ function createSourceMapResolver(opts = {}) {
213
216
  return {
214
217
  async resolve(frame) {
215
218
  try {
216
- return await resolveFrame(frame, fetchMapFor);
219
+ const tracer = await fetchTracerFor(frame.file);
220
+ if (!tracer) return frame;
221
+ return lookupFrame(tracer, frame);
217
222
  } catch {
218
223
  return frame;
219
224
  }
@@ -277,6 +282,48 @@ function unsupportedKinds() {
277
282
  return Object.keys(caps).filter((k) => !caps[k]);
278
283
  }
279
284
 
285
+ // src/leak-trend.ts
286
+ var MIN_SAMPLES = 4;
287
+ var FLOOR_SEGMENTS = 8;
288
+ var MIN_NET_GROWTH = 3;
289
+ function slope(points) {
290
+ const n = points.length;
291
+ if (n < 2) return 0;
292
+ let sx = 0;
293
+ let sy = 0;
294
+ let sxy = 0;
295
+ let sxx = 0;
296
+ for (const { x, y } of points) {
297
+ sx += x;
298
+ sy += y;
299
+ sxy += x * y;
300
+ sxx += x * x;
301
+ }
302
+ const denom = n * sxx - sx * sx;
303
+ if (denom === 0) return 0;
304
+ return (n * sxy - sx * sy) / denom;
305
+ }
306
+ function analyzeLeakTrend(samples) {
307
+ if (samples.length < MIN_SAMPLES) return null;
308
+ const t0 = samples[0].at;
309
+ const segSize = Math.max(1, Math.ceil(samples.length / FLOOR_SEGMENTS));
310
+ const floorPoints = [];
311
+ for (let i = 0; i < samples.length; i += segSize) {
312
+ const seg = samples.slice(i, i + segSize);
313
+ let min = Infinity;
314
+ for (const s of seg) min = Math.min(min, s.retained);
315
+ const mid = seg[Math.floor(seg.length / 2)];
316
+ floorPoints.push({ x: (mid.at - t0) / 6e4, y: min });
317
+ }
318
+ const slopePerMin = slope(floorPoints);
319
+ const spanMin = (samples[samples.length - 1].at - t0) / 6e4;
320
+ const projectedGrowth = slopePerMin * spanMin;
321
+ const recent = floorPoints.slice(Math.floor(floorPoints.length / 2));
322
+ const recentSlope = recent.length >= 2 ? slope(recent) : slopePerMin;
323
+ const leaking = projectedGrowth >= MIN_NET_GROWTH && recentSlope > 0;
324
+ return { leaking, slopePerMin };
325
+ }
326
+
280
327
  // src/correlate.ts
281
328
  var FRAME_MS = 16;
282
329
  function isAnchor(signal) {
@@ -728,37 +775,40 @@ function createNetworkCollector() {
728
775
 
729
776
  // src/collectors/web-vitals.ts
730
777
  var import_web_vitals = require("web-vitals");
778
+ var subscribed = false;
779
+ var sinks = /* @__PURE__ */ new Set();
780
+ function ensureSubscribed() {
781
+ if (subscribed) return true;
782
+ try {
783
+ const fan = (name) => (metric) => {
784
+ for (const sink of sinks) sink(name, metric.value);
785
+ };
786
+ (0, import_web_vitals.onLCP)(fan("LCP"));
787
+ (0, import_web_vitals.onINP)(fan("INP"));
788
+ (0, import_web_vitals.onCLS)(fan("CLS"));
789
+ (0, import_web_vitals.onFCP)(fan("FCP"));
790
+ (0, import_web_vitals.onTTFB)(fan("TTFB"));
791
+ subscribed = true;
792
+ } catch (err) {
793
+ console.warn("[react-perfscope] web-vitals collector failed to subscribe:", err);
794
+ }
795
+ return subscribed;
796
+ }
731
797
  function createWebVitalsCollector() {
732
- let active = false;
733
- let subscribed = false;
734
798
  let emit = () => {
735
799
  };
736
- function makeHandler(name) {
737
- return (metric) => {
738
- if (!active) return;
739
- emit({ kind: "web-vital", name, value: metric.value });
740
- };
741
- }
800
+ const sink = (name, value) => {
801
+ emit({ kind: "web-vital", name, value });
802
+ };
742
803
  return {
743
804
  kind: "web-vital",
744
805
  activate(emitFn) {
806
+ if (!ensureSubscribed()) return;
745
807
  emit = emitFn;
746
- active = true;
747
- if (subscribed) return;
748
- try {
749
- (0, import_web_vitals.onLCP)(makeHandler("LCP"));
750
- (0, import_web_vitals.onINP)(makeHandler("INP"));
751
- (0, import_web_vitals.onCLS)(makeHandler("CLS"));
752
- (0, import_web_vitals.onFCP)(makeHandler("FCP"));
753
- (0, import_web_vitals.onTTFB)(makeHandler("TTFB"));
754
- subscribed = true;
755
- } catch (err) {
756
- console.warn("[react-perfscope] web-vitals collector failed to subscribe:", err);
757
- active = false;
758
- }
808
+ sinks.add(sink);
759
809
  },
760
810
  deactivate() {
761
- active = false;
811
+ sinks.delete(sink);
762
812
  }
763
813
  };
764
814
  }
@@ -766,19 +816,19 @@ function createWebVitalsCollector() {
766
816
  // src/collectors/heap.ts
767
817
  var SAMPLE_INTERVAL_MS = 250;
768
818
  var MAX_SAMPLES = 2e4;
769
- var MIN_SAMPLES = 4;
770
- var FLOOR_SEGMENTS = 8;
819
+ var MIN_SAMPLES2 = 4;
820
+ var FLOOR_SEGMENTS2 = 8;
771
821
  var MB = 1024 * 1024;
772
822
  var GROWING_SLOPE = 1 * MB;
773
823
  var LEAK_SLOPE = 5 * MB;
774
- var MIN_NET_GROWTH = 2 * MB;
824
+ var MIN_NET_GROWTH2 = 2 * MB;
775
825
  function readMemory() {
776
826
  const perf = globalThis.performance;
777
827
  const mem = perf?.memory;
778
828
  if (!mem || typeof mem.usedJSHeapSize !== "number") return null;
779
829
  return mem;
780
830
  }
781
- function slope(points) {
831
+ function slope2(points) {
782
832
  const n = points.length;
783
833
  if (n < 2) return 0;
784
834
  let sx = 0;
@@ -796,9 +846,9 @@ function slope(points) {
796
846
  return (n * sxy - sx * sy) / denom;
797
847
  }
798
848
  function analyzeHeapTrend(samples) {
799
- if (samples.length < MIN_SAMPLES) return null;
849
+ if (samples.length < MIN_SAMPLES2) return null;
800
850
  const t0 = samples[0].at;
801
- const segSize = Math.max(1, Math.ceil(samples.length / FLOOR_SEGMENTS));
851
+ const segSize = Math.max(1, Math.ceil(samples.length / FLOOR_SEGMENTS2));
802
852
  const floorPoints = [];
803
853
  for (let i = 0; i < samples.length; i += segSize) {
804
854
  const seg = samples.slice(i, i + segSize);
@@ -807,13 +857,13 @@ function analyzeHeapTrend(samples) {
807
857
  const mid = seg[Math.floor(seg.length / 2)];
808
858
  floorPoints.push({ x: (mid.at - t0) / 6e4, y: min });
809
859
  }
810
- const slopeBytesPerMin = slope(floorPoints);
860
+ const slopeBytesPerMin = slope2(floorPoints);
811
861
  const spanMin = (samples[samples.length - 1].at - t0) / 6e4;
812
862
  const projectedGrowth = slopeBytesPerMin * spanMin;
813
863
  const recent = floorPoints.slice(Math.floor(floorPoints.length / 2));
814
- const recentSlope = recent.length >= 2 ? slope(recent) : slopeBytesPerMin;
864
+ const recentSlope = recent.length >= 2 ? slope2(recent) : slopeBytesPerMin;
815
865
  let classification = "stable";
816
- const sustained = projectedGrowth >= MIN_NET_GROWTH && recentSlope >= GROWING_SLOPE;
866
+ const sustained = projectedGrowth >= MIN_NET_GROWTH2 && recentSlope >= GROWING_SLOPE;
817
867
  if (sustained) {
818
868
  classification = slopeBytesPerMin >= LEAK_SLOPE && recentSlope >= LEAK_SLOPE ? "leak-suspected" : "growing";
819
869
  }
@@ -851,6 +901,7 @@ function createHeapCollector() {
851
901
 
852
902
  // src/collectors/interaction.ts
853
903
  var DURATION_THRESHOLD = 40;
904
+ var MAX_ENTRIES = 5e3;
854
905
  function selectorFor(target) {
855
906
  const el = target;
856
907
  if (!el || typeof el.tagName !== "string") return void 0;
@@ -879,7 +930,10 @@ function createInteractionCollector() {
879
930
  try {
880
931
  observer = new PerformanceObserver((list) => {
881
932
  if (!active) return;
882
- for (const e of list.getEntries()) entries.push(e);
933
+ for (const e of list.getEntries()) {
934
+ if (entries.length >= MAX_ENTRIES) break;
935
+ entries.push(e);
936
+ }
883
937
  });
884
938
  observer.observe({ type: "event", buffered: false, durationThreshold: DURATION_THRESHOLD });
885
939
  try {
@@ -903,8 +957,10 @@ function createInteractionCollector() {
903
957
  }
904
958
  },
905
959
  finalize(result) {
960
+ const buffered = entries;
961
+ entries = [];
906
962
  const byId = /* @__PURE__ */ new Map();
907
- for (const e of entries) {
963
+ for (const e of buffered) {
908
964
  const id = e.interactionId;
909
965
  if (!id) continue;
910
966
  const group = byId.get(id);
@@ -1146,6 +1202,7 @@ function createSelfProfilingCollector() {
1146
1202
  BUFFER_CAP,
1147
1203
  analyzeFrames,
1148
1204
  analyzeHeapTrend,
1205
+ analyzeLeakTrend,
1149
1206
  attachLazyStack,
1150
1207
  attributeLongTaskSignals,
1151
1208
  attributeWindow,