@react-perfscope/core 0.1.0
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/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/index.cjs +1035 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +367 -0
- package/dist/index.d.ts +367 -0
- package/dist/index.js +990 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
// src/recorder.ts
|
|
2
|
+
var BUFFER_CAP = 1e4;
|
|
3
|
+
function createRecorder() {
|
|
4
|
+
let recording = false;
|
|
5
|
+
let startedAt = 0;
|
|
6
|
+
let buffer = [];
|
|
7
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
8
|
+
const collectors = [];
|
|
9
|
+
function notify(signal) {
|
|
10
|
+
for (const cb of subscribers) {
|
|
11
|
+
try {
|
|
12
|
+
cb(signal);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.warn("[react-perfscope] subscriber threw:", err);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function push(signal) {
|
|
19
|
+
if (!recording) return;
|
|
20
|
+
buffer.push(signal);
|
|
21
|
+
if (buffer.length > BUFFER_CAP) {
|
|
22
|
+
buffer.splice(0, buffer.length - BUFFER_CAP);
|
|
23
|
+
}
|
|
24
|
+
notify(signal);
|
|
25
|
+
}
|
|
26
|
+
const recorder = {
|
|
27
|
+
start() {
|
|
28
|
+
if (recording) return;
|
|
29
|
+
recording = true;
|
|
30
|
+
startedAt = performance.now();
|
|
31
|
+
buffer = [];
|
|
32
|
+
for (const c of collectors) {
|
|
33
|
+
try {
|
|
34
|
+
c.activate(push);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.warn(`[react-perfscope] collector ${c.kind} failed to activate:`, err);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
stop() {
|
|
41
|
+
if (!recording) {
|
|
42
|
+
return { signals: [], startedAt: 0, duration: 0 };
|
|
43
|
+
}
|
|
44
|
+
for (const c of collectors) {
|
|
45
|
+
try {
|
|
46
|
+
c.deactivate();
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.warn(`[react-perfscope] collector ${c.kind} failed to deactivate:`, err);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const duration = performance.now() - startedAt;
|
|
52
|
+
const result = {
|
|
53
|
+
signals: buffer.slice(),
|
|
54
|
+
startedAt,
|
|
55
|
+
duration
|
|
56
|
+
};
|
|
57
|
+
recording = false;
|
|
58
|
+
buffer = [];
|
|
59
|
+
return result;
|
|
60
|
+
},
|
|
61
|
+
isRecording() {
|
|
62
|
+
return recording;
|
|
63
|
+
},
|
|
64
|
+
onSignal(cb) {
|
|
65
|
+
subscribers.add(cb);
|
|
66
|
+
return () => subscribers.delete(cb);
|
|
67
|
+
},
|
|
68
|
+
use(collector) {
|
|
69
|
+
collectors.push(collector);
|
|
70
|
+
},
|
|
71
|
+
__push: push
|
|
72
|
+
};
|
|
73
|
+
return recorder;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/sourcemap.ts
|
|
77
|
+
import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
|
|
78
|
+
var CHROME_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/;
|
|
79
|
+
var FIREFOX_FRAME = /^(.*?)@(.+?):(\d+):(\d+)$/;
|
|
80
|
+
async function resolveFrame(frame, fetchMap) {
|
|
81
|
+
try {
|
|
82
|
+
const map = await fetchMap(frame.file);
|
|
83
|
+
if (!map) return frame;
|
|
84
|
+
const tracer = new TraceMap(map);
|
|
85
|
+
const pos = originalPositionFor(tracer, { line: frame.line, column: frame.col });
|
|
86
|
+
if (pos.source == null || pos.line == null || pos.column == null) {
|
|
87
|
+
return frame;
|
|
88
|
+
}
|
|
89
|
+
const resolved = {
|
|
90
|
+
file: pos.source,
|
|
91
|
+
line: pos.line,
|
|
92
|
+
col: pos.column
|
|
93
|
+
};
|
|
94
|
+
if (pos.name) resolved.fnName = pos.name;
|
|
95
|
+
else if (frame.fnName) resolved.fnName = frame.fnName;
|
|
96
|
+
return resolved;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.warn("[react-perfscope] resolveFrame failed:", err);
|
|
99
|
+
return frame;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function attachLazyStack(target, raw, skipTopFrames = 0) {
|
|
103
|
+
let cached = null;
|
|
104
|
+
Object.defineProperty(target, "stack", {
|
|
105
|
+
enumerable: true,
|
|
106
|
+
configurable: true,
|
|
107
|
+
get() {
|
|
108
|
+
if (cached === null) {
|
|
109
|
+
const all = parseStack(raw);
|
|
110
|
+
cached = skipTopFrames > 0 ? all.slice(skipTopFrames) : all;
|
|
111
|
+
}
|
|
112
|
+
return cached;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function createSourceMapResolver(opts = {}) {
|
|
117
|
+
const f = opts.fetch ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
|
|
118
|
+
const cache = /* @__PURE__ */ new Map();
|
|
119
|
+
function fetchMapFor(sourceUrl) {
|
|
120
|
+
if (!f) return Promise.resolve(null);
|
|
121
|
+
const existing = cache.get(sourceUrl);
|
|
122
|
+
if (existing) return existing;
|
|
123
|
+
const promise = (async () => {
|
|
124
|
+
try {
|
|
125
|
+
const res = await f(sourceUrl);
|
|
126
|
+
if (!res || !res.ok) return null;
|
|
127
|
+
const text = await res.text();
|
|
128
|
+
const m = text.match(/\/\/[#@]\s*sourceMappingURL=(.+?)\s*$/m);
|
|
129
|
+
if (!m) return null;
|
|
130
|
+
const ref = m[1].trim();
|
|
131
|
+
if (ref.startsWith("data:")) {
|
|
132
|
+
const commaIdx = ref.indexOf(",");
|
|
133
|
+
if (commaIdx === -1) return null;
|
|
134
|
+
const header = ref.slice(0, commaIdx);
|
|
135
|
+
const payload = ref.slice(commaIdx + 1);
|
|
136
|
+
const decoded = header.includes("base64") ? typeof atob === "function" ? atob(payload) : globalThis.Buffer.from(payload, "base64").toString("utf8") : decodeURIComponent(payload);
|
|
137
|
+
return JSON.parse(decoded);
|
|
138
|
+
}
|
|
139
|
+
const mapUrl = new URL(ref, sourceUrl).href;
|
|
140
|
+
const mapRes = await f(mapUrl);
|
|
141
|
+
if (!mapRes || !mapRes.ok) return null;
|
|
142
|
+
return await mapRes.json();
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
})();
|
|
147
|
+
cache.set(sourceUrl, promise);
|
|
148
|
+
return promise;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
async resolve(frame) {
|
|
152
|
+
try {
|
|
153
|
+
return await resolveFrame(frame, fetchMapFor);
|
|
154
|
+
} catch {
|
|
155
|
+
return frame;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function parseStack(raw) {
|
|
161
|
+
if (!raw) return [];
|
|
162
|
+
const frames = [];
|
|
163
|
+
for (const line of raw.split("\n")) {
|
|
164
|
+
const chromeMatch = line.match(CHROME_FRAME);
|
|
165
|
+
if (chromeMatch) {
|
|
166
|
+
const [, fnName, file, lineStr, colStr] = chromeMatch;
|
|
167
|
+
const frame = {
|
|
168
|
+
file: file ?? "",
|
|
169
|
+
line: Number(lineStr),
|
|
170
|
+
col: Number(colStr)
|
|
171
|
+
};
|
|
172
|
+
if (fnName) frame.fnName = fnName;
|
|
173
|
+
frames.push(frame);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const firefoxMatch = line.match(FIREFOX_FRAME);
|
|
177
|
+
if (firefoxMatch) {
|
|
178
|
+
const [, fnName, file, lineStr, colStr] = firefoxMatch;
|
|
179
|
+
const frame = {
|
|
180
|
+
file: file ?? "",
|
|
181
|
+
line: Number(lineStr),
|
|
182
|
+
col: Number(colStr)
|
|
183
|
+
};
|
|
184
|
+
if (fnName) frame.fnName = fnName;
|
|
185
|
+
frames.push(frame);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return frames;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/collectors/long-tasks.ts
|
|
192
|
+
var LOAF = "long-animation-frame";
|
|
193
|
+
var LEGACY = "longtask";
|
|
194
|
+
function supportsLoAF() {
|
|
195
|
+
const PO = globalThis.PerformanceObserver;
|
|
196
|
+
const types = PO?.supportedEntryTypes;
|
|
197
|
+
return Array.isArray(types) && types.includes(LOAF);
|
|
198
|
+
}
|
|
199
|
+
function mapScripts(scripts) {
|
|
200
|
+
return scripts.map((s) => ({
|
|
201
|
+
invokerType: s.invokerType ?? "unknown",
|
|
202
|
+
invoker: s.invoker ?? "",
|
|
203
|
+
sourceURL: s.sourceURL ?? "",
|
|
204
|
+
sourceFunctionName: s.sourceFunctionName ?? "",
|
|
205
|
+
charPosition: typeof s.sourceCharPosition === "number" ? s.sourceCharPosition : -1,
|
|
206
|
+
duration: typeof s.duration === "number" ? s.duration : 0
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
function createLongTasksCollector() {
|
|
210
|
+
let observer = null;
|
|
211
|
+
let active = false;
|
|
212
|
+
return {
|
|
213
|
+
kind: "long-task",
|
|
214
|
+
activate(emit) {
|
|
215
|
+
if (typeof PerformanceObserver === "undefined") {
|
|
216
|
+
console.warn("[react-perfscope] PerformanceObserver not supported; long-tasks disabled");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
active = true;
|
|
220
|
+
const useLoAF = supportsLoAF();
|
|
221
|
+
try {
|
|
222
|
+
observer = new PerformanceObserver((list) => {
|
|
223
|
+
if (!active) return;
|
|
224
|
+
for (const entry of list.getEntries()) {
|
|
225
|
+
if (useLoAF) {
|
|
226
|
+
const loaf = entry;
|
|
227
|
+
const scripts = Array.isArray(loaf.scripts) ? mapScripts(loaf.scripts) : [];
|
|
228
|
+
emit({
|
|
229
|
+
kind: "long-task",
|
|
230
|
+
at: entry.startTime,
|
|
231
|
+
duration: entry.duration,
|
|
232
|
+
stack: [],
|
|
233
|
+
scripts,
|
|
234
|
+
...typeof loaf.blockingDuration === "number" ? { blockingDuration: loaf.blockingDuration } : {}
|
|
235
|
+
});
|
|
236
|
+
} else {
|
|
237
|
+
emit({
|
|
238
|
+
kind: "long-task",
|
|
239
|
+
at: entry.startTime,
|
|
240
|
+
duration: entry.duration,
|
|
241
|
+
stack: []
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
observer.observe({ type: useLoAF ? LOAF : LEGACY, buffered: false });
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.warn("[react-perfscope] long-tasks collector failed to start:", err);
|
|
249
|
+
observer = null;
|
|
250
|
+
active = false;
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
deactivate() {
|
|
254
|
+
active = false;
|
|
255
|
+
if (observer) {
|
|
256
|
+
try {
|
|
257
|
+
observer.disconnect();
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
observer = null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/collectors/forced-reflow.ts
|
|
267
|
+
var LAYOUT_GETTERS = [
|
|
268
|
+
"offsetWidth",
|
|
269
|
+
"offsetHeight",
|
|
270
|
+
"offsetLeft",
|
|
271
|
+
"offsetTop",
|
|
272
|
+
"clientWidth",
|
|
273
|
+
"clientHeight",
|
|
274
|
+
"scrollWidth",
|
|
275
|
+
"scrollHeight"
|
|
276
|
+
];
|
|
277
|
+
var LAYOUT_METHODS = ["getBoundingClientRect", "getClientRects"];
|
|
278
|
+
function createForcedReflowCollector() {
|
|
279
|
+
let active = false;
|
|
280
|
+
let emit = () => {
|
|
281
|
+
};
|
|
282
|
+
const saved = [];
|
|
283
|
+
let mutationObserver = null;
|
|
284
|
+
function consumePendingMutations() {
|
|
285
|
+
if (!mutationObserver) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
return mutationObserver.takeRecords().length > 0;
|
|
289
|
+
}
|
|
290
|
+
function patchGetter(proto, key) {
|
|
291
|
+
if (typeof proto !== "object" || proto === null) return;
|
|
292
|
+
const desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
293
|
+
if (!desc || !desc.get) return;
|
|
294
|
+
saved.push({ proto, key, descriptor: desc });
|
|
295
|
+
const originalGet = desc.get;
|
|
296
|
+
Object.defineProperty(proto, key, {
|
|
297
|
+
configurable: true,
|
|
298
|
+
get() {
|
|
299
|
+
if (active) {
|
|
300
|
+
if (!consumePendingMutations()) {
|
|
301
|
+
return originalGet.call(this);
|
|
302
|
+
}
|
|
303
|
+
const at = performance.now();
|
|
304
|
+
const rawStack = new Error().stack;
|
|
305
|
+
const value = originalGet.call(this);
|
|
306
|
+
const duration = performance.now() - at;
|
|
307
|
+
const signal = { kind: "forced-reflow", at, duration };
|
|
308
|
+
attachLazyStack(signal, rawStack, 1);
|
|
309
|
+
emit(signal);
|
|
310
|
+
return value;
|
|
311
|
+
}
|
|
312
|
+
return originalGet.call(this);
|
|
313
|
+
},
|
|
314
|
+
set: desc.set
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function patchMethod(proto, key) {
|
|
318
|
+
if (typeof proto !== "object" || proto === null) return;
|
|
319
|
+
const desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
320
|
+
if (!desc || typeof desc.value !== "function") return;
|
|
321
|
+
saved.push({ proto, key, descriptor: desc });
|
|
322
|
+
const original = desc.value;
|
|
323
|
+
Object.defineProperty(proto, key, {
|
|
324
|
+
configurable: true,
|
|
325
|
+
writable: true,
|
|
326
|
+
value: function patchedLayoutMethod(...args) {
|
|
327
|
+
if (active) {
|
|
328
|
+
if (!consumePendingMutations()) {
|
|
329
|
+
return original.apply(this, args);
|
|
330
|
+
}
|
|
331
|
+
const at = performance.now();
|
|
332
|
+
const rawStack = new Error().stack;
|
|
333
|
+
const value = original.apply(this, args);
|
|
334
|
+
const duration = performance.now() - at;
|
|
335
|
+
const signal = { kind: "forced-reflow", at, duration };
|
|
336
|
+
attachLazyStack(signal, rawStack, 1);
|
|
337
|
+
emit(signal);
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
return original.apply(this, args);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
kind: "forced-reflow",
|
|
346
|
+
activate(emitFn) {
|
|
347
|
+
if (active) return;
|
|
348
|
+
emit = emitFn;
|
|
349
|
+
active = true;
|
|
350
|
+
if (typeof MutationObserver !== "undefined" && typeof document !== "undefined") {
|
|
351
|
+
try {
|
|
352
|
+
mutationObserver = new MutationObserver(() => {
|
|
353
|
+
});
|
|
354
|
+
mutationObserver.observe(document, {
|
|
355
|
+
attributes: true,
|
|
356
|
+
childList: true,
|
|
357
|
+
subtree: true,
|
|
358
|
+
characterData: true
|
|
359
|
+
});
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.warn("[react-perfscope] forced-reflow MutationObserver failed:", err);
|
|
362
|
+
mutationObserver = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (typeof HTMLElement !== "undefined") {
|
|
366
|
+
for (const key of LAYOUT_GETTERS) {
|
|
367
|
+
patchGetter(HTMLElement.prototype, key);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (typeof Element !== "undefined") {
|
|
371
|
+
for (const key of LAYOUT_METHODS) {
|
|
372
|
+
patchMethod(Element.prototype, key);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
deactivate() {
|
|
377
|
+
if (!active) return;
|
|
378
|
+
active = false;
|
|
379
|
+
if (mutationObserver) {
|
|
380
|
+
try {
|
|
381
|
+
mutationObserver.disconnect();
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
mutationObserver = null;
|
|
385
|
+
}
|
|
386
|
+
for (const { proto, key, descriptor } of saved) {
|
|
387
|
+
Object.defineProperty(proto, key, descriptor);
|
|
388
|
+
}
|
|
389
|
+
saved.length = 0;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/collectors/layout-shift.ts
|
|
395
|
+
var PERFSCOPE_HOST_ATTR = "data-perfscope-host";
|
|
396
|
+
function rectsOverlap(a, b, tolerance = 0) {
|
|
397
|
+
return !(a.x + a.width < b.x - tolerance || b.x + b.width < a.x - tolerance || a.y + a.height < b.y - tolerance || b.y + b.height < a.y - tolerance);
|
|
398
|
+
}
|
|
399
|
+
function collectPerfscopeRects() {
|
|
400
|
+
const hosts = Array.from(document.querySelectorAll(`[${PERFSCOPE_HOST_ATTR}]`));
|
|
401
|
+
const rects = [];
|
|
402
|
+
for (const h of hosts) {
|
|
403
|
+
const sr = h.shadowRoot;
|
|
404
|
+
if (sr) {
|
|
405
|
+
for (const child of Array.from(sr.children)) {
|
|
406
|
+
const r = child.getBoundingClientRect();
|
|
407
|
+
if (r.width > 0 && r.height > 0) rects.push(r);
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
const r = h.getBoundingClientRect();
|
|
411
|
+
if (r.width > 0 && r.height > 0) rects.push(r);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return rects;
|
|
415
|
+
}
|
|
416
|
+
function entryIsOnlyFromPerfscope(sources) {
|
|
417
|
+
if (sources.length === 0) return false;
|
|
418
|
+
if (typeof document === "undefined") return false;
|
|
419
|
+
const hostRects = collectPerfscopeRects();
|
|
420
|
+
if (hostRects.length === 0) return false;
|
|
421
|
+
for (const src of sources) {
|
|
422
|
+
let matched = false;
|
|
423
|
+
const node = src.node;
|
|
424
|
+
if (node) {
|
|
425
|
+
if (typeof node.closest === "function") {
|
|
426
|
+
if (node.closest(`[${PERFSCOPE_HOST_ATTR}]`)) matched = true;
|
|
427
|
+
} else {
|
|
428
|
+
let parent = node.parentElement;
|
|
429
|
+
while (parent) {
|
|
430
|
+
if (parent.hasAttribute(PERFSCOPE_HOST_ATTR)) {
|
|
431
|
+
matched = true;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
parent = parent.parentElement;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (!matched) {
|
|
439
|
+
for (const hostRect of hostRects) {
|
|
440
|
+
if (rectsOverlap(src.currentRect, hostRect, 24)) {
|
|
441
|
+
matched = true;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (!matched) return false;
|
|
447
|
+
}
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
function createLayoutShiftCollector() {
|
|
451
|
+
let observer = null;
|
|
452
|
+
let active = false;
|
|
453
|
+
return {
|
|
454
|
+
kind: "layout-shift",
|
|
455
|
+
activate(emit) {
|
|
456
|
+
if (typeof PerformanceObserver === "undefined") {
|
|
457
|
+
console.warn("[react-perfscope] PerformanceObserver not supported; layout-shift disabled");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
active = true;
|
|
461
|
+
try {
|
|
462
|
+
observer = new PerformanceObserver((list) => {
|
|
463
|
+
if (!active) return;
|
|
464
|
+
for (const raw of list.getEntries()) {
|
|
465
|
+
const entry = raw;
|
|
466
|
+
const sources = entry.sources ?? [];
|
|
467
|
+
if (entryIsOnlyFromPerfscope(sources)) continue;
|
|
468
|
+
const sx = typeof window !== "undefined" && window.scrollX || 0;
|
|
469
|
+
const sy = typeof window !== "undefined" && window.scrollY || 0;
|
|
470
|
+
const toDocRect = (r) => new DOMRect(r.x + sx, r.y + sy, r.width, r.height);
|
|
471
|
+
const rects = sources.map((s) => toDocRect(s.currentRect));
|
|
472
|
+
const prevRects = sources.map((s) => {
|
|
473
|
+
const pr = s.previousRect;
|
|
474
|
+
if (!pr) return null;
|
|
475
|
+
if (pr.width === 0 && pr.height === 0) return null;
|
|
476
|
+
return toDocRect(pr);
|
|
477
|
+
});
|
|
478
|
+
emit({
|
|
479
|
+
kind: "layout-shift",
|
|
480
|
+
at: entry.startTime,
|
|
481
|
+
value: entry.value,
|
|
482
|
+
sources: rects,
|
|
483
|
+
previousSources: prevRects
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
observer.observe({ type: "layout-shift", buffered: false });
|
|
488
|
+
} catch (err) {
|
|
489
|
+
console.warn("[react-perfscope] layout-shift collector failed to start:", err);
|
|
490
|
+
observer = null;
|
|
491
|
+
active = false;
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
deactivate() {
|
|
495
|
+
active = false;
|
|
496
|
+
if (observer) {
|
|
497
|
+
try {
|
|
498
|
+
observer.disconnect();
|
|
499
|
+
} catch {
|
|
500
|
+
}
|
|
501
|
+
observer = null;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/collectors/network.ts
|
|
508
|
+
function createNetworkCollector() {
|
|
509
|
+
let observer = null;
|
|
510
|
+
let active = false;
|
|
511
|
+
return {
|
|
512
|
+
kind: "network",
|
|
513
|
+
activate(emit) {
|
|
514
|
+
if (typeof PerformanceObserver === "undefined") {
|
|
515
|
+
console.warn("[react-perfscope] PerformanceObserver not supported; network disabled");
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
active = true;
|
|
519
|
+
try {
|
|
520
|
+
observer = new PerformanceObserver((list) => {
|
|
521
|
+
if (!active) return;
|
|
522
|
+
for (const raw of list.getEntries()) {
|
|
523
|
+
const entry = raw;
|
|
524
|
+
emit({
|
|
525
|
+
kind: "network",
|
|
526
|
+
url: entry.name,
|
|
527
|
+
startedAt: entry.startTime,
|
|
528
|
+
duration: entry.duration,
|
|
529
|
+
size: entry.transferSize ?? 0,
|
|
530
|
+
blocking: entry.renderBlockingStatus === "blocking"
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
observer.observe({ type: "resource", buffered: false });
|
|
535
|
+
} catch (err) {
|
|
536
|
+
console.warn("[react-perfscope] network collector failed to start:", err);
|
|
537
|
+
observer = null;
|
|
538
|
+
active = false;
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
deactivate() {
|
|
542
|
+
active = false;
|
|
543
|
+
if (observer) {
|
|
544
|
+
try {
|
|
545
|
+
observer.disconnect();
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
observer = null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/collectors/web-vitals.ts
|
|
555
|
+
import { onLCP, onINP, onCLS, onFCP, onTTFB } from "web-vitals";
|
|
556
|
+
function createWebVitalsCollector() {
|
|
557
|
+
let active = false;
|
|
558
|
+
let subscribed = false;
|
|
559
|
+
let emit = () => {
|
|
560
|
+
};
|
|
561
|
+
function makeHandler(name) {
|
|
562
|
+
return (metric) => {
|
|
563
|
+
if (!active) return;
|
|
564
|
+
emit({ kind: "web-vital", name, value: metric.value });
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
kind: "web-vital",
|
|
569
|
+
activate(emitFn) {
|
|
570
|
+
emit = emitFn;
|
|
571
|
+
active = true;
|
|
572
|
+
if (subscribed) return;
|
|
573
|
+
try {
|
|
574
|
+
onLCP(makeHandler("LCP"));
|
|
575
|
+
onINP(makeHandler("INP"));
|
|
576
|
+
onCLS(makeHandler("CLS"));
|
|
577
|
+
onFCP(makeHandler("FCP"));
|
|
578
|
+
onTTFB(makeHandler("TTFB"));
|
|
579
|
+
subscribed = true;
|
|
580
|
+
} catch (err) {
|
|
581
|
+
console.warn("[react-perfscope] web-vitals collector failed to subscribe:", err);
|
|
582
|
+
active = false;
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
deactivate() {
|
|
586
|
+
active = false;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/collectors/heap.ts
|
|
592
|
+
var SAMPLE_INTERVAL_MS = 250;
|
|
593
|
+
var MAX_SAMPLES = 2e4;
|
|
594
|
+
var MIN_SAMPLES = 4;
|
|
595
|
+
var FLOOR_SEGMENTS = 8;
|
|
596
|
+
var MB = 1024 * 1024;
|
|
597
|
+
var GROWING_SLOPE = 1 * MB;
|
|
598
|
+
var LEAK_SLOPE = 5 * MB;
|
|
599
|
+
var MIN_NET_GROWTH = 2 * MB;
|
|
600
|
+
function readMemory() {
|
|
601
|
+
const perf = globalThis.performance;
|
|
602
|
+
const mem = perf?.memory;
|
|
603
|
+
if (!mem || typeof mem.usedJSHeapSize !== "number") return null;
|
|
604
|
+
return mem;
|
|
605
|
+
}
|
|
606
|
+
function slope(points) {
|
|
607
|
+
const n = points.length;
|
|
608
|
+
if (n < 2) return 0;
|
|
609
|
+
let sx = 0;
|
|
610
|
+
let sy = 0;
|
|
611
|
+
let sxy = 0;
|
|
612
|
+
let sxx = 0;
|
|
613
|
+
for (const { x, y } of points) {
|
|
614
|
+
sx += x;
|
|
615
|
+
sy += y;
|
|
616
|
+
sxy += x * y;
|
|
617
|
+
sxx += x * x;
|
|
618
|
+
}
|
|
619
|
+
const denom = n * sxx - sx * sx;
|
|
620
|
+
if (denom === 0) return 0;
|
|
621
|
+
return (n * sxy - sx * sy) / denom;
|
|
622
|
+
}
|
|
623
|
+
function analyzeHeapTrend(samples) {
|
|
624
|
+
if (samples.length < MIN_SAMPLES) return null;
|
|
625
|
+
const t0 = samples[0].at;
|
|
626
|
+
const segSize = Math.max(1, Math.ceil(samples.length / FLOOR_SEGMENTS));
|
|
627
|
+
const floorPoints = [];
|
|
628
|
+
for (let i = 0; i < samples.length; i += segSize) {
|
|
629
|
+
const seg = samples.slice(i, i + segSize);
|
|
630
|
+
let min = Infinity;
|
|
631
|
+
for (const s of seg) min = Math.min(min, s.used);
|
|
632
|
+
const mid = seg[Math.floor(seg.length / 2)];
|
|
633
|
+
floorPoints.push({ x: (mid.at - t0) / 6e4, y: min });
|
|
634
|
+
}
|
|
635
|
+
const slopeBytesPerMin = slope(floorPoints);
|
|
636
|
+
const spanMin = (samples[samples.length - 1].at - t0) / 6e4;
|
|
637
|
+
const projectedGrowth = slopeBytesPerMin * spanMin;
|
|
638
|
+
const recent = floorPoints.slice(Math.floor(floorPoints.length / 2));
|
|
639
|
+
const recentSlope = recent.length >= 2 ? slope(recent) : slopeBytesPerMin;
|
|
640
|
+
let classification = "stable";
|
|
641
|
+
const sustained = projectedGrowth >= MIN_NET_GROWTH && recentSlope >= GROWING_SLOPE;
|
|
642
|
+
if (sustained) {
|
|
643
|
+
classification = slopeBytesPerMin >= LEAK_SLOPE && recentSlope >= LEAK_SLOPE ? "leak-suspected" : "growing";
|
|
644
|
+
}
|
|
645
|
+
return { classification, slopeBytesPerMin };
|
|
646
|
+
}
|
|
647
|
+
function createHeapCollector() {
|
|
648
|
+
let samples = [];
|
|
649
|
+
let timer = null;
|
|
650
|
+
function sample() {
|
|
651
|
+
const mem = readMemory();
|
|
652
|
+
if (!mem) return;
|
|
653
|
+
samples.push({ at: performance.now(), used: mem.usedJSHeapSize, total: mem.totalJSHeapSize });
|
|
654
|
+
if (samples.length > MAX_SAMPLES) samples.splice(0, samples.length - MAX_SAMPLES);
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
kind: "heap",
|
|
658
|
+
activate() {
|
|
659
|
+
samples = [];
|
|
660
|
+
if (!readMemory()) return;
|
|
661
|
+
sample();
|
|
662
|
+
timer = setInterval(sample, SAMPLE_INTERVAL_MS);
|
|
663
|
+
},
|
|
664
|
+
deactivate() {
|
|
665
|
+
if (timer == null) return;
|
|
666
|
+
clearInterval(timer);
|
|
667
|
+
timer = null;
|
|
668
|
+
sample();
|
|
669
|
+
},
|
|
670
|
+
finalize(result) {
|
|
671
|
+
if (samples.length === 0) return result;
|
|
672
|
+
return { ...result, heapSamples: samples.slice() };
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/collectors/interaction.ts
|
|
678
|
+
var DURATION_THRESHOLD = 40;
|
|
679
|
+
function selectorFor(target) {
|
|
680
|
+
const el = target;
|
|
681
|
+
if (!el || typeof el.tagName !== "string") return void 0;
|
|
682
|
+
let s = el.tagName.toLowerCase();
|
|
683
|
+
if (el.id) s += `#${el.id}`;
|
|
684
|
+
else if (typeof el.className === "string" && el.className.trim()) {
|
|
685
|
+
s += `.${el.className.trim().split(/\s+/)[0]}`;
|
|
686
|
+
}
|
|
687
|
+
return s;
|
|
688
|
+
}
|
|
689
|
+
function supportsEventTiming() {
|
|
690
|
+
const PO = globalThis.PerformanceObserver;
|
|
691
|
+
const types = PO?.supportedEntryTypes;
|
|
692
|
+
return !types || types.includes("event");
|
|
693
|
+
}
|
|
694
|
+
function createInteractionCollector() {
|
|
695
|
+
let active = false;
|
|
696
|
+
let observer = null;
|
|
697
|
+
let entries = [];
|
|
698
|
+
return {
|
|
699
|
+
kind: "interaction",
|
|
700
|
+
activate() {
|
|
701
|
+
if (typeof PerformanceObserver === "undefined" || !supportsEventTiming()) return;
|
|
702
|
+
active = true;
|
|
703
|
+
entries = [];
|
|
704
|
+
try {
|
|
705
|
+
observer = new PerformanceObserver((list) => {
|
|
706
|
+
if (!active) return;
|
|
707
|
+
for (const e of list.getEntries()) entries.push(e);
|
|
708
|
+
});
|
|
709
|
+
observer.observe({ type: "event", buffered: false, durationThreshold: DURATION_THRESHOLD });
|
|
710
|
+
try {
|
|
711
|
+
observer.observe({ type: "first-input", buffered: false });
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
} catch (err) {
|
|
715
|
+
console.warn("[react-perfscope] interaction collector failed to start:", err);
|
|
716
|
+
observer = null;
|
|
717
|
+
active = false;
|
|
718
|
+
}
|
|
719
|
+
},
|
|
720
|
+
deactivate() {
|
|
721
|
+
active = false;
|
|
722
|
+
if (observer) {
|
|
723
|
+
try {
|
|
724
|
+
observer.disconnect();
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
observer = null;
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
finalize(result) {
|
|
731
|
+
const byId = /* @__PURE__ */ new Map();
|
|
732
|
+
for (const e of entries) {
|
|
733
|
+
const id = e.interactionId;
|
|
734
|
+
if (!id) continue;
|
|
735
|
+
const group = byId.get(id);
|
|
736
|
+
if (group) group.push(e);
|
|
737
|
+
else byId.set(id, [e]);
|
|
738
|
+
}
|
|
739
|
+
const interactions = [];
|
|
740
|
+
for (const group of byId.values()) {
|
|
741
|
+
let duration = 0;
|
|
742
|
+
let startTime = Infinity;
|
|
743
|
+
let processingStart = Infinity;
|
|
744
|
+
let processingEnd = -Infinity;
|
|
745
|
+
let defining = group[0];
|
|
746
|
+
let maxProcessing = -Infinity;
|
|
747
|
+
for (const e of group) {
|
|
748
|
+
if (e.duration > duration) duration = e.duration;
|
|
749
|
+
if (e.startTime < startTime) startTime = e.startTime;
|
|
750
|
+
if (e.processingStart < processingStart) processingStart = e.processingStart;
|
|
751
|
+
if (e.processingEnd > processingEnd) processingEnd = e.processingEnd;
|
|
752
|
+
const proc = e.processingEnd - e.processingStart;
|
|
753
|
+
if (proc > maxProcessing) {
|
|
754
|
+
maxProcessing = proc;
|
|
755
|
+
defining = e;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (duration < DURATION_THRESHOLD) continue;
|
|
759
|
+
const inputDelay = Math.max(0, processingStart - startTime);
|
|
760
|
+
const processing = Math.max(0, processingEnd - processingStart);
|
|
761
|
+
const presentation = Math.max(0, startTime + duration - processingEnd);
|
|
762
|
+
interactions.push({
|
|
763
|
+
kind: "interaction",
|
|
764
|
+
at: startTime,
|
|
765
|
+
eventType: defining.name,
|
|
766
|
+
...selectorFor(defining.target) ? { target: selectorFor(defining.target) } : {},
|
|
767
|
+
duration,
|
|
768
|
+
inputDelay,
|
|
769
|
+
processing,
|
|
770
|
+
presentation
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
if (interactions.length === 0) return result;
|
|
774
|
+
interactions.sort((a, b) => a.at - b.at);
|
|
775
|
+
return { ...result, signals: [...result.signals, ...interactions] };
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/collectors/frames.ts
|
|
781
|
+
var FRAME_BUDGET = 1e3 / 60;
|
|
782
|
+
var BUCKET_MS = 500;
|
|
783
|
+
var MIN_BUCKET_SPAN = 100;
|
|
784
|
+
var MIN_FRAMES = 8;
|
|
785
|
+
var MAX_FRAMES = 2e4;
|
|
786
|
+
function analyzeFrames(frames) {
|
|
787
|
+
if (frames.length < MIN_FRAMES) return null;
|
|
788
|
+
let longestFrameMs = 0;
|
|
789
|
+
let droppedFrames = 0;
|
|
790
|
+
for (let i = 1; i < frames.length; i++) {
|
|
791
|
+
const gap = frames[i] - frames[i - 1];
|
|
792
|
+
if (gap > longestFrameMs) longestFrameMs = gap;
|
|
793
|
+
const dropped = Math.round(gap / FRAME_BUDGET) - 1;
|
|
794
|
+
if (dropped > 0) droppedFrames += dropped;
|
|
795
|
+
}
|
|
796
|
+
const t0 = frames[0];
|
|
797
|
+
const end = frames[frames.length - 1];
|
|
798
|
+
const series = [];
|
|
799
|
+
let minFps = Infinity;
|
|
800
|
+
let idx = 0;
|
|
801
|
+
for (let bStart = t0; bStart < end; bStart += BUCKET_MS) {
|
|
802
|
+
const bEnd = Math.min(bStart + BUCKET_MS, end);
|
|
803
|
+
const span = bEnd - bStart;
|
|
804
|
+
let count = 0;
|
|
805
|
+
while (idx < frames.length && frames[idx] < bEnd) {
|
|
806
|
+
count++;
|
|
807
|
+
idx++;
|
|
808
|
+
}
|
|
809
|
+
if (span < MIN_BUCKET_SPAN) break;
|
|
810
|
+
const fps = count / (span / 1e3);
|
|
811
|
+
series.push({ at: bStart + span / 2, fps });
|
|
812
|
+
if (fps < minFps) minFps = fps;
|
|
813
|
+
}
|
|
814
|
+
if (series.length === 0) return null;
|
|
815
|
+
return { series, minFps, longestFrameMs, droppedFrames };
|
|
816
|
+
}
|
|
817
|
+
function createFrameCollector() {
|
|
818
|
+
let active = false;
|
|
819
|
+
let rafId = null;
|
|
820
|
+
let frames = [];
|
|
821
|
+
function loop(t) {
|
|
822
|
+
if (!active) return;
|
|
823
|
+
frames.push(t);
|
|
824
|
+
if (frames.length > MAX_FRAMES) frames.splice(0, frames.length - MAX_FRAMES);
|
|
825
|
+
rafId = requestAnimationFrame(loop);
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
kind: "frame",
|
|
829
|
+
activate() {
|
|
830
|
+
if (typeof requestAnimationFrame === "undefined") return;
|
|
831
|
+
active = true;
|
|
832
|
+
frames = [];
|
|
833
|
+
rafId = requestAnimationFrame(loop);
|
|
834
|
+
},
|
|
835
|
+
deactivate() {
|
|
836
|
+
active = false;
|
|
837
|
+
if (rafId != null && typeof cancelAnimationFrame !== "undefined") {
|
|
838
|
+
try {
|
|
839
|
+
cancelAnimationFrame(rafId);
|
|
840
|
+
} catch {
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
rafId = null;
|
|
844
|
+
},
|
|
845
|
+
finalize(result) {
|
|
846
|
+
if (frames.length < MIN_FRAMES) return result;
|
|
847
|
+
return { ...result, frames: frames.slice() };
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/collectors/self-profiling.ts
|
|
853
|
+
var SAMPLE_INTERVAL_MS2 = 10;
|
|
854
|
+
var MAX_BUFFER_SIZE = 1e5;
|
|
855
|
+
var MAX_FRAMES_PER_TASK = 5;
|
|
856
|
+
function isUserResource(url) {
|
|
857
|
+
if (!url) return false;
|
|
858
|
+
if (url.includes("/node_modules/")) return false;
|
|
859
|
+
try {
|
|
860
|
+
const path = new URL(url).pathname;
|
|
861
|
+
if (path.startsWith("/@")) return false;
|
|
862
|
+
} catch {
|
|
863
|
+
if (url.startsWith("/@")) return false;
|
|
864
|
+
}
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
function frameToStackFrame(trace, frame) {
|
|
868
|
+
const file = frame.resourceId != null ? trace.resources[frame.resourceId] ?? "" : "";
|
|
869
|
+
const sf = {
|
|
870
|
+
file,
|
|
871
|
+
line: frame.line ?? 0,
|
|
872
|
+
col: frame.column ?? 0
|
|
873
|
+
};
|
|
874
|
+
if (frame.name) sf.fnName = frame.name;
|
|
875
|
+
return sf;
|
|
876
|
+
}
|
|
877
|
+
function leafUserFrame(trace, stackId) {
|
|
878
|
+
let id = stackId;
|
|
879
|
+
let guard = 0;
|
|
880
|
+
while (id != null && guard++ < 1e3) {
|
|
881
|
+
const stack = trace.stacks[id];
|
|
882
|
+
if (!stack) break;
|
|
883
|
+
const frame = trace.frames[stack.frameId];
|
|
884
|
+
if (frame && isUserResource(trace.resources[frame.resourceId ?? -1])) {
|
|
885
|
+
return frame;
|
|
886
|
+
}
|
|
887
|
+
id = stack.parentId;
|
|
888
|
+
}
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
function attributeWindow(trace, start, end) {
|
|
892
|
+
let total = 0;
|
|
893
|
+
const counts = /* @__PURE__ */ new Map();
|
|
894
|
+
for (const sample of trace.samples) {
|
|
895
|
+
if (sample.timestamp < start || sample.timestamp > end) continue;
|
|
896
|
+
total++;
|
|
897
|
+
const frame = leafUserFrame(trace, sample.stackId);
|
|
898
|
+
if (!frame) continue;
|
|
899
|
+
const sf = frameToStackFrame(trace, frame);
|
|
900
|
+
const key = `${sf.file}:${sf.line}:${sf.col}:${sf.fnName ?? ""}`;
|
|
901
|
+
const entry = counts.get(key);
|
|
902
|
+
if (entry) entry.count++;
|
|
903
|
+
else counts.set(key, { frame: sf, count: 1 });
|
|
904
|
+
}
|
|
905
|
+
if (total === 0) return [];
|
|
906
|
+
return [...counts.values()].sort((a, b) => b.count - a.count).slice(0, MAX_FRAMES_PER_TASK).map(({ frame, count }) => ({ frame, selfRatio: count / total, sampleCount: count }));
|
|
907
|
+
}
|
|
908
|
+
function attributeLongTaskSignals(trace, signals) {
|
|
909
|
+
return signals.map((signal) => {
|
|
910
|
+
if (signal.kind !== "long-task") return signal;
|
|
911
|
+
const attribution = attributeWindow(trace, signal.at, signal.at + signal.duration);
|
|
912
|
+
if (attribution.length === 0) return signal;
|
|
913
|
+
return { ...signal, attribution };
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
function attributeInteractionSignals(trace, signals) {
|
|
917
|
+
return signals.map((signal) => {
|
|
918
|
+
if (signal.kind !== "interaction") return signal;
|
|
919
|
+
const start = signal.at + signal.inputDelay;
|
|
920
|
+
const attribution = attributeWindow(trace, start, start + signal.processing);
|
|
921
|
+
if (attribution.length === 0) return signal;
|
|
922
|
+
return { ...signal, attribution };
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
function createSelfProfilingCollector() {
|
|
926
|
+
let profiler = null;
|
|
927
|
+
let tracePromise = null;
|
|
928
|
+
return {
|
|
929
|
+
kind: "long-task",
|
|
930
|
+
activate() {
|
|
931
|
+
tracePromise = null;
|
|
932
|
+
const Ctor = globalThis.Profiler;
|
|
933
|
+
if (typeof Ctor !== "function") return;
|
|
934
|
+
try {
|
|
935
|
+
profiler = new Ctor({ sampleInterval: SAMPLE_INTERVAL_MS2, maxBufferSize: MAX_BUFFER_SIZE });
|
|
936
|
+
} catch (err) {
|
|
937
|
+
console.warn(
|
|
938
|
+
"[react-perfscope] self-profiling unavailable (needs Document-Policy: js-profiling header); long tasks keep LoAF-only attribution:",
|
|
939
|
+
err
|
|
940
|
+
);
|
|
941
|
+
profiler = null;
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
deactivate() {
|
|
945
|
+
if (!profiler) return;
|
|
946
|
+
try {
|
|
947
|
+
tracePromise = profiler.stop();
|
|
948
|
+
} catch (err) {
|
|
949
|
+
console.warn("[react-perfscope] self-profiling stop failed:", err);
|
|
950
|
+
tracePromise = null;
|
|
951
|
+
}
|
|
952
|
+
profiler = null;
|
|
953
|
+
},
|
|
954
|
+
async finalize(result) {
|
|
955
|
+
if (!tracePromise) return result;
|
|
956
|
+
try {
|
|
957
|
+
const trace = await tracePromise;
|
|
958
|
+
const signals = attributeInteractionSignals(trace, attributeLongTaskSignals(trace, result.signals));
|
|
959
|
+
return { ...result, signals };
|
|
960
|
+
} catch (err) {
|
|
961
|
+
console.warn("[react-perfscope] self-profiling finalize failed:", err);
|
|
962
|
+
return result;
|
|
963
|
+
} finally {
|
|
964
|
+
tracePromise = null;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
export {
|
|
970
|
+
analyzeFrames,
|
|
971
|
+
analyzeHeapTrend,
|
|
972
|
+
attachLazyStack,
|
|
973
|
+
attributeLongTaskSignals,
|
|
974
|
+
attributeWindow,
|
|
975
|
+
createForcedReflowCollector,
|
|
976
|
+
createFrameCollector,
|
|
977
|
+
createHeapCollector,
|
|
978
|
+
createInteractionCollector,
|
|
979
|
+
createLayoutShiftCollector,
|
|
980
|
+
createLongTasksCollector,
|
|
981
|
+
createNetworkCollector,
|
|
982
|
+
createRecorder,
|
|
983
|
+
createSelfProfilingCollector,
|
|
984
|
+
createSourceMapResolver,
|
|
985
|
+
createWebVitalsCollector,
|
|
986
|
+
isUserResource,
|
|
987
|
+
parseStack,
|
|
988
|
+
resolveFrame
|
|
989
|
+
};
|
|
990
|
+
//# sourceMappingURL=index.js.map
|