react-perf-recorder 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 +65 -0
- package/claude/README.md +10 -0
- package/claude/agents/perf-recorder.md +44 -0
- package/claude/mcp.json +8 -0
- package/claude/skills/react-perf-recorder/SKILL.md +52 -0
- package/claude/skills/react-perf-recorder/references/causes-and-actions.md +47 -0
- package/claude/skills/react-perf-recorder/references/from-scripts.md +36 -0
- package/claude/skills/react-perf-recorder/references/getting-a-recording.md +41 -0
- package/claude/skills/react-perf-recorder/references/measuring-a-fix.md +42 -0
- package/claude/skills/react-perf-recorder/references/panel.md +21 -0
- package/claude/skills/react-perf-recorder/references/reading-a-recording.md +55 -0
- package/dist/browser/chunk-7DJUCCWG.js +447 -0
- package/dist/browser/chunk-NTY2W4HE.js +182 -0
- package/dist/browser/client.d.ts +589 -0
- package/dist/browser/client.js +7161 -0
- package/dist/browser/index-BlkKhwHe.d.ts +585 -0
- package/dist/browser/plugins/proxy-memoize.d.ts +7 -0
- package/dist/browser/plugins/proxy-memoize.js +41 -0
- package/dist/browser/plugins/react-query.d.ts +5 -0
- package/dist/browser/plugins/react-query.js +74 -0
- package/dist/browser/plugins/zustand.d.ts +11 -0
- package/dist/browser/plugins/zustand.js +171 -0
- package/dist/browser/runtime.d.ts +1 -0
- package/dist/browser/runtime.js +12 -0
- package/dist/cli.js +23119 -0
- package/dist/engine.iife.js +3661 -0
- package/dist/node/chunk-HS2BJBJX.js +170 -0
- package/dist/node/plugin-api-zXFxjYba.d.cts +61 -0
- package/dist/node/plugin-api-zXFxjYba.d.ts +61 -0
- package/dist/node/plugins/proxy-memoize.cjs +214 -0
- package/dist/node/plugins/proxy-memoize.d.cts +16 -0
- package/dist/node/plugins/proxy-memoize.d.ts +16 -0
- package/dist/node/plugins/proxy-memoize.js +51 -0
- package/dist/node/plugins/react-query.cjs +32 -0
- package/dist/node/plugins/react-query.d.cts +6 -0
- package/dist/node/plugins/react-query.d.ts +6 -0
- package/dist/node/plugins/react-query.js +7 -0
- package/dist/node/plugins/zustand.cjs +247 -0
- package/dist/node/plugins/zustand.d.cts +17 -0
- package/dist/node/plugins/zustand.d.ts +17 -0
- package/dist/node/plugins/zustand.js +61 -0
- package/dist/node/vite.cjs +1262 -0
- package/dist/node/vite.d.cts +103 -0
- package/dist/node/vite.d.ts +103 -0
- package/dist/node/vite.js +1072 -0
- package/docs/contributing.md +24 -0
- package/docs/how-it-works.md +34 -0
- package/docs/mcp.md +62 -0
- package/docs/measuring-a-fix.md +65 -0
- package/docs/options.md +22 -0
- package/docs/panel.md +59 -0
- package/docs/plugins.md +57 -0
- package/docs/recording.md +44 -0
- package/package.json +139 -0
|
@@ -0,0 +1,3661 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// src/shared/url.ts
|
|
4
|
+
var JWT = /^eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}$/;
|
|
5
|
+
var OPAQUE = /^[A-Za-z0-9_-]{60,}$/;
|
|
6
|
+
var SECRET_NAME = /^(token|access[-_]?token|id[-_]?token|refresh[-_]?token|jwt|auth|authorization|key|api[-_]?key|secret|password|pwd|sig|signature|session|sid)$/i;
|
|
7
|
+
var MASK = "***";
|
|
8
|
+
var ABSOLUTE = /^[a-z][a-z0-9+.-]*:/i;
|
|
9
|
+
var maskValue = (value) => JWT.test(value) || OPAQUE.test(value) ? MASK : value;
|
|
10
|
+
var maskQuery = (search) => {
|
|
11
|
+
const params = new URLSearchParams(search);
|
|
12
|
+
let touched = false;
|
|
13
|
+
for (const [name, value] of [...params]) {
|
|
14
|
+
const masked = SECRET_NAME.test(name) ? MASK : maskValue(value);
|
|
15
|
+
if (masked === value) continue;
|
|
16
|
+
params.set(name, masked);
|
|
17
|
+
touched = true;
|
|
18
|
+
}
|
|
19
|
+
return touched ? params.toString() : search.replace(/^\?/, "");
|
|
20
|
+
};
|
|
21
|
+
var maskHash = (hash) => {
|
|
22
|
+
const body = hash.replace(/^#/, "");
|
|
23
|
+
if (!body) return "";
|
|
24
|
+
return body.includes("=") ? maskQuery(body) : body.split("/").map(maskValue).join("/");
|
|
25
|
+
};
|
|
26
|
+
function safeUrl(raw) {
|
|
27
|
+
if (!raw) return raw;
|
|
28
|
+
const relative = /^[/?#]/.test(raw);
|
|
29
|
+
if (!relative && !ABSOLUTE.test(raw)) return maskLoose(raw);
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(raw, relative ? "http://localhost" : void 0);
|
|
32
|
+
const path = url.pathname.split("/").map(maskValue).join("/");
|
|
33
|
+
const query = maskQuery(url.search);
|
|
34
|
+
const hash = maskHash(url.hash);
|
|
35
|
+
const tail = `${path}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`;
|
|
36
|
+
return relative ? tail : `${url.protocol}//${url.host}${tail}`;
|
|
37
|
+
} catch {
|
|
38
|
+
return maskLoose(raw);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
var maskLoose = (raw) => raw.replace(/eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}/g, MASK).replace(new RegExp(`(${SECRET_NAME.source.slice(1, -1)})=[^&\\s]+`, "gi"), `$1=${MASK}`);
|
|
42
|
+
|
|
43
|
+
// src/core/stack.ts
|
|
44
|
+
var V8_FRAME = /^\s*at (?:(?:async )?(.+?) \()?(.+?):(\d+):(\d+)\)?\s*$/;
|
|
45
|
+
var GECKO_FRAME = /^\s*(.*?)@(.+?):(\d+):(\d+)\s*$/;
|
|
46
|
+
var servedPath = (url) => url.replace(/^[a-z]+:\/\/[^/]+/, "").split(/[?#]/)[0].replace(/^\//, "");
|
|
47
|
+
function parseStack(stack) {
|
|
48
|
+
const frames = [];
|
|
49
|
+
for (const line of stack.split("\n")) {
|
|
50
|
+
const m = V8_FRAME.exec(line) ?? GECKO_FRAME.exec(line);
|
|
51
|
+
if (!m) continue;
|
|
52
|
+
frames.push({ fn: (m[1] ?? "").replace(/^Object\./, "").replace(/ \[as .+\]$/, ""), url: m[2], line: Number(m[3]), column: Number(m[4]) });
|
|
53
|
+
}
|
|
54
|
+
return frames;
|
|
55
|
+
}
|
|
56
|
+
var OWN = (() => {
|
|
57
|
+
const url = (parseStack(new Error().stack ?? "")[0]?.url ?? "").split(/[?#]/)[0];
|
|
58
|
+
const at = Math.max(url.lastIndexOf("/dist/"), url.lastIndexOf("/src/"));
|
|
59
|
+
return at >= 0 ? [`${url.slice(0, at)}/dist/`, `${url.slice(0, at)}/src/`] : [];
|
|
60
|
+
})();
|
|
61
|
+
var packageOf = (path) => {
|
|
62
|
+
const parts = path.split("/").filter(Boolean);
|
|
63
|
+
return parts[0]?.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0] ?? "";
|
|
64
|
+
};
|
|
65
|
+
function libraryOf(url) {
|
|
66
|
+
const path = url.split(/[?#]/)[0];
|
|
67
|
+
if (OWN.some((prefix) => path.startsWith(prefix))) return "react-perf-recorder";
|
|
68
|
+
const deps = /\/deps\/([^/]+)\.js$/.exec(path);
|
|
69
|
+
if (deps && (url.includes("?v=") || path.includes("/.vite"))) return deps[1].startsWith("chunk-") ? "" : packageOf(deps[1].replace(/_/g, "/"));
|
|
70
|
+
const at = path.lastIndexOf("/node_modules/");
|
|
71
|
+
if (at >= 0) return packageOf(path.slice(at + "/node_modules/".length));
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/core/react-compat.ts
|
|
76
|
+
var BOTTOM_FRAME = /react_stack_bottom_frame/;
|
|
77
|
+
var ownerStackSites = /* @__PURE__ */ new WeakMap();
|
|
78
|
+
var sawSite = false;
|
|
79
|
+
var sawFiberWithoutSite = false;
|
|
80
|
+
function siteOf(f) {
|
|
81
|
+
const legacy = f._debugSource;
|
|
82
|
+
if (legacy?.fileName) {
|
|
83
|
+
sawSite = true;
|
|
84
|
+
return { url: legacy.fileName, line: legacy.lineNumber, column: legacy.columnNumber ?? 0, exact: true };
|
|
85
|
+
}
|
|
86
|
+
const stack = f._debugStack;
|
|
87
|
+
if (stack instanceof Error) {
|
|
88
|
+
const site = ownerStackSite(stack);
|
|
89
|
+
if (site) sawSite = true;
|
|
90
|
+
else sawFiberWithoutSite = true;
|
|
91
|
+
return site;
|
|
92
|
+
}
|
|
93
|
+
sawFiberWithoutSite = true;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function ownerStackSite(error) {
|
|
97
|
+
const known2 = ownerStackSites.get(error);
|
|
98
|
+
if (known2 !== void 0) return known2;
|
|
99
|
+
const holder = Error;
|
|
100
|
+
const previous = holder.prepareStackTrace;
|
|
101
|
+
let text = "";
|
|
102
|
+
try {
|
|
103
|
+
holder.prepareStackTrace = void 0;
|
|
104
|
+
text = error.stack ?? "";
|
|
105
|
+
} catch {
|
|
106
|
+
text = "";
|
|
107
|
+
} finally {
|
|
108
|
+
holder.prepareStackTrace = previous;
|
|
109
|
+
}
|
|
110
|
+
const frames = parseStack(text);
|
|
111
|
+
const frame = frames[1];
|
|
112
|
+
const site = frame && !BOTTOM_FRAME.test(frame.fn) ? { url: frame.url, line: frame.line, column: frame.column, exact: false } : null;
|
|
113
|
+
ownerStackSites.set(error, site);
|
|
114
|
+
return site;
|
|
115
|
+
}
|
|
116
|
+
function sourcesUnavailable() {
|
|
117
|
+
return sawFiberWithoutSite && !sawSite;
|
|
118
|
+
}
|
|
119
|
+
var ContextProviderTag = 10;
|
|
120
|
+
var ContextConsumerTag = 9;
|
|
121
|
+
var isProviderTag = (tag) => tag === ContextProviderTag;
|
|
122
|
+
var isConsumerTag = (tag) => tag === ContextConsumerTag;
|
|
123
|
+
function contextOf(f) {
|
|
124
|
+
const type = f.type;
|
|
125
|
+
return type?._context ?? type ?? null;
|
|
126
|
+
}
|
|
127
|
+
var HOOKS_WITHOUT_CELLS = /* @__PURE__ */ new Set(["useContext", "useDebugValue", "use", "useMemoCache", "useHostTransitionStatus", "useFormStatus"]);
|
|
128
|
+
var HOOK_CELLS = {
|
|
129
|
+
useSyncExternalStore: 2,
|
|
130
|
+
useTransition: 2,
|
|
131
|
+
useActionState: 3,
|
|
132
|
+
useFormState: 3
|
|
133
|
+
};
|
|
134
|
+
var hookCells = (type) => HOOKS_WITHOUT_CELLS.has(type) ? 0 : HOOK_CELLS[type] ?? 1;
|
|
135
|
+
var LANES_18 = [
|
|
136
|
+
[1, "Sync"],
|
|
137
|
+
[2, "InputContinuousHydration"],
|
|
138
|
+
[4, "InputContinuous"],
|
|
139
|
+
[8, "DefaultHydration"],
|
|
140
|
+
[16, "Default"],
|
|
141
|
+
[32, "TransitionHydration"],
|
|
142
|
+
[4194240, "Transition"],
|
|
143
|
+
[31 << 22, "Retry"],
|
|
144
|
+
[1 << 27, "SelectiveHydration"],
|
|
145
|
+
[1 << 28, "IdleHydration"],
|
|
146
|
+
[1 << 29, "Idle"],
|
|
147
|
+
[1 << 30, "Offscreen"]
|
|
148
|
+
];
|
|
149
|
+
var LANES_19 = [
|
|
150
|
+
[1, "SyncHydration"],
|
|
151
|
+
[2, "Sync"],
|
|
152
|
+
[4, "InputContinuousHydration"],
|
|
153
|
+
[8, "InputContinuous"],
|
|
154
|
+
[16, "DefaultHydration"],
|
|
155
|
+
[32, "Default"],
|
|
156
|
+
[64, "TransitionHydration"],
|
|
157
|
+
[261888, "Transition"],
|
|
158
|
+
[3932160, "TransitionDeferred"],
|
|
159
|
+
[62914560, "Retry"],
|
|
160
|
+
[1 << 26, "SelectiveHydration"],
|
|
161
|
+
[1 << 27, "IdleHydration"],
|
|
162
|
+
[1 << 28, "Idle"],
|
|
163
|
+
[1 << 29, "Offscreen"],
|
|
164
|
+
[1 << 30, "Deferred"]
|
|
165
|
+
];
|
|
166
|
+
function laneLabel(lanes) {
|
|
167
|
+
if (!lanes) return void 0;
|
|
168
|
+
const lowest = lanes & -lanes;
|
|
169
|
+
for (const [mask, label] of majorVersion() >= 19 ? LANES_19 : LANES_18) if (lowest & mask) return label;
|
|
170
|
+
return `lane:${lowest}`;
|
|
171
|
+
}
|
|
172
|
+
var captured = /* @__PURE__ */ new Set();
|
|
173
|
+
function captureRenderers() {
|
|
174
|
+
const target2 = globalThis;
|
|
175
|
+
let hook = target2.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
176
|
+
if (!hook) {
|
|
177
|
+
const renderers = /* @__PURE__ */ new Map();
|
|
178
|
+
const noop = () => {
|
|
179
|
+
};
|
|
180
|
+
hook = {
|
|
181
|
+
renderers,
|
|
182
|
+
supportsFiber: true,
|
|
183
|
+
isDisabled: false,
|
|
184
|
+
inject(renderer2) {
|
|
185
|
+
const id = renderers.size + 1;
|
|
186
|
+
renderers.set(id, renderer2);
|
|
187
|
+
return id;
|
|
188
|
+
},
|
|
189
|
+
onCommitFiberRoot: noop,
|
|
190
|
+
onCommitFiberUnmount: noop,
|
|
191
|
+
onPostCommitFiberRoot: noop,
|
|
192
|
+
onScheduleFiberRoot: noop,
|
|
193
|
+
checkDCE: noop,
|
|
194
|
+
on: noop,
|
|
195
|
+
off: noop,
|
|
196
|
+
emit: noop,
|
|
197
|
+
sub: () => noop
|
|
198
|
+
};
|
|
199
|
+
target2.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
|
|
200
|
+
}
|
|
201
|
+
for (const renderer2 of hook.renderers?.values() ?? []) captured.add(renderer2);
|
|
202
|
+
const original = hook.inject;
|
|
203
|
+
if (typeof original === "function" && !original.rprCaptured) {
|
|
204
|
+
const inject = function(renderer2) {
|
|
205
|
+
captured.add(renderer2);
|
|
206
|
+
return original.call(this, renderer2);
|
|
207
|
+
};
|
|
208
|
+
inject.rprCaptured = true;
|
|
209
|
+
hook.inject = inject;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function knownRenderers() {
|
|
213
|
+
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
214
|
+
return [...captured, ...hook?.renderers?.values() ?? []];
|
|
215
|
+
}
|
|
216
|
+
function reactVersion() {
|
|
217
|
+
return knownRenderers().find((r) => r.version)?.version ?? null;
|
|
218
|
+
}
|
|
219
|
+
function renderer() {
|
|
220
|
+
return knownRenderers().find((r) => r.currentDispatcherRef) ?? null;
|
|
221
|
+
}
|
|
222
|
+
function majorVersion() {
|
|
223
|
+
return Number.parseInt(reactVersion() ?? "", 10) || 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/core/commit-hook.ts
|
|
227
|
+
var RecorderError = class extends Error {
|
|
228
|
+
constructor(code, message, owner) {
|
|
229
|
+
super(message);
|
|
230
|
+
this.code = code;
|
|
231
|
+
this.owner = owner;
|
|
232
|
+
this.name = "RecorderError";
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
function hookOwner(root) {
|
|
236
|
+
const setter = Object.getOwnPropertyDescriptor(root, "current")?.set;
|
|
237
|
+
if (!setter) return null;
|
|
238
|
+
return setter.owner ?? "unknown script";
|
|
239
|
+
}
|
|
240
|
+
function hookCommits(roots, owner, onCommit, onUpdate) {
|
|
241
|
+
for (const root of roots) {
|
|
242
|
+
const busy = hookOwner(root);
|
|
243
|
+
if (busy) throw new RecorderError("BUSY", `root.current is already hooked by ${busy}: wait for it to finish or call its stop()`, busy);
|
|
244
|
+
}
|
|
245
|
+
const errors = [];
|
|
246
|
+
const overhead = { commitMs: 0, maxCommitMs: 0 };
|
|
247
|
+
const restores = roots.map((root) => {
|
|
248
|
+
let current = root.current;
|
|
249
|
+
let lanes = 0;
|
|
250
|
+
let passthrough = false;
|
|
251
|
+
const set = (fiber) => {
|
|
252
|
+
current = fiber;
|
|
253
|
+
const taken = lanes;
|
|
254
|
+
lanes = 0;
|
|
255
|
+
if (passthrough) return;
|
|
256
|
+
const started = performance.now();
|
|
257
|
+
try {
|
|
258
|
+
onCommit({ root, fiber, lanes: taken });
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (errors.length < 3) errors.push(String(error?.stack || error).slice(0, 300));
|
|
261
|
+
}
|
|
262
|
+
const ms = performance.now() - started;
|
|
263
|
+
overhead.commitMs += ms;
|
|
264
|
+
overhead.maxCommitMs = Math.max(overhead.maxCommitMs, ms);
|
|
265
|
+
};
|
|
266
|
+
set.owner = `react-perf-recorder:${owner}`;
|
|
267
|
+
Object.defineProperty(root, "current", { configurable: true, enumerable: true, get: () => current, set });
|
|
268
|
+
const pendingDescriptor = Object.getOwnPropertyDescriptor(root, "pendingLanes");
|
|
269
|
+
let pendingLanes = root.pendingLanes ?? 0;
|
|
270
|
+
const hasPending = pendingDescriptor && "value" in pendingDescriptor && pendingDescriptor.configurable;
|
|
271
|
+
if (hasPending) {
|
|
272
|
+
Object.defineProperty(root, "pendingLanes", {
|
|
273
|
+
configurable: true,
|
|
274
|
+
enumerable: true,
|
|
275
|
+
get: () => pendingLanes,
|
|
276
|
+
set: (value) => {
|
|
277
|
+
const added = value & ~pendingLanes;
|
|
278
|
+
lanes |= pendingLanes & ~value;
|
|
279
|
+
pendingLanes = value;
|
|
280
|
+
if (!added || passthrough || !onUpdate) return;
|
|
281
|
+
try {
|
|
282
|
+
onUpdate();
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (errors.length < 3) errors.push(String(error?.stack || error).slice(0, 300));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return () => {
|
|
290
|
+
if (Object.getOwnPropertyDescriptor(root, "current")?.set === set) {
|
|
291
|
+
Object.defineProperty(root, "current", { configurable: true, enumerable: true, writable: true, value: current });
|
|
292
|
+
} else {
|
|
293
|
+
passthrough = true;
|
|
294
|
+
}
|
|
295
|
+
if (hasPending) Object.defineProperty(root, "pendingLanes", { configurable: true, enumerable: true, writable: true, value: pendingLanes });
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
let active = true;
|
|
299
|
+
return {
|
|
300
|
+
overhead,
|
|
301
|
+
stop() {
|
|
302
|
+
if (active) restores.forEach((restore) => restore());
|
|
303
|
+
active = false;
|
|
304
|
+
return errors;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/core/sites.ts
|
|
310
|
+
var positionKey = (p) => `${p.url}:${p.line}:${p.column}`;
|
|
311
|
+
var known = /* @__PURE__ */ new Map();
|
|
312
|
+
var asked = /* @__PURE__ */ new Set();
|
|
313
|
+
var queue = /* @__PURE__ */ new Map();
|
|
314
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
315
|
+
var mapper = null;
|
|
316
|
+
var inFlight = null;
|
|
317
|
+
var scheduled = null;
|
|
318
|
+
function mappedSite(position) {
|
|
319
|
+
const key = positionKey(position);
|
|
320
|
+
const site = known.get(key);
|
|
321
|
+
if (site) return site;
|
|
322
|
+
if (site === "" || !mapper) return void 0;
|
|
323
|
+
if (!asked.has(key)) {
|
|
324
|
+
asked.add(key);
|
|
325
|
+
queue.set(key, position);
|
|
326
|
+
schedule();
|
|
327
|
+
}
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
function schedule() {
|
|
331
|
+
if (scheduled || inFlight) return;
|
|
332
|
+
scheduled = setTimeout(() => {
|
|
333
|
+
scheduled = null;
|
|
334
|
+
void flush();
|
|
335
|
+
}, 0);
|
|
336
|
+
}
|
|
337
|
+
function flush() {
|
|
338
|
+
if (!mapper || !queue.size) return Promise.resolve();
|
|
339
|
+
const positions = [...queue.values()];
|
|
340
|
+
queue.clear();
|
|
341
|
+
const request = mapper(positions).then((sites) => {
|
|
342
|
+
for (const [key, site] of Object.entries(sites)) known.set(key, site);
|
|
343
|
+
for (const position of positions) if (!known.has(positionKey(position))) known.set(positionKey(position), "");
|
|
344
|
+
if (Object.keys(sites).length) for (const listener of listeners) listener();
|
|
345
|
+
}).catch(() => {
|
|
346
|
+
for (const position of positions) asked.delete(positionKey(position));
|
|
347
|
+
}).finally(() => {
|
|
348
|
+
inFlight = null;
|
|
349
|
+
if (queue.size) schedule();
|
|
350
|
+
});
|
|
351
|
+
inFlight = request;
|
|
352
|
+
return request;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/core/fiber.ts
|
|
356
|
+
var Tag = {
|
|
357
|
+
FunctionComponent: 0,
|
|
358
|
+
ClassComponent: 1,
|
|
359
|
+
IndeterminateComponent: 2,
|
|
360
|
+
HostRoot: 3,
|
|
361
|
+
HostPortal: 4,
|
|
362
|
+
HostComponent: 5,
|
|
363
|
+
HostText: 6,
|
|
364
|
+
ForwardRef: 11,
|
|
365
|
+
MemoComponent: 14,
|
|
366
|
+
SimpleMemoComponent: 15,
|
|
367
|
+
HostHoistable: 26,
|
|
368
|
+
HostSingleton: 27
|
|
369
|
+
};
|
|
370
|
+
var ProfileMode = 2;
|
|
371
|
+
var isHost = (f) => f.tag === Tag.HostComponent || f.tag === Tag.HostHoistable || f.tag === Tag.HostSingleton;
|
|
372
|
+
var isComposite = (f) => typeof f.type === "function" || Boolean(f.type?.render || f.type?.type);
|
|
373
|
+
var mountedInPlace = (f) => f.alternate === null && isComposite(f) && f.return !== null && f.return.alternate !== null && f.return.tag !== Tag.HostRoot;
|
|
374
|
+
var hasHooks = (f) => f.tag === Tag.FunctionComponent || f.tag === Tag.ForwardRef || f.tag === Tag.SimpleMemoComponent || f.tag === Tag.IndeterminateComponent;
|
|
375
|
+
var hasProfileTimings = (f) => (f.mode & ProfileMode) !== 0 && typeof f.actualDuration === "number";
|
|
376
|
+
function nameOf(f) {
|
|
377
|
+
if (isProviderTag(f.tag)) return `Provider(${contextOf(f)?.displayName || "context"})`;
|
|
378
|
+
if (isConsumerTag(f.tag)) return `Consumer(${contextOf(f)?.displayName || "context"})`;
|
|
379
|
+
const t = f.type;
|
|
380
|
+
if (t == null || typeof t === "string") return null;
|
|
381
|
+
if (typeof t === "function") return t.displayName || f.elementType?.displayName || t.name || "Anonymous";
|
|
382
|
+
if (typeof t === "object") {
|
|
383
|
+
if (t.displayName) return t.displayName;
|
|
384
|
+
if (t.render) return t.render.displayName || t.render.name || "ForwardRef";
|
|
385
|
+
if (t.type) return t.type.displayName || t.type.name || "Memo";
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
var isProvider = (name) => name.startsWith("Provider(");
|
|
390
|
+
function wrapsProvider(f) {
|
|
391
|
+
const child = currentOf(f).child;
|
|
392
|
+
return Boolean(child && !child.sibling && isProvider(nameOf(child) ?? ""));
|
|
393
|
+
}
|
|
394
|
+
var libraryByType = /* @__PURE__ */ new WeakMap();
|
|
395
|
+
function isLibraryFiber(f) {
|
|
396
|
+
const type = typeof f.type === "function" || f.type && typeof f.type === "object" ? f.type : null;
|
|
397
|
+
const known2 = type ? libraryByType.get(type) : void 0;
|
|
398
|
+
if (known2 !== void 0) return known2;
|
|
399
|
+
const library = definedInPackage(f);
|
|
400
|
+
if (type) libraryByType.set(type, library);
|
|
401
|
+
return library;
|
|
402
|
+
}
|
|
403
|
+
function definedInPackage(f) {
|
|
404
|
+
const site = siteOf(f.child ?? f);
|
|
405
|
+
return !site || libraryOf(site.url) !== null;
|
|
406
|
+
}
|
|
407
|
+
function sourceOf(f, root = "") {
|
|
408
|
+
const site = siteOf(f);
|
|
409
|
+
if (!site) return "";
|
|
410
|
+
const file = relativeFile(site.url, root);
|
|
411
|
+
if (site.exact) return `${file}:${site.line}`;
|
|
412
|
+
return mappedSite(site) || file;
|
|
413
|
+
}
|
|
414
|
+
function generatedSourceOf(f) {
|
|
415
|
+
const site = siteOf(f);
|
|
416
|
+
return site && !site.exact ? { url: site.url, line: site.line, column: site.column } : void 0;
|
|
417
|
+
}
|
|
418
|
+
function siteKeyOf(f, root = "") {
|
|
419
|
+
const site = siteOf(f);
|
|
420
|
+
return site ? `${relativeFile(site.url, root)}:${site.line}:${site.column}` : "";
|
|
421
|
+
}
|
|
422
|
+
function relativeFile(fileName, root = "") {
|
|
423
|
+
const file = fileName.replace(/^[a-z]+:\/\/[^/]+/, "").replace(/[?#].*$/, "");
|
|
424
|
+
if (root && file.startsWith(root)) return file.slice(root.length).replace(/^\/+/, "");
|
|
425
|
+
const i = file.lastIndexOf("/src/");
|
|
426
|
+
return i >= 0 ? file.slice(i + 1) : file.replace(/^\/+/, "").split("/").slice(-3).join("/");
|
|
427
|
+
}
|
|
428
|
+
var fiberKey = null;
|
|
429
|
+
function fiberFromNode(node) {
|
|
430
|
+
for (let el = node; el; el = el.parentNode) {
|
|
431
|
+
if (!fiberKey) fiberKey = Object.keys(el).find((k) => k.startsWith("__reactFiber$")) ?? null;
|
|
432
|
+
const fiber = fiberKey ? el[fiberKey] : void 0;
|
|
433
|
+
if (fiber) return fiber;
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
function eachFiber(visit) {
|
|
438
|
+
for (const root of findRoots()) {
|
|
439
|
+
const stack = [root.current];
|
|
440
|
+
while (stack.length) {
|
|
441
|
+
const f = stack.pop();
|
|
442
|
+
if (visit(f) === false) return;
|
|
443
|
+
if (f.sibling) stack.push(f.sibling);
|
|
444
|
+
if (f.child) stack.push(f.child);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function hostRootOf(f) {
|
|
449
|
+
let node = f;
|
|
450
|
+
while (node?.return) node = node.return;
|
|
451
|
+
return node?.tag === Tag.HostRoot ? node.stateNode : null;
|
|
452
|
+
}
|
|
453
|
+
var created = /* @__PURE__ */ new Set();
|
|
454
|
+
function findRoots(doc = document) {
|
|
455
|
+
const roots = /* @__PURE__ */ new Set();
|
|
456
|
+
for (const ref of created) {
|
|
457
|
+
const root = ref.deref();
|
|
458
|
+
if (!root) created.delete(ref);
|
|
459
|
+
else if (root.containerInfo?.isConnected && root.current) roots.add(root);
|
|
460
|
+
}
|
|
461
|
+
const candidates = [doc.getElementById("root"), ...Array.from(doc.body?.children ?? [])];
|
|
462
|
+
for (const el of Array.from(doc.body?.children ?? [])) candidates.push(...Array.from(el.children));
|
|
463
|
+
for (const el of candidates) {
|
|
464
|
+
if (!el) continue;
|
|
465
|
+
const key = Object.keys(el).find((k) => k.startsWith("__reactContainer$"));
|
|
466
|
+
const container = key ? el[key] : void 0;
|
|
467
|
+
if (container?.stateNode) roots.add(container.stateNode);
|
|
468
|
+
}
|
|
469
|
+
return [...roots];
|
|
470
|
+
}
|
|
471
|
+
function nearestHosts(f, limit = 500) {
|
|
472
|
+
const out = [];
|
|
473
|
+
const stack = f.child ? [f.child] : [];
|
|
474
|
+
if (isHost(f)) return [f.stateNode];
|
|
475
|
+
while (stack.length && out.length < limit) {
|
|
476
|
+
const node = stack.pop();
|
|
477
|
+
if (node.sibling) stack.push(node.sibling);
|
|
478
|
+
if (isHost(node)) {
|
|
479
|
+
out.push(node.stateNode);
|
|
480
|
+
} else if (node.tag === Tag.HostText) {
|
|
481
|
+
const parent = node.stateNode.parentElement;
|
|
482
|
+
if (parent && !out.includes(parent)) out.push(parent);
|
|
483
|
+
} else if (node.child) {
|
|
484
|
+
stack.push(node.child);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
function compositeChain(f) {
|
|
490
|
+
const chain = [];
|
|
491
|
+
for (let node = f; node; node = node.return) if (isComposite(node)) chain.push(node);
|
|
492
|
+
return chain.reverse();
|
|
493
|
+
}
|
|
494
|
+
function currentOf(f) {
|
|
495
|
+
let top = f;
|
|
496
|
+
while (top.return) top = top.return;
|
|
497
|
+
if (top.tag !== Tag.HostRoot || top.stateNode?.current === top) return f;
|
|
498
|
+
return f.alternate ?? f;
|
|
499
|
+
}
|
|
500
|
+
function compositeChildren(f, skip, limit = 100) {
|
|
501
|
+
const out = [];
|
|
502
|
+
const stack = [];
|
|
503
|
+
const first = currentOf(f).child;
|
|
504
|
+
if (first) stack.push(first);
|
|
505
|
+
while (stack.length && out.length < limit) {
|
|
506
|
+
const node = stack.pop();
|
|
507
|
+
if (node.sibling) stack.push(node.sibling);
|
|
508
|
+
if (isComposite(node) && !skip(node)) out.push(node);
|
|
509
|
+
else if (node.child) stack.push(node.child);
|
|
510
|
+
}
|
|
511
|
+
return out;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/core/dom.ts
|
|
515
|
+
var touchedHas = (touched, f) => touched.has(f) || f.alternate !== null && touched.has(f.alternate);
|
|
516
|
+
var DomWatcher = class {
|
|
517
|
+
constructor() {
|
|
518
|
+
this.counts = { text: 0, attr: 0, child: 0 };
|
|
519
|
+
this.observer = null;
|
|
520
|
+
this.scopeHosts = null;
|
|
521
|
+
}
|
|
522
|
+
setScopeHosts(hosts) {
|
|
523
|
+
this.scopeHosts = hosts;
|
|
524
|
+
}
|
|
525
|
+
start(target2 = document.body) {
|
|
526
|
+
this.observer = new MutationObserver((records) => this.consume(records, null));
|
|
527
|
+
this.observer.observe(target2, {
|
|
528
|
+
subtree: true,
|
|
529
|
+
characterData: true,
|
|
530
|
+
characterDataOldValue: true,
|
|
531
|
+
attributes: true,
|
|
532
|
+
attributeOldValue: true,
|
|
533
|
+
childList: true
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
takeForCommit() {
|
|
537
|
+
const touched = /* @__PURE__ */ new Set();
|
|
538
|
+
if (this.observer) this.consume(this.observer.takeRecords(), touched);
|
|
539
|
+
return touched;
|
|
540
|
+
}
|
|
541
|
+
stop() {
|
|
542
|
+
if (this.observer) this.consume(this.observer.takeRecords(), null);
|
|
543
|
+
this.observer?.disconnect();
|
|
544
|
+
this.observer = null;
|
|
545
|
+
}
|
|
546
|
+
inScope(node) {
|
|
547
|
+
const hosts = this.scopeHosts;
|
|
548
|
+
if (!hosts) return true;
|
|
549
|
+
for (const host of hosts) if (host.contains(node)) return true;
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
valueNow(m) {
|
|
553
|
+
if (m.type !== "attributes") return m.target.data ?? null;
|
|
554
|
+
const element = m.target;
|
|
555
|
+
return m.attributeName && typeof element.getAttribute === "function" ? element.getAttribute(m.attributeName) : null;
|
|
556
|
+
}
|
|
557
|
+
recordKey(m) {
|
|
558
|
+
return m.type === "attributes" ? `a:${m.attributeName}` : "t";
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Compares the oldest overwritten value with the current one: React 19 blanks a form field's `name` and writes
|
|
562
|
+
* it straight back on every render, two writes that change nothing.
|
|
563
|
+
*/
|
|
564
|
+
changedSomething(m, before) {
|
|
565
|
+
if (m.type === "childList") return true;
|
|
566
|
+
const oldest = before.get(m.target)?.get(this.recordKey(m));
|
|
567
|
+
return (oldest === void 0 ? m.oldValue : oldest) !== this.valueNow(m);
|
|
568
|
+
}
|
|
569
|
+
/** One half of each fiber pair is enough, as readers check both; host fibers are skipped, nobody asks about them. */
|
|
570
|
+
mark(node, touched) {
|
|
571
|
+
for (let f = fiberFromNode(node); f; f = f.return) {
|
|
572
|
+
if (isHost(f) || f.tag === Tag.HostText) continue;
|
|
573
|
+
if (touchedHas(touched, f)) return;
|
|
574
|
+
touched.add(f);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
consume(records, touched) {
|
|
578
|
+
const before = /* @__PURE__ */ new Map();
|
|
579
|
+
for (const m of records) {
|
|
580
|
+
if (m.type === "childList") continue;
|
|
581
|
+
let perNode = before.get(m.target);
|
|
582
|
+
if (!perNode) before.set(m.target, perNode = /* @__PURE__ */ new Map());
|
|
583
|
+
const key = this.recordKey(m);
|
|
584
|
+
if (!perNode.has(key)) perNode.set(key, m.oldValue);
|
|
585
|
+
}
|
|
586
|
+
for (const m of records) {
|
|
587
|
+
if (!this.changedSomething(m, before)) continue;
|
|
588
|
+
if (touched) {
|
|
589
|
+
this.mark(m.target, touched);
|
|
590
|
+
if (m.type === "childList") {
|
|
591
|
+
m.addedNodes.forEach((node) => this.mark(node, touched));
|
|
592
|
+
if (m.removedNodes.length) {
|
|
593
|
+
const sibling = m.previousSibling ?? m.nextSibling;
|
|
594
|
+
if (sibling) this.mark(sibling, touched);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (!this.inScope(m.target)) continue;
|
|
599
|
+
if (m.type === "characterData") this.counts.text++;
|
|
600
|
+
else if (m.type === "attributes") this.counts.attr++;
|
|
601
|
+
else this.counts.child++;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// src/core/scope.ts
|
|
607
|
+
function scopeFromFiber(target2, projectRoot = "") {
|
|
608
|
+
const chain = [];
|
|
609
|
+
for (let node = target2; node; node = node.return) chain.push(node);
|
|
610
|
+
chain.reverse();
|
|
611
|
+
return { kind: "scope", chain, name: nameOf(target2) ?? String(target2.type ?? "root"), source: sourceOf(target2, projectRoot) };
|
|
612
|
+
}
|
|
613
|
+
var sameKind = (a, b) => a.key === b.key && a.index === b.index && (a.elementType === b.elementType || (nameOf(a) ?? a.type) === (nameOf(b) ?? b.type));
|
|
614
|
+
var ScopeTracker = class {
|
|
615
|
+
constructor(handle) {
|
|
616
|
+
this.state = "attached";
|
|
617
|
+
this.remounts = 0;
|
|
618
|
+
this.lostAtMs = [];
|
|
619
|
+
this.anchors = handle.chain.slice();
|
|
620
|
+
this.name = handle.name;
|
|
621
|
+
this.source = handle.source;
|
|
622
|
+
}
|
|
623
|
+
get target() {
|
|
624
|
+
return this.anchors[this.anchors.length - 1];
|
|
625
|
+
}
|
|
626
|
+
/** Names of the composite ancestors above the scope, nearest last. */
|
|
627
|
+
ancestorNames() {
|
|
628
|
+
return this.anchors.slice(0, -1).map((f) => nameOf(f)).filter((n) => Boolean(n));
|
|
629
|
+
}
|
|
630
|
+
/** Walks the chain in a committed tree; `visit` snapshots each chain fiber and reports whether it rendered. */
|
|
631
|
+
resolve(committedRoot, visit, atMs) {
|
|
632
|
+
const first = this.anchors[0];
|
|
633
|
+
if (committedRoot !== first && committedRoot.alternate !== first) return { status: "untouched" };
|
|
634
|
+
const fibers = [];
|
|
635
|
+
const rendered = [];
|
|
636
|
+
let structural = false;
|
|
637
|
+
let node = committedRoot;
|
|
638
|
+
for (let i = 0; i < this.anchors.length; i++) {
|
|
639
|
+
if (i > 0) {
|
|
640
|
+
const parent = fibers[i - 1];
|
|
641
|
+
if (!structural && this.state === "attached" && parent.alternate && parent.child === parent.alternate.child) return { status: "untouched" };
|
|
642
|
+
const anchor = this.anchors[i];
|
|
643
|
+
let found = null;
|
|
644
|
+
if (!structural) {
|
|
645
|
+
for (let c = parent.child; c && !found; c = c.sibling) if (c === anchor || c.alternate === anchor) found = c;
|
|
646
|
+
}
|
|
647
|
+
if (!found) {
|
|
648
|
+
for (let c = parent.child; c && !found; c = c.sibling) if (sameKind(c, anchor)) found = c;
|
|
649
|
+
if (found) structural = true;
|
|
650
|
+
}
|
|
651
|
+
if (!found) return this.markLost(atMs);
|
|
652
|
+
node = found;
|
|
653
|
+
}
|
|
654
|
+
fibers.push(node);
|
|
655
|
+
rendered.push(visit(node).rendered);
|
|
656
|
+
}
|
|
657
|
+
this.anchors = fibers;
|
|
658
|
+
const remounted = structural || this.state === "lost";
|
|
659
|
+
if (remounted) this.remounts++;
|
|
660
|
+
this.state = "attached";
|
|
661
|
+
return { status: "found", fibers, rendered, remounted };
|
|
662
|
+
}
|
|
663
|
+
markLost(atMs) {
|
|
664
|
+
if (this.state === "attached") this.lostAtMs.push(Math.round(atMs));
|
|
665
|
+
this.state = "lost";
|
|
666
|
+
return { status: "lost" };
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// src/core/live-highlight.ts
|
|
671
|
+
var PERFORMED_WORK = 1;
|
|
672
|
+
var ranNow = (f) => f.alternate !== null && isComposite(f) && (f.flags & PERFORMED_WORK) !== 0;
|
|
673
|
+
var LiveHighlight = class {
|
|
674
|
+
constructor(sink2, scope) {
|
|
675
|
+
this.sink = sink2;
|
|
676
|
+
this.hook = null;
|
|
677
|
+
this.dom = new DomWatcher();
|
|
678
|
+
this.scope = scope ? new ScopeTracker(scope) : null;
|
|
679
|
+
}
|
|
680
|
+
/** False when there is no React root or another script holds its commit hook. */
|
|
681
|
+
start() {
|
|
682
|
+
const roots = this.scope ? [hostRootOf(this.scope.target)].filter((r) => Boolean(r)) : findRoots();
|
|
683
|
+
if (!roots.length || roots.some((root) => hookOwner(root))) return false;
|
|
684
|
+
this.dom.start();
|
|
685
|
+
this.hook = hookCommits(roots, "live-highlight", (info) => this.onCommit(info));
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
stop() {
|
|
689
|
+
this.hook?.stop();
|
|
690
|
+
this.hook = null;
|
|
691
|
+
this.dom.stop();
|
|
692
|
+
}
|
|
693
|
+
onCommit({ fiber }) {
|
|
694
|
+
const touched = this.dom.takeForCommit();
|
|
695
|
+
let start = fiber;
|
|
696
|
+
if (this.scope) {
|
|
697
|
+
const res = this.scope.resolve(fiber, (f) => ({ rendered: ranNow(f) }), 0);
|
|
698
|
+
if (res.status !== "found") return;
|
|
699
|
+
start = res.fibers[res.fibers.length - 1];
|
|
700
|
+
}
|
|
701
|
+
const pairs = [];
|
|
702
|
+
const withoutDom = /* @__PURE__ */ new Set();
|
|
703
|
+
const mounted = /* @__PURE__ */ new Set();
|
|
704
|
+
const stack = [[start, null]];
|
|
705
|
+
while (stack.length) {
|
|
706
|
+
const [f, pending] = stack.pop();
|
|
707
|
+
let next = pending;
|
|
708
|
+
if (ranNow(f) && (!pending || pending[2] && !isLibraryFiber(f))) {
|
|
709
|
+
next = [nameOf(f) ?? "Anonymous", f, isLibraryFiber(f)];
|
|
710
|
+
if (!touchedHas(touched, f)) withoutDom.add(f);
|
|
711
|
+
} else if (!pending && mountedInPlace(f)) {
|
|
712
|
+
next = [nameOf(f) ?? "Anonymous", f, isLibraryFiber(f)];
|
|
713
|
+
mounted.add(f);
|
|
714
|
+
}
|
|
715
|
+
if (next && !next[2] && (isHost(f) || f.tag === Tag.HostText)) {
|
|
716
|
+
const el = isHost(f) ? f.stateNode : f.stateNode.parentElement;
|
|
717
|
+
if (el) pairs.push([el, next[0], next[1]]);
|
|
718
|
+
next = null;
|
|
719
|
+
}
|
|
720
|
+
if (f !== start && f.sibling) stack.push([f.sibling, pending]);
|
|
721
|
+
if (f.child && !(f.alternate && f.child === f.alternate.child)) stack.push([f.child, next]);
|
|
722
|
+
}
|
|
723
|
+
if (pairs.length) this.sink.flash(pairs, withoutDom, mounted);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// src/shared/same-content.ts
|
|
728
|
+
var REACT_ELEMENT = /* @__PURE__ */ Symbol.for("react.element");
|
|
729
|
+
var REACT_TRANSITIONAL_ELEMENT = /* @__PURE__ */ Symbol.for("react.transitional.element");
|
|
730
|
+
var ELEMENT_SKIP = /* @__PURE__ */ new Set(["_owner", "_store", "_self", "_source", "_debugInfo", "_debugStack", "_debugTask"]);
|
|
731
|
+
var isElement = (value) => {
|
|
732
|
+
const tag = value.$$typeof;
|
|
733
|
+
return tag === REACT_ELEMENT || tag === REACT_TRANSITIONAL_ELEMENT;
|
|
734
|
+
};
|
|
735
|
+
function sameContent(a, b, budget = 5e4) {
|
|
736
|
+
const stack = [[a, b]];
|
|
737
|
+
const seen = /* @__PURE__ */ new WeakMap();
|
|
738
|
+
let nodes = 0;
|
|
739
|
+
while (stack.length) {
|
|
740
|
+
const [x, y] = stack.pop();
|
|
741
|
+
if (Object.is(x, y)) continue;
|
|
742
|
+
if (++nodes > budget) return "unknown";
|
|
743
|
+
if (typeof x !== typeof y) return false;
|
|
744
|
+
if (typeof x === "function") continue;
|
|
745
|
+
if (typeof x !== "object" || x === null || y === null) return false;
|
|
746
|
+
const ox = x;
|
|
747
|
+
const oy = y;
|
|
748
|
+
if (seen.get(ox) === oy) continue;
|
|
749
|
+
seen.set(ox, oy);
|
|
750
|
+
if (Object.getPrototypeOf(ox) !== Object.getPrototypeOf(oy)) return false;
|
|
751
|
+
if (typeof Node !== "undefined" && ox instanceof Node) return false;
|
|
752
|
+
if (Array.isArray(ox)) {
|
|
753
|
+
const ay = oy;
|
|
754
|
+
if (ox.length !== ay.length) return false;
|
|
755
|
+
for (let i = 0; i < ox.length; i++) stack.push([ox[i], ay[i]]);
|
|
756
|
+
} else if (ox instanceof Date) {
|
|
757
|
+
if (ox.getTime() !== oy.getTime()) return false;
|
|
758
|
+
} else if (ox instanceof Map) {
|
|
759
|
+
const my = oy;
|
|
760
|
+
if (ox.size !== my.size) return false;
|
|
761
|
+
for (const [k, v] of ox) {
|
|
762
|
+
if (!my.has(k)) return false;
|
|
763
|
+
stack.push([v, my.get(k)]);
|
|
764
|
+
}
|
|
765
|
+
} else if (ox instanceof Set) {
|
|
766
|
+
const sy = oy;
|
|
767
|
+
if (ox.size !== sy.size) return false;
|
|
768
|
+
for (const v of ox) if (!sy.has(v)) return false;
|
|
769
|
+
} else if (ArrayBuffer.isView(ox)) {
|
|
770
|
+
const bx = new Uint8Array(ox.buffer, ox.byteOffset, ox.byteLength);
|
|
771
|
+
const view = oy;
|
|
772
|
+
const by = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
773
|
+
if (bx.length !== by.length) return false;
|
|
774
|
+
for (let i = 0; i < bx.length; i++) if (bx[i] !== by[i]) return false;
|
|
775
|
+
} else {
|
|
776
|
+
const element = isElement(ox);
|
|
777
|
+
const keys = Object.keys(ox).filter((k) => !element || !ELEMENT_SKIP.has(k));
|
|
778
|
+
const keysY = Object.keys(oy).filter((k) => !element || !ELEMENT_SKIP.has(k));
|
|
779
|
+
if (keys.length !== keysY.length) return false;
|
|
780
|
+
for (const k of keys) {
|
|
781
|
+
if (!Object.prototype.hasOwnProperty.call(oy, k)) return false;
|
|
782
|
+
stack.push([ox[k], oy[k]]);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/shared/stats.ts
|
|
790
|
+
var topEntries = (map, n) => [...map].sort((a, b) => b[1] - a[1]).slice(0, n);
|
|
791
|
+
function medianGap(times) {
|
|
792
|
+
if (times.length < 2) return null;
|
|
793
|
+
const gaps = times.slice(1).map((t, i) => t - times[i]).sort((a, b) => a - b);
|
|
794
|
+
return gaps[Math.floor(gaps.length / 2)];
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/shared/schema.ts
|
|
798
|
+
var RECORDING_SCHEMA = "react-perf-recorder/recording";
|
|
799
|
+
var GLOBAL_KEY = "__REACT_PERF_RECORDER__";
|
|
800
|
+
var CLIENT_HEADER = "x-react-perf-recorder";
|
|
801
|
+
|
|
802
|
+
// src/shared/segments.ts
|
|
803
|
+
var AMBIENT_POINTER = /^(pointer|mouse)(move|over|out|enter|leave)$/;
|
|
804
|
+
var eventName = (type) => type && AMBIENT_POINTER.test(type) ? "pointer" : type;
|
|
805
|
+
var USER_EVENTS = /* @__PURE__ */ new Set([
|
|
806
|
+
"click",
|
|
807
|
+
"input",
|
|
808
|
+
"change",
|
|
809
|
+
"keydown",
|
|
810
|
+
"keyup",
|
|
811
|
+
"keypress",
|
|
812
|
+
"pointerdown",
|
|
813
|
+
"pointerup",
|
|
814
|
+
"mousedown",
|
|
815
|
+
"mouseup",
|
|
816
|
+
"touchstart",
|
|
817
|
+
"touchend",
|
|
818
|
+
"submit",
|
|
819
|
+
"focusin",
|
|
820
|
+
"focusout",
|
|
821
|
+
"focus",
|
|
822
|
+
"blur",
|
|
823
|
+
"wheel",
|
|
824
|
+
"scroll",
|
|
825
|
+
"popstate"
|
|
826
|
+
]);
|
|
827
|
+
var LATENCY_TYPES = {
|
|
828
|
+
click: ["pointerdown", "pointerup", "click", "mousedown", "mouseup"],
|
|
829
|
+
typing: ["keydown", "keypress", "keyup", "beforeinput", "input"],
|
|
830
|
+
change: ["pointerdown", "pointerup", "click", "change", "input"],
|
|
831
|
+
key: ["keydown", "keyup", "keypress"],
|
|
832
|
+
submit: ["keydown", "keyup", "click", "pointerup", "submit"]
|
|
833
|
+
};
|
|
834
|
+
function buildSegments(actions, commits, latency = [], frames = [], quietMs = 1e3) {
|
|
835
|
+
const sorted = actions.slice().sort((a, b) => a.atMs - b.atMs);
|
|
836
|
+
const byTime = commits.slice().sort((a, b) => a.t - b.t);
|
|
837
|
+
const segments = [];
|
|
838
|
+
let c = 0;
|
|
839
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
840
|
+
const action = sorted[i];
|
|
841
|
+
const nextStart = sorted[i + 1]?.atMs ?? Infinity;
|
|
842
|
+
while (c < byTime.length && byTime[c].t < action.atMs) c++;
|
|
843
|
+
const segment = {
|
|
844
|
+
action: action.id,
|
|
845
|
+
atMs: action.atMs,
|
|
846
|
+
durationMs: 0,
|
|
847
|
+
commits: 0,
|
|
848
|
+
renders: 0,
|
|
849
|
+
reaction: { commits: 0, renders: 0 },
|
|
850
|
+
background: { commits: 0, renders: 0 },
|
|
851
|
+
topRoots: [],
|
|
852
|
+
commitIds: [],
|
|
853
|
+
longFrames: 0,
|
|
854
|
+
maxFrameMs: 0
|
|
855
|
+
};
|
|
856
|
+
const roots = /* @__PURE__ */ new Map();
|
|
857
|
+
let last = action.endMs;
|
|
858
|
+
let maxReaction = 0;
|
|
859
|
+
for (let j = c; j < byTime.length; j++) {
|
|
860
|
+
const commit = byTime[j];
|
|
861
|
+
if (commit.t >= nextStart || commit.t - last > quietMs) break;
|
|
862
|
+
last = Math.max(last, commit.t);
|
|
863
|
+
segment.commits++;
|
|
864
|
+
segment.renders += commit.n;
|
|
865
|
+
segment.commitIds.push(commit.i);
|
|
866
|
+
const bucket = commit.event && USER_EVENTS.has(commit.event) ? segment.reaction : segment.background;
|
|
867
|
+
bucket.commits++;
|
|
868
|
+
bucket.renders += commit.n;
|
|
869
|
+
if (bucket === segment.reaction) maxReaction = Math.max(maxReaction, commit.n);
|
|
870
|
+
for (const [root, cascade] of commit.roots ?? []) roots.set(root, (roots.get(root) ?? 0) + cascade);
|
|
871
|
+
}
|
|
872
|
+
const end = Math.min(nextStart, Math.max(last, action.endMs));
|
|
873
|
+
segment.durationMs = Math.max(0, Math.round(end - action.atMs));
|
|
874
|
+
segment.topRoots = [...roots].sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
875
|
+
if (action.kind === "typing" && action.chars) {
|
|
876
|
+
segment.perChar = {
|
|
877
|
+
commits: +(segment.reaction.commits / action.chars).toFixed(2),
|
|
878
|
+
renders: Math.round(segment.reaction.renders / action.chars),
|
|
879
|
+
maxRenders: maxReaction
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const types = LATENCY_TYPES[action.kind];
|
|
883
|
+
if (types) {
|
|
884
|
+
const matched = latency.filter(
|
|
885
|
+
(e) => types.includes(e.type) && e.atMs >= action.atMs - 50 && e.atMs <= Math.max(action.endMs, action.atMs) + 50
|
|
886
|
+
);
|
|
887
|
+
if (matched.length) segment.latency = matched.reduce((worst, e) => e.duration > worst.duration ? e : worst);
|
|
888
|
+
}
|
|
889
|
+
for (const frame of frames) {
|
|
890
|
+
if (frame.atMs + frame.duration < action.atMs || frame.atMs > end) continue;
|
|
891
|
+
segment.longFrames++;
|
|
892
|
+
segment.maxFrameMs = Math.max(segment.maxFrameMs, frame.duration);
|
|
893
|
+
}
|
|
894
|
+
segments.push(segment);
|
|
895
|
+
}
|
|
896
|
+
return segments;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/core/actions.ts
|
|
900
|
+
var KEYS = /* @__PURE__ */ new Set(["Enter", "Escape", "Tab", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "PageUp", "PageDown", "Home", "End"]);
|
|
901
|
+
var SECRET_AUTOCOMPLETE = /(password|one-time-code|cc-number|cc-csc|cc-exp)/;
|
|
902
|
+
var TEXT_INPUT = /^(text|search|email|tel|url|number|password|)$/;
|
|
903
|
+
var TYPING_GAP_MS = 1500;
|
|
904
|
+
var SCROLL_GAP_MS = 300;
|
|
905
|
+
var INTERACTIVE = 'button, a, [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="checkbox"], [role="switch"], label, input, select, textarea, summary, [data-testid]';
|
|
906
|
+
function shortText(el, max) {
|
|
907
|
+
if (el === document.body || el === document.documentElement) return "";
|
|
908
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
909
|
+
let text = "";
|
|
910
|
+
for (let node = walker.nextNode(); node && text.length < max * 2; node = walker.nextNode()) text += ` ${node.nodeValue ?? ""}`;
|
|
911
|
+
return text.replace(/\s+/g, " ").trim().slice(0, max);
|
|
912
|
+
}
|
|
913
|
+
function isSecretField(el, secretSelector) {
|
|
914
|
+
if (secretSelector && el.matches(secretSelector)) return true;
|
|
915
|
+
if (el instanceof HTMLInputElement && el.type === "password") return true;
|
|
916
|
+
return SECRET_AUTOCOMPLETE.test(el.getAttribute("autocomplete") ?? "");
|
|
917
|
+
}
|
|
918
|
+
var isTextField = (el) => el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement && TEXT_INPUT.test(el.type);
|
|
919
|
+
var ActionTracker = class {
|
|
920
|
+
constructor(options, now, emit) {
|
|
921
|
+
this.options = options;
|
|
922
|
+
this.now = now;
|
|
923
|
+
this.emit = emit;
|
|
924
|
+
this.actions = [];
|
|
925
|
+
this.nextId = 1;
|
|
926
|
+
this.typing = null;
|
|
927
|
+
this.scrolling = /* @__PURE__ */ new Map();
|
|
928
|
+
this.listeners = [];
|
|
929
|
+
}
|
|
930
|
+
start() {
|
|
931
|
+
const on = (type, handler) => {
|
|
932
|
+
const listener = (event) => {
|
|
933
|
+
if (this.isOwn(event)) return;
|
|
934
|
+
try {
|
|
935
|
+
handler(event);
|
|
936
|
+
} catch {
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
window.addEventListener(type, listener, { capture: true, passive: true });
|
|
940
|
+
this.listeners.push([type, listener]);
|
|
941
|
+
};
|
|
942
|
+
on("input", (e) => this.onInput(e));
|
|
943
|
+
on("change", (e) => this.onChange(e));
|
|
944
|
+
on("click", (e) => this.onClick(e));
|
|
945
|
+
on("keydown", (e) => this.onKey(e));
|
|
946
|
+
on("submit", (e) => this.push("submit", e.target));
|
|
947
|
+
on("scroll", (e) => this.onScroll(e));
|
|
948
|
+
}
|
|
949
|
+
/** Called by the navigation tracker: back and forward are user actions, push and replace are consequences. */
|
|
950
|
+
navigation(url) {
|
|
951
|
+
this.flushTyping();
|
|
952
|
+
this.record({ id: this.nextId++, kind: "navigation", atMs: Math.round(this.now()), endMs: Math.round(this.now()), url });
|
|
953
|
+
}
|
|
954
|
+
stop() {
|
|
955
|
+
for (const [type, listener] of this.listeners) window.removeEventListener(type, listener, { capture: true });
|
|
956
|
+
this.listeners = [];
|
|
957
|
+
this.flushTyping();
|
|
958
|
+
for (const [target2, pending] of this.scrolling) {
|
|
959
|
+
clearTimeout(pending.timer);
|
|
960
|
+
this.scrolling.delete(target2);
|
|
961
|
+
this.record(pending.action);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
describe(el, event) {
|
|
965
|
+
const target2 = { tag: el.tagName.toLowerCase() };
|
|
966
|
+
const testId = el.closest("[data-testid]");
|
|
967
|
+
if (testId && (testId === el || testId.contains(el))) target2.testId = testId.getAttribute("data-testid") ?? void 0;
|
|
968
|
+
const name = el.getAttribute("name");
|
|
969
|
+
if (name) target2.name = name;
|
|
970
|
+
const label = el.getAttribute("aria-label") ?? el.getAttribute("placeholder") ?? void 0;
|
|
971
|
+
if (label) target2.label = label.slice(0, 60);
|
|
972
|
+
const role = el.getAttribute("role");
|
|
973
|
+
if (role) target2.role = role;
|
|
974
|
+
if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
|
|
975
|
+
const text = shortText(el, 40);
|
|
976
|
+
if (text) target2.text = text;
|
|
977
|
+
}
|
|
978
|
+
const id = el.getAttribute("id");
|
|
979
|
+
if (id) target2.id = id;
|
|
980
|
+
const href = el.getAttribute("href");
|
|
981
|
+
if (href) target2.href = href.slice(0, 200);
|
|
982
|
+
if (el.hasAttribute("disabled")) target2.disabled = true;
|
|
983
|
+
if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) target2.checked = el.checked;
|
|
984
|
+
const selector = this.selectorOf(el);
|
|
985
|
+
if (selector) {
|
|
986
|
+
target2.selector = selector;
|
|
987
|
+
const like = document.querySelectorAll(selector);
|
|
988
|
+
if (like.length > 1) target2.nth = [...like].indexOf(el);
|
|
989
|
+
}
|
|
990
|
+
const box = el.getBoundingClientRect();
|
|
991
|
+
if (box.width || box.height) {
|
|
992
|
+
target2.box = { x: Math.round(box.x), y: Math.round(box.y), w: Math.round(box.width), h: Math.round(box.height) };
|
|
993
|
+
}
|
|
994
|
+
const pointer = event;
|
|
995
|
+
if (pointer && typeof pointer.clientX === "number" && (pointer.clientX || pointer.clientY)) {
|
|
996
|
+
target2.point = { x: Math.round(pointer.clientX), y: Math.round(pointer.clientY) };
|
|
997
|
+
}
|
|
998
|
+
const owner = this.ownerOf(el);
|
|
999
|
+
if (owner) {
|
|
1000
|
+
target2.component = nameOf(owner) ?? void 0;
|
|
1001
|
+
const source = sourceOf(owner, this.options.projectRoot);
|
|
1002
|
+
if (source) target2.source = source;
|
|
1003
|
+
const generated = generatedSourceOf(owner);
|
|
1004
|
+
if (generated) target2.generatedSource = generated;
|
|
1005
|
+
const path = this.pathOf(owner);
|
|
1006
|
+
if (path.length) target2.path = path;
|
|
1007
|
+
}
|
|
1008
|
+
const inScope = this.options.inScope(el);
|
|
1009
|
+
if (inScope !== void 0) target2.inScope = inScope;
|
|
1010
|
+
return target2;
|
|
1011
|
+
}
|
|
1012
|
+
/** `[data-testid="send"]`, `#amount`, `button[name="save"]`: enough to find the element again. */
|
|
1013
|
+
selectorOf(el) {
|
|
1014
|
+
const tag = el.tagName.toLowerCase();
|
|
1015
|
+
const testId = el.getAttribute("data-testid");
|
|
1016
|
+
if (testId) return `[data-testid="${CSS.escape(testId)}"]`;
|
|
1017
|
+
const id = el.getAttribute("id");
|
|
1018
|
+
if (id) return `#${CSS.escape(id)}`;
|
|
1019
|
+
const own2 = ["name", "role", "aria-label", "type", "href"].map((attr) => [attr, el.getAttribute(attr)]).find(([, value]) => value);
|
|
1020
|
+
const self = own2 ? `${tag}[${own2[0]}="${CSS.escape(own2[1])}"]` : tag;
|
|
1021
|
+
const anchor = el.parentElement?.closest("[data-testid]");
|
|
1022
|
+
const inside = anchor?.getAttribute("data-testid");
|
|
1023
|
+
return inside ? `[data-testid="${CSS.escape(inside)}"] ${self}` : self;
|
|
1024
|
+
}
|
|
1025
|
+
/** The component that rendered the element: the first with a name of its own, wrappers and providers skipped. */
|
|
1026
|
+
ownerOf(el) {
|
|
1027
|
+
for (let f = fiberFromNode(el); f; f = f.return) {
|
|
1028
|
+
const name = nameOf(f);
|
|
1029
|
+
if (name && !isProvider(name) && !this.options.wrapperPattern.test(name)) return f;
|
|
1030
|
+
}
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
/** The app's components above the one that rendered the element, nearest last: `Layout › Chat › MessageRow`. */
|
|
1034
|
+
pathOf(owner) {
|
|
1035
|
+
const path = [];
|
|
1036
|
+
for (let f = owner.return; f && path.length < 4; f = f.return) {
|
|
1037
|
+
const name = nameOf(f);
|
|
1038
|
+
if (!name || isProvider(name) || this.options.wrapperPattern.test(name) || isLibraryFiber(f) || wrapsProvider(f)) continue;
|
|
1039
|
+
path.push(name);
|
|
1040
|
+
}
|
|
1041
|
+
return path.reverse();
|
|
1042
|
+
}
|
|
1043
|
+
isOwn(event) {
|
|
1044
|
+
const host = this.options.ownHost;
|
|
1045
|
+
return Boolean(host && event.composedPath().includes(host));
|
|
1046
|
+
}
|
|
1047
|
+
record(action) {
|
|
1048
|
+
if (this.actions.length >= 5e3) return;
|
|
1049
|
+
this.actions.push(action);
|
|
1050
|
+
this.emit(action);
|
|
1051
|
+
}
|
|
1052
|
+
push(kind, el, extra = {}, event) {
|
|
1053
|
+
if (!el) return;
|
|
1054
|
+
this.flushTyping();
|
|
1055
|
+
const at = Math.round(this.now());
|
|
1056
|
+
this.record({ id: this.nextId++, kind, atMs: at, endMs: at, target: this.describe(el, event), ...extra });
|
|
1057
|
+
}
|
|
1058
|
+
valueFields(el) {
|
|
1059
|
+
if (isSecretField(el, this.options.secretSelector)) return { secret: true };
|
|
1060
|
+
const value = el.value ?? "";
|
|
1061
|
+
return { length: value.length, ...this.options.values ? { value: value.slice(0, 200) } : {} };
|
|
1062
|
+
}
|
|
1063
|
+
onInput(event) {
|
|
1064
|
+
const el = event.target;
|
|
1065
|
+
if (!(el instanceof Element) || !(isTextField(el) || el.isContentEditable)) return;
|
|
1066
|
+
const now = Math.round(this.now());
|
|
1067
|
+
if (this.typing && this.typing.el === el && now - this.typing.action.endMs < TYPING_GAP_MS) {
|
|
1068
|
+
const action = this.typing.action;
|
|
1069
|
+
action.chars = (action.chars ?? 0) + 1;
|
|
1070
|
+
action.endMs = now;
|
|
1071
|
+
Object.assign(action, this.valueFields(el));
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
this.flushTyping();
|
|
1075
|
+
this.typing = {
|
|
1076
|
+
el,
|
|
1077
|
+
action: { id: this.nextId++, kind: "typing", atMs: now, endMs: now, target: this.describe(el), chars: 1, ...this.valueFields(el) }
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
onChange(event) {
|
|
1081
|
+
const el = event.target;
|
|
1082
|
+
if (!(el instanceof Element) || isTextField(el)) return;
|
|
1083
|
+
const extra = isSecretField(el, this.options.secretSelector) ? { secret: true } : this.options.values ? {
|
|
1084
|
+
value: el instanceof HTMLInputElement && /checkbox|radio/.test(el.type) ? String(el.checked) : String(el.value ?? "").slice(0, 200)
|
|
1085
|
+
} : {};
|
|
1086
|
+
this.push("change", el, extra);
|
|
1087
|
+
}
|
|
1088
|
+
onClick(event) {
|
|
1089
|
+
const el = event.target instanceof Element ? event.target : null;
|
|
1090
|
+
this.push("click", el?.closest(INTERACTIVE) ?? el, {}, event);
|
|
1091
|
+
}
|
|
1092
|
+
onKey(event) {
|
|
1093
|
+
if (!KEYS.has(event.key) || event.repeat) return;
|
|
1094
|
+
const el = event.target instanceof Element ? event.target : document.activeElement;
|
|
1095
|
+
this.push("key", el ?? document.body, { key: event.key });
|
|
1096
|
+
}
|
|
1097
|
+
onScroll(event) {
|
|
1098
|
+
const target2 = event.target;
|
|
1099
|
+
const el = target2 === document ? document.scrollingElement : target2;
|
|
1100
|
+
if (!(el instanceof Element)) return;
|
|
1101
|
+
const top = Math.round(el.scrollTop);
|
|
1102
|
+
const now = Math.round(this.now());
|
|
1103
|
+
const pending = this.scrolling.get(target2);
|
|
1104
|
+
if (pending) {
|
|
1105
|
+
clearTimeout(pending.timer);
|
|
1106
|
+
const scroll = pending.action.scroll;
|
|
1107
|
+
scroll.pixels += Math.abs(top - scroll.to);
|
|
1108
|
+
scroll.to = top;
|
|
1109
|
+
pending.action.endMs = now;
|
|
1110
|
+
}
|
|
1111
|
+
const entry = pending ?? {
|
|
1112
|
+
action: {
|
|
1113
|
+
id: this.nextId++,
|
|
1114
|
+
kind: "scroll",
|
|
1115
|
+
atMs: now,
|
|
1116
|
+
endMs: now,
|
|
1117
|
+
target: this.describe(el),
|
|
1118
|
+
scroll: { from: top, to: top, pixels: 0 }
|
|
1119
|
+
},
|
|
1120
|
+
timer: 0
|
|
1121
|
+
};
|
|
1122
|
+
entry.timer = setTimeout(() => {
|
|
1123
|
+
this.scrolling.delete(target2);
|
|
1124
|
+
this.record(entry.action);
|
|
1125
|
+
}, SCROLL_GAP_MS);
|
|
1126
|
+
this.scrolling.set(target2, entry);
|
|
1127
|
+
}
|
|
1128
|
+
flushTyping() {
|
|
1129
|
+
if (!this.typing) return;
|
|
1130
|
+
this.record(this.typing.action);
|
|
1131
|
+
this.typing = null;
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
// src/core/reasons.ts
|
|
1136
|
+
var MAX_PROPS = 10;
|
|
1137
|
+
var same = (a, b) => sameContent(a, b, 2e4) === true;
|
|
1138
|
+
var sameCheap = (a, b) => sameContent(a, b, 2e3) === true;
|
|
1139
|
+
function snapshotOf(f) {
|
|
1140
|
+
return { props: f.memoizedProps, state: f.memoizedState, ctx: f.dependencies?.firstContext ?? null };
|
|
1141
|
+
}
|
|
1142
|
+
function didRender(prev, f) {
|
|
1143
|
+
return Boolean(prev) && (prev.props !== f.memoizedProps || prev.state !== f.memoizedState || prev.ctx !== (f.dependencies?.firstContext ?? null));
|
|
1144
|
+
}
|
|
1145
|
+
function propsReason(before, after) {
|
|
1146
|
+
const a = before || {};
|
|
1147
|
+
const b = after || {};
|
|
1148
|
+
const changed = [];
|
|
1149
|
+
const sameShape = [];
|
|
1150
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
1151
|
+
if (key === "children" && a[key] === b[key]) continue;
|
|
1152
|
+
if (a[key] === b[key]) continue;
|
|
1153
|
+
(key in a && key in b && same(a[key], b[key]) ? sameShape : changed).push(key);
|
|
1154
|
+
}
|
|
1155
|
+
return {
|
|
1156
|
+
kind: "props",
|
|
1157
|
+
...changed.length ? { changed: changed.slice(0, MAX_PROPS) } : {},
|
|
1158
|
+
...sameShape.length ? { sameRef: sameShape.slice(0, MAX_PROPS) } : {}
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function reasonsOf(prev, f, describe) {
|
|
1162
|
+
if (prev.props !== f.memoizedProps) return [propsReason(prev.props, f.memoizedProps)];
|
|
1163
|
+
const out = [];
|
|
1164
|
+
if (hasHooks(f)) {
|
|
1165
|
+
let a = prev.state;
|
|
1166
|
+
let b = f.memoizedState;
|
|
1167
|
+
let before = null;
|
|
1168
|
+
for (let i = 0; a && b && i < 1e3; i++) {
|
|
1169
|
+
if (b.queue && a.memoizedState !== b.memoizedState) {
|
|
1170
|
+
const mark = same(a.memoizedState, b.memoizedState) ? { sameContent: true } : null;
|
|
1171
|
+
if (b.queue.getSnapshot) {
|
|
1172
|
+
const deps = Array.isArray(before?.memoizedState) ? before.memoizedState[1] : null;
|
|
1173
|
+
const selector = Array.isArray(deps) && typeof deps[2] === "function" ? describe.selector(deps[2]) : "";
|
|
1174
|
+
const store = Array.isArray(deps) && typeof deps[0] === "function" ? describe.store(deps[0]) : null;
|
|
1175
|
+
out.push({ kind: "store", hook: i, ...store ? { store } : {}, ...selector ? { selector } : {}, ...mark });
|
|
1176
|
+
} else if (b.queue.lastRenderedReducer) {
|
|
1177
|
+
out.push({ kind: "state", hook: i, ...mark });
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
before = b;
|
|
1181
|
+
a = a.next;
|
|
1182
|
+
b = b.next;
|
|
1183
|
+
}
|
|
1184
|
+
} else if (f.tag === Tag.ClassComponent && prev.state !== f.memoizedState) {
|
|
1185
|
+
out.push({ kind: "state", ...same(prev.state, f.memoizedState) ? { sameContent: true } : {} });
|
|
1186
|
+
}
|
|
1187
|
+
const current = f.dependencies?.firstContext ?? null;
|
|
1188
|
+
if (current && prev.ctx !== current) {
|
|
1189
|
+
const old = /* @__PURE__ */ new Map();
|
|
1190
|
+
for (let d = prev.ctx; d; d = d.next) old.set(d.context, d.memoizedValue);
|
|
1191
|
+
for (let d = current; d; d = d.next) {
|
|
1192
|
+
if (!old.has(d.context) || old.get(d.context) === d.memoizedValue) continue;
|
|
1193
|
+
out.push({
|
|
1194
|
+
kind: "context",
|
|
1195
|
+
context: d.context.displayName || "(unnamed)",
|
|
1196
|
+
contextObject: d.context,
|
|
1197
|
+
...same(old.get(d.context), d.memoizedValue) ? { sameContent: true } : {}
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (!out.length && hasHooks(f) && prev.state !== f.memoizedState) return [{ kind: "bailout" }];
|
|
1202
|
+
return out.length ? out : [{ kind: "unknown" }];
|
|
1203
|
+
}
|
|
1204
|
+
function parentReason(prev, f, describe) {
|
|
1205
|
+
if (prev.props === f.memoizedProps) return reasonsOf(prev, f, describe);
|
|
1206
|
+
const a = prev.props || {};
|
|
1207
|
+
const b = f.memoizedProps || {};
|
|
1208
|
+
const changed = [];
|
|
1209
|
+
const sameShape = [];
|
|
1210
|
+
let children = false;
|
|
1211
|
+
const compare = (key) => {
|
|
1212
|
+
if (a[key] === b[key]) return;
|
|
1213
|
+
if (key === "children") children = true;
|
|
1214
|
+
else (key in a && key in b && sameCheap(a[key], b[key]) ? sameShape : changed).push(key);
|
|
1215
|
+
};
|
|
1216
|
+
for (const key in a) compare(key);
|
|
1217
|
+
for (const key in b) if (!(key in a)) compare(key);
|
|
1218
|
+
if (!changed.length && !sameShape.length) return [{ kind: "parent", ...children ? { children: true } : { equal: true } }];
|
|
1219
|
+
return [
|
|
1220
|
+
{
|
|
1221
|
+
kind: "parent",
|
|
1222
|
+
...changed.length ? { changed: changed.slice(0, MAX_PROPS) } : {},
|
|
1223
|
+
...sameShape.length ? { sameRef: sameShape.slice(0, MAX_PROPS) } : {},
|
|
1224
|
+
...children ? { children: true } : {}
|
|
1225
|
+
}
|
|
1226
|
+
];
|
|
1227
|
+
}
|
|
1228
|
+
function hookTypeAt(f, index) {
|
|
1229
|
+
const types = f._debugHookTypes;
|
|
1230
|
+
if (!types) return void 0;
|
|
1231
|
+
let cell = 0;
|
|
1232
|
+
for (const type of types) {
|
|
1233
|
+
const cells = hookCells(type);
|
|
1234
|
+
if (!cells) continue;
|
|
1235
|
+
if (index < cell + cells) return type;
|
|
1236
|
+
cell += cells;
|
|
1237
|
+
}
|
|
1238
|
+
return void 0;
|
|
1239
|
+
}
|
|
1240
|
+
var GENERIC_NAMES = /* @__PURE__ */ new Set(["", "anonymous", "selector", "select", "fn", "memoized", "memoizedFn"]);
|
|
1241
|
+
function fallbackSelectorLabel(fn) {
|
|
1242
|
+
const name = fn.name.replace(/^bound /, "");
|
|
1243
|
+
if (!GENERIC_NAMES.has(name)) return name;
|
|
1244
|
+
return String(fn).replace(/\s+/g, " ").slice(0, 100);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// src/core/memo-hits.ts
|
|
1248
|
+
var HOOK_TAGS = /* @__PURE__ */ new Set([Tag.FunctionComponent, Tag.ForwardRef, Tag.SimpleMemoComponent]);
|
|
1249
|
+
var MAX_LISTED = 30;
|
|
1250
|
+
var MAX_INSPECTED = 15;
|
|
1251
|
+
var cellsByType = /* @__PURE__ */ new WeakMap();
|
|
1252
|
+
function memoCells(f) {
|
|
1253
|
+
const type = f.type;
|
|
1254
|
+
if (!type || typeof type !== "function" && typeof type !== "object") return null;
|
|
1255
|
+
const known2 = cellsByType.get(type);
|
|
1256
|
+
if (known2 !== void 0) return known2;
|
|
1257
|
+
const types = f._debugHookTypes;
|
|
1258
|
+
if (!types) return null;
|
|
1259
|
+
const cells = [];
|
|
1260
|
+
let cell = 0;
|
|
1261
|
+
for (const t of types) {
|
|
1262
|
+
const n = hookCells(t);
|
|
1263
|
+
if (!n) continue;
|
|
1264
|
+
if (t === "useMemo" || t === "useCallback") cells.push([cell, t]);
|
|
1265
|
+
cell += n;
|
|
1266
|
+
}
|
|
1267
|
+
const result = cells.length ? cells : null;
|
|
1268
|
+
cellsByType.set(type, result);
|
|
1269
|
+
return result;
|
|
1270
|
+
}
|
|
1271
|
+
var MemoHits = class {
|
|
1272
|
+
constructor(sourceOf2) {
|
|
1273
|
+
this.sourceOf = sourceOf2;
|
|
1274
|
+
this.components = /* @__PURE__ */ new Map();
|
|
1275
|
+
}
|
|
1276
|
+
track(name, f, previousHooks) {
|
|
1277
|
+
if (!HOOK_TAGS.has(f.tag)) return;
|
|
1278
|
+
const cells = memoCells(f);
|
|
1279
|
+
if (!cells) return;
|
|
1280
|
+
let entry = this.components.get(name);
|
|
1281
|
+
if (!entry) this.components.set(name, entry = { source: this.sourceOf(f), hooks: /* @__PURE__ */ new Map(), latest: null });
|
|
1282
|
+
let before = previousHooks;
|
|
1283
|
+
let after = f.memoizedState;
|
|
1284
|
+
let at = 0;
|
|
1285
|
+
let missed = false;
|
|
1286
|
+
for (const [cell, kind] of cells) {
|
|
1287
|
+
for (; at < cell && before && after; at++) {
|
|
1288
|
+
before = before.next;
|
|
1289
|
+
after = after.next;
|
|
1290
|
+
}
|
|
1291
|
+
if (!before || !after) break;
|
|
1292
|
+
const old = before.memoizedState;
|
|
1293
|
+
const now = after.memoizedState;
|
|
1294
|
+
if (!Array.isArray(old) || !Array.isArray(now)) continue;
|
|
1295
|
+
let hook = entry.hooks.get(cell);
|
|
1296
|
+
if (!hook) entry.hooks.set(cell, hook = { kind, renders: 0, recomputed: 0, noDeps: false, deps: /* @__PURE__ */ new Map() });
|
|
1297
|
+
hook.renders++;
|
|
1298
|
+
if (old === now) continue;
|
|
1299
|
+
hook.recomputed++;
|
|
1300
|
+
missed = true;
|
|
1301
|
+
const oldDeps = old[1];
|
|
1302
|
+
const newDeps = now[1];
|
|
1303
|
+
if (!oldDeps || !newDeps) {
|
|
1304
|
+
hook.noDeps = true;
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
for (let i = 0; i < Math.max(oldDeps.length, newDeps.length); i++) {
|
|
1308
|
+
if (Object.is(oldDeps[i], newDeps[i])) continue;
|
|
1309
|
+
let dep = hook.deps.get(i);
|
|
1310
|
+
if (!dep) hook.deps.set(i, dep = { changed: 0, sameContent: 0 });
|
|
1311
|
+
dep.changed++;
|
|
1312
|
+
if (sameCheap(oldDeps[i], newDeps[i])) dep.sameContent++;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (missed) entry.latest = new WeakRef(f);
|
|
1316
|
+
}
|
|
1317
|
+
/** The hooks that recomputed on half their renders or more, worst first; the worst are named by `inspect`. */
|
|
1318
|
+
result(inspect) {
|
|
1319
|
+
const listed = [];
|
|
1320
|
+
for (const [component, entry] of this.components) {
|
|
1321
|
+
for (const [hook, agg] of entry.hooks) {
|
|
1322
|
+
if (agg.renders < 2 || agg.recomputed * 2 < agg.renders) continue;
|
|
1323
|
+
listed.push({
|
|
1324
|
+
component,
|
|
1325
|
+
...entry.source ? { source: entry.source } : {},
|
|
1326
|
+
hook,
|
|
1327
|
+
kind: agg.kind,
|
|
1328
|
+
renders: agg.renders,
|
|
1329
|
+
recomputed: agg.recomputed,
|
|
1330
|
+
...agg.noDeps ? { noDeps: true } : {},
|
|
1331
|
+
deps: [...agg.deps].map(([index, d]) => ({ index, ...d })).sort((a, b) => b.changed - a.changed),
|
|
1332
|
+
fiber: entry.latest?.deref()
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
listed.sort((a, b) => b.recomputed - a.recomputed || b.renders - a.renders);
|
|
1337
|
+
const top = listed.slice(0, MAX_LISTED);
|
|
1338
|
+
const named = /* @__PURE__ */ new Map();
|
|
1339
|
+
for (const stat of top) {
|
|
1340
|
+
const fiber = stat.fiber;
|
|
1341
|
+
delete stat.fiber;
|
|
1342
|
+
if (!fiber) continue;
|
|
1343
|
+
if (!named.has(fiber) && named.size < MAX_INSPECTED) named.set(fiber, inspect(fiber));
|
|
1344
|
+
const info = named.get(fiber)?.get(stat.hook);
|
|
1345
|
+
if (info) stat.info = info;
|
|
1346
|
+
}
|
|
1347
|
+
return top;
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
// src/core/env/frames.ts
|
|
1352
|
+
var supported = (type) => typeof PerformanceObserver !== "undefined" && (PerformanceObserver.supportedEntryTypes ?? []).includes(type);
|
|
1353
|
+
var shortSource = (url, root) => {
|
|
1354
|
+
if (!url) return "";
|
|
1355
|
+
const path = url.replace(/^https?:\/\/[^/]+/, "").replace(/[?#].*$/, "");
|
|
1356
|
+
if (root && path.startsWith(root)) return path.slice(root.length).replace(/^\/+/, "");
|
|
1357
|
+
return path.replace(/^\/@fs\//, "/").replace(/^\//, "");
|
|
1358
|
+
};
|
|
1359
|
+
var FrameWatcher = class {
|
|
1360
|
+
constructor(options) {
|
|
1361
|
+
this.options = options;
|
|
1362
|
+
this.longTasks = { count: 0, maxMs: 0, totalMs: 0 };
|
|
1363
|
+
this.loaf = [];
|
|
1364
|
+
this.latency = [];
|
|
1365
|
+
/** Index in `latency` of the worst entry of each interaction, so the same interaction is kept once. */
|
|
1366
|
+
this.worstByInteraction = /* @__PURE__ */ new Map();
|
|
1367
|
+
this.observers = [];
|
|
1368
|
+
}
|
|
1369
|
+
start() {
|
|
1370
|
+
const { t0 } = this.options;
|
|
1371
|
+
const at = (time) => Math.round(time - t0);
|
|
1372
|
+
if (supported("longtask")) {
|
|
1373
|
+
this.observe("longtask", (entry) => {
|
|
1374
|
+
if (entry.startTime < t0) return;
|
|
1375
|
+
this.longTasks.count++;
|
|
1376
|
+
this.longTasks.totalMs += Math.round(entry.duration);
|
|
1377
|
+
this.longTasks.maxMs = Math.max(this.longTasks.maxMs, Math.round(entry.duration));
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
if (supported("long-animation-frame")) {
|
|
1381
|
+
this.observe("long-animation-frame", (raw) => {
|
|
1382
|
+
const entry = raw;
|
|
1383
|
+
if (entry.startTime < t0 || this.loaf.length >= 500) return;
|
|
1384
|
+
const scripts = (entry.scripts ?? []).slice().sort((a, b) => b.duration - a.duration).slice(0, 5).map((s) => {
|
|
1385
|
+
const source = shortSource(s.sourceURL, this.options.projectRoot);
|
|
1386
|
+
return {
|
|
1387
|
+
invoker: String(s.invoker ?? s.sourceFunctionName ?? "").slice(0, 120),
|
|
1388
|
+
source: s.sourceCharPosition != null && s.sourceCharPosition >= 0 ? `${source}@${s.sourceCharPosition}` : source,
|
|
1389
|
+
duration: Math.round(s.duration),
|
|
1390
|
+
layout: Math.round(s.forcedStyleAndLayoutDuration ?? 0),
|
|
1391
|
+
.../react-perf-recorder/.test(s.sourceURL ?? "") ? { own: true } : {}
|
|
1392
|
+
};
|
|
1393
|
+
});
|
|
1394
|
+
const frame = {
|
|
1395
|
+
atMs: at(entry.startTime),
|
|
1396
|
+
duration: Math.round(entry.duration),
|
|
1397
|
+
blocking: Math.round(entry.blockingDuration ?? 0),
|
|
1398
|
+
commits: this.options.countCommits(at(entry.startTime), at(entry.startTime + entry.duration)),
|
|
1399
|
+
scripts
|
|
1400
|
+
};
|
|
1401
|
+
this.loaf.push(frame);
|
|
1402
|
+
this.options.onFrame(frame);
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
if (supported("event")) {
|
|
1406
|
+
this.observe(
|
|
1407
|
+
"event",
|
|
1408
|
+
(raw) => {
|
|
1409
|
+
const entry = raw;
|
|
1410
|
+
if (!entry.interactionId || entry.startTime < t0 || this.latency.length >= 2e3) return;
|
|
1411
|
+
const latency = {
|
|
1412
|
+
atMs: at(entry.startTime),
|
|
1413
|
+
type: entry.name,
|
|
1414
|
+
duration: Math.round(entry.duration),
|
|
1415
|
+
inputDelay: Math.round(entry.processingStart - entry.startTime),
|
|
1416
|
+
processing: Math.round(entry.processingEnd - entry.processingStart),
|
|
1417
|
+
presentation: Math.max(0, Math.round(entry.startTime + entry.duration - entry.processingEnd)),
|
|
1418
|
+
interactionId: entry.interactionId
|
|
1419
|
+
};
|
|
1420
|
+
const seen = this.worstByInteraction.get(latency.interactionId);
|
|
1421
|
+
if (seen !== void 0 && this.latency[seen].duration >= latency.duration) return;
|
|
1422
|
+
if (seen !== void 0) this.latency[seen] = latency;
|
|
1423
|
+
else {
|
|
1424
|
+
this.worstByInteraction.set(latency.interactionId, this.latency.length);
|
|
1425
|
+
this.latency.push(latency);
|
|
1426
|
+
}
|
|
1427
|
+
this.options.onLatency(latency);
|
|
1428
|
+
},
|
|
1429
|
+
{ durationThreshold: 16 }
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
stop() {
|
|
1434
|
+
for (const [observer, onEntry] of this.observers) {
|
|
1435
|
+
observer.takeRecords?.().forEach(onEntry);
|
|
1436
|
+
observer.disconnect();
|
|
1437
|
+
}
|
|
1438
|
+
this.observers = [];
|
|
1439
|
+
}
|
|
1440
|
+
observe(type, onEntry, extra = {}) {
|
|
1441
|
+
try {
|
|
1442
|
+
const observer = new PerformanceObserver((list) => list.getEntries().forEach(onEntry));
|
|
1443
|
+
observer.observe({ type, buffered: false, ...extra });
|
|
1444
|
+
this.observers.push([observer, onEntry]);
|
|
1445
|
+
} catch {
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
// src/core/env/navigations.ts
|
|
1451
|
+
function trackHistory(now, onNavigation) {
|
|
1452
|
+
const original = { pushState: history.pushState, replaceState: history.replaceState };
|
|
1453
|
+
const wrapped = {};
|
|
1454
|
+
const here = () => safeUrl(location.pathname + location.search);
|
|
1455
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
1456
|
+
const fn = function(...args) {
|
|
1457
|
+
const before = here();
|
|
1458
|
+
const result = original[method].apply(this, args);
|
|
1459
|
+
const url = here();
|
|
1460
|
+
onNavigation({
|
|
1461
|
+
type: method === "pushState" ? "push" : "replace",
|
|
1462
|
+
atMs: Math.round(now()),
|
|
1463
|
+
url,
|
|
1464
|
+
...url === before ? { sameUrl: true } : {}
|
|
1465
|
+
});
|
|
1466
|
+
return result;
|
|
1467
|
+
};
|
|
1468
|
+
wrapped[method] = fn;
|
|
1469
|
+
history[method] = fn;
|
|
1470
|
+
}
|
|
1471
|
+
const onPop = () => onNavigation({ type: "pop", atMs: Math.round(now()), url: here() });
|
|
1472
|
+
window.addEventListener("popstate", onPop);
|
|
1473
|
+
return () => {
|
|
1474
|
+
for (const method of ["pushState", "replaceState"]) if (history[method] === wrapped[method]) history[method] = original[method];
|
|
1475
|
+
window.removeEventListener("popstate", onPop);
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// src/shared/summary.ts
|
|
1480
|
+
var perSec = (n, ms) => ms > 0 ? +(n * 1e3 / ms).toFixed(2) : 0;
|
|
1481
|
+
function hookChain(hook, mode = "full") {
|
|
1482
|
+
if (!hook) return "";
|
|
1483
|
+
const steps = hook.path ?? [];
|
|
1484
|
+
const at = hook.library && hook.libraryAt !== void 0 && hook.libraryAt < steps.length ? hook.libraryAt : -1;
|
|
1485
|
+
if (!steps.length) return hook.type ?? "";
|
|
1486
|
+
if (at < 0) return steps.join(" \u203A ");
|
|
1487
|
+
if (mode === "short") return [...steps.slice(0, at), `${hook.library}.${steps[at]}`].join(" \u203A ");
|
|
1488
|
+
return [...steps.slice(0, at), `[${hook.library}] ${steps[at]}`, ...steps.slice(at + 1)].join(" \u203A ");
|
|
1489
|
+
}
|
|
1490
|
+
function hookText(hook, mode = "full") {
|
|
1491
|
+
if (!hook) return "";
|
|
1492
|
+
const site = hook.site ? ` @ ${hook.site}${hook.code ? ` ${hook.code}` : ""}` : "";
|
|
1493
|
+
return `${hookChain(hook, mode)}${site}`;
|
|
1494
|
+
}
|
|
1495
|
+
var contextKey = (name) => `ctx:${name}`;
|
|
1496
|
+
function hookOf(root, reason) {
|
|
1497
|
+
if (!reason) return void 0;
|
|
1498
|
+
if (reason.kind === "context") return reason.context ? root.hooks?.[contextKey(reason.context)] : void 0;
|
|
1499
|
+
return reason.hook !== void 0 ? root.hooks?.[reason.hook] : void 0;
|
|
1500
|
+
}
|
|
1501
|
+
var names = (list, max = 5) => (list ?? []).slice(0, max).join(", ");
|
|
1502
|
+
function reasonText(reason) {
|
|
1503
|
+
const mark = reason.sameContent ? " SAME-CONTENT" : "";
|
|
1504
|
+
const props = [names(reason.changed), reason.sameRef?.length ? `same: ${names(reason.sameRef)}` : ""].filter(Boolean).join(" | ");
|
|
1505
|
+
switch (reason.kind) {
|
|
1506
|
+
case "state":
|
|
1507
|
+
return reason.hook === void 0 ? `class state${mark}` : `state #${reason.hook}${mark}`;
|
|
1508
|
+
case "store":
|
|
1509
|
+
return `external store #${reason.hook}${mark}${reason.store ? ` [${reason.store}]` : ""}${reason.selector ? ` ${reason.selector}` : ""}`;
|
|
1510
|
+
case "context":
|
|
1511
|
+
return `context ${reason.context || "(unnamed)"}${mark}`;
|
|
1512
|
+
case "props":
|
|
1513
|
+
return `props: ${props || "(new object)"}`;
|
|
1514
|
+
case "parent":
|
|
1515
|
+
if (reason.equal) return "parent: props equal";
|
|
1516
|
+
if (!props) return "parent: children";
|
|
1517
|
+
return `parent: props ${props}${reason.children ? " +children" : ""}`;
|
|
1518
|
+
case "bailout":
|
|
1519
|
+
return "bailout: state set to the same value";
|
|
1520
|
+
default:
|
|
1521
|
+
return "unknown";
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
var textOf = (reason) => reason.text ?? reasonText(reason);
|
|
1525
|
+
var reasonsById = (reasons = []) => new Map(reasons.map((r) => [r.i, r]));
|
|
1526
|
+
function reasonLine(root, reason, n, mode = "full") {
|
|
1527
|
+
if (!reason) return `${n}\xD7 unknown`;
|
|
1528
|
+
const hook = hookText(hookOf(root, reason), mode);
|
|
1529
|
+
return `${n}\xD7 ${textOf(reason)}${hook ? ` \xB7 ${hook}` : ""}`;
|
|
1530
|
+
}
|
|
1531
|
+
function rootLine(root, durationMs, reasons, mode = "full") {
|
|
1532
|
+
return {
|
|
1533
|
+
root: root.name,
|
|
1534
|
+
source: root.source,
|
|
1535
|
+
path: root.path,
|
|
1536
|
+
hits: root.hits,
|
|
1537
|
+
hitsPerSec: perSec(root.hits, durationMs),
|
|
1538
|
+
instances: root.instances,
|
|
1539
|
+
perHit: root.perHit,
|
|
1540
|
+
noDomChange: root.noDomChange,
|
|
1541
|
+
...root.mounts ? { mounts: root.mounts } : {},
|
|
1542
|
+
...root.renderMs ? { renderMsPerHit: +(root.renderMs / Math.max(1, root.hits)).toFixed(2) } : {},
|
|
1543
|
+
reasons: root.reasons.slice(0, 3).map(([id, n]) => reasonLine(root, reasons.get(id), n, mode)),
|
|
1544
|
+
causes: root.causes.slice(0, 3).map(([k, n]) => `${n}\xD7 ${k}`),
|
|
1545
|
+
...root.lanes.length ? { lanes: root.lanes.map(([l, n]) => `${l}:${n}`).join(" ") } : {}
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function memoWhy(m) {
|
|
1549
|
+
if (m.noDeps) return "no dependency array: it runs on every render";
|
|
1550
|
+
const dep = m.deps[0];
|
|
1551
|
+
if (!dep) return "its dependencies changed";
|
|
1552
|
+
const name = m.info?.deps?.[dep.index];
|
|
1553
|
+
const which = name ? `\`${name}\`` : `dependency ${dep.index + 1}`;
|
|
1554
|
+
return dep.sameContent === dep.changed ? `${which} is a new object with the same content every time` : dep.sameContent ? `${which} changed ${dep.changed}\xD7, ${dep.sameContent} of them to the same content` : `${which} changed ${dep.changed}\xD7`;
|
|
1555
|
+
}
|
|
1556
|
+
function memoLine(m) {
|
|
1557
|
+
const site = m.info?.site ? ` \xB7 ${m.info.site}${m.info.code ? ` ${m.info.code}` : ""}` : m.source ? ` \xB7 ${m.source}` : "";
|
|
1558
|
+
return `${m.component} \xB7 ${m.kind} #${m.hook} \xB7 recomputed ${m.recomputed} of ${m.renders} renders \u2014 ${memoWhy(m)}${site}`;
|
|
1559
|
+
}
|
|
1560
|
+
function actionText(action) {
|
|
1561
|
+
const t = action.target;
|
|
1562
|
+
const field = t ? t.testId ?? t.name ?? t.label ?? t.text ?? t.tag : "";
|
|
1563
|
+
const where = t?.component ? ` in ${t.component}` : "";
|
|
1564
|
+
switch (action.kind) {
|
|
1565
|
+
case "typing":
|
|
1566
|
+
return `typing ${action.chars ?? 0} chars into \xAB${field}\xBB${where}${action.secret ? " (secret)" : action.value !== void 0 ? ` = ${JSON.stringify(action.value)}` : ""}`;
|
|
1567
|
+
case "key":
|
|
1568
|
+
return `${action.key} on \xAB${field}\xBB${where}`;
|
|
1569
|
+
case "scroll":
|
|
1570
|
+
return `scroll \xAB${field}\xBB ${action.scroll?.pixels ?? 0}px`;
|
|
1571
|
+
case "navigation":
|
|
1572
|
+
return `back/forward to ${action.url}`;
|
|
1573
|
+
default:
|
|
1574
|
+
return `${action.kind} \xAB${field}\xBB${where}${action.value !== void 0 ? ` = ${JSON.stringify(action.value)}` : ""}`;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function summarize(rec, top = 5, hooks = "full") {
|
|
1578
|
+
const ms = rec.durationMs;
|
|
1579
|
+
const reasons = reasonsById(rec.reasons);
|
|
1580
|
+
const allRoots = [...rec.roots, ...rec.outsideRoots];
|
|
1581
|
+
const actionsById = new Map(rec.actions.map((a) => [a.id, a]));
|
|
1582
|
+
const actions = rec.segments.slice().sort((a, b) => b.renders - a.renders).slice(0, 10).sort((a, b) => a.atMs - b.atMs).map((s) => {
|
|
1583
|
+
const action = actionsById.get(s.action);
|
|
1584
|
+
const root = s.topRoots[0] ? allRoots[s.topRoots[0][0]] : void 0;
|
|
1585
|
+
return {
|
|
1586
|
+
id: s.action,
|
|
1587
|
+
what: action ? actionText(action) : `action ${s.action}`,
|
|
1588
|
+
atSec: +(s.atMs / 1e3).toFixed(1),
|
|
1589
|
+
commits: s.commits,
|
|
1590
|
+
renders: s.renders,
|
|
1591
|
+
reaction: s.reaction.renders,
|
|
1592
|
+
...s.perChar ? { perChar: `${s.perChar.renders} renders, ${s.perChar.commits} commits per char (max ${s.perChar.maxRenders})` } : {},
|
|
1593
|
+
...s.latency ? { latencyMs: s.latency.duration } : {},
|
|
1594
|
+
...s.longFrames ? { longFrames: s.longFrames } : {},
|
|
1595
|
+
...root ? {
|
|
1596
|
+
topRoot: `${root.name} \xD7${s.topRoots[0][1]} \u2014 ${root.reasons[0] ? reasonLine(root, reasons.get(root.reasons[0][0]), root.reasons[0][1], hooks) : ""}`
|
|
1597
|
+
} : {}
|
|
1598
|
+
};
|
|
1599
|
+
});
|
|
1600
|
+
const texts2 = rec.totals.domTextChanges;
|
|
1601
|
+
return {
|
|
1602
|
+
...rec.id ? { id: rec.id } : {},
|
|
1603
|
+
...rec.status ? { status: rec.status } : {},
|
|
1604
|
+
...rec.partial ? { partial: true } : {},
|
|
1605
|
+
createdAt: rec.createdAt,
|
|
1606
|
+
...rec.label ? { label: rec.label } : {},
|
|
1607
|
+
source: rec.tool.source,
|
|
1608
|
+
url: rec.page.url,
|
|
1609
|
+
viewport: rec.page.viewport,
|
|
1610
|
+
durationSec: +(ms / 1e3).toFixed(1),
|
|
1611
|
+
scope: rec.scope ? { name: rec.scope.name, source: rec.scope.source, state: rec.scope.state, remounts: rec.scope.remounts } : null,
|
|
1612
|
+
totals: {
|
|
1613
|
+
commits: rec.totals.commits,
|
|
1614
|
+
commitsPerSec: perSec(rec.totals.commits, ms),
|
|
1615
|
+
commitsInScope: rec.totals.commitsInScope,
|
|
1616
|
+
renders: rec.totals.renders,
|
|
1617
|
+
rendersPerScopeCommit: rec.totals.rendersPerScopeCommit,
|
|
1618
|
+
rendersFromOutside: rec.totals.rendersFromOutside,
|
|
1619
|
+
rendersWithoutDom: rec.totals.rendersWithoutDom,
|
|
1620
|
+
domTextChanges: texts2,
|
|
1621
|
+
rendersPerTextChange: texts2 ? +(rec.totals.renders / texts2).toFixed(1) : null
|
|
1622
|
+
},
|
|
1623
|
+
topRoots: rec.roots.slice(0, top).map((r) => rootLine(r, ms, reasons, hooks)),
|
|
1624
|
+
outsideRoots: rec.outsideRoots.slice(0, 3).map((r) => rootLine(r, ms, reasons, hooks)),
|
|
1625
|
+
topCauses: rec.causes.slice(0, 5).map((c) => ({
|
|
1626
|
+
key: c.key,
|
|
1627
|
+
events: c.events,
|
|
1628
|
+
commits: c.commits,
|
|
1629
|
+
...c.keys ? {
|
|
1630
|
+
keys: Object.entries(c.keys).sort((a, b) => b[1].changed + b[1].sameContent - (a[1].changed + a[1].sameContent)).slice(0, 6).map(([k, v]) => `${k}${v.sameContent ? ` (same content ${v.sameContent}/${v.changed + v.sameContent + v.unknown})` : ""}`).join(", ")
|
|
1631
|
+
} : {}
|
|
1632
|
+
})),
|
|
1633
|
+
actions,
|
|
1634
|
+
plugins: Object.fromEntries(
|
|
1635
|
+
Object.entries(rec.plugins).filter(([, section]) => section.active ?? Boolean(section.highlights?.length)).map(([name, section]) => [name, { version: section.version, highlights: section.highlights.slice(0, 3) }])
|
|
1636
|
+
),
|
|
1637
|
+
frames: {
|
|
1638
|
+
longTasks: rec.frames.longTasks.count,
|
|
1639
|
+
maxLongTaskMs: rec.frames.longTasks.maxMs,
|
|
1640
|
+
longFrames: rec.frames.loaf.length,
|
|
1641
|
+
worstFrameMs: rec.frames.loaf.reduce((m, f) => Math.max(m, f.duration), 0)
|
|
1642
|
+
},
|
|
1643
|
+
overhead: rec.overhead,
|
|
1644
|
+
...rec.memos?.length ? { memos: rec.memos.slice(0, 5).map(memoLine) } : {},
|
|
1645
|
+
warnings: [...rec.warnings, ...rec.errors.map((e) => `error: ${e}`)].slice(0, 10)
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// src/core/hook-names.ts
|
|
1650
|
+
var RENDER_MARK = "__rprInspectRender";
|
|
1651
|
+
var DISPATCHER_MARK = "__rpr_";
|
|
1652
|
+
var MEMO_CACHE_SENTINEL = /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel");
|
|
1653
|
+
var NOT_PENDING = Object.freeze({ pending: false, data: null, method: null, action: null });
|
|
1654
|
+
function inspectHooks(fiber) {
|
|
1655
|
+
const render = renderFunctionOf(fiber);
|
|
1656
|
+
const ref = renderer()?.currentDispatcherRef;
|
|
1657
|
+
if (!render || !ref) return null;
|
|
1658
|
+
const log = [];
|
|
1659
|
+
let hook = fiber.memoizedState;
|
|
1660
|
+
let index = 0;
|
|
1661
|
+
const next = () => {
|
|
1662
|
+
const current = hook;
|
|
1663
|
+
if (hook) hook = hook.next;
|
|
1664
|
+
index++;
|
|
1665
|
+
return current;
|
|
1666
|
+
};
|
|
1667
|
+
const logged = (primitive, first, context) => log.push({ primitive, index: first, context, stack: new Error().stack ?? "" });
|
|
1668
|
+
const stateHook = (primitive, count = 1) => {
|
|
1669
|
+
const first = index;
|
|
1670
|
+
const current = next();
|
|
1671
|
+
for (let i = 1; i < count; i++) next();
|
|
1672
|
+
logged(primitive, first);
|
|
1673
|
+
return current;
|
|
1674
|
+
};
|
|
1675
|
+
const noop = () => {
|
|
1676
|
+
};
|
|
1677
|
+
const readContext = (context) => context._currentValue;
|
|
1678
|
+
const dispatcher = {
|
|
1679
|
+
readContext,
|
|
1680
|
+
useContext: function __rpr_useContext(context) {
|
|
1681
|
+
logged("Context", null, context);
|
|
1682
|
+
return readContext(context);
|
|
1683
|
+
},
|
|
1684
|
+
useState: function __rpr_useState() {
|
|
1685
|
+
const h = stateHook("State");
|
|
1686
|
+
return [h?.memoizedState, noop];
|
|
1687
|
+
},
|
|
1688
|
+
useReducer: function __rpr_useReducer() {
|
|
1689
|
+
const h = stateHook("Reducer");
|
|
1690
|
+
return [h?.memoizedState, noop];
|
|
1691
|
+
},
|
|
1692
|
+
// A copy: a component that writes its ref in render would otherwise leave this run's noop setters in the real one.
|
|
1693
|
+
useRef: function __rpr_useRef() {
|
|
1694
|
+
const real = stateHook("Ref")?.memoizedState;
|
|
1695
|
+
return { current: real?.current };
|
|
1696
|
+
},
|
|
1697
|
+
useEffect: function __rpr_useEffect() {
|
|
1698
|
+
stateHook("Effect");
|
|
1699
|
+
},
|
|
1700
|
+
useLayoutEffect: function __rpr_useLayoutEffect() {
|
|
1701
|
+
stateHook("LayoutEffect");
|
|
1702
|
+
},
|
|
1703
|
+
useInsertionEffect: function __rpr_useInsertionEffect() {
|
|
1704
|
+
stateHook("InsertionEffect");
|
|
1705
|
+
},
|
|
1706
|
+
useImperativeHandle: function __rpr_useImperativeHandle() {
|
|
1707
|
+
stateHook("ImperativeHandle");
|
|
1708
|
+
},
|
|
1709
|
+
useCallback: function __rpr_useCallback(callback) {
|
|
1710
|
+
const h = stateHook("Callback");
|
|
1711
|
+
return Array.isArray(h?.memoizedState) ? h.memoizedState[0] : callback;
|
|
1712
|
+
},
|
|
1713
|
+
useMemo: function __rpr_useMemo(create) {
|
|
1714
|
+
const h = stateHook("Memo");
|
|
1715
|
+
return Array.isArray(h?.memoizedState) ? h.memoizedState[0] : create();
|
|
1716
|
+
},
|
|
1717
|
+
useDebugValue: function __rpr_useDebugValue() {
|
|
1718
|
+
},
|
|
1719
|
+
useDeferredValue: function __rpr_useDeferredValue(value) {
|
|
1720
|
+
const h = stateHook("DeferredValue");
|
|
1721
|
+
return h ? h.memoizedState : value;
|
|
1722
|
+
},
|
|
1723
|
+
// React keeps an isPending state hook and the start function hook.
|
|
1724
|
+
useTransition: function __rpr_useTransition() {
|
|
1725
|
+
const h = stateHook("Transition", 2);
|
|
1726
|
+
return [Boolean(h?.memoizedState), noop];
|
|
1727
|
+
},
|
|
1728
|
+
// The store hook plus the effect that subscribes to it.
|
|
1729
|
+
useSyncExternalStore: function __rpr_useSyncExternalStore(_subscribe, getSnapshot) {
|
|
1730
|
+
const h = stateHook("SyncExternalStore", 2);
|
|
1731
|
+
return h ? h.memoizedState : getSnapshot();
|
|
1732
|
+
},
|
|
1733
|
+
useId: function __rpr_useId() {
|
|
1734
|
+
return stateHook("Id")?.memoizedState ?? "";
|
|
1735
|
+
},
|
|
1736
|
+
useMutableSource: function __rpr_useMutableSource() {
|
|
1737
|
+
stateHook("MutableSource");
|
|
1738
|
+
},
|
|
1739
|
+
useCacheRefresh: function __rpr_useCacheRefresh() {
|
|
1740
|
+
stateHook("CacheRefresh");
|
|
1741
|
+
return noop;
|
|
1742
|
+
},
|
|
1743
|
+
// ---- React 19 ------------------------------------------------------------------------------------------------
|
|
1744
|
+
// Without these the stand-in throws at the first one and the component's hooks are named only up to it.
|
|
1745
|
+
/** A context is read like `useContext`; a promise gives up its value only if it already has one. */
|
|
1746
|
+
use: function __rpr_use(usable) {
|
|
1747
|
+
if (usable && typeof usable === "object") {
|
|
1748
|
+
if ("_currentValue" in usable) {
|
|
1749
|
+
const context = usable;
|
|
1750
|
+
logged("Context", null, context);
|
|
1751
|
+
return readContext(context);
|
|
1752
|
+
}
|
|
1753
|
+
const thenable = usable;
|
|
1754
|
+
if (typeof thenable.then === "function") {
|
|
1755
|
+
if (thenable.status === "fulfilled") return thenable.value;
|
|
1756
|
+
if (thenable.status === "rejected") throw thenable.reason;
|
|
1757
|
+
throw new Error("use() is still pending");
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return void 0;
|
|
1761
|
+
},
|
|
1762
|
+
// The state, the pending flag and the action queue.
|
|
1763
|
+
useActionState: function __rpr_useActionState(_action, initialState) {
|
|
1764
|
+
const h = stateHook("ActionState", 3);
|
|
1765
|
+
return [h ? h.memoizedState : initialState, noop, false];
|
|
1766
|
+
},
|
|
1767
|
+
useFormState: function __rpr_useFormState(_action, initialState) {
|
|
1768
|
+
const h = stateHook("FormState", 3);
|
|
1769
|
+
return [h ? h.memoizedState : initialState, noop, false];
|
|
1770
|
+
},
|
|
1771
|
+
useOptimistic: function __rpr_useOptimistic(passthrough) {
|
|
1772
|
+
const h = stateHook("Optimistic");
|
|
1773
|
+
return [h ? h.memoizedState : passthrough, noop];
|
|
1774
|
+
},
|
|
1775
|
+
useEffectEvent: function __rpr_useEffectEvent(callback) {
|
|
1776
|
+
stateHook("EffectEvent");
|
|
1777
|
+
return typeof callback === "function" ? callback : noop;
|
|
1778
|
+
},
|
|
1779
|
+
// The compiler's cache lives on the fiber's update queue, not in the hook list, so it takes no cell; the
|
|
1780
|
+
// sentinel makes the compiled body recompute rather than read.
|
|
1781
|
+
useMemoCache: function __rpr_useMemoCache(size) {
|
|
1782
|
+
return new Array(typeof size === "number" ? size : 0).fill(MEMO_CACHE_SENTINEL);
|
|
1783
|
+
},
|
|
1784
|
+
// What `useFormStatus` reads; outside a form action there is nothing pending.
|
|
1785
|
+
useHostTransitionStatus: function __rpr_useHostTransitionStatus() {
|
|
1786
|
+
return NOT_PENDING;
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
const restoreContexts = setupContexts(fiber);
|
|
1790
|
+
const errorCtor = Error;
|
|
1791
|
+
const stackLimit = errorCtor.stackTraceLimit;
|
|
1792
|
+
errorCtor.stackTraceLimit = 100;
|
|
1793
|
+
const useH = ref.H !== void 0 || !("current" in ref);
|
|
1794
|
+
const previous = useH ? ref.H : ref.current;
|
|
1795
|
+
if (useH) ref.H = dispatcher;
|
|
1796
|
+
else ref.current = dispatcher;
|
|
1797
|
+
const props = fiber.memoizedProps;
|
|
1798
|
+
const second = fiber.tag === Tag.ForwardRef ? fiber.ref ?? null : void 0;
|
|
1799
|
+
const marker = {
|
|
1800
|
+
[RENDER_MARK]() {
|
|
1801
|
+
render(props, second);
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
try {
|
|
1805
|
+
marker[RENDER_MARK]();
|
|
1806
|
+
} catch {
|
|
1807
|
+
} finally {
|
|
1808
|
+
if (useH) ref.H = previous;
|
|
1809
|
+
else ref.current = previous;
|
|
1810
|
+
restoreContexts();
|
|
1811
|
+
errorCtor.stackTraceLimit = stackLimit;
|
|
1812
|
+
}
|
|
1813
|
+
return buildInfo(fiber, log);
|
|
1814
|
+
}
|
|
1815
|
+
function renderFunctionOf(fiber) {
|
|
1816
|
+
if (fiber.tag === Tag.ForwardRef) return typeof fiber.type?.render === "function" ? fiber.type.render : null;
|
|
1817
|
+
if (fiber.tag === Tag.FunctionComponent || fiber.tag === Tag.SimpleMemoComponent || fiber.tag === Tag.IndeterminateComponent) {
|
|
1818
|
+
return typeof fiber.type === "function" && !fiber.type.prototype?.isReactComponent ? fiber.type : null;
|
|
1819
|
+
}
|
|
1820
|
+
return null;
|
|
1821
|
+
}
|
|
1822
|
+
function setupContexts(fiber) {
|
|
1823
|
+
const saved = /* @__PURE__ */ new Map();
|
|
1824
|
+
for (let node = fiber.return; node; node = node.return) {
|
|
1825
|
+
if (node.tag !== 10) continue;
|
|
1826
|
+
const context = node.type?._context ?? node.type;
|
|
1827
|
+
if (context && !saved.has(context)) {
|
|
1828
|
+
saved.set(context, context._currentValue);
|
|
1829
|
+
context._currentValue = node.memoizedProps?.value;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
return () => saved.forEach((value, context) => context._currentValue = value);
|
|
1833
|
+
}
|
|
1834
|
+
var reactExport = (primitive) => `use${primitive}`;
|
|
1835
|
+
function buildInfo(fiber, log) {
|
|
1836
|
+
const hooks = /* @__PURE__ */ new Map();
|
|
1837
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
1838
|
+
for (const entry of log) {
|
|
1839
|
+
if (entry.index === null && !entry.context) continue;
|
|
1840
|
+
const frames = parseStack(entry.stack);
|
|
1841
|
+
const renderAt = frames.findIndex((f) => f.fn.includes(RENDER_MARK));
|
|
1842
|
+
const dispatcherAt = frames.findIndex((f) => f.fn.includes(DISPATCHER_MARK));
|
|
1843
|
+
if (renderAt < 1 || dispatcherAt < 0 || dispatcherAt >= renderAt) continue;
|
|
1844
|
+
let start = dispatcherAt + 1;
|
|
1845
|
+
if (start < renderAt && frames[start].fn.split(".").pop() === reactExport(entry.primitive)) start++;
|
|
1846
|
+
const component = renderAt - 1;
|
|
1847
|
+
const steps = frames.slice(start, component).map((f) => ({ name: f.fn.split(".").pop() || "", library: libraryOf(f.url) })).filter((step) => step.name && step.library !== "react-perf-recorder").reverse();
|
|
1848
|
+
const site = frames[component];
|
|
1849
|
+
const at = steps.findIndex((step) => step.library !== null);
|
|
1850
|
+
const library = at >= 0 ? steps.slice(at).find((step) => step.library)?.library || "deps" : void 0;
|
|
1851
|
+
const info = {
|
|
1852
|
+
...entry.index !== null ? { type: hookTypeAt(fiber, entry.index) } : {},
|
|
1853
|
+
path: [...steps.map((step) => step.name), entry.primitive],
|
|
1854
|
+
...library ? { library, libraryAt: at } : {},
|
|
1855
|
+
...site ? { generated: { url: site.url, line: site.line, column: site.column } } : {}
|
|
1856
|
+
};
|
|
1857
|
+
if (entry.index !== null) hooks.set(entry.index, info);
|
|
1858
|
+
else if (entry.context && !contexts.has(entry.context)) contexts.set(entry.context, info);
|
|
1859
|
+
}
|
|
1860
|
+
return { hooks, contexts };
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
// src/core/env/origin.ts
|
|
1864
|
+
var EFFECT_FRAMES = /flushPassiveEffects|flushPendingEffects|commitPassiveMount|commitHookEffectList/;
|
|
1865
|
+
var REACT_FRAMES = /^(react-dom|react|scheduler)$/;
|
|
1866
|
+
function captureStack() {
|
|
1867
|
+
const holder = Error;
|
|
1868
|
+
const limit = holder.stackTraceLimit;
|
|
1869
|
+
holder.stackTraceLimit = 40;
|
|
1870
|
+
try {
|
|
1871
|
+
return new Error().stack ?? "";
|
|
1872
|
+
} finally {
|
|
1873
|
+
holder.stackTraceLimit = limit;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
function updateOrigin() {
|
|
1877
|
+
const frames = parseStack(captureStack()).slice(1);
|
|
1878
|
+
const own2 = frames.filter((f) => libraryOf(f.url) !== "react-perf-recorder");
|
|
1879
|
+
if (!own2.length) return null;
|
|
1880
|
+
const inEffect = own2.some((f) => EFFECT_FRAMES.test(f.fn));
|
|
1881
|
+
const kind = inEffect ? "effect" : "update";
|
|
1882
|
+
const react = own2[0].url;
|
|
1883
|
+
const app = own2.find((f) => f.url !== react && libraryOf(f.url) === null);
|
|
1884
|
+
if (app) {
|
|
1885
|
+
const name2 = app.fn.split(".").pop()?.replace(/^bound /, "") ?? "";
|
|
1886
|
+
const file = servedPath(app.url);
|
|
1887
|
+
return { text: `${kind}${name2 ? ` ${name2}` : ""} @ ${file}`, kind };
|
|
1888
|
+
}
|
|
1889
|
+
const outside = own2.find((f) => {
|
|
1890
|
+
const library2 = libraryOf(f.url);
|
|
1891
|
+
return f.url !== react && library2 !== null && !REACT_FRAMES.test(library2);
|
|
1892
|
+
});
|
|
1893
|
+
if (!outside) return null;
|
|
1894
|
+
const library = libraryOf(outside.url) || "package";
|
|
1895
|
+
const name = outside.fn.split(".").pop()?.replace(/^bound /, "") ?? "";
|
|
1896
|
+
return { text: `${kind}${name ? ` ${name}` : ""} (${library})`, kind };
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// src/core/env/timers.ts
|
|
1900
|
+
var sink = null;
|
|
1901
|
+
var running = null;
|
|
1902
|
+
var installed = false;
|
|
1903
|
+
var texts = /* @__PURE__ */ new WeakMap();
|
|
1904
|
+
var own = /* @__PURE__ */ new WeakMap();
|
|
1905
|
+
var order = 0;
|
|
1906
|
+
var nextOrder = () => ++order;
|
|
1907
|
+
function setTimerSink(next) {
|
|
1908
|
+
sink = next;
|
|
1909
|
+
}
|
|
1910
|
+
function runningTimer() {
|
|
1911
|
+
return running ? timerText(running) : null;
|
|
1912
|
+
}
|
|
1913
|
+
function runningTimerLibrary() {
|
|
1914
|
+
return running ? libraryOfCaller(running) : null;
|
|
1915
|
+
}
|
|
1916
|
+
var callers = /* @__PURE__ */ new WeakMap();
|
|
1917
|
+
function libraryOfCaller(t) {
|
|
1918
|
+
let library = callers.get(t.origin);
|
|
1919
|
+
if (library === void 0) {
|
|
1920
|
+
const caller = parseStack(t.origin.stack ?? "").slice(1).find((f) => libraryOf(f.url) !== "react-perf-recorder");
|
|
1921
|
+
callers.set(t.origin, library = caller ? libraryOf(caller.url) : null);
|
|
1922
|
+
}
|
|
1923
|
+
return library;
|
|
1924
|
+
}
|
|
1925
|
+
function timerText(t) {
|
|
1926
|
+
let text = texts.get(t.origin);
|
|
1927
|
+
if (text) return text;
|
|
1928
|
+
const frames = parseStack(t.origin.stack ?? "").slice(1).filter((f) => libraryOf(f.url) !== "react-perf-recorder");
|
|
1929
|
+
const app = frames.find((f) => libraryOf(f.url) === null);
|
|
1930
|
+
const name = (app?.fn.split(".").pop() || t.fn.name || "").replace(/^bound /, "");
|
|
1931
|
+
if (app) {
|
|
1932
|
+
const file = servedPath(app.url);
|
|
1933
|
+
text = `timer ${t.kind}${name ? ` ${name}` : ""} @ ${file}`;
|
|
1934
|
+
} else {
|
|
1935
|
+
const library = frames.map((f) => libraryOf(f.url)).find(Boolean);
|
|
1936
|
+
text = `timer ${t.kind}${library ? ` (${library})` : ""}`;
|
|
1937
|
+
}
|
|
1938
|
+
texts.set(t.origin, text);
|
|
1939
|
+
return text;
|
|
1940
|
+
}
|
|
1941
|
+
function scheduledByRecorder(stack) {
|
|
1942
|
+
const caller = parseStack(stack)[1];
|
|
1943
|
+
return caller !== void 0 && libraryOf(caller.url) === "react-perf-recorder";
|
|
1944
|
+
}
|
|
1945
|
+
function ours(t) {
|
|
1946
|
+
let result = own.get(t.origin);
|
|
1947
|
+
if (result === void 0) own.set(t.origin, result = scheduledByRecorder(t.origin.stack ?? ""));
|
|
1948
|
+
return result;
|
|
1949
|
+
}
|
|
1950
|
+
function wrap(kind) {
|
|
1951
|
+
const target2 = window;
|
|
1952
|
+
const original = target2[kind];
|
|
1953
|
+
if (typeof original !== "function") return;
|
|
1954
|
+
const wrapped = function(callback, ...rest) {
|
|
1955
|
+
if (typeof callback !== "function") return original.call(window, callback, ...rest);
|
|
1956
|
+
const scheduled2 = { kind, fn: callback, origin: new Error() };
|
|
1957
|
+
return original.call(
|
|
1958
|
+
window,
|
|
1959
|
+
function(...args) {
|
|
1960
|
+
const s = sink;
|
|
1961
|
+
if (!s) return callback.apply(this, args);
|
|
1962
|
+
const outer = running;
|
|
1963
|
+
const startedAt = nextOrder();
|
|
1964
|
+
running = scheduled2;
|
|
1965
|
+
try {
|
|
1966
|
+
return callback.apply(this, args);
|
|
1967
|
+
} finally {
|
|
1968
|
+
running = outer;
|
|
1969
|
+
s.after(
|
|
1970
|
+
() => timerText(scheduled2),
|
|
1971
|
+
() => ours(scheduled2),
|
|
1972
|
+
() => libraryOfCaller(scheduled2),
|
|
1973
|
+
startedAt
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
},
|
|
1977
|
+
...rest
|
|
1978
|
+
);
|
|
1979
|
+
};
|
|
1980
|
+
Object.defineProperty(wrapped, "name", { value: kind });
|
|
1981
|
+
target2[kind] = wrapped;
|
|
1982
|
+
}
|
|
1983
|
+
function installTimers() {
|
|
1984
|
+
if (installed || typeof window === "undefined") return;
|
|
1985
|
+
installed = true;
|
|
1986
|
+
wrap("setTimeout");
|
|
1987
|
+
wrap("setInterval");
|
|
1988
|
+
wrap("requestAnimationFrame");
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// src/core/recorder.ts
|
|
1992
|
+
var PATH_DEPTH = 4;
|
|
1993
|
+
function pathText(ref, base) {
|
|
1994
|
+
const names2 = [];
|
|
1995
|
+
for (let r = ref; r && names2.length < PATH_DEPTH; r = r.up) names2.push(r.name);
|
|
1996
|
+
for (let i = 0; i < base.length && names2.length < PATH_DEPTH; i++) names2.push(base[i]);
|
|
1997
|
+
return names2.join(" < ");
|
|
1998
|
+
}
|
|
1999
|
+
var MAX_TIMES = 2e3;
|
|
2000
|
+
var SAMPLED_PARENTS = 50;
|
|
2001
|
+
var MAX_UPDATE_NOTES = 10;
|
|
2002
|
+
var MAX_CAUSE_KEYS = 300;
|
|
2003
|
+
var MAX_CHAIN_NODES = 5e4;
|
|
2004
|
+
var CHAINS_PER_COMPONENT = 3;
|
|
2005
|
+
var MAX_CHAIN_LINKS = 20;
|
|
2006
|
+
var WAYS_PER_COMMIT = 30;
|
|
2007
|
+
var MAX_SEGMENT_COMMITS = 2e4;
|
|
2008
|
+
var currentEventType = () => {
|
|
2009
|
+
const event = globalThis.event;
|
|
2010
|
+
return event && typeof event.type === "string" ? event.type : void 0;
|
|
2011
|
+
};
|
|
2012
|
+
var messageSource = () => {
|
|
2013
|
+
const event = globalThis.event;
|
|
2014
|
+
const target2 = event?.type === "message" ? event.target : null;
|
|
2015
|
+
if (!target2 || target2 === globalThis || typeof MessagePort !== "undefined" && target2 instanceof MessagePort) return void 0;
|
|
2016
|
+
return target2.constructor?.name || void 0;
|
|
2017
|
+
};
|
|
2018
|
+
var shallowEqual = (x, y) => {
|
|
2019
|
+
const a = x;
|
|
2020
|
+
const b = y;
|
|
2021
|
+
if (a === b) return true;
|
|
2022
|
+
if (!a || !b) return false;
|
|
2023
|
+
const keys = Object.keys(a);
|
|
2024
|
+
return keys.length === Object.keys(b).length && keys.every((key) => Object.is(a[key], b[key]));
|
|
2025
|
+
};
|
|
2026
|
+
var medianGapMs = (times) => {
|
|
2027
|
+
const gap = medianGap(times);
|
|
2028
|
+
return gap === null ? null : Math.round(gap);
|
|
2029
|
+
};
|
|
2030
|
+
var Recorder = class {
|
|
2031
|
+
constructor(deps, options) {
|
|
2032
|
+
this.deps = deps;
|
|
2033
|
+
this.options = options;
|
|
2034
|
+
this.t0 = performance.now();
|
|
2035
|
+
this.startedAt = /* @__PURE__ */ new Date();
|
|
2036
|
+
this.records = /* @__PURE__ */ new WeakMap();
|
|
2037
|
+
this.rootsByKey = /* @__PURE__ */ new Map();
|
|
2038
|
+
this.rootList = [];
|
|
2039
|
+
this.reasonIds = /* @__PURE__ */ new Map();
|
|
2040
|
+
this.reasonIdsByFields = /* @__PURE__ */ new Map();
|
|
2041
|
+
this.memoHits = new MemoHits((f) => sourceOf(f, this.config.projectRoot));
|
|
2042
|
+
this.reasonList = [];
|
|
2043
|
+
this.components = /* @__PURE__ */ new Map();
|
|
2044
|
+
this.watch = /* @__PURE__ */ new Map();
|
|
2045
|
+
this.zoneNodes = /* @__PURE__ */ new Map();
|
|
2046
|
+
this.zones = /* @__PURE__ */ new Map();
|
|
2047
|
+
this.causeStats = /* @__PURE__ */ new Map();
|
|
2048
|
+
this.commitList = [];
|
|
2049
|
+
this.segmentCommits = [];
|
|
2050
|
+
this.commitTimes = [];
|
|
2051
|
+
this.bigCommits = [];
|
|
2052
|
+
this.navigations = [];
|
|
2053
|
+
this.hmr = [];
|
|
2054
|
+
this.errors = [];
|
|
2055
|
+
this.warnings = [];
|
|
2056
|
+
this.dom = new DomWatcher();
|
|
2057
|
+
/** Whether childLanes can be trusted to point at fresh updates; React 19 answers no and the walk widens. */
|
|
2058
|
+
this.narrowUpdateWalk = true;
|
|
2059
|
+
this.hook = null;
|
|
2060
|
+
this.stopHistory = null;
|
|
2061
|
+
this.roots = [];
|
|
2062
|
+
this.conditions = {};
|
|
2063
|
+
this.truncated = false;
|
|
2064
|
+
/** Commits so far, kept or not: the list stops at the timeline limit, the ids do not. */
|
|
2065
|
+
this.commitSeq = 0;
|
|
2066
|
+
this.stopped = false;
|
|
2067
|
+
this.overlayMs = 0;
|
|
2068
|
+
this.highlighted = false;
|
|
2069
|
+
/** Fibers a cause already names since the last commit. */
|
|
2070
|
+
this.claimed = /* @__PURE__ */ new WeakSet();
|
|
2071
|
+
/** Where updates of this commit window came from, used only for the components no other event explains. */
|
|
2072
|
+
this.origins = [];
|
|
2073
|
+
this.frameCount = 0;
|
|
2074
|
+
this.counting = false;
|
|
2075
|
+
this.totals = {
|
|
2076
|
+
commits: 0,
|
|
2077
|
+
commitsInScope: 0,
|
|
2078
|
+
renders: 0,
|
|
2079
|
+
mounts: 0,
|
|
2080
|
+
rendersFromOutside: 0,
|
|
2081
|
+
rendersWithoutDom: 0,
|
|
2082
|
+
causesDropped: 0,
|
|
2083
|
+
lanes: {}
|
|
2084
|
+
};
|
|
2085
|
+
/**
|
|
2086
|
+
* Left out of a path: an unnamed wrapper, a provider-only component, a package's own. Cached by type, as the
|
|
2087
|
+
* answer is the same for every instance.
|
|
2088
|
+
*/
|
|
2089
|
+
this.structuralByType = /* @__PURE__ */ new WeakMap();
|
|
2090
|
+
this.chainNodes = [];
|
|
2091
|
+
/** Interned links: parent id → name → reason id → node id. */
|
|
2092
|
+
this.chainIndex = /* @__PURE__ */ new Map();
|
|
2093
|
+
/** A root's links are told apart by the root itself: two roots of one name start two chains. */
|
|
2094
|
+
this.rootChains = /* @__PURE__ */ new Map();
|
|
2095
|
+
this.config = deps.config;
|
|
2096
|
+
this.wrapperRe = new RegExp(this.config.wrapperPattern || "^$");
|
|
2097
|
+
this.prune = options.prune !== false;
|
|
2098
|
+
this.scope = options.scope ? new ScopeTracker(options.scope) : null;
|
|
2099
|
+
for (const name of options.watch ?? []) this.watch.set(name, { mounted: 0, renders: 0, byRoot: /* @__PURE__ */ new Map() });
|
|
2100
|
+
this.frames = new FrameWatcher({
|
|
2101
|
+
t0: this.t0,
|
|
2102
|
+
projectRoot: this.config.projectRoot,
|
|
2103
|
+
countCommits: (from, to) => this.countCommits(from, to),
|
|
2104
|
+
onFrame: (frame) => this.emit({ k: "frame", frame }),
|
|
2105
|
+
onLatency: (entry) => this.emit({ k: "latency", entry })
|
|
2106
|
+
});
|
|
2107
|
+
this.actions = options.actions === false ? null : new ActionTracker(
|
|
2108
|
+
{
|
|
2109
|
+
values: this.config.actions.values,
|
|
2110
|
+
secretSelector: this.config.actions.secretSelector,
|
|
2111
|
+
wrapperPattern: this.wrapperRe,
|
|
2112
|
+
projectRoot: this.config.projectRoot,
|
|
2113
|
+
ownHost: deps.ownHost,
|
|
2114
|
+
inScope: (el) => this.scope ? this.scopeHosts().some((h) => h.contains(el)) : void 0
|
|
2115
|
+
},
|
|
2116
|
+
() => this.now(),
|
|
2117
|
+
(action) => this.emit({ k: "action", action })
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
structural(name, f) {
|
|
2121
|
+
const type = f.type;
|
|
2122
|
+
const keyed = type !== null && (typeof type === "object" || typeof type === "function");
|
|
2123
|
+
const known2 = keyed ? this.structuralByType.get(type) : void 0;
|
|
2124
|
+
if (known2 !== void 0) return known2;
|
|
2125
|
+
const result = this.wrapperRe.test(name) || isProvider(name) || wrapsProvider(f) || isLibraryFiber(f);
|
|
2126
|
+
if (keyed) this.structuralByType.set(type, result);
|
|
2127
|
+
return result;
|
|
2128
|
+
}
|
|
2129
|
+
now() {
|
|
2130
|
+
return performance.now() - this.t0;
|
|
2131
|
+
}
|
|
2132
|
+
get startConditions() {
|
|
2133
|
+
return this.conditions;
|
|
2134
|
+
}
|
|
2135
|
+
get scopeInfo() {
|
|
2136
|
+
return this.scope ? { name: this.scope.name, source: this.scope.source } : null;
|
|
2137
|
+
}
|
|
2138
|
+
start() {
|
|
2139
|
+
this.roots = this.scope ? [hostRootOf(this.scope.target)].filter((r) => Boolean(r)) : findRoots();
|
|
2140
|
+
if (!this.roots.length) throw new RecorderError("NO_ROOT", "React 18 dev root not found on the page");
|
|
2141
|
+
for (const root of this.roots) {
|
|
2142
|
+
const owner = hookOwner(root);
|
|
2143
|
+
if (owner) throw new RecorderError("BUSY", `root.current is already hooked by ${owner}: wait for it to finish or call its stop()`, owner);
|
|
2144
|
+
}
|
|
2145
|
+
this.resolveZones();
|
|
2146
|
+
for (const root of this.roots) this.seed(root.current);
|
|
2147
|
+
this.conditions = this.readConditions();
|
|
2148
|
+
this.deps.plugins.start(this.pluginSession(), this.t0);
|
|
2149
|
+
if (this.scope) this.dom.setScopeHosts(this.scopeHosts());
|
|
2150
|
+
this.dom.start();
|
|
2151
|
+
this.hook = hookCommits(
|
|
2152
|
+
this.roots,
|
|
2153
|
+
this.options.source ?? "panel",
|
|
2154
|
+
(info) => this.onCommit(info),
|
|
2155
|
+
() => this.noteUpdate()
|
|
2156
|
+
);
|
|
2157
|
+
this.deps.plugins.targets = () => this.freshUpdates();
|
|
2158
|
+
setTimerSink({
|
|
2159
|
+
after: (text, ours2, library, startedAt) => {
|
|
2160
|
+
const plugins = this.deps.plugins;
|
|
2161
|
+
if (!this.freshUpdates(false).size) return;
|
|
2162
|
+
if (ours2()) return;
|
|
2163
|
+
if (plugins.hasWaiting && plugins.deliver(library(), () => this.freshUpdates(), startedAt)) return;
|
|
2164
|
+
plugins.emit("core", { type: text() }, this.freshUpdates());
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
this.frames.start();
|
|
2168
|
+
if (this.options.frames) {
|
|
2169
|
+
this.counting = true;
|
|
2170
|
+
const tick = () => {
|
|
2171
|
+
if (!this.counting) return;
|
|
2172
|
+
this.frameCount++;
|
|
2173
|
+
requestAnimationFrame(tick);
|
|
2174
|
+
};
|
|
2175
|
+
requestAnimationFrame(tick);
|
|
2176
|
+
}
|
|
2177
|
+
this.actions?.start();
|
|
2178
|
+
this.stopHistory = trackHistory(
|
|
2179
|
+
() => this.now(),
|
|
2180
|
+
(nav) => {
|
|
2181
|
+
if (this.navigations.length < 500) this.navigations.push(nav);
|
|
2182
|
+
this.emit({ k: "nav", nav });
|
|
2183
|
+
this.deps.plugins.emit("core", { type: `navigation ${nav.type}` });
|
|
2184
|
+
if (nav.type === "pop") this.actions?.navigation(nav.url);
|
|
2185
|
+
}
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
noteHmr(type, paths) {
|
|
2189
|
+
const entry = { atMs: Math.round(this.now()), type, paths: paths.slice(0, 10) };
|
|
2190
|
+
this.hmr.push(entry);
|
|
2191
|
+
this.emit({ k: "hmr", ...entry });
|
|
2192
|
+
if (!this.warnings.some((w) => w.startsWith("HMR")))
|
|
2193
|
+
this.warnings.push("HMR update during the recording: commits around it include react-refresh work");
|
|
2194
|
+
}
|
|
2195
|
+
/** What the panel shows while the recording runs: counters and the roots leading so far, with their reason. */
|
|
2196
|
+
live() {
|
|
2197
|
+
const elapsedMs = Math.round(this.now());
|
|
2198
|
+
return {
|
|
2199
|
+
commits: this.totals.commits,
|
|
2200
|
+
commitsInScope: this.totals.commitsInScope,
|
|
2201
|
+
renders: this.totals.renders,
|
|
2202
|
+
rendersPerSec: elapsedMs > 200 ? Math.round(this.totals.renders * 1e3 / elapsedMs) : 0,
|
|
2203
|
+
elapsedMs,
|
|
2204
|
+
scopeState: this.scope?.state ?? null,
|
|
2205
|
+
topRoots: [...this.rootList].sort((a, b) => b.cascade - a.cascade).slice(0, 3).map((agg) => ({
|
|
2206
|
+
name: agg.name,
|
|
2207
|
+
hits: agg.hits,
|
|
2208
|
+
perHit: agg.hits ? Math.round(agg.cascade / agg.hits) : 0,
|
|
2209
|
+
...(() => {
|
|
2210
|
+
const info = this.reasonList[topEntries(agg.reasons, 1)[0]?.[0] ?? -1];
|
|
2211
|
+
return info ? { reason: textOf(info), info } : { reason: "" };
|
|
2212
|
+
})()
|
|
2213
|
+
}))
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
stop() {
|
|
2217
|
+
if (this.stopped) throw new RecorderError("NOT_RECORDING", "recording already stopped");
|
|
2218
|
+
this.stopped = true;
|
|
2219
|
+
setTimerSink(null);
|
|
2220
|
+
this.deps.plugins.targets = null;
|
|
2221
|
+
const hookErrors = this.hook?.stop() ?? [];
|
|
2222
|
+
this.errors.push(...hookErrors);
|
|
2223
|
+
this.dom.stop();
|
|
2224
|
+
this.frames.stop();
|
|
2225
|
+
this.counting = false;
|
|
2226
|
+
this.actions?.stop();
|
|
2227
|
+
this.stopHistory?.();
|
|
2228
|
+
const sections = this.deps.plugins.stop(this.pluginSession());
|
|
2229
|
+
this.warnings.push(...this.deps.plugins.warnings.splice(0));
|
|
2230
|
+
const conditionsAfter = this.readConditions();
|
|
2231
|
+
this.overlayMs += this.deps.highlight?.takeCostMs?.() ?? 0;
|
|
2232
|
+
const durationMs = Math.round(this.now());
|
|
2233
|
+
this.emit({ k: "end", atMs: durationMs });
|
|
2234
|
+
return this.build(durationMs, sections, conditionsAfter);
|
|
2235
|
+
}
|
|
2236
|
+
// ---- commits -------------------------------------------------------------------------------------------------
|
|
2237
|
+
onCommit({ fiber, lanes }) {
|
|
2238
|
+
const started = performance.now();
|
|
2239
|
+
const t = Math.round(started - this.t0);
|
|
2240
|
+
this.totals.commits++;
|
|
2241
|
+
if (this.commitTimes.length < MAX_SEGMENT_COMMITS) this.commitTimes.push(t);
|
|
2242
|
+
const lane = laneLabel(lanes);
|
|
2243
|
+
const event = currentEventType();
|
|
2244
|
+
const source = event === "message" ? messageSource() : void 0;
|
|
2245
|
+
this.claimed = /* @__PURE__ */ new WeakSet();
|
|
2246
|
+
const origins = this.origins;
|
|
2247
|
+
this.origins = [];
|
|
2248
|
+
const timer = runningTimer();
|
|
2249
|
+
if (timer && !(this.deps.plugins.hasWaiting && this.deps.plugins.deliver(runningTimerLibrary(), () => null)))
|
|
2250
|
+
this.deps.plugins.emit("core", { type: timer });
|
|
2251
|
+
const causes = this.deps.plugins.drain();
|
|
2252
|
+
this.deps.plugins.commit(this.pluginSession());
|
|
2253
|
+
const c = {
|
|
2254
|
+
t,
|
|
2255
|
+
renders: 0,
|
|
2256
|
+
renderMs: 0,
|
|
2257
|
+
noDom: 0,
|
|
2258
|
+
cascade: /* @__PURE__ */ new Map(),
|
|
2259
|
+
sampledParents: this.options.sampleReasons ? /* @__PURE__ */ new Map() : null,
|
|
2260
|
+
reasons: /* @__PURE__ */ new Map(),
|
|
2261
|
+
outside: null,
|
|
2262
|
+
pairs: [],
|
|
2263
|
+
withoutDom: /* @__PURE__ */ new Set(),
|
|
2264
|
+
mounted: /* @__PURE__ */ new Set(),
|
|
2265
|
+
ways: this.options.sampleReasons ? null : /* @__PURE__ */ new Map(),
|
|
2266
|
+
touched: this.dom.takeForCommit()
|
|
2267
|
+
};
|
|
2268
|
+
if (this.scope) {
|
|
2269
|
+
const prevs = [];
|
|
2270
|
+
const resolution = this.scope.resolve(fiber, (f) => this.visitChain(f, prevs), t);
|
|
2271
|
+
if (resolution.status === "found") this.scanScope(resolution, prevs, c);
|
|
2272
|
+
else if (resolution.status === "lost" && this.scope.lostAtMs.at(-1) === t) this.emit({ k: "scope", atMs: t, state: "lost" });
|
|
2273
|
+
} else {
|
|
2274
|
+
this.scan(fiber, false, "", null, c, false);
|
|
2275
|
+
}
|
|
2276
|
+
this.finishCommit(c, causes, lane, event, source, origins);
|
|
2277
|
+
}
|
|
2278
|
+
visitChain(f, prevs) {
|
|
2279
|
+
const prev = this.prevOf(f);
|
|
2280
|
+
prevs.push(prev);
|
|
2281
|
+
const rendered = didRender(prev, f);
|
|
2282
|
+
this.remember(f);
|
|
2283
|
+
return { rendered };
|
|
2284
|
+
}
|
|
2285
|
+
scanScope(res, prevs, c) {
|
|
2286
|
+
const { fibers, rendered } = res;
|
|
2287
|
+
const n = fibers.length;
|
|
2288
|
+
const target2 = fibers[n - 1];
|
|
2289
|
+
if (res.remounted) {
|
|
2290
|
+
this.emit({ k: "scope", atMs: c.t, state: "remounted" });
|
|
2291
|
+
this.dom.setScopeHosts(this.scopeHosts());
|
|
2292
|
+
}
|
|
2293
|
+
const parentRendered = n >= 2 ? rendered[n - 2] : false;
|
|
2294
|
+
let rootKey = null;
|
|
2295
|
+
if (rendered[n - 1] && parentRendered) {
|
|
2296
|
+
let top = n - 2;
|
|
2297
|
+
while (top > 0 && rendered[top - 1]) top--;
|
|
2298
|
+
let j = top;
|
|
2299
|
+
while (j < n - 1 && !nameOf(fibers[j])) j++;
|
|
2300
|
+
if (j < n - 1 && prevs[j]) {
|
|
2301
|
+
const { agg } = this.hitRoot(fibers[j], nameOf(fibers[j]), this.chainPath(fibers, j), prevs[j], c, true);
|
|
2302
|
+
c.outside = agg;
|
|
2303
|
+
rootKey = agg.key;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
const prevTarget = prevs[n - 1];
|
|
2307
|
+
if (prevTarget) {
|
|
2308
|
+
this.records.set(target2, prevTarget);
|
|
2309
|
+
if (target2.alternate) this.records.set(target2.alternate, prevTarget);
|
|
2310
|
+
}
|
|
2311
|
+
this.scan(target2, parentRendered, this.chainPath(fibers, n - 1), rootKey, c, true);
|
|
2312
|
+
if (c.renders) this.dom.setScopeHosts(nearestHosts(target2));
|
|
2313
|
+
}
|
|
2314
|
+
chainPath(fibers, upTo) {
|
|
2315
|
+
const names2 = [];
|
|
2316
|
+
for (let i = upTo - 1; i >= 0 && names2.length < 4; i--) {
|
|
2317
|
+
const name = nameOf(fibers[i]);
|
|
2318
|
+
if (name && !this.structural(name, fibers[i])) names2.push(name);
|
|
2319
|
+
}
|
|
2320
|
+
return names2.join(" < ");
|
|
2321
|
+
}
|
|
2322
|
+
scan(start, parentRendered, path, rootKey, c, isolate) {
|
|
2323
|
+
const highlight = Boolean(this.deps.highlight) && this.deps.highlight.enabled !== false && this.options.highlight !== false;
|
|
2324
|
+
if (highlight) this.highlighted = true;
|
|
2325
|
+
const base = path ? path.split(" < ") : [];
|
|
2326
|
+
const stack = [[start, parentRendered, null, rootKey, null, null, -1]];
|
|
2327
|
+
while (stack.length) {
|
|
2328
|
+
const [f, parentDid, currentPath, currentKey, pending, zoneTag, currentChain] = stack.pop();
|
|
2329
|
+
let chain = currentChain;
|
|
2330
|
+
const prev = this.prevOf(f);
|
|
2331
|
+
const rendered = didRender(prev, f);
|
|
2332
|
+
const name = nameOf(f);
|
|
2333
|
+
let key = currentKey;
|
|
2334
|
+
let nextPending = pending;
|
|
2335
|
+
const zone = isHost(f) && this.zoneNodes.size ? this.zoneNodes.get(f.stateNode) ?? zoneTag : zoneTag;
|
|
2336
|
+
if (name && !prev && isComposite(f)) {
|
|
2337
|
+
if (highlight && !nextPending && mountedInPlace(f)) {
|
|
2338
|
+
nextPending = [name, f, isLibraryFiber(f)];
|
|
2339
|
+
c.mounted.add(f);
|
|
2340
|
+
}
|
|
2341
|
+
this.totals.mounts++;
|
|
2342
|
+
this.componentOf(name, f).mounts++;
|
|
2343
|
+
const agg = currentKey ? this.rootsByKey.get(currentKey) : void 0;
|
|
2344
|
+
if (agg) agg.mounts++;
|
|
2345
|
+
}
|
|
2346
|
+
if (name && rendered) {
|
|
2347
|
+
c.renders++;
|
|
2348
|
+
this.totals.renders++;
|
|
2349
|
+
this.memoHits.track(name, f, prev.state);
|
|
2350
|
+
const wasted = !touchedHas(c.touched, f);
|
|
2351
|
+
const comp = this.componentOf(name, f);
|
|
2352
|
+
comp.renders++;
|
|
2353
|
+
if (wasted) {
|
|
2354
|
+
comp.withoutDom++;
|
|
2355
|
+
c.noDom++;
|
|
2356
|
+
this.totals.rendersWithoutDom++;
|
|
2357
|
+
c.withoutDom.add(f);
|
|
2358
|
+
}
|
|
2359
|
+
let reasons = null;
|
|
2360
|
+
const ownWork = parentDid && isComposite(f) && prev !== void 0 && (prev.props === f.memoizedProps || (f.tag === Tag.MemoComponent || f.tag === Tag.SimpleMemoComponent) && shallowEqual(prev.props, f.memoizedProps));
|
|
2361
|
+
let rootAgg = null;
|
|
2362
|
+
if (!parentDid || ownWork) {
|
|
2363
|
+
const hit = this.hitRoot(f, name, pathText(currentPath, base), prev, c, false);
|
|
2364
|
+
key = hit.agg.key;
|
|
2365
|
+
reasons = hit.reasons;
|
|
2366
|
+
rootAgg = hit.agg;
|
|
2367
|
+
} else if (isComposite(f) && !isProvider(name)) {
|
|
2368
|
+
comp.byParent++;
|
|
2369
|
+
const sampled = c.sampledParents?.get(comp) ?? 0;
|
|
2370
|
+
if (sampled < SAMPLED_PARENTS) {
|
|
2371
|
+
c.sampledParents?.set(comp, sampled + 1);
|
|
2372
|
+
reasons = parentReason(prev, f, this.deps.plugins);
|
|
2373
|
+
} else comp.sampled = true;
|
|
2374
|
+
}
|
|
2375
|
+
let firstId = -1;
|
|
2376
|
+
for (const reason of reasons ?? []) {
|
|
2377
|
+
const id = this.reasonId(reason);
|
|
2378
|
+
if (firstId < 0) firstId = id;
|
|
2379
|
+
comp.reasons.set(id, (comp.reasons.get(id) ?? 0) + 1);
|
|
2380
|
+
}
|
|
2381
|
+
if (rootAgg) {
|
|
2382
|
+
chain = this.options.sampleReasons ? -1 : this.chainLink(-1, name, firstId, rootAgg);
|
|
2383
|
+
if (chain >= 0) c.ways?.set(chain, (c.ways.get(chain) ?? 0) + 1);
|
|
2384
|
+
} else if (chain >= 0 && isComposite(f) && !isProvider(name) && !comp.library && !comp.wrapper) {
|
|
2385
|
+
chain = this.chainLink(chain, name, firstId);
|
|
2386
|
+
comp.chains.set(chain, (comp.chains.get(chain) ?? 0) + 1);
|
|
2387
|
+
c.ways?.set(chain, (c.ways.get(chain) ?? 0) + 1);
|
|
2388
|
+
}
|
|
2389
|
+
const agg = key ? this.rootsByKey.get(key) : void 0;
|
|
2390
|
+
if (agg) {
|
|
2391
|
+
agg.cascade++;
|
|
2392
|
+
c.cascade.set(agg, (c.cascade.get(agg) ?? 0) + 1);
|
|
2393
|
+
if (agg.outside) this.totals.rendersFromOutside++;
|
|
2394
|
+
}
|
|
2395
|
+
const w = this.watch.get(name);
|
|
2396
|
+
if (w) {
|
|
2397
|
+
w.renders++;
|
|
2398
|
+
w.byRoot.set(agg ?? null, (w.byRoot.get(agg ?? null) ?? 0) + 1);
|
|
2399
|
+
}
|
|
2400
|
+
if (zone) {
|
|
2401
|
+
const z = this.zones.get(zone);
|
|
2402
|
+
if (z) z.renders++;
|
|
2403
|
+
}
|
|
2404
|
+
if (highlight && isComposite(f) && (!nextPending || nextPending[2] && !isLibraryFiber(f))) nextPending = [name, f, isLibraryFiber(f)];
|
|
2405
|
+
}
|
|
2406
|
+
if (nextPending && !nextPending[2] && (isHost(f) || f.tag === Tag.HostText)) {
|
|
2407
|
+
const el = isHost(f) ? f.stateNode : f.stateNode.parentElement;
|
|
2408
|
+
if (el) c.pairs.push([el, nextPending[0], nextPending[1]]);
|
|
2409
|
+
nextPending = null;
|
|
2410
|
+
}
|
|
2411
|
+
if (rendered || !prev) this.remember(f);
|
|
2412
|
+
if (!(isolate && f === start) && f.sibling) stack.push([f.sibling, parentDid, currentPath, currentKey, pending, zoneTag, currentChain]);
|
|
2413
|
+
const untouched = this.prune && f.alternate !== null && f.child === f.alternate.child;
|
|
2414
|
+
if (f.child && !untouched) {
|
|
2415
|
+
const childPath = name && !this.structural(name, f) ? { name, up: currentPath } : currentPath;
|
|
2416
|
+
stack.push([f.child, rendered, childPath, rendered ? key : currentKey, nextPending, zone, rendered ? chain : -1]);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
chainLink(up, name, reason, root) {
|
|
2421
|
+
let byReason;
|
|
2422
|
+
if (root) {
|
|
2423
|
+
byReason = this.rootChains.get(root);
|
|
2424
|
+
if (!byReason) this.rootChains.set(root, byReason = /* @__PURE__ */ new Map());
|
|
2425
|
+
} else {
|
|
2426
|
+
let byName = this.chainIndex.get(up);
|
|
2427
|
+
if (!byName) this.chainIndex.set(up, byName = /* @__PURE__ */ new Map());
|
|
2428
|
+
byReason = byName.get(name);
|
|
2429
|
+
if (!byReason) byName.set(name, byReason = /* @__PURE__ */ new Map());
|
|
2430
|
+
}
|
|
2431
|
+
const known2 = byReason.get(reason);
|
|
2432
|
+
if (known2 !== void 0) return known2;
|
|
2433
|
+
if (this.chainNodes.length >= MAX_CHAIN_NODES) return -1;
|
|
2434
|
+
const id = this.chainNodes.length;
|
|
2435
|
+
this.chainNodes.push({ up, name, reason, ...root ? { root } : {} });
|
|
2436
|
+
byReason.set(reason, id);
|
|
2437
|
+
return id;
|
|
2438
|
+
}
|
|
2439
|
+
/** The links of a chain from its root down; one past 20 keeps its root, the two below it and the last sixteen. */
|
|
2440
|
+
chainLinks(id, rootIndex) {
|
|
2441
|
+
const links = [];
|
|
2442
|
+
for (let at = id; at >= 0; at = this.chainNodes[at].up) {
|
|
2443
|
+
const node = this.chainNodes[at];
|
|
2444
|
+
const root = node.root ? rootIndex(node.root) : void 0;
|
|
2445
|
+
links.push({ name: node.name, ...node.reason >= 0 ? { reason: node.reason } : {}, ...root !== void 0 ? { root } : {} });
|
|
2446
|
+
}
|
|
2447
|
+
links.reverse();
|
|
2448
|
+
return links.length > MAX_CHAIN_LINKS ? [...links.slice(0, 3), { name: "\u2026", skipped: links.length - (MAX_CHAIN_LINKS - 1) }, ...links.slice(-(MAX_CHAIN_LINKS - 4))] : links;
|
|
2449
|
+
}
|
|
2450
|
+
/** A commit's busiest links and every link above them, so the tree they make has no holes. */
|
|
2451
|
+
commitWays(ways) {
|
|
2452
|
+
const kept = new Map(topEntries(ways, WAYS_PER_COMMIT));
|
|
2453
|
+
for (const id of [...kept.keys()])
|
|
2454
|
+
for (let up = this.chainNodes[id].up; up >= 0 && !kept.has(up); up = this.chainNodes[up].up) kept.set(up, ways.get(up) ?? 0);
|
|
2455
|
+
return [...kept];
|
|
2456
|
+
}
|
|
2457
|
+
/** The links the commits point at, numbered afresh from 0, and each commit's links in those numbers. */
|
|
2458
|
+
exportNodes(rootIndex) {
|
|
2459
|
+
const used = /* @__PURE__ */ new Set();
|
|
2460
|
+
for (const commit of this.commitList) for (const [id] of commit.ways ?? []) used.add(id);
|
|
2461
|
+
const ids = [...used].sort((a, b) => a - b);
|
|
2462
|
+
const renumber = new Map(ids.map((id, i) => [id, i]));
|
|
2463
|
+
const nodes = ids.map((id) => {
|
|
2464
|
+
const node = this.chainNodes[id];
|
|
2465
|
+
const root = node.root ? rootIndex(node.root) : void 0;
|
|
2466
|
+
return {
|
|
2467
|
+
up: node.up >= 0 ? renumber.get(node.up) ?? -1 : -1,
|
|
2468
|
+
name: node.name,
|
|
2469
|
+
...node.reason >= 0 ? { reason: node.reason } : {},
|
|
2470
|
+
...root !== void 0 ? { root } : {}
|
|
2471
|
+
};
|
|
2472
|
+
});
|
|
2473
|
+
return { nodes, renumber };
|
|
2474
|
+
}
|
|
2475
|
+
componentOf(name, f) {
|
|
2476
|
+
let comp = this.components.get(name);
|
|
2477
|
+
if (!comp) {
|
|
2478
|
+
comp = {
|
|
2479
|
+
renders: 0,
|
|
2480
|
+
mounts: 0,
|
|
2481
|
+
library: isLibraryFiber(f),
|
|
2482
|
+
wrapper: this.wrapperRe.test(name) || isProvider(name) || wrapsProvider(f),
|
|
2483
|
+
withoutDom: 0,
|
|
2484
|
+
byParent: 0,
|
|
2485
|
+
memo: f.tag === Tag.MemoComponent || f.tag === Tag.SimpleMemoComponent,
|
|
2486
|
+
reasons: /* @__PURE__ */ new Map(),
|
|
2487
|
+
chains: /* @__PURE__ */ new Map()
|
|
2488
|
+
};
|
|
2489
|
+
this.components.set(name, comp);
|
|
2490
|
+
}
|
|
2491
|
+
return comp;
|
|
2492
|
+
}
|
|
2493
|
+
hitRoot(f, name, path, prev, c, outside) {
|
|
2494
|
+
const source = sourceOf(f, this.config.projectRoot);
|
|
2495
|
+
const generated = generatedSourceOf(f);
|
|
2496
|
+
const key = `${outside ? "outside|" : ""}${name}|${siteKeyOf(f, this.config.projectRoot) || source}|${path}`;
|
|
2497
|
+
let agg = this.rootsByKey.get(key);
|
|
2498
|
+
if (!agg) {
|
|
2499
|
+
agg = {
|
|
2500
|
+
index: this.rootList.length,
|
|
2501
|
+
key,
|
|
2502
|
+
name,
|
|
2503
|
+
source,
|
|
2504
|
+
generated,
|
|
2505
|
+
path,
|
|
2506
|
+
outside,
|
|
2507
|
+
hits: 0,
|
|
2508
|
+
instances: 0,
|
|
2509
|
+
inCommit: 0,
|
|
2510
|
+
lastCommit: -1,
|
|
2511
|
+
cascade: 0,
|
|
2512
|
+
times: [],
|
|
2513
|
+
reasons: /* @__PURE__ */ new Map(),
|
|
2514
|
+
causes: /* @__PURE__ */ new Map(),
|
|
2515
|
+
lanes: /* @__PURE__ */ new Map(),
|
|
2516
|
+
noDomChange: 0,
|
|
2517
|
+
renderMs: 0,
|
|
2518
|
+
mounts: 0,
|
|
2519
|
+
library: isLibraryFiber(f),
|
|
2520
|
+
hookIdx: /* @__PURE__ */ new Set(),
|
|
2521
|
+
contexts: /* @__PURE__ */ new Map(),
|
|
2522
|
+
latest: null
|
|
2523
|
+
};
|
|
2524
|
+
this.rootsByKey.set(key, agg);
|
|
2525
|
+
this.rootList.push(agg);
|
|
2526
|
+
this.emit({
|
|
2527
|
+
k: "root",
|
|
2528
|
+
i: agg.index,
|
|
2529
|
+
key,
|
|
2530
|
+
name,
|
|
2531
|
+
source,
|
|
2532
|
+
path,
|
|
2533
|
+
...generated ? { generatedSource: generated } : {},
|
|
2534
|
+
...outside ? { outside: true } : {}
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
if (agg.lastCommit !== this.totals.commits) {
|
|
2538
|
+
agg.lastCommit = this.totals.commits;
|
|
2539
|
+
agg.hits++;
|
|
2540
|
+
agg.inCommit = 0;
|
|
2541
|
+
if (agg.times.length < MAX_TIMES) agg.times.push(c.t);
|
|
2542
|
+
agg.latest = new WeakRef(f);
|
|
2543
|
+
}
|
|
2544
|
+
agg.inCommit++;
|
|
2545
|
+
agg.instances = Math.max(agg.instances, agg.inCommit);
|
|
2546
|
+
const ids = c.reasons.get(agg) ?? /* @__PURE__ */ new Set();
|
|
2547
|
+
const reasons = reasonsOf(prev, f, this.deps.plugins);
|
|
2548
|
+
for (const reason of reasons) {
|
|
2549
|
+
const id = this.reasonId(reason);
|
|
2550
|
+
agg.reasons.set(id, (agg.reasons.get(id) ?? 0) + 1);
|
|
2551
|
+
if (reason.hook !== void 0) agg.hookIdx.add(reason.hook);
|
|
2552
|
+
if (reason.contextObject && reason.context) agg.contexts.set(contextKey(reason.context), reason.contextObject);
|
|
2553
|
+
ids.add(id);
|
|
2554
|
+
}
|
|
2555
|
+
c.reasons.set(agg, ids);
|
|
2556
|
+
if (!outside && !touchedHas(c.touched, f)) agg.noDomChange++;
|
|
2557
|
+
if (hasProfileTimings(f)) {
|
|
2558
|
+
agg.renderMs += f.actualDuration;
|
|
2559
|
+
c.renderMs += f.actualDuration;
|
|
2560
|
+
}
|
|
2561
|
+
if (!outside) c.cascade.set(agg, c.cascade.get(agg) ?? 0);
|
|
2562
|
+
return { agg, reasons };
|
|
2563
|
+
}
|
|
2564
|
+
finishCommit(c, causes, lane, event, source, origins) {
|
|
2565
|
+
if (lane) this.totals.lanes[lane] = (this.totals.lanes[lane] ?? 0) + 1;
|
|
2566
|
+
if (!c.renders) {
|
|
2567
|
+
this.totals.causesDropped += causes.length;
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
this.totals.commitsInScope++;
|
|
2571
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2572
|
+
const targets = /* @__PURE__ */ new Map();
|
|
2573
|
+
const someoneAimed = causes.some((cause) => cause.fibers?.size);
|
|
2574
|
+
for (const cause of causes) {
|
|
2575
|
+
if (someoneAimed && cause.aimed && !cause.fibers) {
|
|
2576
|
+
this.totals.causesDropped++;
|
|
2577
|
+
continue;
|
|
2578
|
+
}
|
|
2579
|
+
const key = this.attachCause(cause);
|
|
2580
|
+
keys.add(key);
|
|
2581
|
+
if (cause.fibers?.size) targets.set(key, /* @__PURE__ */ new Set([...targets.get(key) ?? [], ...cause.fibers]));
|
|
2582
|
+
}
|
|
2583
|
+
if (event && USER_EVENTS.has(event)) keys.add(this.attachCause({ plugin: "core", type: `input ${eventName(event)}`, atMs: c.t }));
|
|
2584
|
+
else if (source) keys.add(this.attachCause({ plugin: "core", type: `message ${source}`, atMs: c.t }));
|
|
2585
|
+
const claimedByEvents = new Set([...targets.values()].flatMap((set) => [...set]));
|
|
2586
|
+
for (const origin of origins) {
|
|
2587
|
+
if (origin.kind === "update" && origin.event && origin.event === event) continue;
|
|
2588
|
+
if ([...origin.fibers].some((f) => claimedByEvents.has(f))) continue;
|
|
2589
|
+
const key = this.attachCause({ plugin: "core", type: origin.text, atMs: c.t });
|
|
2590
|
+
keys.add(key);
|
|
2591
|
+
targets.set(key, /* @__PURE__ */ new Set([...targets.get(key) ?? [], ...origin.fibers]));
|
|
2592
|
+
}
|
|
2593
|
+
if (!keys.size) keys.add(this.attachCause({ plugin: "core", type: "none", atMs: c.t }));
|
|
2594
|
+
for (const key of keys) this.causeStats.get(key).commits++;
|
|
2595
|
+
const involved = /* @__PURE__ */ new Set([...c.cascade.keys(), ...c.outside ? [c.outside] : []]);
|
|
2596
|
+
const fiberOf = (agg) => agg.latest?.deref();
|
|
2597
|
+
const hits = (key, agg) => {
|
|
2598
|
+
const set = targets.get(key);
|
|
2599
|
+
if (!set) return true;
|
|
2600
|
+
const f = fiberOf(agg);
|
|
2601
|
+
return f !== void 0 && set.has(f) || ![...involved].some((other) => set.has(fiberOf(other)));
|
|
2602
|
+
};
|
|
2603
|
+
for (const agg of involved) {
|
|
2604
|
+
const own2 = [...keys].filter((key) => hits(key, agg));
|
|
2605
|
+
for (const key of own2.length ? own2 : keys) agg.causes.set(key, (agg.causes.get(key) ?? 0) + 1);
|
|
2606
|
+
if (lane && agg.lastCommit === this.totals.commits) agg.lanes.set(lane, (agg.lanes.get(lane) ?? 0) + 1);
|
|
2607
|
+
}
|
|
2608
|
+
const ranked = [...c.cascade].sort((a, b) => b[1] - a[1]);
|
|
2609
|
+
const roots = ranked.slice(0, 5).map(([agg, n]) => [agg.index, n]);
|
|
2610
|
+
const previous = this.commitList[this.commitList.length - 1];
|
|
2611
|
+
const record = {
|
|
2612
|
+
i: this.commitSeq++,
|
|
2613
|
+
atMs: c.t,
|
|
2614
|
+
...previous ? { sinceMs: +(c.t - previous.atMs).toFixed(1) } : {},
|
|
2615
|
+
renders: c.renders,
|
|
2616
|
+
...c.renderMs ? { ms: +c.renderMs.toFixed(2) } : {},
|
|
2617
|
+
...lane ? { lane } : {},
|
|
2618
|
+
...event ? { event } : {},
|
|
2619
|
+
...keys.size ? { causeIds: [...keys].map((key) => this.causeStats.get(key).i) } : {},
|
|
2620
|
+
...ranked.length ? { roots: ranked.slice(0, 10).map(([agg, hits2]) => ({ i: agg.index, hits: hits2, reasonIds: [...c.reasons.get(agg) ?? []] })) } : {},
|
|
2621
|
+
...c.outside ? { outside: c.outside.index } : {},
|
|
2622
|
+
...c.noDom ? { noDom: c.noDom } : {},
|
|
2623
|
+
...c.ways?.size ? { ways: this.commitWays(c.ways) } : {}
|
|
2624
|
+
};
|
|
2625
|
+
const limit = this.options.timeline ?? this.config.timelineLimit;
|
|
2626
|
+
if (this.commitList.length < limit) this.commitList.push(record);
|
|
2627
|
+
else this.truncated = true;
|
|
2628
|
+
if (c.renders >= (this.options.bigCommit ?? this.config.bigCommit)) this.bigCommits.push(record.i);
|
|
2629
|
+
if (this.segmentCommits.length < MAX_SEGMENT_COMMITS) this.segmentCommits.push({ i: record.i, t: c.t, n: c.renders, event, roots });
|
|
2630
|
+
this.emit({
|
|
2631
|
+
k: "commit",
|
|
2632
|
+
t: c.t,
|
|
2633
|
+
n: c.renders,
|
|
2634
|
+
...record.ms ? { ms: record.ms } : {},
|
|
2635
|
+
...lane ? { lane } : {},
|
|
2636
|
+
...event ? { event } : {},
|
|
2637
|
+
roots: ranked.map(([agg, n]) => [agg.index, n, [...c.reasons.get(agg) ?? []]]),
|
|
2638
|
+
causes: [...keys],
|
|
2639
|
+
...c.outside ? { outside: c.outside.index } : {},
|
|
2640
|
+
...c.noDom ? { noDom: c.noDom } : {}
|
|
2641
|
+
});
|
|
2642
|
+
if (c.pairs.length && this.deps.highlight) {
|
|
2643
|
+
const started = performance.now();
|
|
2644
|
+
this.deps.highlight.flash(c.pairs, c.withoutDom, c.mounted);
|
|
2645
|
+
this.overlayMs += performance.now() - started;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
attachCause(cause) {
|
|
2649
|
+
let key = `${cause.plugin}:${cause.type}`;
|
|
2650
|
+
if (!this.causeStats.has(key) && this.causeStats.size >= MAX_CAUSE_KEYS) key = `${cause.plugin}:other`;
|
|
2651
|
+
let stat = this.causeStats.get(key);
|
|
2652
|
+
if (!stat) {
|
|
2653
|
+
stat = { i: this.causeStats.size, key, plugin: cause.plugin, type: key.slice(cause.plugin.length + 1), events: 0, commits: 0 };
|
|
2654
|
+
this.causeStats.set(key, stat);
|
|
2655
|
+
}
|
|
2656
|
+
stat.events++;
|
|
2657
|
+
if (cause.changes?.length) {
|
|
2658
|
+
stat.keys ??= {};
|
|
2659
|
+
for (const change of cause.changes.slice(0, 40)) {
|
|
2660
|
+
const k = stat.keys[change.key] ??= { changed: 0, sameContent: 0, unknown: 0 };
|
|
2661
|
+
const same2 = sameContent(change.prev, change.next, 2e4);
|
|
2662
|
+
if (same2 === true) k.sameContent++;
|
|
2663
|
+
else if (same2 === "unknown") k.unknown++;
|
|
2664
|
+
else k.changed++;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
return key;
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* An update was just scheduled and nothing else explains it: no user event, no timer callback, no store or query
|
|
2671
|
+
* event yet. The stack still holds the code that asked for it, so the cause names that code.
|
|
2672
|
+
*/
|
|
2673
|
+
noteUpdate() {
|
|
2674
|
+
if (this.origins.length >= MAX_UPDATE_NOTES || runningTimer()) return;
|
|
2675
|
+
const fibers = this.freshUpdates(false);
|
|
2676
|
+
if (!fibers.size) return;
|
|
2677
|
+
const origin = updateOrigin();
|
|
2678
|
+
if (origin) this.origins.push({ ...origin, fibers, event: currentEventType() });
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Unclaimed fibers updated since the last commit. React 19 sets childLanes only at render, so once a narrow walk
|
|
2682
|
+
* misses what a wide one finds, the walk stays wide.
|
|
2683
|
+
*/
|
|
2684
|
+
freshUpdates(claim = true) {
|
|
2685
|
+
const lanes = this.roots.reduce((all, root) => all | (root.pendingLanes ?? 0), 0);
|
|
2686
|
+
if (!lanes) return /* @__PURE__ */ new Set();
|
|
2687
|
+
if (!this.narrowUpdateWalk) return this.scanUpdates(lanes, false, claim);
|
|
2688
|
+
const narrow = this.scanUpdates(lanes, true, claim);
|
|
2689
|
+
if (narrow.size) return narrow;
|
|
2690
|
+
const wide = this.scanUpdates(lanes, false, claim);
|
|
2691
|
+
if (wide.size) this.narrowUpdateWalk = false;
|
|
2692
|
+
return wide;
|
|
2693
|
+
}
|
|
2694
|
+
scanUpdates(lanes, narrow, claim) {
|
|
2695
|
+
const out = /* @__PURE__ */ new Set();
|
|
2696
|
+
const stack = this.roots.map((root) => root.current);
|
|
2697
|
+
const limit = narrow ? 2e3 : 2e4;
|
|
2698
|
+
for (let visits = 0; stack.length && visits < limit; visits++) {
|
|
2699
|
+
const f = stack.pop();
|
|
2700
|
+
if ((f.lanes ?? 0) & lanes && !this.claimed.has(f)) {
|
|
2701
|
+
out.add(f);
|
|
2702
|
+
if (f.alternate) out.add(f.alternate);
|
|
2703
|
+
if (claim) {
|
|
2704
|
+
this.claimed.add(f);
|
|
2705
|
+
if (f.alternate) this.claimed.add(f.alternate);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
for (let child = f.child; child; child = child.sibling)
|
|
2709
|
+
if (!narrow || ((child.lanes ?? 0) | (child.childLanes ?? 0)) & lanes) stack.push(child);
|
|
2710
|
+
}
|
|
2711
|
+
return out;
|
|
2712
|
+
}
|
|
2713
|
+
// ---- snapshots and helpers ------------------------------------------------------------------------------------
|
|
2714
|
+
prevOf(f) {
|
|
2715
|
+
return this.records.get(f) ?? (f.alternate ? this.records.get(f.alternate) : void 0);
|
|
2716
|
+
}
|
|
2717
|
+
remember(f) {
|
|
2718
|
+
const snapshot = snapshotOf(f);
|
|
2719
|
+
this.records.set(f, snapshot);
|
|
2720
|
+
if (f.alternate) this.records.set(f.alternate, snapshot);
|
|
2721
|
+
}
|
|
2722
|
+
seed(start) {
|
|
2723
|
+
const stack = [[start, null]];
|
|
2724
|
+
while (stack.length) {
|
|
2725
|
+
const [f, zoneTag] = stack.pop();
|
|
2726
|
+
this.remember(f);
|
|
2727
|
+
const name = nameOf(f);
|
|
2728
|
+
const w = name ? this.watch.get(name) : void 0;
|
|
2729
|
+
if (w) w.mounted++;
|
|
2730
|
+
const zone = isHost(f) && this.zoneNodes.size ? this.zoneNodes.get(f.stateNode) ?? zoneTag : zoneTag;
|
|
2731
|
+
if (zone && name) this.zones.get(zone).mounted++;
|
|
2732
|
+
if (f.sibling) stack.push([f.sibling, zoneTag]);
|
|
2733
|
+
if (f.child) stack.push([f.child, zone]);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
pluginSession() {
|
|
2737
|
+
return { scope: this.scopeInfo, findFibers: (pred, limit) => this.findFibers(pred, limit) };
|
|
2738
|
+
}
|
|
2739
|
+
findFibers(pred, limit = Infinity) {
|
|
2740
|
+
const out = [];
|
|
2741
|
+
for (const root of this.roots.length ? this.roots : findRoots()) {
|
|
2742
|
+
const stack = [root.current];
|
|
2743
|
+
while (stack.length && out.length < limit) {
|
|
2744
|
+
const f = stack.pop();
|
|
2745
|
+
try {
|
|
2746
|
+
if (pred(f)) out.push(f);
|
|
2747
|
+
} catch {
|
|
2748
|
+
}
|
|
2749
|
+
if (f.sibling) stack.push(f.sibling);
|
|
2750
|
+
if (f.child) stack.push(f.child);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
return out;
|
|
2754
|
+
}
|
|
2755
|
+
resolveZones() {
|
|
2756
|
+
for (const [name, spec] of Object.entries(this.options.zones ?? {})) {
|
|
2757
|
+
const { selector, viaAriaControls } = typeof spec === "string" ? { selector: spec, viaAriaControls: false } : spec;
|
|
2758
|
+
let el = document.querySelector(selector);
|
|
2759
|
+
if (el && viaAriaControls) {
|
|
2760
|
+
const id = el.getAttribute("aria-controls");
|
|
2761
|
+
el = id ? document.getElementById(id) : null;
|
|
2762
|
+
}
|
|
2763
|
+
this.zones.set(name, { renders: 0, mounted: 0, found: Boolean(el) });
|
|
2764
|
+
if (el) this.zoneNodes.set(el, name);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
scopeHosts() {
|
|
2768
|
+
return this.scope ? nearestHosts(this.scope.target) : [];
|
|
2769
|
+
}
|
|
2770
|
+
countCommits(from, to) {
|
|
2771
|
+
let n = 0;
|
|
2772
|
+
for (const t of this.commitTimes) if (t >= from && t <= to) n++;
|
|
2773
|
+
return n;
|
|
2774
|
+
}
|
|
2775
|
+
/** One entry per distinct reason; everything else — roots, components, commits — points at it by id. */
|
|
2776
|
+
reasonId(reason) {
|
|
2777
|
+
const fieldKey = reason.kind === "parent" ? `parent|${reason.changed?.join(",") ?? ""}|${reason.sameRef?.join(",") ?? ""}|${reason.children ? 1 : 0}${reason.equal ? 1 : 0}` : JSON.stringify({ ...reason, contextObject: void 0 });
|
|
2778
|
+
const known2 = this.reasonIdsByFields.get(fieldKey);
|
|
2779
|
+
if (known2 !== void 0) return known2;
|
|
2780
|
+
const { contextObject: _context, ...fields } = reason;
|
|
2781
|
+
const text = reasonText(fields);
|
|
2782
|
+
let id = this.reasonIds.get(text);
|
|
2783
|
+
if (id === void 0) {
|
|
2784
|
+
id = this.reasonIds.size;
|
|
2785
|
+
this.reasonIds.set(text, id);
|
|
2786
|
+
const info = { i: id, ...fields };
|
|
2787
|
+
this.reasonList.push(info);
|
|
2788
|
+
this.emit({ k: "reason", info });
|
|
2789
|
+
}
|
|
2790
|
+
this.reasonIdsByFields.set(fieldKey, id);
|
|
2791
|
+
return id;
|
|
2792
|
+
}
|
|
2793
|
+
emit(event) {
|
|
2794
|
+
if (this.stopped && event.k !== "end") return;
|
|
2795
|
+
try {
|
|
2796
|
+
this.deps.onEvent(event);
|
|
2797
|
+
} catch {
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
readConditions() {
|
|
2801
|
+
return {
|
|
2802
|
+
viewport: `${innerWidth}\xD7${innerHeight}`,
|
|
2803
|
+
url: safeUrl(location.pathname + location.search),
|
|
2804
|
+
dpr: devicePixelRatio,
|
|
2805
|
+
...this.deps.plugins.conditions()
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
// ---- result ----------------------------------------------------------------------------------------------------
|
|
2809
|
+
hookInfo(agg) {
|
|
2810
|
+
if (!agg.hookIdx.size && !agg.contexts.size) return void 0;
|
|
2811
|
+
const fiber = agg.latest?.deref();
|
|
2812
|
+
if (!fiber) return void 0;
|
|
2813
|
+
const out = {};
|
|
2814
|
+
let names2 = null;
|
|
2815
|
+
if (this.options.hookNames !== false && isMounted(fiber)) {
|
|
2816
|
+
try {
|
|
2817
|
+
names2 = inspectHooks(fiber);
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
this.warnings.push(`hook names for ${agg.name}: ${String(error?.message ?? error).slice(0, 120)}`);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
for (const index of agg.hookIdx) out[index] = names2?.hooks.get(index) ?? { type: hookTypeAt(fiber, index) };
|
|
2823
|
+
for (const [key, context] of agg.contexts) {
|
|
2824
|
+
const info = names2?.contexts.get(context);
|
|
2825
|
+
if (info) out[key] = info;
|
|
2826
|
+
}
|
|
2827
|
+
return out;
|
|
2828
|
+
}
|
|
2829
|
+
rootStat(agg, withHooks) {
|
|
2830
|
+
return {
|
|
2831
|
+
key: agg.key,
|
|
2832
|
+
name: agg.name,
|
|
2833
|
+
source: agg.source,
|
|
2834
|
+
...agg.generated ? { generatedSource: agg.generated } : {},
|
|
2835
|
+
path: agg.path,
|
|
2836
|
+
hits: agg.hits,
|
|
2837
|
+
instances: agg.instances,
|
|
2838
|
+
cascade: agg.cascade,
|
|
2839
|
+
perHit: agg.hits ? Math.round(agg.cascade / agg.hits) : 0,
|
|
2840
|
+
medianGapMs: medianGapMs(agg.times),
|
|
2841
|
+
firstAtMs: agg.times[0] ?? 0,
|
|
2842
|
+
lastAtMs: agg.times.at(-1) ?? 0,
|
|
2843
|
+
reasons: topEntries(agg.reasons, 8),
|
|
2844
|
+
causes: topEntries(agg.causes, 8),
|
|
2845
|
+
lanes: topEntries(agg.lanes, 5),
|
|
2846
|
+
noDomChange: agg.noDomChange,
|
|
2847
|
+
...agg.renderMs ? { renderMs: +agg.renderMs.toFixed(1) } : {},
|
|
2848
|
+
...agg.mounts ? { mounts: agg.mounts } : {},
|
|
2849
|
+
...agg.library ? { library: true } : {},
|
|
2850
|
+
...withHooks ? { hooks: this.hookInfo(agg) } : {},
|
|
2851
|
+
...agg.outside ? { scopeRenders: agg.cascade } : {}
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
build(durationMs, sections, conditionsAfter) {
|
|
2855
|
+
const inside = this.rootList.filter((r) => !r.outside).sort((a, b) => b.cascade - a.cascade);
|
|
2856
|
+
const outside = this.rootList.filter((r) => r.outside).sort((a, b) => b.cascade - a.cascade);
|
|
2857
|
+
const hooksFor = /* @__PURE__ */ new Set([...inside.slice(0, 30), ...outside.slice(0, 10)]);
|
|
2858
|
+
const statsByIndex = /* @__PURE__ */ new Map();
|
|
2859
|
+
for (const agg of this.rootList) statsByIndex.set(agg.index, this.rootStat(agg, hooksFor.has(agg)));
|
|
2860
|
+
const remap = /* @__PURE__ */ new Map();
|
|
2861
|
+
const orderedInside = inside.map((agg) => agg.index);
|
|
2862
|
+
const allOrdered = [...orderedInside, ...outside.map((agg) => agg.index)];
|
|
2863
|
+
allOrdered.forEach((index, i) => remap.set(index, i));
|
|
2864
|
+
const mapRoots = (pairs) => pairs?.map(([index, n]) => [remap.get(index), n]);
|
|
2865
|
+
const rootsOrdered = allOrdered.map((index) => statsByIndex.get(index));
|
|
2866
|
+
const ways = this.exportNodes((agg) => remap.get(agg.index));
|
|
2867
|
+
const changed = Object.fromEntries(
|
|
2868
|
+
Object.keys({ ...this.conditions, ...conditionsAfter }).filter((key) => this.conditions[key] !== conditionsAfter[key]).map((key) => [key, [this.conditions[key] ?? null, conditionsAfter[key] ?? null]])
|
|
2869
|
+
);
|
|
2870
|
+
const actions = this.actions?.actions ?? [];
|
|
2871
|
+
const segments = buildSegments(
|
|
2872
|
+
actions,
|
|
2873
|
+
this.segmentCommits.map((c) => ({ ...c, roots: mapRoots(c.roots) })),
|
|
2874
|
+
this.frames.latency,
|
|
2875
|
+
this.frames.loaf
|
|
2876
|
+
);
|
|
2877
|
+
const actionOfCommit = /* @__PURE__ */ new Map();
|
|
2878
|
+
for (const segment of segments) for (const i of segment.commitIds ?? []) actionOfCommit.set(i, segment.action);
|
|
2879
|
+
const commitsOfAction = new Map(segments.map((s) => [s.action, s.commitIds ?? []]));
|
|
2880
|
+
const kept = (i) => i < this.commitList.length;
|
|
2881
|
+
for (const action of actions) {
|
|
2882
|
+
const ids = commitsOfAction.get(action.id)?.filter(kept);
|
|
2883
|
+
if (ids?.length) action.commitIds = ids;
|
|
2884
|
+
}
|
|
2885
|
+
const memos = this.memoHits.result((fiber) => {
|
|
2886
|
+
if (this.options.hookNames === false || !isMounted(fiber)) return null;
|
|
2887
|
+
try {
|
|
2888
|
+
return inspectHooks(fiber)?.hooks ?? null;
|
|
2889
|
+
} catch {
|
|
2890
|
+
return null;
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2893
|
+
const commits = this.totals.commits;
|
|
2894
|
+
const commitsInScope = this.totals.commitsInScope;
|
|
2895
|
+
return {
|
|
2896
|
+
schema: RECORDING_SCHEMA,
|
|
2897
|
+
version: 2,
|
|
2898
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2899
|
+
...this.options.label ? { label: this.options.label } : {},
|
|
2900
|
+
tool: { version: this.config.version, source: this.options.source ?? "panel", plugins: this.deps.plugins.info() },
|
|
2901
|
+
page: {
|
|
2902
|
+
url: safeUrl(location.href),
|
|
2903
|
+
title: document.title,
|
|
2904
|
+
viewport: `${innerWidth}\xD7${innerHeight}`,
|
|
2905
|
+
dpr: devicePixelRatio,
|
|
2906
|
+
userAgent: navigator.userAgent
|
|
2907
|
+
},
|
|
2908
|
+
react: { version: reactVersion(), roots: this.roots.length, profileTimings: this.rootList.some((r) => r.renderMs > 0) },
|
|
2909
|
+
...this.options.meta ? { meta: this.options.meta } : {},
|
|
2910
|
+
options: optionsJson(this.options),
|
|
2911
|
+
startedAt: this.startedAt.toISOString(),
|
|
2912
|
+
durationMs,
|
|
2913
|
+
scope: this.scope ? {
|
|
2914
|
+
name: this.scope.name,
|
|
2915
|
+
// Asked again rather than kept from when the area was picked: by now the dev server may have mapped it.
|
|
2916
|
+
source: sourceOf(this.scope.target, this.config.projectRoot) || this.scope.source,
|
|
2917
|
+
path: this.scope.ancestorNames().slice(-6),
|
|
2918
|
+
state: this.scope.state,
|
|
2919
|
+
remounts: this.scope.remounts,
|
|
2920
|
+
lostAtMs: this.scope.lostAtMs
|
|
2921
|
+
} : null,
|
|
2922
|
+
totals: {
|
|
2923
|
+
commits,
|
|
2924
|
+
commitsInScope,
|
|
2925
|
+
renders: this.totals.renders,
|
|
2926
|
+
mounts: this.totals.mounts,
|
|
2927
|
+
rendersPerCommit: commits ? +(this.totals.renders / commits).toFixed(1) : 0,
|
|
2928
|
+
rendersPerScopeCommit: commitsInScope ? +(this.totals.renders / commitsInScope).toFixed(1) : 0,
|
|
2929
|
+
rendersFromOutside: this.totals.rendersFromOutside,
|
|
2930
|
+
rendersWithoutDom: this.totals.rendersWithoutDom,
|
|
2931
|
+
domTextChanges: this.dom.counts.text,
|
|
2932
|
+
causesDropped: this.totals.causesDropped,
|
|
2933
|
+
lanes: this.totals.lanes
|
|
2934
|
+
},
|
|
2935
|
+
roots: rootsOrdered.slice(0, orderedInside.length),
|
|
2936
|
+
outsideRoots: rootsOrdered.slice(orderedInside.length),
|
|
2937
|
+
// The app's own components first: a UI kit fills the top with its wrappers and internals otherwise.
|
|
2938
|
+
components: [...this.components].sort(
|
|
2939
|
+
(a, b) => Number(a[1].library || a[1].wrapper) - Number(b[1].library || b[1].wrapper) || b[1].renders + b[1].mounts - (a[1].renders + a[1].mounts)
|
|
2940
|
+
).slice(0, 150).map(([name, s]) => ({
|
|
2941
|
+
name,
|
|
2942
|
+
renders: s.renders,
|
|
2943
|
+
...s.mounts ? { mounts: s.mounts } : {},
|
|
2944
|
+
...s.library ? { library: true } : {},
|
|
2945
|
+
...s.wrapper ? { wrapper: true } : {},
|
|
2946
|
+
withoutDom: s.withoutDom,
|
|
2947
|
+
byParent: s.byParent,
|
|
2948
|
+
...s.memo ? { memo: true } : {},
|
|
2949
|
+
reasons: topEntries(s.reasons, 4),
|
|
2950
|
+
...s.chains.size ? {
|
|
2951
|
+
chains: topEntries(s.chains, CHAINS_PER_COMPONENT).map(([id, n]) => ({
|
|
2952
|
+
n,
|
|
2953
|
+
links: this.chainLinks(id, (agg) => remap.get(agg.index))
|
|
2954
|
+
}))
|
|
2955
|
+
} : {},
|
|
2956
|
+
...s.sampled ? { sampled: true } : {}
|
|
2957
|
+
})),
|
|
2958
|
+
...this.watch.size ? {
|
|
2959
|
+
watch: Object.fromEntries(
|
|
2960
|
+
[...this.watch].map(([name, w]) => [
|
|
2961
|
+
name,
|
|
2962
|
+
{
|
|
2963
|
+
mounted: w.mounted,
|
|
2964
|
+
renders: w.renders,
|
|
2965
|
+
byRoot: [...w.byRoot].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([agg, n]) => [agg ? remap.get(agg.index) : null, n])
|
|
2966
|
+
}
|
|
2967
|
+
])
|
|
2968
|
+
)
|
|
2969
|
+
} : {},
|
|
2970
|
+
...this.zones.size ? { zones: Object.fromEntries(this.zones) } : {},
|
|
2971
|
+
causes: [...this.causeStats.values()].sort((a, b) => b.commits - a.commits),
|
|
2972
|
+
actions,
|
|
2973
|
+
segments: segments.map(({ commitIds: _ids, ...rest }) => rest),
|
|
2974
|
+
...memos.length ? { memos } : {},
|
|
2975
|
+
latency: this.frames.latency,
|
|
2976
|
+
reasons: this.reasonList,
|
|
2977
|
+
...ways.nodes.length ? { chainNodes: ways.nodes } : {},
|
|
2978
|
+
commits: {
|
|
2979
|
+
list: this.commitList.map((commit) => ({
|
|
2980
|
+
...commit,
|
|
2981
|
+
...commit.ways ? { ways: commit.ways.map(([id, n]) => [ways.renumber.get(id), n]) } : {},
|
|
2982
|
+
...commit.roots ? { roots: commit.roots.map((r) => ({ ...r, i: remap.get(r.i) })) } : {},
|
|
2983
|
+
...commit.outside !== void 0 ? { outside: remap.get(commit.outside) } : {},
|
|
2984
|
+
...actionOfCommit.get(commit.i) !== void 0 ? { actionId: actionOfCommit.get(commit.i) } : {}
|
|
2985
|
+
})),
|
|
2986
|
+
truncated: this.truncated
|
|
2987
|
+
},
|
|
2988
|
+
bigCommits: this.bigCommits.filter((i) => i < this.commitList.length),
|
|
2989
|
+
frames: {
|
|
2990
|
+
longTasks: this.frames.longTasks,
|
|
2991
|
+
loaf: this.frames.loaf,
|
|
2992
|
+
...this.options.frames && durationMs ? { fps: +(this.frameCount * 1e3 / durationMs).toFixed(1) } : {}
|
|
2993
|
+
},
|
|
2994
|
+
dom: { ...this.dom.counts },
|
|
2995
|
+
navigations: this.navigations,
|
|
2996
|
+
hmr: this.hmr,
|
|
2997
|
+
conditions: this.conditions,
|
|
2998
|
+
...Object.keys(changed).length ? { conditionsChanged: changed } : {},
|
|
2999
|
+
plugins: sections,
|
|
3000
|
+
overhead: {
|
|
3001
|
+
commitMs: +(this.hook?.overhead.commitMs ?? 0).toFixed(1),
|
|
3002
|
+
maxCommitMs: +(this.hook?.overhead.maxCommitMs ?? 0).toFixed(1),
|
|
3003
|
+
overlayMs: +this.overlayMs.toFixed(1),
|
|
3004
|
+
highlight: this.highlighted
|
|
3005
|
+
},
|
|
3006
|
+
warnings: [
|
|
3007
|
+
...this.warnings,
|
|
3008
|
+
...[...this.components.values()].some((comp) => comp.sampled) ? [
|
|
3009
|
+
`reasons of renders caused by a parent were worked out for ${SAMPLED_PARENTS} instances of a component a commit: their counts are a sample`
|
|
3010
|
+
] : [],
|
|
3011
|
+
...sourcesUnavailable() ? [
|
|
3012
|
+
`React ${reactVersion() ?? "19.0"} tells nothing about where a component comes from: no files, and the app's own components cannot be told from a package's. React 19.1 or newer brings both back.`
|
|
3013
|
+
] : [],
|
|
3014
|
+
...this.highlighted ? ["highlight was on: drawing the outlines costs main-thread time, so timings and long frames read high"] : []
|
|
3015
|
+
],
|
|
3016
|
+
errors: this.errors
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
function isMounted(fiber) {
|
|
3021
|
+
const root = hostRootOf(fiber);
|
|
3022
|
+
if (!root) return false;
|
|
3023
|
+
let top = fiber;
|
|
3024
|
+
while (top?.return) top = top.return;
|
|
3025
|
+
return top === root.current || top === root.current.alternate;
|
|
3026
|
+
}
|
|
3027
|
+
function optionsJson(options) {
|
|
3028
|
+
const out = {};
|
|
3029
|
+
for (const [key, value] of Object.entries(options)) {
|
|
3030
|
+
if (key === "scope" || key === "meta" || value === void 0) continue;
|
|
3031
|
+
out[key] = value;
|
|
3032
|
+
}
|
|
3033
|
+
return out;
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
// src/core/transport.ts
|
|
3037
|
+
var FLUSH_MS = 2e3;
|
|
3038
|
+
var HEARTBEAT_MS = 1e4;
|
|
3039
|
+
var BATCH = 200;
|
|
3040
|
+
var PENDING_KEY = "react-perf-recorder:session";
|
|
3041
|
+
var SessionWriter = class {
|
|
3042
|
+
constructor(endpoint, meta) {
|
|
3043
|
+
this.endpoint = endpoint;
|
|
3044
|
+
this.id = null;
|
|
3045
|
+
this.failed = null;
|
|
3046
|
+
this.token = null;
|
|
3047
|
+
this.queue = [];
|
|
3048
|
+
this.timer = null;
|
|
3049
|
+
this.heartbeat = null;
|
|
3050
|
+
this.sending = Promise.resolve();
|
|
3051
|
+
this.opening = this.open(meta);
|
|
3052
|
+
}
|
|
3053
|
+
push(event) {
|
|
3054
|
+
if (this.failed) return;
|
|
3055
|
+
this.queue.push(event);
|
|
3056
|
+
if (this.queue.length >= BATCH) void this.flush();
|
|
3057
|
+
else if (!this.timer) this.timer = setTimeout(() => void this.flush(), FLUSH_MS);
|
|
3058
|
+
}
|
|
3059
|
+
flush() {
|
|
3060
|
+
if (this.timer) clearTimeout(this.timer);
|
|
3061
|
+
this.timer = null;
|
|
3062
|
+
this.sending = this.sending.then(async () => {
|
|
3063
|
+
await this.opening;
|
|
3064
|
+
if (!this.id || this.failed || !this.queue.length) return;
|
|
3065
|
+
const events = this.queue.splice(0);
|
|
3066
|
+
await this.post(`sessions/${this.id}/events`, { events }).catch((error) => {
|
|
3067
|
+
this.failed = `events not saved: ${String(error?.message ?? error)}`;
|
|
3068
|
+
});
|
|
3069
|
+
});
|
|
3070
|
+
return this.sending;
|
|
3071
|
+
}
|
|
3072
|
+
/** Page is going away: send what is left without waiting; the server marks the session interrupted. */
|
|
3073
|
+
beacon(atMs) {
|
|
3074
|
+
if (!this.id || !this.token || this.failed) return;
|
|
3075
|
+
const events = [...this.queue.splice(0), { k: "end", atMs }];
|
|
3076
|
+
const url = `${this.endpoint}/sessions/${this.id}/events?token=${encodeURIComponent(this.token)}&end=1`;
|
|
3077
|
+
try {
|
|
3078
|
+
navigator.sendBeacon?.(url, new Blob([JSON.stringify({ events })], { type: "text/plain" }));
|
|
3079
|
+
} catch {
|
|
3080
|
+
}
|
|
3081
|
+
this.clearPending();
|
|
3082
|
+
}
|
|
3083
|
+
async finish(recording) {
|
|
3084
|
+
await this.flush();
|
|
3085
|
+
this.stopHeartbeat();
|
|
3086
|
+
this.clearPending();
|
|
3087
|
+
if (!this.id || this.failed) return null;
|
|
3088
|
+
try {
|
|
3089
|
+
const saved = await this.post(`sessions/${this.id}/finish`, { recording });
|
|
3090
|
+
return saved;
|
|
3091
|
+
} catch (error) {
|
|
3092
|
+
this.failed = `recording not saved: ${String(error?.message ?? error)}`;
|
|
3093
|
+
return null;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
static takeInterrupted() {
|
|
3097
|
+
try {
|
|
3098
|
+
const raw = sessionStorage.getItem(PENDING_KEY);
|
|
3099
|
+
if (!raw) return null;
|
|
3100
|
+
sessionStorage.removeItem(PENDING_KEY);
|
|
3101
|
+
return JSON.parse(raw);
|
|
3102
|
+
} catch {
|
|
3103
|
+
return null;
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
async open(meta) {
|
|
3107
|
+
try {
|
|
3108
|
+
const { id, token } = await this.post("sessions", meta);
|
|
3109
|
+
this.id = id;
|
|
3110
|
+
this.token = token;
|
|
3111
|
+
try {
|
|
3112
|
+
sessionStorage.setItem(PENDING_KEY, JSON.stringify({ id }));
|
|
3113
|
+
} catch {
|
|
3114
|
+
}
|
|
3115
|
+
this.heartbeat = setInterval(() => void this.post(`sessions/${this.id}/events`, { events: [] }).catch(() => {
|
|
3116
|
+
}), HEARTBEAT_MS);
|
|
3117
|
+
} catch (error) {
|
|
3118
|
+
this.failed = `dev server unavailable: ${String(error?.message ?? error)}`;
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
stopHeartbeat() {
|
|
3122
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
3123
|
+
this.heartbeat = null;
|
|
3124
|
+
}
|
|
3125
|
+
clearPending() {
|
|
3126
|
+
try {
|
|
3127
|
+
sessionStorage.removeItem(PENDING_KEY);
|
|
3128
|
+
} catch {
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
async post(path, body) {
|
|
3132
|
+
const response = await fetch(`${this.endpoint}/${path}`, {
|
|
3133
|
+
method: "POST",
|
|
3134
|
+
headers: { "content-type": "application/json", [CLIENT_HEADER]: "1" },
|
|
3135
|
+
body: JSON.stringify(body)
|
|
3136
|
+
});
|
|
3137
|
+
if (!response.ok) throw new Error(`${response.status} ${await response.text().catch(() => "")}`.trim());
|
|
3138
|
+
return response.json();
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
|
|
3142
|
+
// src/core/engine.ts
|
|
3143
|
+
function applySites(recording, sites) {
|
|
3144
|
+
for (const action of recording.actions ?? []) {
|
|
3145
|
+
const own2 = action.target?.generatedSource;
|
|
3146
|
+
if (!own2) continue;
|
|
3147
|
+
const mapped = sites[`${own2.url}:${own2.line}:${own2.column}`];
|
|
3148
|
+
if (mapped) action.target.source = mapped.site;
|
|
3149
|
+
delete action.target.generatedSource;
|
|
3150
|
+
}
|
|
3151
|
+
for (const root of [...recording.roots, ...recording.outsideRoots]) {
|
|
3152
|
+
const own2 = root.generatedSource;
|
|
3153
|
+
if (own2) {
|
|
3154
|
+
const ownMapped = sites[`${own2.url}:${own2.line}:${own2.column}`];
|
|
3155
|
+
if (ownMapped) root.source = ownMapped.site;
|
|
3156
|
+
delete root.generatedSource;
|
|
3157
|
+
}
|
|
3158
|
+
for (const hook of Object.values(root.hooks ?? {})) {
|
|
3159
|
+
const g = hook.generated;
|
|
3160
|
+
if (!g) continue;
|
|
3161
|
+
const mapped = sites[`${g.url}:${g.line}:${g.column}`];
|
|
3162
|
+
if (mapped) {
|
|
3163
|
+
hook.site = mapped.site;
|
|
3164
|
+
if (mapped.code) hook.code = mapped.code;
|
|
3165
|
+
}
|
|
3166
|
+
delete hook.generated;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
for (const memo of recording.memos ?? []) {
|
|
3170
|
+
const g = memo.info?.generated;
|
|
3171
|
+
if (!g) continue;
|
|
3172
|
+
const mapped = sites[`${g.url}:${g.line}:${g.column}`];
|
|
3173
|
+
if (mapped) {
|
|
3174
|
+
memo.info.site = mapped.site;
|
|
3175
|
+
if (mapped.code) memo.info.code = mapped.code;
|
|
3176
|
+
if (mapped.deps) memo.info.deps = mapped.deps;
|
|
3177
|
+
}
|
|
3178
|
+
delete memo.info.generated;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
var Engine = class {
|
|
3182
|
+
constructor(config, plugins, highlight = null, ownHost = null) {
|
|
3183
|
+
this.config = config;
|
|
3184
|
+
this.plugins = plugins;
|
|
3185
|
+
this.highlight = highlight;
|
|
3186
|
+
this.ownHost = ownHost;
|
|
3187
|
+
this.last = null;
|
|
3188
|
+
this.recorder = null;
|
|
3189
|
+
/** The area the running recording was started with, as it was found: a load recording finds it before the panel does. */
|
|
3190
|
+
this.recordingScope = null;
|
|
3191
|
+
this.writer = null;
|
|
3192
|
+
this.timer = null;
|
|
3193
|
+
/** `saved`: a recording that stopped on its own at the length limit, once it is saved — no one else asked for it. */
|
|
3194
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
3195
|
+
/** The stop the length limit made, for whoever was waiting to stop it themselves. */
|
|
3196
|
+
this.autoStop = null;
|
|
3197
|
+
this.idle = null;
|
|
3198
|
+
this.idleHighlight = null;
|
|
3199
|
+
this.idleRetry = null;
|
|
3200
|
+
this.wrapperRe = new RegExp(config.wrapperPattern || "^$");
|
|
3201
|
+
}
|
|
3202
|
+
/** The panel and its highlight canvas: events inside the panel are not user actions. */
|
|
3203
|
+
attachUi(highlight, ownHost) {
|
|
3204
|
+
this.highlight = highlight;
|
|
3205
|
+
this.ownHost = ownHost;
|
|
3206
|
+
}
|
|
3207
|
+
get version() {
|
|
3208
|
+
return this.config.version;
|
|
3209
|
+
}
|
|
3210
|
+
get recording() {
|
|
3211
|
+
return Boolean(this.recorder);
|
|
3212
|
+
}
|
|
3213
|
+
get scopeOfRecording() {
|
|
3214
|
+
return this.recordingScope;
|
|
3215
|
+
}
|
|
3216
|
+
onChange(listener) {
|
|
3217
|
+
this.listeners.add(listener);
|
|
3218
|
+
return () => this.listeners.delete(listener);
|
|
3219
|
+
}
|
|
3220
|
+
status() {
|
|
3221
|
+
const roots = findRoots();
|
|
3222
|
+
const busy = roots.map((root) => hookOwner(root)).find(Boolean) ?? null;
|
|
3223
|
+
return {
|
|
3224
|
+
version: this.version,
|
|
3225
|
+
recording: this.recording,
|
|
3226
|
+
busyOwner: this.recording ? null : busy,
|
|
3227
|
+
react: { found: roots.length > 0, version: reactVersion(), roots: roots.length },
|
|
3228
|
+
scope: this.recorder?.scopeInfo ?? null,
|
|
3229
|
+
sessionId: this.writer?.id ?? null,
|
|
3230
|
+
plugins: this.plugins.info()
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Outlines renders in the area while nothing is recorded. A recording takes the commit hook over and, with
|
|
3235
|
+
* `highlight` on, keeps drawing; this resumes after it stops.
|
|
3236
|
+
*/
|
|
3237
|
+
highlightWhenIdle(on, scope = null) {
|
|
3238
|
+
this.idle = on ? { scope } : null;
|
|
3239
|
+
this.syncIdleHighlight();
|
|
3240
|
+
}
|
|
3241
|
+
get idleHighlighting() {
|
|
3242
|
+
return Boolean(this.idleHighlight);
|
|
3243
|
+
}
|
|
3244
|
+
syncIdleHighlight() {
|
|
3245
|
+
this.idleHighlight?.stop();
|
|
3246
|
+
this.idleHighlight = null;
|
|
3247
|
+
if (this.idleRetry) clearTimeout(this.idleRetry);
|
|
3248
|
+
this.idleRetry = null;
|
|
3249
|
+
if (!this.idle || this.recorder || !this.highlight) return;
|
|
3250
|
+
try {
|
|
3251
|
+
const live = new LiveHighlight(this.highlight, this.resolveScope(this.idle.scope));
|
|
3252
|
+
if (live.start()) this.idleHighlight = live;
|
|
3253
|
+
} catch {
|
|
3254
|
+
}
|
|
3255
|
+
if (!this.idleHighlight) this.idleRetry = setTimeout(() => this.syncIdleHighlight(), 1e3);
|
|
3256
|
+
}
|
|
3257
|
+
start(options = {}) {
|
|
3258
|
+
if (this.recorder) throw new RecorderError("ALREADY", "a recording is already running");
|
|
3259
|
+
const scope = this.resolveScope(options.scope ?? null);
|
|
3260
|
+
this.idleHighlight?.stop();
|
|
3261
|
+
this.idleHighlight = null;
|
|
3262
|
+
const writerRef = { current: null };
|
|
3263
|
+
const recorder = new Recorder(
|
|
3264
|
+
{
|
|
3265
|
+
config: this.config,
|
|
3266
|
+
plugins: this.plugins,
|
|
3267
|
+
ownHost: this.ownHost,
|
|
3268
|
+
highlight: options.highlight === false ? null : this.highlight,
|
|
3269
|
+
onEvent: (event) => writerRef.current?.push(event)
|
|
3270
|
+
},
|
|
3271
|
+
{ ...options, scope }
|
|
3272
|
+
);
|
|
3273
|
+
try {
|
|
3274
|
+
recorder.start();
|
|
3275
|
+
} catch (error) {
|
|
3276
|
+
this.syncIdleHighlight();
|
|
3277
|
+
throw error;
|
|
3278
|
+
}
|
|
3279
|
+
this.recorder = recorder;
|
|
3280
|
+
this.recordingScope = scope;
|
|
3281
|
+
if (this.config.endpoint && options.save !== false) {
|
|
3282
|
+
writerRef.current = this.writer = new SessionWriter(this.config.endpoint, {
|
|
3283
|
+
source: options.source ?? "api",
|
|
3284
|
+
label: options.label,
|
|
3285
|
+
page: {
|
|
3286
|
+
url: safeUrl(location.href),
|
|
3287
|
+
title: document.title,
|
|
3288
|
+
viewport: `${innerWidth}\xD7${innerHeight}`,
|
|
3289
|
+
dpr: devicePixelRatio,
|
|
3290
|
+
userAgent: navigator.userAgent
|
|
3291
|
+
},
|
|
3292
|
+
scope: recorder.scopeInfo,
|
|
3293
|
+
conditions: recorder.startConditions,
|
|
3294
|
+
plugins: this.plugins.info()
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
this.autoStop = null;
|
|
3298
|
+
this.timer = setTimeout(() => {
|
|
3299
|
+
const stop = this.autoStop = this.stop();
|
|
3300
|
+
stop.then((saved) => this.listeners.forEach((l) => l("saved", saved))).catch(() => {
|
|
3301
|
+
});
|
|
3302
|
+
}, this.config.maxDurationMs);
|
|
3303
|
+
this.listeners.forEach((l) => l("started"));
|
|
3304
|
+
return { scope: recorder.scopeInfo };
|
|
3305
|
+
}
|
|
3306
|
+
/** Stops, builds the recording and, with a dev server, saves it; resolves with the id of the saved session. */
|
|
3307
|
+
async stop() {
|
|
3308
|
+
const recorder = this.recorder;
|
|
3309
|
+
if (!recorder) throw new RecorderError("NOT_RECORDING", "no recording is running");
|
|
3310
|
+
if (this.timer) clearTimeout(this.timer);
|
|
3311
|
+
this.recorder = null;
|
|
3312
|
+
this.recordingScope = null;
|
|
3313
|
+
const writer = this.writer;
|
|
3314
|
+
this.writer = null;
|
|
3315
|
+
let recording;
|
|
3316
|
+
try {
|
|
3317
|
+
recording = recorder.stop();
|
|
3318
|
+
} finally {
|
|
3319
|
+
this.syncIdleHighlight();
|
|
3320
|
+
this.listeners.forEach((l) => l("stopped"));
|
|
3321
|
+
}
|
|
3322
|
+
if (writer) {
|
|
3323
|
+
const saved = await writer.finish(recording);
|
|
3324
|
+
if (saved) {
|
|
3325
|
+
Object.assign(recording, { id: saved.id, dir: saved.dir });
|
|
3326
|
+
applySites(recording, saved.sites ?? {});
|
|
3327
|
+
} else recording.saveError = writer.failed ?? "not saved";
|
|
3328
|
+
}
|
|
3329
|
+
this.last = recording;
|
|
3330
|
+
return recording;
|
|
3331
|
+
}
|
|
3332
|
+
async record(durationMs, options = {}) {
|
|
3333
|
+
this.start(options);
|
|
3334
|
+
await new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
3335
|
+
if (!this.recorder && this.autoStop) return this.autoStop;
|
|
3336
|
+
return this.stop();
|
|
3337
|
+
}
|
|
3338
|
+
live() {
|
|
3339
|
+
return this.recorder?.live() ?? null;
|
|
3340
|
+
}
|
|
3341
|
+
noteHmr(type, paths) {
|
|
3342
|
+
this.recorder?.noteHmr(type, paths);
|
|
3343
|
+
}
|
|
3344
|
+
/** The page unloads mid-recording: flush what is left; the server marks the session interrupted. */
|
|
3345
|
+
interrupt() {
|
|
3346
|
+
if (!this.recorder) return;
|
|
3347
|
+
this.writer?.beacon(Math.round(this.recorder.now()));
|
|
3348
|
+
}
|
|
3349
|
+
/**
|
|
3350
|
+
* The app's own components on the page right now, by name. A script that was told to record inside one and did
|
|
3351
|
+
* not find it can say what there is instead of failing into nothing.
|
|
3352
|
+
*/
|
|
3353
|
+
componentNames(limit = 60) {
|
|
3354
|
+
const names2 = /* @__PURE__ */ new Set();
|
|
3355
|
+
eachFiber((f) => {
|
|
3356
|
+
const name = nameOf(f);
|
|
3357
|
+
if (name && !isLibraryFiber(f)) names2.add(name);
|
|
3358
|
+
return names2.size < limit;
|
|
3359
|
+
});
|
|
3360
|
+
return [...names2];
|
|
3361
|
+
}
|
|
3362
|
+
/**
|
|
3363
|
+
* Every instance of a component on the page now, found by its name and file. A line that moved since the
|
|
3364
|
+
* recording (the code was edited) still finds it by the file; failing that, by the name alone.
|
|
3365
|
+
*/
|
|
3366
|
+
findComponents(name, source, limit = 50) {
|
|
3367
|
+
const named = [];
|
|
3368
|
+
eachFiber((f) => {
|
|
3369
|
+
const inner = f.return !== null && isComposite(f.return) && nameOf(f.return) === name;
|
|
3370
|
+
if (nameOf(f) === name && isComposite(f) && !inner) named.push(f);
|
|
3371
|
+
return named.length < limit * 4;
|
|
3372
|
+
});
|
|
3373
|
+
if (!source) return named.slice(0, limit);
|
|
3374
|
+
const file = source.replace(/:\d+(:\d+)?$/, "");
|
|
3375
|
+
const same2 = named.filter((f) => sourceOf(f, this.config.projectRoot) === source);
|
|
3376
|
+
const sameFile = named.filter((f) => sourceOf(f, this.config.projectRoot).replace(/:\d+(:\d+)?$/, "") === file);
|
|
3377
|
+
return (same2.length ? same2 : sameFile.length ? sameFile : named).slice(0, limit);
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* The topmost of the app's own components, as the tree shows them with these filters: where a tree of the whole
|
|
3381
|
+
* app opens when no area has been picked yet.
|
|
3382
|
+
*/
|
|
3383
|
+
topComponent(shown) {
|
|
3384
|
+
for (const root of findRoots()) {
|
|
3385
|
+
const [top] = compositeChildren(root.current, (f) => this.hidden(this.ownerOf(f), shown), 1);
|
|
3386
|
+
if (top) return top;
|
|
3387
|
+
}
|
|
3388
|
+
return null;
|
|
3389
|
+
}
|
|
3390
|
+
/** Composite ancestors of an element, nearest first. */
|
|
3391
|
+
owners(el) {
|
|
3392
|
+
const host = fiberFromNode(el);
|
|
3393
|
+
return host ? this.ownersOfFiber(host) : [];
|
|
3394
|
+
}
|
|
3395
|
+
/** The fiber itself when it is a component, then its composite ancestors, nearest first. */
|
|
3396
|
+
ownersOfFiber(fiber) {
|
|
3397
|
+
return compositeChain(currentOf(fiber)).reverse().map((f) => this.ownerOf(f));
|
|
3398
|
+
}
|
|
3399
|
+
ownerOf(fiber) {
|
|
3400
|
+
const name = nameOf(fiber) ?? "Anonymous";
|
|
3401
|
+
return {
|
|
3402
|
+
name,
|
|
3403
|
+
source: sourceOf(fiber, this.config.projectRoot),
|
|
3404
|
+
wrapper: this.wrapperRe.test(name),
|
|
3405
|
+
provider: isProvider(name) || wrapsProvider(fiber),
|
|
3406
|
+
library: isLibraryFiber(fiber),
|
|
3407
|
+
fiber
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
/** Nearest components below one; what is hidden is walked through, not stopped at. */
|
|
3411
|
+
childOwners(fiber, shown) {
|
|
3412
|
+
return compositeChildren(fiber, (f) => this.hidden(this.ownerOf(f), shown)).map((f) => this.ownerOf(f));
|
|
3413
|
+
}
|
|
3414
|
+
/** A component the tree leaves out: the checkboxes decide, and an unnamed wrapper follows the library one. */
|
|
3415
|
+
hidden(owner, shown) {
|
|
3416
|
+
return !shown.library && (owner.library || owner.wrapper) || !shown.providers && owner.provider;
|
|
3417
|
+
}
|
|
3418
|
+
scopeFromFiber(fiber) {
|
|
3419
|
+
return scopeFromFiber(fiber, this.config.projectRoot);
|
|
3420
|
+
}
|
|
3421
|
+
scopeFromElement(el, level = 0) {
|
|
3422
|
+
const owners = this.owners(el).filter((o) => !o.wrapper && !o.library && !o.provider);
|
|
3423
|
+
const owner = owners[Math.min(level, owners.length - 1)];
|
|
3424
|
+
if (!owner) throw new RecorderError("SCOPE_NOT_FOUND", "no React component owns this element");
|
|
3425
|
+
return this.scopeFromFiber(owner.fiber);
|
|
3426
|
+
}
|
|
3427
|
+
/** Finds the component again after a reload by its composite path, e.g. ['OrdersPanel', 'PositionTable']. */
|
|
3428
|
+
scopeFromNames(names2) {
|
|
3429
|
+
const target2 = names2.at(-1);
|
|
3430
|
+
let found = null;
|
|
3431
|
+
eachFiber((f) => {
|
|
3432
|
+
if (!target2 || nameOf(f) !== target2) return;
|
|
3433
|
+
const chain = compositeChain(f).map((x) => nameOf(x)).filter((n) => Boolean(n));
|
|
3434
|
+
let i = names2.length - 1;
|
|
3435
|
+
for (let j = chain.length - 1; j >= 0 && i >= 0; j--) if (chain[j] === names2[i]) i--;
|
|
3436
|
+
if (i < 0) found = f;
|
|
3437
|
+
return i >= 0;
|
|
3438
|
+
});
|
|
3439
|
+
if (found) return this.scopeFromFiber(found);
|
|
3440
|
+
throw new RecorderError("SCOPE_NOT_FOUND", `component ${names2.join(" > ")} is not mounted`);
|
|
3441
|
+
}
|
|
3442
|
+
resolveScope(spec) {
|
|
3443
|
+
if (!spec) return null;
|
|
3444
|
+
if ("kind" in spec) return spec;
|
|
3445
|
+
if ("names" in spec) return this.scopeFromNames(spec.names);
|
|
3446
|
+
const el = document.querySelector(spec.selector);
|
|
3447
|
+
if (!el) throw new RecorderError("SCOPE_NOT_FOUND", `no element matches ${spec.selector}`);
|
|
3448
|
+
if (spec.component) {
|
|
3449
|
+
const owner = this.owners(el).find((o) => o.name === spec.component);
|
|
3450
|
+
if (!owner) throw new RecorderError("SCOPE_NOT_FOUND", `${spec.component} does not own ${spec.selector}`);
|
|
3451
|
+
return this.scopeFromFiber(owner.fiber);
|
|
3452
|
+
}
|
|
3453
|
+
return this.scopeFromElement(el, spec.level ?? 0);
|
|
3454
|
+
}
|
|
3455
|
+
};
|
|
3456
|
+
|
|
3457
|
+
// src/core/plugins.ts
|
|
3458
|
+
var WAIT_MS = 1e3;
|
|
3459
|
+
var MAX_BUFFER = 5e3;
|
|
3460
|
+
var PluginHost = class {
|
|
3461
|
+
constructor(entries) {
|
|
3462
|
+
this.loaded = [];
|
|
3463
|
+
this.recording = false;
|
|
3464
|
+
/** Set by the recorder: components that got updates since the last commit, to aim a cause at their roots. */
|
|
3465
|
+
this.targets = null;
|
|
3466
|
+
this.buffer = [];
|
|
3467
|
+
/** `waitForTimer` events, until a timer of their plugin's packages runs. */
|
|
3468
|
+
this.waiting = [];
|
|
3469
|
+
this.owners = /* @__PURE__ */ new Map();
|
|
3470
|
+
/** Plugins whose events a timer of theirs has delivered in this recording. */
|
|
3471
|
+
this.delivering = /* @__PURE__ */ new Set();
|
|
3472
|
+
this.t0 = 0;
|
|
3473
|
+
this.warnings = [];
|
|
3474
|
+
for (const [factoryOrPlugin, options] of entries) {
|
|
3475
|
+
try {
|
|
3476
|
+
const plugin = typeof factoryOrPlugin === "function" ? factoryOrPlugin(options) : factoryOrPlugin;
|
|
3477
|
+
if (!plugin?.name) throw new Error("plugin without a name");
|
|
3478
|
+
this.loaded.push({ plugin });
|
|
3479
|
+
for (const name of plugin.packages ?? []) this.owners.set(name, plugin.name);
|
|
3480
|
+
} catch (error) {
|
|
3481
|
+
this.loaded.push({ plugin: { name: `plugin#${this.loaded.length}` }, error: String(error?.message ?? error) });
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
setupAll() {
|
|
3486
|
+
for (const entry of this.loaded) {
|
|
3487
|
+
if (entry.error || !entry.plugin.setup) continue;
|
|
3488
|
+
this.guard(entry, () => entry.plugin.setup(this.context(entry.plugin.name)));
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
info() {
|
|
3492
|
+
return this.loaded.map(({ plugin, error }) => ({ name: plugin.name, sectionVersion: plugin.sectionVersion ?? 1, ...error ? { error } : {} }));
|
|
3493
|
+
}
|
|
3494
|
+
context(name) {
|
|
3495
|
+
const host = this;
|
|
3496
|
+
return {
|
|
3497
|
+
get recording() {
|
|
3498
|
+
return host.recording;
|
|
3499
|
+
},
|
|
3500
|
+
emitCause: (event) => host.emit(name, event),
|
|
3501
|
+
now: () => host.now(),
|
|
3502
|
+
warn: (message) => host.warnings.push(`${name}: ${message}`)
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
/** Aimed only when the caller says it can be: an event that runs before React cannot say whom it woke. */
|
|
3506
|
+
emit(plugin, event, fibers) {
|
|
3507
|
+
if (!this.recording || this.buffer.length >= MAX_BUFFER) return null;
|
|
3508
|
+
if (event.waitForTimer && this.waiting.length < MAX_BUFFER) {
|
|
3509
|
+
const same2 = event.merge ? this.waiting.find((w) => w.plugin === plugin && w.merge === event.merge) : void 0;
|
|
3510
|
+
if (same2) {
|
|
3511
|
+
same2.type = event.type;
|
|
3512
|
+
return same2;
|
|
3513
|
+
}
|
|
3514
|
+
const cause2 = {
|
|
3515
|
+
plugin,
|
|
3516
|
+
type: event.type,
|
|
3517
|
+
atMs: Math.round(this.now()),
|
|
3518
|
+
changes: event.changes,
|
|
3519
|
+
data: event.data,
|
|
3520
|
+
merge: event.merge,
|
|
3521
|
+
order: nextOrder(),
|
|
3522
|
+
emittedMs: performance.now()
|
|
3523
|
+
};
|
|
3524
|
+
this.waiting.push(cause2);
|
|
3525
|
+
return cause2;
|
|
3526
|
+
}
|
|
3527
|
+
const aimed = fibers ?? (event.aim ? this.targets?.() : void 0);
|
|
3528
|
+
const cause = {
|
|
3529
|
+
plugin,
|
|
3530
|
+
type: event.type,
|
|
3531
|
+
atMs: Math.round(this.now()),
|
|
3532
|
+
changes: event.changes,
|
|
3533
|
+
data: event.data,
|
|
3534
|
+
...aimed ? { aimed: true } : {},
|
|
3535
|
+
fibers: aimed?.size ? aimed : void 0
|
|
3536
|
+
};
|
|
3537
|
+
this.buffer.push(cause);
|
|
3538
|
+
return cause;
|
|
3539
|
+
}
|
|
3540
|
+
get hasWaiting() {
|
|
3541
|
+
return this.waiting.length > 0;
|
|
3542
|
+
}
|
|
3543
|
+
/**
|
|
3544
|
+
* A timer of `library` updated components: the events its plugin had waiting since before it started go to them,
|
|
3545
|
+
* or to the commit as it is when `fibers` is null (the commit ran inside the timer). Returns whether it was a
|
|
3546
|
+
* plugin's delivery, which then needs no cause of its own. A timer that updated no one is not called: a library
|
|
3547
|
+
* runs other timers too (garbage collection, the next poll), and one of those must not take the events.
|
|
3548
|
+
*/
|
|
3549
|
+
deliver(library, fibers, startedAt = Infinity) {
|
|
3550
|
+
const plugin = library ? this.owners.get(library) : void 0;
|
|
3551
|
+
if (!plugin) return false;
|
|
3552
|
+
const mine = this.waiting.filter((w) => w.plugin === plugin && w.order < startedAt);
|
|
3553
|
+
if (!mine.length) return false;
|
|
3554
|
+
this.waiting = this.waiting.filter((w) => !mine.includes(w));
|
|
3555
|
+
this.delivering.add(plugin);
|
|
3556
|
+
const aimed = fibers();
|
|
3557
|
+
for (const { merge, order: order2, emittedMs, ...cause } of mine) this.buffer.push(aimed ? { ...cause, aimed: true, fibers: aimed } : cause);
|
|
3558
|
+
return true;
|
|
3559
|
+
}
|
|
3560
|
+
drain() {
|
|
3561
|
+
if (this.waiting.length && performance.now() - this.waiting[0].emittedMs > WAIT_MS) {
|
|
3562
|
+
const cutoff = performance.now() - WAIT_MS;
|
|
3563
|
+
for (const { merge, order: order2, emittedMs, ...cause } of this.waiting.filter((w) => w.emittedMs <= cutoff))
|
|
3564
|
+
if (!this.delivering.has(cause.plugin)) this.buffer.push(cause);
|
|
3565
|
+
this.waiting = this.waiting.filter((w) => w.emittedMs > cutoff);
|
|
3566
|
+
}
|
|
3567
|
+
if (!this.buffer.length) return this.buffer;
|
|
3568
|
+
const out = this.buffer;
|
|
3569
|
+
this.buffer = [];
|
|
3570
|
+
return out;
|
|
3571
|
+
}
|
|
3572
|
+
now() {
|
|
3573
|
+
return performance.now() - this.t0;
|
|
3574
|
+
}
|
|
3575
|
+
selector(fn, depth = 0) {
|
|
3576
|
+
if (depth > 3) return fallbackSelectorLabel(fn);
|
|
3577
|
+
return this.describe(fn, "selector", depth) ?? fallbackSelectorLabel(fn);
|
|
3578
|
+
}
|
|
3579
|
+
store(getSnapshot) {
|
|
3580
|
+
return this.describe(getSnapshot, "store", 0);
|
|
3581
|
+
}
|
|
3582
|
+
describe(fn, kind, depth) {
|
|
3583
|
+
for (const entry of this.loaded) {
|
|
3584
|
+
if (entry.error || !entry.plugin.describe) continue;
|
|
3585
|
+
try {
|
|
3586
|
+
const label = entry.plugin.describe(fn, kind, (inner) => this.selector(inner, depth + 1));
|
|
3587
|
+
if (label != null) return label;
|
|
3588
|
+
} catch {
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
return null;
|
|
3592
|
+
}
|
|
3593
|
+
start(session, t0) {
|
|
3594
|
+
this.t0 = t0;
|
|
3595
|
+
this.buffer = [];
|
|
3596
|
+
this.waiting = [];
|
|
3597
|
+
this.delivering.clear();
|
|
3598
|
+
this.recording = true;
|
|
3599
|
+
for (const entry of this.loaded) {
|
|
3600
|
+
if (entry.error || !entry.plugin.start) continue;
|
|
3601
|
+
this.guard(entry, () => entry.plugin.start({ ...this.context(entry.plugin.name), ...session }));
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
commit(session) {
|
|
3605
|
+
for (const entry of this.loaded) {
|
|
3606
|
+
if (entry.error || !entry.plugin.commit) continue;
|
|
3607
|
+
this.guard(entry, () => entry.plugin.commit({ ...this.context(entry.plugin.name), ...session }));
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
stop(session) {
|
|
3611
|
+
this.recording = false;
|
|
3612
|
+
this.buffer = [];
|
|
3613
|
+
this.waiting = [];
|
|
3614
|
+
const sections = {};
|
|
3615
|
+
for (const entry of this.loaded) {
|
|
3616
|
+
if (entry.error || !entry.plugin.stop) continue;
|
|
3617
|
+
this.guard(entry, () => {
|
|
3618
|
+
const section = entry.plugin.stop({ ...this.context(entry.plugin.name), ...session });
|
|
3619
|
+
if (section) sections[entry.plugin.name] = section;
|
|
3620
|
+
});
|
|
3621
|
+
}
|
|
3622
|
+
return sections;
|
|
3623
|
+
}
|
|
3624
|
+
conditions() {
|
|
3625
|
+
const out = {};
|
|
3626
|
+
for (const entry of this.loaded) {
|
|
3627
|
+
if (entry.error || !entry.plugin.conditions) continue;
|
|
3628
|
+
this.guard(entry, () => Object.assign(out, entry.plugin.conditions()));
|
|
3629
|
+
}
|
|
3630
|
+
return out;
|
|
3631
|
+
}
|
|
3632
|
+
guard(entry, fn) {
|
|
3633
|
+
try {
|
|
3634
|
+
fn();
|
|
3635
|
+
} catch (error) {
|
|
3636
|
+
this.warnings.push(`${entry.plugin.name}: ${String(error?.message ?? error).slice(0, 200)}`);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
};
|
|
3640
|
+
|
|
3641
|
+
// src/iife/engine.ts
|
|
3642
|
+
var target = window;
|
|
3643
|
+
if (!target[GLOBAL_KEY]) {
|
|
3644
|
+
const engine = new Engine(
|
|
3645
|
+
{
|
|
3646
|
+
version: true ? "0.1.0" : "dev",
|
|
3647
|
+
projectRoot: "",
|
|
3648
|
+
wrapperPattern: "^(Anonymous|ForwardRef|Memo)$",
|
|
3649
|
+
actions: { values: false, secretSelector: "" },
|
|
3650
|
+
maxDurationMs: 6e5,
|
|
3651
|
+
bigCommit: 150,
|
|
3652
|
+
timelineLimit: 5e3,
|
|
3653
|
+
endpoint: null
|
|
3654
|
+
},
|
|
3655
|
+
new PluginHost([])
|
|
3656
|
+
);
|
|
3657
|
+
captureRenderers();
|
|
3658
|
+
installTimers();
|
|
3659
|
+
target[GLOBAL_KEY] = { version: engine.version, engine, panel: null, format: { summarize, reasonLine, hookText, actionText } };
|
|
3660
|
+
}
|
|
3661
|
+
})();
|