@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 +107 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -4
- package/dist/index.d.ts +49 -4
- package/dist/index.js +106 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
720
|
-
var
|
|
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
|
|
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
|
|
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 <
|
|
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 /
|
|
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 =
|
|
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 ?
|
|
813
|
+
const recentSlope = recent.length >= 2 ? slope2(recent) : slopeBytesPerMin;
|
|
765
814
|
let classification = "stable";
|
|
766
|
-
const sustained = projectedGrowth >=
|
|
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())
|
|
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
|
|
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,
|