@atlassian/chippy 0.0.0-stage → 0.0.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +188 -2
  3. package/bin/chippy.mjs +134 -0
  4. package/bin/pack.mjs +184 -0
  5. package/lib/avalanche.js +2570 -0
  6. package/lib/avalanche.js.map +7 -0
  7. package/lib/bootstrap.d.ts +1 -0
  8. package/lib/bootstrap.js +51 -0
  9. package/lib/bootstrap.js.map +7 -0
  10. package/lib/browser.js +15 -0
  11. package/lib/browser.js.map +7 -0
  12. package/lib/extension/avalanche.es.js +5396 -0
  13. package/lib/extension/avalanche.es.js.map +1 -0
  14. package/lib/extension/chippy-page-bridge.js +1833 -0
  15. package/lib/extension/inspect-react.js +216 -0
  16. package/lib/extension/layout-lab.js +1453 -0
  17. package/lib/extension/panel-dom.js +90 -0
  18. package/lib/extension/resolve-probes.js +342 -0
  19. package/lib/mcp-skill/LICENSE +201 -0
  20. package/lib/mcp-skill/README.md +150 -0
  21. package/lib/mcp-skill/SKILL.md +330 -0
  22. package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
  23. package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
  24. package/lib/mcp-skill/extension/avalanche.es.js +5396 -0
  25. package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
  26. package/lib/mcp-skill/extension/avalanche.iife.js +5397 -0
  27. package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
  28. package/lib/mcp-skill/extension/background.js +323 -0
  29. package/lib/mcp-skill/extension/chippy-page-bridge.js +1833 -0
  30. package/lib/mcp-skill/extension/content-script.js +167 -0
  31. package/lib/mcp-skill/extension/devtools-hook-shim.js +68 -0
  32. package/lib/mcp-skill/extension/early-bootstrap.js +47 -0
  33. package/lib/mcp-skill/extension/icon-128.png +0 -0
  34. package/lib/mcp-skill/extension/icon-16.png +0 -0
  35. package/lib/mcp-skill/extension/icon-32.png +0 -0
  36. package/lib/mcp-skill/extension/icon-48.png +0 -0
  37. package/lib/mcp-skill/extension/inspect-react.js +216 -0
  38. package/lib/mcp-skill/extension/layout-lab.js +1453 -0
  39. package/lib/mcp-skill/extension/logo.svg +14 -0
  40. package/lib/mcp-skill/extension/manifest.json +69 -0
  41. package/lib/mcp-skill/extension/panel-dom.js +90 -0
  42. package/lib/mcp-skill/extension/panel-settings.js +191 -0
  43. package/lib/mcp-skill/extension/popup.html +41 -0
  44. package/lib/mcp-skill/extension/popup.js +80 -0
  45. package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
  46. package/lib/mcp-skill/extension/resolve-probes.js +342 -0
  47. package/lib/mcp-skill/package.json +1 -0
  48. package/lib/mcp-skill/references/README.md +56 -0
  49. package/lib/mcp-skill/references/active-stimulus.md +85 -0
  50. package/lib/mcp-skill/references/avalanche-detector.md +731 -0
  51. package/lib/mcp-skill/references/bdd-router.md +310 -0
  52. package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
  53. package/lib/mcp-skill/references/command-output-formats.md +1075 -0
  54. package/lib/mcp-skill/references/command-surface.md +183 -0
  55. package/lib/mcp-skill/references/cynefin.md +99 -0
  56. package/lib/mcp-skill/references/debug-hook.md +161 -0
  57. package/lib/mcp-skill/references/fixing-performance.md +490 -0
  58. package/lib/mcp-skill/references/hook-metrics.md +199 -0
  59. package/lib/mcp-skill/references/hooks-inspection.md +61 -0
  60. package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
  61. package/lib/mcp-skill/references/mcp.md +122 -0
  62. package/lib/mcp-skill/references/mode-a-injection.md +68 -0
  63. package/lib/mcp-skill/references/operating-model.md +128 -0
  64. package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
  65. package/lib/mcp-skill/references/playwright-probes.md +205 -0
  66. package/lib/mcp-skill/references/procedures.md +545 -0
  67. package/lib/mcp-skill/references/recipes.txt +114 -0
  68. package/lib/mcp-skill/references/stability-render.md +428 -0
  69. package/lib/mcp-skill/references/test-mode-playbook.md +193 -0
  70. package/lib/mcp-skill/references/troubleshooting.md +54 -0
  71. package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
  72. package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
  73. package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
  74. package/lib/mcp-skill/scripts/chippy-helper-server.js +1710 -0
  75. package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
  76. package/lib/mcp-skill/scripts/chippy.sh +1218 -0
  77. package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
  78. package/lib/mcp-skill/scripts/helper-access.js +144 -0
  79. package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
  80. package/lib/mcp-skill/scripts/search-recipes.js +248 -0
  81. package/lib/mcp-skill/snippets/README.md +89 -0
  82. package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
  83. package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
  84. package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
  85. package/lib/mcp-skill/snippets/find-prop.js +83 -0
  86. package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
  87. package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
  88. package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
  89. package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
  90. package/lib/package.json +1 -0
  91. package/lib/plugin.js +119 -0
  92. package/lib/plugin.js.map +7 -0
  93. package/lib/storybook/avalanche-panel-model.js +79 -0
  94. package/lib/storybook/brand.jsx +6 -0
  95. package/lib/storybook/connection-fallback.jsx +47 -0
  96. package/lib/storybook/constants.js +15 -0
  97. package/lib/storybook/context-card.jsx +171 -0
  98. package/lib/storybook/manager.jsx +403 -0
  99. package/lib/storybook/preset.js +7 -0
  100. package/lib/storybook/preview.js +263 -0
  101. package/lib/storybook/probe-annotation-editor.jsx +52 -0
  102. package/lib/storybook/probe-intent.js +23 -0
  103. package/lib/storybook/setup.js +93 -0
  104. package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
  105. package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
  106. package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
  107. package/lib/types/avalanche/detector.d.ts +183 -0
  108. package/lib/types/avalanche/interaction-capture.d.ts +25 -0
  109. package/lib/types/avalanche/internals.d.ts +189 -0
  110. package/lib/types/avalanche/report-serializer.d.ts +283 -0
  111. package/lib/types/avalanche/reporter.d.ts +610 -0
  112. package/lib/types/chippy-bridge/fiber-source.d.ts +37 -0
  113. package/lib/types/chippy-bridge/react-renderer.d.ts +49 -0
  114. package/lib/types/chippy-bridge/source-map.d.ts +30 -0
  115. package/lib/types/debug-render/core.d.ts +72 -0
  116. package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
  117. package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
  118. package/lib/types/layout-lab/marker-context.d.ts +94 -0
  119. package/lib/types/package/avalanche.d.ts +6 -0
  120. package/lib/types/package/browser.d.ts +10 -0
  121. package/lib/types/package/plugin.d.ts +15 -0
  122. package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
  123. package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
  124. package/lib/types/plugins/index.d.ts +49 -0
  125. package/lib/types/plugins/jira/context-provider.d.ts +36 -0
  126. package/package.json +104 -4
@@ -0,0 +1,2570 @@
1
+ // src/chippy-bridge/source-map.ts
2
+ var DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3
+ var VALUE_OF = new Int8Array(128).fill(-1);
4
+ for (let index = 0; index < DIGITS.length; index += 1) {
5
+ VALUE_OF[DIGITS.charCodeAt(index)] = index;
6
+ }
7
+
8
+ // src/chippy-bridge/fiber-source.ts
9
+ var V8_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/;
10
+ var AT_FRAME = /^\s*(?:(.*?)@)(.+?):(\d+):(\d+)$/;
11
+ var MAX_CALL_SITE_FRAMES = 4;
12
+ var resolvedFiberSources = /* @__PURE__ */ new WeakMap();
13
+ function parseStackFrames(stack) {
14
+ const frames = [];
15
+ for (const raw of stack.split("\n")) {
16
+ const line = raw.trimEnd();
17
+ if (line === "" || line.includes("(eval at ")) continue;
18
+ const match = V8_FRAME.exec(line) ?? AT_FRAME.exec(line);
19
+ if (!match) continue;
20
+ const [, name, url = "", lineDigits = "", columnDigits = ""] = match;
21
+ if (url === "" || url.startsWith("data:")) continue;
22
+ const at = Number(lineDigits);
23
+ const column = Number(columnDigits);
24
+ if (!Number.isFinite(at) || !Number.isFinite(column) || at < 1) continue;
25
+ frames.push({
26
+ url,
27
+ line: at,
28
+ column,
29
+ ...name !== void 0 && name !== "" ? { function: name } : {}
30
+ });
31
+ }
32
+ return frames;
33
+ }
34
+ function isVendorPath(path) {
35
+ return path.includes("node_modules") || path.includes("/.vite/deps/") || path.includes("/.yarn/");
36
+ }
37
+ function rawLocation(frame) {
38
+ return { fileName: frame.url, lineNumber: frame.line, columnNumber: frame.column };
39
+ }
40
+ function locationFromOriginal(original) {
41
+ return { fileName: original.source, lineNumber: original.line, columnNumber: original.column };
42
+ }
43
+ function writerLocationOf(frames, originalFor) {
44
+ for (const frame of frames) {
45
+ if (isVendorPath(frame.url)) continue;
46
+ const original = originalFor(frame);
47
+ if (original === null) return rawLocation(frame);
48
+ if (isVendorPath(original.source)) continue;
49
+ return locationFromOriginal(original);
50
+ }
51
+ return null;
52
+ }
53
+ function debugSourceOf(value) {
54
+ if (!value || typeof value !== "object") return null;
55
+ const source = value;
56
+ if (typeof source.fileName !== "string" || typeof source.lineNumber !== "number") return null;
57
+ return {
58
+ fileName: source.fileName,
59
+ lineNumber: source.lineNumber,
60
+ columnNumber: typeof source.columnNumber === "number" ? source.columnNumber : null
61
+ };
62
+ }
63
+ function recordedSource(fiber) {
64
+ const direct = debugSourceOf(fiber._debugSource);
65
+ if (direct) return direct;
66
+ const type = fiber.type;
67
+ return type && (typeof type === "object" || typeof type === "function") ? debugSourceOf(type._debugSource) : null;
68
+ }
69
+ function getFiberStackFrames(fiber) {
70
+ if (!fiber || !fiber._debugStack || typeof fiber._debugStack !== "object") return [];
71
+ const stack = fiber._debugStack.stack;
72
+ if (typeof stack !== "string" || stack === "") return [];
73
+ const candidates = [];
74
+ for (const frame of parseStackFrames(stack)) {
75
+ if (isVendorPath(frame.url)) continue;
76
+ candidates.push(frame);
77
+ if (candidates.length >= MAX_CALL_SITE_FRAMES) break;
78
+ }
79
+ return candidates;
80
+ }
81
+ function getFiberSourceCandidate(fiber) {
82
+ if (!fiber) return null;
83
+ const recorded = recordedSource(fiber);
84
+ if (recorded) return recorded;
85
+ return writerLocationOf(getFiberStackFrames(fiber), () => null);
86
+ }
87
+ function getFiberSource(fiber) {
88
+ if (!fiber) return null;
89
+ const recorded = recordedSource(fiber);
90
+ if (recorded) return recorded;
91
+ const cached = resolvedFiberSources.get(fiber);
92
+ if (cached !== void 0) return cached;
93
+ return getFiberSourceCandidate(fiber);
94
+ }
95
+
96
+ // src/avalanche/internals.tsx
97
+ var PerformedWork = 1;
98
+ var Placement = 2;
99
+ var Hydrating = 4096;
100
+ var COMPONENT_TAGS = /* @__PURE__ */ new Set([0, 1, 2, 11, 14, 15]);
101
+ var CONTEXT_PROVIDER_TAG = 10;
102
+ var SuspenseTag = 13;
103
+ var ChildDeletion = 16;
104
+ var Update = 4;
105
+ function isComponentFiber(tag) {
106
+ return COMPONENT_TAGS.has(tag);
107
+ }
108
+ function isContextProviderFiber(tag) {
109
+ return tag === CONTEXT_PROVIDER_TAG;
110
+ }
111
+ function isSuspenseFiber(tag) {
112
+ return tag === SuspenseTag;
113
+ }
114
+ function didSuspenseStateChange(fiber) {
115
+ const currSuspended = fiber.memoizedState !== null;
116
+ if (!fiber.alternate) {
117
+ if (currSuspended) {
118
+ return { entered: true, left: false };
119
+ }
120
+ return null;
121
+ }
122
+ const prevSuspended = fiber.alternate.memoizedState !== null;
123
+ if (prevSuspended === currSuspended) return null;
124
+ return { entered: currSuspended, left: prevSuspended };
125
+ }
126
+ function didContextValueChange(fiber) {
127
+ if (fiber.tag !== CONTEXT_PROVIDER_TAG || !fiber.alternate) return false;
128
+ const alternateFiber = fiber.alternate;
129
+ const currentFiber = fiber;
130
+ const prevProps = alternateFiber.memoizedProps;
131
+ const currentProps = currentFiber.memoizedProps;
132
+ return prevProps && currentProps && !Object.is(prevProps.value, currentProps.value);
133
+ }
134
+ function isReactInternalComponentType(type) {
135
+ return typeof type === "symbol";
136
+ }
137
+ function getComponentName(fiber) {
138
+ const type = fiber.type;
139
+ const elementType = fiber.elementType;
140
+ const named = type?.displayName || type?.name || elementType?.displayName || elementType?.name;
141
+ if (named) return named;
142
+ const source = getFiberSourceCandidate(fiber);
143
+ return source && `<${source.fileName}:${source.lineNumber}>` || "Anonymous";
144
+ }
145
+ function isStateHook(hook) {
146
+ const queue = hook.queue;
147
+ if (!queue) return false;
148
+ return typeof queue.dispatch === "function" || "value" in queue && "getSnapshot" in queue;
149
+ }
150
+ function isExternalStoreHook(hook) {
151
+ const queue = hook.queue;
152
+ return queue && typeof queue.dispatch !== "function" && "value" in queue && "getSnapshot" in queue;
153
+ }
154
+ function detectHookStateChanges(prevHookChain, currentHookChain) {
155
+ if (prevHookChain === currentHookChain) {
156
+ return {
157
+ hasChange: false,
158
+ actionable: false,
159
+ changedIndices: [],
160
+ changedHooks: [],
161
+ hasExternalStoreChange: false
162
+ };
163
+ }
164
+ let prevHook = prevHookChain;
165
+ let currentHook = currentHookChain;
166
+ let memoizedStateIndex = 0;
167
+ const changedIndices = [];
168
+ const changedHooks = [];
169
+ let hasExternalStoreChange = false;
170
+ while (prevHook && currentHook) {
171
+ if (isStateHook(currentHook)) {
172
+ const prevValueRaw = prevHook.memoizedState === void 0 ? prevHook.queue?.lastRenderedState : prevHook.memoizedState;
173
+ const curValueRaw = currentHook.memoizedState === void 0 ? currentHook.queue?.lastRenderedState : currentHook.memoizedState;
174
+ if (!Object.is(prevValueRaw, curValueRaw)) {
175
+ const isExternal = isExternalStoreHook(currentHook);
176
+ if (isExternal) hasExternalStoreChange = true;
177
+ changedIndices.push(memoizedStateIndex);
178
+ changedHooks.push({
179
+ prevValue: prevValueRaw,
180
+ currentValue: curValueRaw,
181
+ ...isExternal && { isExternalStore: true }
182
+ });
183
+ }
184
+ }
185
+ prevHook = prevHook.next;
186
+ currentHook = currentHook.next;
187
+ memoizedStateIndex++;
188
+ }
189
+ return {
190
+ hasChange: changedIndices.length > 0,
191
+ actionable: changedIndices.length > 0,
192
+ changedIndices,
193
+ changedHooks,
194
+ hasExternalStoreChange
195
+ };
196
+ }
197
+ function detectHookThrashing(prevHookChain, currentHookChain, mode = "non-primitive", valueProvenance = null) {
198
+ if (prevHookChain === currentHookChain) {
199
+ return { effects: [], callbacks: [], memos: [] };
200
+ }
201
+ const thrashedEffects = [];
202
+ const thrashedCallbacks = [];
203
+ const thrashedMemos = [];
204
+ let prevHook = prevHookChain;
205
+ let currentHook = currentHookChain;
206
+ let hookIndex = 0;
207
+ while (prevHook && currentHook) {
208
+ if (!isStateHook(currentHook)) {
209
+ const prevValue = prevHook.memoizedState;
210
+ const currentValue = currentHook.memoizedState;
211
+ let prevDeps = null;
212
+ let currentDeps = null;
213
+ let hookType = null;
214
+ if (prevValue && currentValue && typeof prevValue === "object" && typeof currentValue === "object" && "deps" in prevValue && "deps" in currentValue && "tag" in currentValue) {
215
+ prevDeps = prevValue.deps;
216
+ currentDeps = currentValue.deps;
217
+ hookType = "effect";
218
+ } else if (Array.isArray(prevValue) && Array.isArray(currentValue) && prevValue.length === 2 && currentValue.length === 2 && Array.isArray(prevValue[1]) && Array.isArray(currentValue[1])) {
219
+ prevDeps = prevValue[1];
220
+ currentDeps = currentValue[1];
221
+ hookType = typeof currentValue[0] === "function" ? "callback" : "memo";
222
+ }
223
+ if (hookType && Array.isArray(prevDeps) && Array.isArray(currentDeps)) {
224
+ const changedDepIndices = [];
225
+ const changedDepOrigins = [];
226
+ for (let j = 0; j < currentDeps.length; j++) {
227
+ if (!Object.is(prevDeps[j], currentDeps[j])) {
228
+ changedDepIndices.push(j);
229
+ let origin = null;
230
+ const dep = currentDeps[j];
231
+ if (valueProvenance && dep != null && (typeof dep === "object" || typeof dep === "function")) {
232
+ origin = valueProvenance.get(dep) || null;
233
+ }
234
+ changedDepOrigins.push(origin);
235
+ }
236
+ }
237
+ let shouldReport = changedDepIndices.length > 0;
238
+ if (shouldReport && mode === "non-primitive") {
239
+ shouldReport = changedDepIndices.some((idx) => {
240
+ const dep = currentDeps[idx];
241
+ const t = typeof dep;
242
+ return !(dep == null || t === "string" || t === "number" || t === "boolean" || t === "symbol" || t === "bigint");
243
+ });
244
+ }
245
+ if (shouldReport) {
246
+ const thrashInfo = {
247
+ hookIndex,
248
+ hookType,
249
+ deps: currentDeps,
250
+ changedDepIndices,
251
+ changedDepOrigins
252
+ };
253
+ if (hookType === "effect") {
254
+ const effectValue = currentValue;
255
+ thrashInfo.effectTag = effectValue.tag;
256
+ if (effectValue.create) {
257
+ thrashInfo.hookBody = effectValue.create;
258
+ }
259
+ thrashedEffects.push(thrashInfo);
260
+ } else if (hookType === "callback") {
261
+ const callbackFn = currentValue[0];
262
+ thrashInfo.hookBody = callbackFn;
263
+ thrashedCallbacks.push(thrashInfo);
264
+ } else {
265
+ const memoValue = currentValue[0];
266
+ thrashInfo.memoValue = memoValue;
267
+ thrashInfo.hookBody = memoValue;
268
+ thrashedMemos.push(thrashInfo);
269
+ }
270
+ }
271
+ }
272
+ }
273
+ prevHook = prevHook.next;
274
+ currentHook = currentHook.next;
275
+ hookIndex++;
276
+ }
277
+ return {
278
+ effects: thrashedEffects,
279
+ callbacks: thrashedCallbacks,
280
+ memos: thrashedMemos
281
+ };
282
+ }
283
+ function didFiberUpdate(fiber) {
284
+ return (fiber.flags & PerformedWork) !== 0;
285
+ }
286
+ function didFiberSelfUpdate(fiber) {
287
+ if (!didFiberUpdate(fiber)) return null;
288
+ const alt = fiber.alternate;
289
+ if (!alt) return null;
290
+ const prevState = alt.memoizedState;
291
+ const currentState = fiber.memoizedState;
292
+ const result = detectHookStateChanges(prevState, currentState);
293
+ return result.hasChange ? result : null;
294
+ }
295
+ function isDevelopment() {
296
+ return true;
297
+ }
298
+
299
+ // src/avalanche/commit-stats-indices.ts
300
+ var CS_RESTARTS = 2;
301
+ var CS_UNMOUNTED_COMPONENTS = 8;
302
+ var CS_UNMOUNTED_SUBTREES = 9;
303
+
304
+ // src/avalanche/report-serializer.tsx
305
+ function serializeReport(report) {
306
+ const sources = report.sources.map((source) => ({
307
+ sourceId: source.sourceId,
308
+ depth: source.depth,
309
+ causedUpdates: source.causedUpdates,
310
+ collateralUpdates: source.collateralUpdates,
311
+ maxCollateralUpdates: source.maxCollateralUpdates,
312
+ maxPenetrationDepth: source.maxPenetrationDepth,
313
+ ...source.restartWaste !== void 0 && { restartWaste: source.restartWaste },
314
+ ...source.maxRestartWaste !== void 0 && { maxRestartWaste: source.maxRestartWaste }
315
+ }));
316
+ const sourcesByCommit = report.sourcesByCommit ? Object.fromEntries(
317
+ Object.entries(report.sourcesByCommit).map(([commitId, commitSources]) => [
318
+ commitId,
319
+ commitSources.map((src) => ({
320
+ sourceId: src.sourceId,
321
+ depth: src.depth,
322
+ collateral: src.collateral,
323
+ rank: src.rank,
324
+ sourceIndex: src.sourceIndex,
325
+ ...(src.trashers || src.hooks) && { trashers: src.trashers || src.hooks },
326
+ ...src.isRepeat && { isRepeat: true }
327
+ }))
328
+ ])
329
+ ) : void 0;
330
+ const suspenseByCommit = {};
331
+ if (report.suspenseByCommit) {
332
+ for (const [k, evts] of Object.entries(report.suspenseByCommit)) {
333
+ suspenseByCommit[Number(k)] = evts.map((e) => ({
334
+ depth: e.depth,
335
+ entered: e.entered,
336
+ left: e.left
337
+ }));
338
+ }
339
+ }
340
+ return {
341
+ meta: report.meta,
342
+ sources,
343
+ commitStats: report.commitStats,
344
+ suspenseByCommit,
345
+ ...sourcesByCommit && { sourcesByCommit },
346
+ ...report.interactionsByCommit && { interactionsByCommit: report.interactionsByCommit },
347
+ ...report.renderedByCommit && { renderedByCommit: report.renderedByCommit },
348
+ ...(report.metricsByCommit || report.atlByCommit) && {
349
+ metricsByCommit: report.metricsByCommit || report.atlByCommit,
350
+ atlByCommit: report.metricsByCommit || report.atlByCommit
351
+ },
352
+ ...report.timing && { timing: report.timing }
353
+ };
354
+ }
355
+
356
+ // src/avalanche/commit-metrics-adapter.ts
357
+ var active = null;
358
+ var REQUIRED_METHODS = ["isAvailable", "snapshot", "measure", "attach", "isAttached", "reattach", "detach"];
359
+ function registerCommitMetricsAdapter(adapter) {
360
+ if (!adapter || typeof adapter.id !== "string" || !adapter.id) {
361
+ throw new TypeError("registerCommitMetricsAdapter expects an adapter with a non-empty id");
362
+ }
363
+ for (const method of REQUIRED_METHODS) {
364
+ if (typeof adapter[method] !== "function") {
365
+ throw new TypeError(`commit metrics adapter ${adapter.id} is missing ${method}()`);
366
+ }
367
+ }
368
+ active = adapter;
369
+ return () => {
370
+ if (active === adapter) active = null;
371
+ };
372
+ }
373
+ function getCommitMetricsAdapter() {
374
+ return active;
375
+ }
376
+ function adapterIdFromMode(mode) {
377
+ return typeof mode === "string" && mode.startsWith("adapter:") ? mode.slice("adapter:".length) : null;
378
+ }
379
+
380
+ // src/avalanche/reporter.tsx
381
+ function isTrueSource(s) {
382
+ const commits = Object.values(s.updateHistory);
383
+ if (commits.length === 0) return false;
384
+ for (let i = 0; i < commits.length; i++) {
385
+ const entry = commits[i];
386
+ if (!entry || entry === "repeat") {
387
+ } else if (typeof entry === "object" && entry !== null) {
388
+ if (Object.values(entry).some((hook) => hook !== "repeat")) {
389
+ return true;
390
+ }
391
+ }
392
+ }
393
+ return false;
394
+ }
395
+ function buildReport(state, thrashMode, topN, sortBy, options = {}, detectorConfig) {
396
+ const { minCollateral = 1 } = options;
397
+ const toHumanReadableId = (internalId, type, depth) => {
398
+ const componentName = getComponentName({ type });
399
+ const match = internalId.match(/#(\d+)@/);
400
+ const instanceNum = match ? match[1] : "1";
401
+ return `${componentName}#${instanceNum}@${depth}`;
402
+ };
403
+ const idMapping = /* @__PURE__ */ new Map();
404
+ const allSources = Array.from(state.sources.entries()).map(([internalId, s]) => {
405
+ const humanId = toHumanReadableId(internalId, s.type, s.depth);
406
+ idMapping.set(internalId, humanId);
407
+ return {
408
+ sourceId: humanId,
409
+ ...s,
410
+ maxPenetrationDepth: Math.max(0, s.maxPenetrationDepth - s.depth),
411
+ // Convert to relative depth (never negative!)
412
+ // Initialize restart waste tracking
413
+ restartWaste: 0,
414
+ maxRestartWaste: 0,
415
+ restartWasteByCommit: {}
416
+ };
417
+ }).filter(isTrueSource);
418
+ Object.entries(state.commitStats).forEach(([commitIdStr, stats]) => {
419
+ const commitId = Number(commitIdStr);
420
+ const restarts = stats[CS_RESTARTS];
421
+ if (restarts === 0) return;
422
+ const sourcesInCommit = allSources.filter(
423
+ (source) => source.updateHistory[commitId] !== void 0
424
+ );
425
+ if (sourcesInCommit.length === 0) {
426
+ return;
427
+ }
428
+ const restartsPerSource = restarts / sourcesInCommit.length;
429
+ for (const sourceItem of sourcesInCommit) {
430
+ sourceItem.restartWaste += restartsPerSource;
431
+ sourceItem.restartWasteByCommit[commitId] = restartsPerSource;
432
+ if (restartsPerSource > sourceItem.maxRestartWaste) {
433
+ sourceItem.maxRestartWaste = restartsPerSource;
434
+ }
435
+ }
436
+ });
437
+ if (thrashMode === "full") {
438
+ for (const sourceItem of allSources) {
439
+ const amplifiedByThisSource = [];
440
+ state.amplifierSources.forEach((amplifierData, amplifierInternalId) => {
441
+ const amplifierHumanId = idMapping.get(amplifierInternalId) || toHumanReadableId(amplifierInternalId, amplifierData.type, amplifierData.depth);
442
+ const causedByHumanId = amplifierData.causedBySourceId ? idMapping.get(amplifierData.causedBySourceId) : null;
443
+ if (causedByHumanId === sourceItem.sourceId || amplifierHumanId === sourceItem.sourceId) {
444
+ amplifiedByThisSource.push({
445
+ sourceId: amplifierHumanId,
446
+ type: amplifierData.type,
447
+ depth: amplifierData.depth,
448
+ effectCount: amplifierData.effectCount,
449
+ callbackCount: amplifierData.callbackCount,
450
+ memoCount: amplifierData.memoCount,
451
+ hooksTotal: amplifierData.hooksTotal,
452
+ commitIds: amplifierData.commitIds,
453
+ byCommit: amplifierData.byCommit,
454
+ lastAmplification: amplifierData.lastAmplification
455
+ });
456
+ }
457
+ });
458
+ if (amplifiedByThisSource.length > 0) {
459
+ sourceItem.amplified = amplifiedByThisSource;
460
+ }
461
+ }
462
+ }
463
+ const sources = [...allSources].sort((a, b) => {
464
+ const aTotalWaste = a.collateralUpdates + (a.restartWaste || 0);
465
+ const bTotalWaste = b.collateralUpdates + (b.restartWaste || 0);
466
+ return bTotalWaste - aTotalWaste || b.maxCollateralUpdates - a.maxCollateralUpdates || (b.maxRestartWaste || 0) - (a.maxRestartWaste || 0);
467
+ }).slice(0, topN);
468
+ const coverage = state.commits > 0 ? Math.round(state.commitsWithMemoizedUpdaters / state.commits * 100) + "%" : "0%";
469
+ const timingEntries = Object.values(state.commitTimings);
470
+ const totalDetectorOverhead = timingEntries.reduce((sum, t) => sum + t.detectorOverhead, 0);
471
+ const totalTimeSpan = timingEntries.reduce((sum, t) => sum + t.timeSinceLastCommit, 0);
472
+ const avgDetectorOverhead = timingEntries.length > 0 ? totalDetectorOverhead / timingEntries.length : 0;
473
+ const avgTimeBetweenCommits = timingEntries.length > 0 ? totalTimeSpan / timingEntries.length : 0;
474
+ const topNLookup = /* @__PURE__ */ new Map();
475
+ for (let i = 0; i < sources.length; i++) {
476
+ topNLookup.set(sources[i].sourceId, { rank: i + 1, index: i });
477
+ }
478
+ const sourcesByCommit = {};
479
+ allSources.forEach((source) => {
480
+ Object.entries(source.updateHistory).forEach(([commitIdStr, data]) => {
481
+ const commitId = Number(commitIdStr);
482
+ if (typeof data === "string" && data !== "repeat") {
483
+ return;
484
+ }
485
+ const collateral = source.collateralByCommit?.[commitId] ?? 0;
486
+ if (collateral < minCollateral) {
487
+ return;
488
+ }
489
+ if (!sourcesByCommit[commitId]) {
490
+ sourcesByCommit[commitId] = [];
491
+ }
492
+ const isRepeat = data === "repeat";
493
+ let hookChanges;
494
+ if (typeof data === "object" && data !== null) {
495
+ hookChanges = Object.entries(data).map(([index, hookData]) => ({
496
+ index: Number(index),
497
+ prevValue: (
498
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
499
+ hookData !== "repeat" ? hookData.prevValue : void 0
500
+ ),
501
+ currentValue: hookData !== "repeat" ? (
502
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
503
+ hookData.currentValue
504
+ ) : void 0
505
+ }));
506
+ }
507
+ const lookup = topNLookup.get(source.sourceId);
508
+ const rank = lookup?.rank ?? null;
509
+ const sourceIndex = lookup?.index ?? null;
510
+ const entry = {
511
+ sourceId: source.sourceId,
512
+ type: source.type,
513
+ depth: source.depth,
514
+ collateral,
515
+ rank,
516
+ sourceIndex
517
+ };
518
+ if (hookChanges && hookChanges.length > 0) {
519
+ entry.hooks = hookChanges;
520
+ }
521
+ if (isRepeat) {
522
+ entry.isRepeat = true;
523
+ }
524
+ sourcesByCommit[commitId].push(entry);
525
+ });
526
+ });
527
+ const hasMetricsData = Object.keys(state.metricsByCommit).length > 0;
528
+ const integrationMode = detectorConfig?.integrationMode ?? "none";
529
+ const metricsAdapter = adapterIdFromMode(integrationMode);
530
+ const metricsAvailable = metricsAdapter !== null && hasMetricsData;
531
+ const meta = {
532
+ version: 6,
533
+ // v6: CommitStats[8,9] = unmountedComponents/unmountedSubtrees; suspenseByCommit
534
+ commits: state.commits,
535
+ config: {
536
+ detectionLevel: detectorConfig?.detectionLevel ?? "full",
537
+ effectThrashMode: detectorConfig?.effectThrashMode ?? "non-primitive",
538
+ captureFibers: detectorConfig?.captureFibers ?? false,
539
+ historySize: detectorConfig?.historySize ?? 1
540
+ },
541
+ memoizedUpdatersSupport: {
542
+ available: state.memoizedUpdatersAvailable,
543
+ commitsWithData: state.commitsWithMemoizedUpdaters,
544
+ coverage
545
+ },
546
+ integrationMode,
547
+ metricsAdapter,
548
+ // Per-field availability — fiber-walk fields are always real measurements
549
+ // because recordTree() runs in every mode. The only adapter-exclusive
550
+ // datapoint is the render-counter delta (restarts, metricsByCommit).
551
+ dataAvailability: {
552
+ collateral: true,
553
+ sources: true,
554
+ restarts: metricsAvailable,
555
+ performedWork: true,
556
+ effects: true,
557
+ memos: true,
558
+ callbacks: true,
559
+ contextUpdates: true,
560
+ metricsByCommit: metricsAvailable,
561
+ atlByCommit: metricsAvailable
562
+ },
563
+ timing: {
564
+ totalDetectorOverhead,
565
+ avgDetectorOverhead,
566
+ totalTimeSpan,
567
+ avgTimeBetweenCommits
568
+ }
569
+ };
570
+ const base = {
571
+ meta,
572
+ sources,
573
+ sourcesByCommit,
574
+ commitStats: state.commitStats,
575
+ commitTimings: state.commitTimings,
576
+ suspenseByCommit: state.suspenseByCommit,
577
+ unmountsByCommit: state.unmountsByCommit,
578
+ interactionsByCommit: state.interactionsByCommit,
579
+ renderedByCommit: state.renderedByCommit,
580
+ ...hasMetricsData && { metricsByCommit: state.metricsByCommit, atlByCommit: state.metricsByCommit },
581
+ toSerializable() {
582
+ return serializeReport(this);
583
+ }
584
+ };
585
+ if (thrashMode === "full") {
586
+ const getSortValue = (amplifier) => {
587
+ if (sortBy === "effect") return amplifier.effectCount;
588
+ if (sortBy === "callback") return amplifier.callbackCount;
589
+ if (sortBy === "memo") return amplifier.memoCount;
590
+ if (sortBy === "total") return amplifier.hooksTotal;
591
+ return amplifier.effectCount;
592
+ };
593
+ const getLinkedSortValue = (as) => {
594
+ if (sortBy === "effect") return as.effectCount;
595
+ if (sortBy === "callback") return as.callbackCount;
596
+ if (sortBy === "memo") return as.memoCount;
597
+ if (sortBy === "total") return as.hooksTotal;
598
+ return as.effectCount;
599
+ };
600
+ const amplifierSources = Array.from(state.amplifierSources.entries()).map(([internalId, lt]) => {
601
+ const humanId = idMapping.get(internalId) || toHumanReadableId(internalId, lt.type, lt.depth);
602
+ const sourceData = state.sources.get(internalId);
603
+ return {
604
+ sourceId: humanId,
605
+ type: lt.type,
606
+ depth: lt.depth,
607
+ collateralUpdates: sourceData?.collateralUpdates ?? 0,
608
+ effectCount: lt.effectCount,
609
+ callbackCount: lt.callbackCount,
610
+ memoCount: lt.memoCount,
611
+ hooksTotal: lt.hooksTotal,
612
+ commitIds: lt.commitIds,
613
+ byCommit: lt.byCommit,
614
+ causedBySourceId: lt.causedBySourceId ? idMapping.get(lt.causedBySourceId) || lt.causedBySourceId : null,
615
+ lastAmplification: lt.lastAmplification
616
+ };
617
+ }).sort((a, b) => getLinkedSortValue(b) - getLinkedSortValue(a) || b.hooksTotal - a.hooksTotal).slice(0, topN);
618
+ return {
619
+ ...base,
620
+ amplifiers: Array.from(state.amplifiers.entries()).map(([type, s]) => ({ type, ...s })).sort(
621
+ (a, b) => getSortValue(b) - getSortValue(a) || b.hooksTotal - a.hooksTotal || b.maxHooksPerCommit - a.maxHooksPerCommit
622
+ ).slice(0, topN),
623
+ amplifierSources
624
+ };
625
+ }
626
+ if (thrashMode === "counts" && state.thrashCounts) {
627
+ meta.amplifiersMetrics = {
628
+ totalHookThrashes: state.thrashCounts.totalHooks,
629
+ affectedComponentCount: state.thrashCounts.affectedComponents.size
630
+ };
631
+ }
632
+ return base;
633
+ }
634
+
635
+ // src/chippy-bridge/react-renderer.ts
636
+ function getReactRenderers(scope = globalThis) {
637
+ const g = scope;
638
+ const hook = g.__REACT_DEVTOOLS_GLOBAL_HOOK__;
639
+ if (!hook || !hook.renderers || typeof hook.renderers.entries !== "function") {
640
+ return [];
641
+ }
642
+ const out = [];
643
+ for (const [, renderer] of hook.renderers.entries()) {
644
+ if (renderer) out.push(renderer);
645
+ }
646
+ return out;
647
+ }
648
+ function getDispatcherRef(scope = globalThis) {
649
+ for (const renderer of getReactRenderers(scope)) {
650
+ const ref = renderer.currentDispatcherRef;
651
+ if (ref && typeof ref === "object") return ref;
652
+ }
653
+ return null;
654
+ }
655
+
656
+ // src/debug-render/core.ts
657
+ function parseStackFrames2(stack) {
658
+ return stack.split("\n").slice(1).map((line) => {
659
+ const m = line.match(/^\s*at\s+([^\s(]+)\s*\(([^)]+)\)/) || line.match(/^\s*at\s+()(.+)$/);
660
+ if (!m) return null;
661
+ const name = (m[1] || "").trim();
662
+ const locRaw = (m[2] || "").trim();
663
+ const tail = locRaw.match(/:(\d+):(\d+)\)?$/);
664
+ const loc = tail ? `${tail[1]}:${tail[2]}` : locRaw;
665
+ return name ? { name, loc } : null;
666
+ }).filter((f) => !!f);
667
+ }
668
+ var HOOK_METHODS = [
669
+ "useCallback",
670
+ "useContext",
671
+ "useDebugValue",
672
+ "useDeferredValue",
673
+ "useEffect",
674
+ "useImperativeHandle",
675
+ "useInsertionEffect",
676
+ "useLayoutEffect",
677
+ "useMemo",
678
+ "useReducer",
679
+ "useRef",
680
+ "useState",
681
+ "useSyncExternalStore",
682
+ "useTransition",
683
+ "useId",
684
+ "useCacheRefresh",
685
+ "useMemoCache"
686
+ ];
687
+ var HOOK_PRIMITIVE = {
688
+ useCallback: "Callback",
689
+ useContext: "Context",
690
+ useDebugValue: "DebugValue",
691
+ useDeferredValue: "DeferredValue",
692
+ useEffect: "Effect",
693
+ useImperativeHandle: "ImperativeHandle",
694
+ useInsertionEffect: "InsertionEffect",
695
+ useLayoutEffect: "LayoutEffect",
696
+ useMemo: "Memo",
697
+ useReducer: "Reducer",
698
+ useRef: "Ref",
699
+ useState: "State",
700
+ useSyncExternalStore: "SyncExternalStore",
701
+ useTransition: "Transition",
702
+ useId: "Id",
703
+ useCacheRefresh: "CacheRefresh",
704
+ useMemoCache: "MemoCache"
705
+ };
706
+ function isReactInternalFrameName(name) {
707
+ return name === "atlDispatcherHook" || name === "record" || name.startsWith("Object.") || name === "renderWithHooks" || name === "updateFunctionComponent" || name === "mountIndeterminateComponent" || name === "beginWork" || /^beginWork\$/.test(name) || name === "performUnitOfWork" || name === "workLoopSync" || name === "renderRootSync" || name === "performConcurrentWorkOnRoot" || name === "workLoop" || name === "flushWork";
708
+ }
709
+ function deriveComponentName(frames) {
710
+ for (const { name } of frames) {
711
+ if (!name) continue;
712
+ if (isReactInternalFrameName(name)) continue;
713
+ if (/^use[A-Z0-9_]/.test(name)) continue;
714
+ return name;
715
+ }
716
+ return "Unknown";
717
+ }
718
+ function createDebugRender(options = {}) {
719
+ const maxEventsPerComponent = options.maxEventsPerComponent ?? 600;
720
+ const eventsByComponent = /* @__PURE__ */ new Map();
721
+ const patchedDispatchers = /* @__PURE__ */ new WeakMap();
722
+ let installed = false;
723
+ let originalDescriptor = void 0;
724
+ let dispatcherRef = null;
725
+ let lastSeenDispatcher = null;
726
+ function record(primitive, args) {
727
+ const stack = new Error().stack || "";
728
+ const frames = parseStackFrames2(stack);
729
+ const componentName = deriveComponentName(frames);
730
+ const event = {
731
+ primitive,
732
+ stack,
733
+ frames,
734
+ componentName,
735
+ args
736
+ };
737
+ const list = eventsByComponent.get(componentName) || [];
738
+ list.push(event);
739
+ if (list.length > maxEventsPerComponent) {
740
+ list.splice(0, list.length - maxEventsPerComponent);
741
+ }
742
+ eventsByComponent.set(componentName, list);
743
+ }
744
+ function patchDispatcher(dispatcher) {
745
+ if (!dispatcher || typeof dispatcher !== "object") return;
746
+ const obj = dispatcher;
747
+ if (patchedDispatchers.has(obj)) return;
748
+ const restoreMap = /* @__PURE__ */ new Map();
749
+ for (const methodName of HOOK_METHODS) {
750
+ const original = obj[methodName];
751
+ if (typeof original !== "function") continue;
752
+ restoreMap.set(methodName, original);
753
+ const primitive = HOOK_PRIMITIVE[methodName] || methodName;
754
+ obj[methodName] = function atlDispatcherHook(...args) {
755
+ try {
756
+ record(primitive, args);
757
+ } catch (_) {
758
+ }
759
+ return original.apply(this, args);
760
+ };
761
+ }
762
+ patchedDispatchers.set(obj, restoreMap);
763
+ }
764
+ function unpatchDispatcher(dispatcher) {
765
+ if (!dispatcher || typeof dispatcher !== "object") return;
766
+ const obj = dispatcher;
767
+ const restoreMap = patchedDispatchers.get(obj);
768
+ if (!restoreMap) return;
769
+ for (const [methodName, fn] of restoreMap.entries()) {
770
+ obj[methodName] = fn;
771
+ }
772
+ patchedDispatchers.delete(obj);
773
+ }
774
+ function install() {
775
+ if (installed) return { ok: true };
776
+ const ref = getDispatcherRef();
777
+ if (!ref) return { ok: false, reason: "ReactCurrentDispatcher not found" };
778
+ dispatcherRef = ref;
779
+ patchDispatcher(ref.current);
780
+ lastSeenDispatcher = ref.current;
781
+ originalDescriptor = Object.getOwnPropertyDescriptor(ref, "current") || null;
782
+ let stored = ref.current;
783
+ try {
784
+ Object.defineProperty(ref, "current", {
785
+ configurable: true,
786
+ get() {
787
+ return stored;
788
+ },
789
+ set(next) {
790
+ stored = next;
791
+ if (next !== lastSeenDispatcher) {
792
+ lastSeenDispatcher = next;
793
+ patchDispatcher(next);
794
+ }
795
+ }
796
+ });
797
+ } catch (e) {
798
+ dispatcherRef = null;
799
+ return { ok: false, reason: "cannot define property on ReactCurrentDispatcher" };
800
+ }
801
+ installed = true;
802
+ return { ok: true };
803
+ }
804
+ function uninstall() {
805
+ if (!installed) return;
806
+ if (dispatcherRef) {
807
+ try {
808
+ if (originalDescriptor) {
809
+ Object.defineProperty(dispatcherRef, "current", originalDescriptor);
810
+ } else {
811
+ Object.defineProperty(dispatcherRef, "current", {
812
+ configurable: true,
813
+ writable: true,
814
+ enumerable: true,
815
+ value: lastSeenDispatcher
816
+ });
817
+ }
818
+ } catch (_) {
819
+ }
820
+ }
821
+ unpatchDispatcher(lastSeenDispatcher);
822
+ installed = false;
823
+ dispatcherRef = null;
824
+ originalDescriptor = void 0;
825
+ lastSeenDispatcher = null;
826
+ }
827
+ function reset() {
828
+ eventsByComponent.clear();
829
+ }
830
+ function rawEvents(componentName) {
831
+ return (eventsByComponent.get(componentName) || []).slice();
832
+ }
833
+ function allComponents() {
834
+ return Array.from(eventsByComponent.keys());
835
+ }
836
+ function status() {
837
+ const g = globalThis;
838
+ const hasDevtoolsHook = !!g.__REACT_DEVTOOLS_GLOBAL_HOOK__;
839
+ const rendererCount = getReactRenderers().length;
840
+ const ref = dispatcherRef || getDispatcherRef();
841
+ const hasDispatcherRef = !!ref;
842
+ let totalCapturedEvents = 0;
843
+ for (const list of eventsByComponent.values()) totalCapturedEvents += list.length;
844
+ let reason;
845
+ let hint;
846
+ if (!hasDevtoolsHook) {
847
+ reason = "no __REACT_DEVTOOLS_GLOBAL_HOOK__ on window";
848
+ hint = "devtools-hook-shim must run before react-dom evaluates (extension document_start, or Playwright addInitScript)";
849
+ } else if (rendererCount === 0) {
850
+ reason = "__REACT_DEVTOOLS_GLOBAL_HOOK__ present but renderers Map is empty";
851
+ hint = "react-dom evaluated before the hook shim was installed; reload the page after installing the shim, or use addInitScript before navigation";
852
+ } else if (!hasDispatcherRef) {
853
+ reason = "renderer registered but no currentDispatcherRef";
854
+ hint = "unsupported react-dom version, or hook was replaced after registration";
855
+ } else if (!installed) {
856
+ reason = "debug-render not installed";
857
+ hint = "detector did not call debugRender.install() \u2014 check avalanche install() lifecycle";
858
+ } else if (totalCapturedEvents === 0) {
859
+ reason = "capture engine installed but no hook events recorded yet";
860
+ hint = "trigger at least one render of the component you want to inspect after install";
861
+ }
862
+ const ok = installed && hasDispatcherRef && totalCapturedEvents > 0;
863
+ return {
864
+ ok,
865
+ installed,
866
+ hasDevtoolsHook,
867
+ rendererCount,
868
+ hasDispatcherRef,
869
+ totalCapturedComponents: eventsByComponent.size,
870
+ totalCapturedEvents,
871
+ reason,
872
+ hint
873
+ };
874
+ }
875
+ return {
876
+ install,
877
+ uninstall,
878
+ reset,
879
+ isInstalled: () => installed,
880
+ dispatcherRef: () => dispatcherRef || getDispatcherRef(),
881
+ rawEvents,
882
+ allComponents,
883
+ status,
884
+ recordEventForTest(event) {
885
+ const list = eventsByComponent.get(event.componentName) || [];
886
+ list.push(event);
887
+ eventsByComponent.set(event.componentName, list);
888
+ }
889
+ };
890
+ }
891
+
892
+ // src/avalanche/commit-surface-ownership.ts
893
+ function createCommitSurfaceOwnership() {
894
+ let adapterObserved = false;
895
+ let devtoolsCaptures = /* @__PURE__ */ new WeakMap();
896
+ return {
897
+ get adapterObserved() {
898
+ return adapterObserved;
899
+ },
900
+ recordDevtools(root, capture) {
901
+ devtoolsCaptures.set(root, { current: root.current, capture });
902
+ },
903
+ observeAdapter(root) {
904
+ const previous = devtoolsCaptures.get(root);
905
+ adapterObserved = true;
906
+ devtoolsCaptures.delete(root);
907
+ return previous?.current === root.current ? previous.capture : null;
908
+ },
909
+ reset() {
910
+ adapterObserved = false;
911
+ devtoolsCaptures = /* @__PURE__ */ new WeakMap();
912
+ }
913
+ };
914
+ }
915
+
916
+ // src/layout-lab/fiber-walk.ts
917
+ function getDisplayName(type) {
918
+ if (!type) return null;
919
+ if (typeof type === "string") return type;
920
+ const obj = type;
921
+ return obj.displayName || obj.name || null;
922
+ }
923
+ function isUserComponent(fiber) {
924
+ if (!fiber) return false;
925
+ const name = getDisplayName(fiber.type);
926
+ if (!name || typeof fiber.type === "string") return false;
927
+ if (["hookified", "unboundStoryFn", "Anonymous"].includes(name)) return false;
928
+ return true;
929
+ }
930
+ function getHookSummary(fiber) {
931
+ const MAX_HOOKS = 500;
932
+ const seen = /* @__PURE__ */ new Set();
933
+ let hookNode = fiber?.memoizedState || null;
934
+ let hookCount = 0;
935
+ let hookCountTruncated = false;
936
+ while (hookNode) {
937
+ if (seen.has(hookNode)) break;
938
+ if (hookCount >= MAX_HOOKS) {
939
+ hookCountTruncated = true;
940
+ break;
941
+ }
942
+ seen.add(hookNode);
943
+ hookCount += 1;
944
+ hookNode = hookNode.next || null;
945
+ }
946
+ return { hookCount, hookCountTruncated, hookCountMax: MAX_HOOKS };
947
+ }
948
+ function getOwnerChain(fiber) {
949
+ const chain = [];
950
+ let current = fiber?._debugOwner || null;
951
+ while (current) {
952
+ if (isUserComponent(current)) {
953
+ chain.push({
954
+ name: getDisplayName(current.type),
955
+ key: current.key || null,
956
+ props: current.memoizedProps ? Object.keys(current.memoizedProps) : [],
957
+ ...getHookSummary(current),
958
+ source: describeFiberSource(current)
959
+ });
960
+ }
961
+ current = current._debugOwner || null;
962
+ }
963
+ return chain;
964
+ }
965
+ function getReturnComponentChain(fiber) {
966
+ const chain = [];
967
+ let current = fiber?.return || null;
968
+ while (current) {
969
+ if (isUserComponent(current)) {
970
+ chain.push({
971
+ name: getDisplayName(current.type),
972
+ key: current.key || null,
973
+ props: current.memoizedProps ? Object.keys(current.memoizedProps) : [],
974
+ ...getHookSummary(current),
975
+ source: describeFiberSource(current)
976
+ });
977
+ }
978
+ current = current.return || null;
979
+ }
980
+ return chain;
981
+ }
982
+ function describeSource(source) {
983
+ if (!source) return null;
984
+ return {
985
+ fileName: source.fileName || null,
986
+ lineNumber: source.lineNumber || null,
987
+ columnNumber: source.columnNumber || null
988
+ };
989
+ }
990
+ function describeFiberSource(fiber) {
991
+ return describeSource(getFiberSource(fiber));
992
+ }
993
+ function describeFiberTarget(fiber, domNode) {
994
+ if (!fiber && !domNode) return null;
995
+ return {
996
+ domTag: domNode?.tagName?.toLowerCase() || null,
997
+ domText: domNode?.textContent?.trim().slice(0, 60) || "",
998
+ fiberType: getDisplayName(fiber?.type) || getDisplayName(fiber?.elementType) || null,
999
+ source: describeFiberSource(fiber)
1000
+ };
1001
+ }
1002
+ function describeComponent(fiber) {
1003
+ if (!fiber) return null;
1004
+ return {
1005
+ name: getDisplayName(fiber.type),
1006
+ key: fiber.key || null,
1007
+ props: fiber.memoizedProps || {},
1008
+ ...getHookSummary(fiber),
1009
+ source: describeFiberSource(fiber),
1010
+ stateNode: fiber.stateNode instanceof Element ? fiber.stateNode.tagName.toLowerCase() : null,
1011
+ owners: getOwnerChain(fiber),
1012
+ parents: getReturnComponentChain(fiber)
1013
+ };
1014
+ }
1015
+
1016
+ // src/avalanche/interaction-capture.ts
1017
+ var EXPLICIT_INTERACTION_EVENTS = ["click", "keydown", "keyup", "input", "change", "submit"];
1018
+ var NOISY_INTERACTION_EVENTS = ["wheel", "scroll", "pointermove", "mousemove", "touchmove"];
1019
+ var ALL_INTERACTION_EVENTS = [
1020
+ ...EXPLICIT_INTERACTION_EVENTS,
1021
+ "pointerdown",
1022
+ "pointerup",
1023
+ "dblclick",
1024
+ "focusin",
1025
+ "focusout",
1026
+ ...NOISY_INTERACTION_EVENTS
1027
+ ];
1028
+ var MAX_INTERACTION_ACTIONS = 200;
1029
+ function createInteractionCapture(options = {}) {
1030
+ const {
1031
+ interactionCapture = "explicit",
1032
+ interactionEvents = [],
1033
+ interactionWindowMs = 1e3
1034
+ } = options;
1035
+ const state = {
1036
+ actions: [],
1037
+ byCommit: {}
1038
+ };
1039
+ let nextActionId = 1;
1040
+ let installed = false;
1041
+ function eventNames() {
1042
+ const base = interactionCapture === "off" ? [] : interactionCapture === "explicit" ? EXPLICIT_INTERACTION_EVENTS : interactionCapture === "noisy" ? EXPLICIT_INTERACTION_EVENTS.concat(NOISY_INTERACTION_EVENTS) : ALL_INTERACTION_EVENTS;
1043
+ return Array.from(new Set(base.concat(interactionEvents).map((name) => String(name).trim()).filter(Boolean)));
1044
+ }
1045
+ function findReactFiberKey(node) {
1046
+ return Object.getOwnPropertyNames(node).find((key) => key.startsWith("__reactFiber$")) || null;
1047
+ }
1048
+ function nearestFiberFromNode(node) {
1049
+ let current = node instanceof Element ? node : null;
1050
+ while (current) {
1051
+ const key = findReactFiberKey(current);
1052
+ if (key) return current[key] || null;
1053
+ current = current.parentElement;
1054
+ }
1055
+ return null;
1056
+ }
1057
+ function nearestComponentFiber(node) {
1058
+ let fiber = nearestFiberFromNode(node);
1059
+ while (fiber) {
1060
+ if (isComponentFiber(fiber.tag)) return fiber;
1061
+ fiber = fiber.return || null;
1062
+ }
1063
+ return null;
1064
+ }
1065
+ function nearestTestId(node) {
1066
+ let current = node instanceof Element ? node : null;
1067
+ while (current) {
1068
+ const value = current.getAttribute("data-testid");
1069
+ if (value) return value;
1070
+ current = current.parentElement;
1071
+ }
1072
+ return null;
1073
+ }
1074
+ function describeActionLocation(target) {
1075
+ const domTarget = target instanceof Element ? target : null;
1076
+ const fiber = nearestComponentFiber(target);
1077
+ const describedTarget = describeFiberTarget(fiber, domTarget);
1078
+ const describedComponent = describeComponent(fiber);
1079
+ return {
1080
+ dom: {
1081
+ tag: describedTarget?.domTag || null,
1082
+ text: describedTarget?.domText || "",
1083
+ nearestTestId: nearestTestId(target)
1084
+ },
1085
+ targetFiber: describedComponent ? {
1086
+ name: describedComponent.name,
1087
+ key: describedComponent.key,
1088
+ source: describedComponent.source,
1089
+ ownerStackKind: describedComponent.owners.length > 0 ? "owners" : "parents",
1090
+ ownerStack: describedComponent.owners.length > 0 ? describedComponent.owners : describedComponent.parents
1091
+ } : null
1092
+ };
1093
+ }
1094
+ function capture(event) {
1095
+ const pathTarget = typeof event.composedPath === "function" ? event.composedPath()[0] : null;
1096
+ const target = pathTarget instanceof Element ? pathTarget : event.target;
1097
+ state.actions.push({
1098
+ id: nextActionId++,
1099
+ action: event.type,
1100
+ timestamp: performance.now(),
1101
+ location: describeActionLocation(target)
1102
+ });
1103
+ if (state.actions.length > MAX_INTERACTION_ACTIONS) {
1104
+ state.actions.splice(0, state.actions.length - MAX_INTERACTION_ACTIONS);
1105
+ }
1106
+ }
1107
+ return {
1108
+ state,
1109
+ install() {
1110
+ if (installed) return;
1111
+ const names = eventNames();
1112
+ if (names.length === 0 || typeof document === "undefined") return;
1113
+ for (const name of names) document.addEventListener(name, capture, true);
1114
+ installed = true;
1115
+ },
1116
+ uninstall() {
1117
+ if (!installed || typeof document === "undefined") return;
1118
+ for (const name of eventNames()) document.removeEventListener(name, capture, true);
1119
+ installed = false;
1120
+ },
1121
+ reset() {
1122
+ state.actions.length = 0;
1123
+ for (const key of Object.keys(state.byCommit)) {
1124
+ delete state.byCommit[Number(key)];
1125
+ }
1126
+ nextActionId = 1;
1127
+ },
1128
+ linkRecent(commitId, commitTime) {
1129
+ const windowMs = Math.max(0, Number(interactionWindowMs) || 0);
1130
+ if (windowMs === 0 || state.actions.length === 0) return;
1131
+ const linked = state.actions.filter((action) => action.timestamp <= commitTime && commitTime - action.timestamp <= windowMs).map((action) => ({
1132
+ id: action.id,
1133
+ lastCommitId: Math.max(0, commitId - 1),
1134
+ deltaTime: Math.round((commitTime - action.timestamp) * 100) / 100,
1135
+ action: action.action,
1136
+ location: action.location
1137
+ }));
1138
+ if (linked.length > 0) state.byCommit[commitId] = linked;
1139
+ while (state.actions.length > 0 && commitTime - state.actions[0].timestamp > windowMs) {
1140
+ state.actions.shift();
1141
+ }
1142
+ }
1143
+ };
1144
+ }
1145
+
1146
+ // src/avalanche/detector.tsx
1147
+ function summarizeFiberSource(fiber) {
1148
+ const source = getFiberSourceCandidate(fiber);
1149
+ return source ? { fileName: source.fileName, lineNumber: source.lineNumber } : null;
1150
+ }
1151
+ function createAvalancheDetector(options = {}) {
1152
+ const {
1153
+ ignore = [],
1154
+ effectThrashMode = "non-primitive",
1155
+ captureFibers = isDevelopment(),
1156
+ detectionMode = isDevelopment() ? "full" : "amplifiers",
1157
+ historySize = isDevelopment() ? 10 : 1
1158
+ } = options;
1159
+ const mode = detectionMode;
1160
+ let metricsAdapter = null;
1161
+ const resolveMetricsAdapter = () => options.metricsAdapter === void 0 ? getCommitMetricsAdapter() : options.metricsAdapter;
1162
+ const ignoreSet = new Set(ignore);
1163
+ let detectionLevel;
1164
+ let thrashMode;
1165
+ if (mode === true) {
1166
+ detectionLevel = "full";
1167
+ thrashMode = "full";
1168
+ } else if (mode === false) {
1169
+ detectionLevel = "stats";
1170
+ thrashMode = false;
1171
+ } else if (mode === "stats") {
1172
+ detectionLevel = "stats";
1173
+ thrashMode = false;
1174
+ } else if (mode === "sources") {
1175
+ detectionLevel = "sources";
1176
+ thrashMode = false;
1177
+ } else if (mode === "amplifiers") {
1178
+ detectionLevel = "amplifiers";
1179
+ thrashMode = "full";
1180
+ } else if (mode === "amplifierMetrics") {
1181
+ detectionLevel = "amplifierMetrics";
1182
+ thrashMode = "counts";
1183
+ } else if (mode === "full") {
1184
+ detectionLevel = "full";
1185
+ thrashMode = "full";
1186
+ }
1187
+ const enableSourceTracking = detectionLevel !== "stats";
1188
+ const enableAmplifiers = thrashMode !== false;
1189
+ const valueProvenance = /* @__PURE__ */ new WeakMap();
1190
+ class StableSourceIdManager {
1191
+ idMap = /* @__PURE__ */ new WeakMap();
1192
+ // WeakMap only accepts objects/functions, not symbols. Store only valid keys.
1193
+ typeToId = /* @__PURE__ */ new WeakMap();
1194
+ instanceCounters = /* @__PURE__ */ new Map();
1195
+ // "typeId@depth" -> instance count
1196
+ nextTypeId = 1;
1197
+ getSourceId(fiber, type, depth) {
1198
+ let sourceId = this.idMap.get(fiber);
1199
+ if (sourceId) return sourceId;
1200
+ if (fiber.alternate) {
1201
+ sourceId = this.idMap.get(fiber.alternate);
1202
+ if (sourceId) {
1203
+ this.idMap.set(fiber, sourceId);
1204
+ return sourceId;
1205
+ }
1206
+ }
1207
+ const canBeWeakMapKey = typeof type === "object" && type !== null || typeof type === "function";
1208
+ const weakKey = canBeWeakMapKey ? type : null;
1209
+ let typeId = weakKey ? this.typeToId.get(weakKey) : void 0;
1210
+ if (typeId === void 0) {
1211
+ typeId = this.nextTypeId++;
1212
+ if (weakKey) {
1213
+ this.typeToId.set(weakKey, typeId);
1214
+ }
1215
+ }
1216
+ const baseKey = `${typeId}@${depth}`;
1217
+ const instanceNum = this.instanceCounters.get(baseKey) || 1;
1218
+ this.instanceCounters.set(baseKey, instanceNum + 1);
1219
+ sourceId = `T${typeId}#${instanceNum}@${depth}`;
1220
+ this.idMap.set(fiber, sourceId);
1221
+ if (fiber.alternate) {
1222
+ this.idMap.set(fiber.alternate, sourceId);
1223
+ }
1224
+ return sourceId;
1225
+ }
1226
+ clear() {
1227
+ this.instanceCounters.clear();
1228
+ this.typeToId = /* @__PURE__ */ new WeakMap();
1229
+ this.nextTypeId = 1;
1230
+ }
1231
+ }
1232
+ const sourceIdManager = new StableSourceIdManager();
1233
+ const debugRender = createDebugRender();
1234
+ const interactionCapture = createInteractionCapture(options);
1235
+ function getComponentDisplayName(type) {
1236
+ if (!type) return "Unknown";
1237
+ return type.displayName || type.name || String(type);
1238
+ }
1239
+ function isDebugInternalFrame(frame, componentName) {
1240
+ const name = frame.name || "";
1241
+ return !!(metricsAdapter ?? resolveMetricsAdapter())?.frameFilters?.includes(name) || name === "record" || name === componentName || name.includes("react-dom") || name.includes("renderWithHooks") || name.includes("mount") || name.includes("update");
1242
+ }
1243
+ function parentFramesForEvent(event, componentName) {
1244
+ return event.frames.filter((frame) => !isDebugInternalFrame(frame, componentName));
1245
+ }
1246
+ function captureMetricsSnapshot() {
1247
+ return metricsAdapter?.snapshot() ?? null;
1248
+ }
1249
+ const lastSeenHookState = /* @__PURE__ */ new WeakMap();
1250
+ function captureHookStateSnapshot(fiber) {
1251
+ const snapshot = [];
1252
+ let hook = fiber.memoizedState;
1253
+ while (hook) {
1254
+ snapshot.push(hook.memoizedState);
1255
+ hook = hook.next;
1256
+ }
1257
+ return snapshot;
1258
+ }
1259
+ function detectChangesFromLastSeen(fiber, currentSnapshot) {
1260
+ const lastSeen = lastSeenHookState.get(fiber) || fiber.alternate && lastSeenHookState.get(fiber.alternate);
1261
+ const changedIndices = [];
1262
+ const changedHooks = [];
1263
+ if (!lastSeen) {
1264
+ return { changedIndices, changedHooks };
1265
+ }
1266
+ const maxLen = Math.max(lastSeen.length, currentSnapshot.length);
1267
+ for (let i = 0; i < maxLen; i++) {
1268
+ const prev = lastSeen[i];
1269
+ const curr = currentSnapshot[i];
1270
+ if (!Object.is(prev, curr)) {
1271
+ changedIndices.push(i);
1272
+ changedHooks.push({ prevValue: prev, currentValue: curr });
1273
+ }
1274
+ }
1275
+ return { changedIndices, changedHooks };
1276
+ }
1277
+ const state = {
1278
+ commits: 0,
1279
+ sources: /* @__PURE__ */ new Map(),
1280
+ amplifiers: /* @__PURE__ */ new Map(),
1281
+ amplifierSources: /* @__PURE__ */ new Map(),
1282
+ thrashCounts: thrashMode === "counts" ? { totalHooks: 0, affectedComponents: /* @__PURE__ */ new Set() } : null,
1283
+ memoizedUpdatersAvailable: false,
1284
+ commitsWithMemoizedUpdaters: 0,
1285
+ // Per-commit stats: [collateral, effects, memos, callbacks]
1286
+ commitStats: {},
1287
+ // Per-commit adapter counter deltas
1288
+ metricsByCommit: {},
1289
+ lastMetricsSnapshot: null,
1290
+ // Per-commit timing data
1291
+ commitTimings: {},
1292
+ lastCommitEndTime: performance.now(),
1293
+ // Per-commit Suspense boundary transitions
1294
+ suspenseByCommit: {},
1295
+ unmountsByCommit: {},
1296
+ interactionActions: interactionCapture.state.actions,
1297
+ interactionsByCommit: interactionCapture.state.byCommit,
1298
+ renderedByCommit: {}
1299
+ };
1300
+ function _pushToHistory(history, item, limit) {
1301
+ history.push(item);
1302
+ if (history.length > limit) {
1303
+ history.splice(0, history.length - limit);
1304
+ }
1305
+ }
1306
+ function trimHistoryToSize(updHistory, limit) {
1307
+ const keys = Object.keys(updHistory).map(Number).sort((a, b) => a - b);
1308
+ const keysToKeep = keys.slice(keys.length - limit);
1309
+ const trimmed = {};
1310
+ keysToKeep.forEach((key) => {
1311
+ trimmed[key] = updHistory[key];
1312
+ });
1313
+ return trimmed;
1314
+ }
1315
+ function findLastConcreteHook(history, hookIndex, beforeCommitId) {
1316
+ const commitIds = Object.keys(history).map(Number).filter((id) => id < beforeCommitId).sort((a, b) => b - a);
1317
+ for (const commitId of commitIds) {
1318
+ const entry = history[commitId];
1319
+ if (entry && entry !== "repeat" && typeof entry !== "string") {
1320
+ const prev = entry[hookIndex];
1321
+ if (prev && prev !== "repeat" && typeof prev === "object") return prev;
1322
+ }
1323
+ }
1324
+ return null;
1325
+ }
1326
+ function summarizePropValue(value) {
1327
+ const isArray = Array.isArray(value);
1328
+ const type = value === null ? "null" : isArray ? "array" : typeof value;
1329
+ const isFunction = typeof value === "function";
1330
+ const isNull = value === null;
1331
+ const isPrimitive = value == null || type === "string" || type === "number" || type === "boolean" || type === "undefined" || type === "symbol" || type === "bigint";
1332
+ const primitiveDisplay = isPrimitive ? String(value) : void 0;
1333
+ const summary = {
1334
+ type,
1335
+ isArray,
1336
+ isFunction,
1337
+ isNull,
1338
+ preview: primitiveDisplay || (isFunction ? "function" : isArray ? `array(${value.length})` : "object")
1339
+ };
1340
+ if (primitiveDisplay !== void 0) {
1341
+ summary.display = primitiveDisplay;
1342
+ }
1343
+ if (typeof value === "string" || isArray) {
1344
+ summary.length = value.length;
1345
+ }
1346
+ if (isArray) {
1347
+ summary.sample = value.slice(0, 3).map((item) => summarizePropValue(item).preview);
1348
+ } else if (value && typeof value === "object") {
1349
+ const keys = Object.keys(value).slice(0, 5);
1350
+ summary.keysTop = keys;
1351
+ summary.length = Object.keys(value).length;
1352
+ if (keys.length > 0) {
1353
+ summary.preview = `object{${keys.join(",")}}`;
1354
+ }
1355
+ } else if (isFunction) {
1356
+ const fn = value;
1357
+ summary.preview = fn.name ? `function ${fn.name}` : "function";
1358
+ }
1359
+ return summary;
1360
+ }
1361
+ function diffProps(prevProps, currentProps) {
1362
+ const changes = [];
1363
+ const prev = prevProps && typeof prevProps === "object" ? prevProps : {};
1364
+ const current = currentProps && typeof currentProps === "object" ? currentProps : {};
1365
+ const keys = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(current)]);
1366
+ keys.forEach((name) => {
1367
+ const prevHas = Object.prototype.hasOwnProperty.call(prev, name);
1368
+ const currentHas = Object.prototype.hasOwnProperty.call(current, name);
1369
+ const prevValue = prev[name];
1370
+ const currentValue = current[name];
1371
+ if (!prevHas && currentHas) {
1372
+ changes.push({ name, kind: "added", prev: summarizePropValue(void 0), current: summarizePropValue(currentValue) });
1373
+ } else if (prevHas && !currentHas) {
1374
+ changes.push({ name, kind: "removed", prev: summarizePropValue(prevValue), current: summarizePropValue(void 0) });
1375
+ } else if (Object.is(prevValue, currentValue)) {
1376
+ } else if (prevValue !== null && currentValue !== null && (typeof prevValue === "object" || typeof prevValue === "function" || typeof currentValue === "object" || typeof currentValue === "function")) {
1377
+ changes.push({ name, kind: "ref-changed", prev: summarizePropValue(prevValue), current: summarizePropValue(currentValue) });
1378
+ } else {
1379
+ changes.push({ name, kind: "scalar-changed", prev: summarizePropValue(prevValue), current: summarizePropValue(currentValue) });
1380
+ }
1381
+ });
1382
+ return changes;
1383
+ }
1384
+ function pushUpdateHistory(updHistory, commitId, changedHookIndices, changedHooks) {
1385
+ const entry = {};
1386
+ for (let i = 0; i < changedHookIndices.length; i++) {
1387
+ const hookIndex = changedHookIndices[i];
1388
+ const hook = changedHooks[i];
1389
+ if (hook === "repeat") {
1390
+ entry[hookIndex] = "repeat";
1391
+ } else {
1392
+ const prevConcrete = findLastConcreteHook(updHistory, hookIndex, commitId);
1393
+ entry[hookIndex] = prevConcrete && typeof prevConcrete === "object" && Object.is(hook.prevValue, prevConcrete.prevValue) && Object.is(hook.currentValue, prevConcrete.currentValue) ? "repeat" : hook;
1394
+ }
1395
+ }
1396
+ const newEntry = Object.values(entry).every((v) => v === "repeat") ? "repeat" : entry;
1397
+ return { ...updHistory, [commitId]: newEntry };
1398
+ }
1399
+ function recordTree(rootFiber, memoizedUpdaters = null) {
1400
+ if (memoizedUpdaters && memoizedUpdaters.size > 0) {
1401
+ state.memoizedUpdatersAvailable = true;
1402
+ state.commitsWithMemoizedUpdaters++;
1403
+ }
1404
+ const perCommit = {
1405
+ selfUpdated: /* @__PURE__ */ new Map(),
1406
+ thrashing: /* @__PURE__ */ new Map(),
1407
+ /** Linked thrashing: keyed by sourceId, tracks per-component thrashing with linkage */
1408
+ linkedThrashing: /* @__PURE__ */ new Map(),
1409
+ rendered: [],
1410
+ selfFibers: /* @__PURE__ */ new WeakMap(),
1411
+ sourceFibers: /* @__PURE__ */ new Set(),
1412
+ // Track source fibers to exclude from collateral count
1413
+ blastBySourceId: /* @__PURE__ */ new Map(),
1414
+ depthBySourceId: /* @__PURE__ */ new Map(),
1415
+ // Simple counts for commitStats
1416
+ totalCollateral: 0,
1417
+ committedComponentCount: 0,
1418
+ // Components with PerformedWork that committed
1419
+ effectCount: 0,
1420
+ memoCount: 0,
1421
+ callbackCount: 0,
1422
+ contextUpdates: 0,
1423
+ // Unmount tracking (populated by deletions-array walk in visit())
1424
+ unmountedComponents: 0,
1425
+ unmountedSubtrees: 0,
1426
+ // Bounded identity samples for branch starts (topmost component fibers in
1427
+ // each deleted subtree). Cap to keep payload small.
1428
+ unmountSamples: null,
1429
+ // Suspense boundary transition events collected during this commit's visit pass
1430
+ suspenseEvents: null
1431
+ };
1432
+ function visit(fiber, depth = 0, currentOwnerSourceId = null) {
1433
+ if (!fiber) return;
1434
+ const hasWorkInSubtree = (
1435
+ // eslint-disable-next-line no-bitwise -- React fiber flags are bitmasks, bitwise AND is required
1436
+ (fiber.flags & PerformedWork) !== 0 || // eslint-disable-next-line no-bitwise
1437
+ (fiber.subtreeFlags & PerformedWork) !== 0 || // fiber.flags & ChildDeletion = this fiber has children being deleted this commit
1438
+ // (React sets ChildDeletion on fiber.flags, NOT subtreeFlags, at commit time)
1439
+ // eslint-disable-next-line no-bitwise
1440
+ (fiber.flags & ChildDeletion) !== 0 || // subtreeFlags propagation — some descendant has deletions
1441
+ // eslint-disable-next-line no-bitwise
1442
+ (fiber.subtreeFlags & ChildDeletion) !== 0 || // Suspense transitioning: React sets Update (4) on the Suspense fiber's flags at commit.
1443
+ // eslint-disable-next-line no-bitwise
1444
+ isSuspenseFiber(fiber.tag) && (fiber.flags & Update) !== 0 || // subtreeFlags & Update: a descendant (e.g. Suspense) received an Update this commit.
1445
+ // React propagates subtreeFlags upward — HostRoot and all intermediate host fibers
1446
+ // will have Update bit set if any descendant has it. This ensures we don't skip
1447
+ // the path to a Suspense boundary when no PerformedWork exists higher in the tree.
1448
+ // eslint-disable-next-line no-bitwise
1449
+ (fiber.subtreeFlags & Update) !== 0
1450
+ );
1451
+ if (!hasWorkInSubtree) {
1452
+ if (fiber.sibling) visit(fiber.sibling, depth, currentOwnerSourceId);
1453
+ return;
1454
+ }
1455
+ if (isContextProviderFiber(fiber.tag) && didContextValueChange(fiber)) {
1456
+ perCommit.contextUpdates++;
1457
+ }
1458
+ if ((fiber.flags & ChildDeletion) !== 0 && fiber.deletions && fiber.deletions.length > 0) {
1459
+ for (let di = 0; di < fiber.deletions.length; di++) {
1460
+ let collectDeletions = function(df, parentIsComponent, branchStartDepth, currentBranch) {
1461
+ const dfIsComponent = isComponentFiber(df.tag);
1462
+ let nextBranch = currentBranch;
1463
+ if (dfIsComponent) {
1464
+ perCommit.unmountedComponents++;
1465
+ if (!parentIsComponent) {
1466
+ perCommit.unmountedSubtrees++;
1467
+ if (perCommit.unmountSamples == null) perCommit.unmountSamples = [];
1468
+ if (perCommit.unmountSamples.length < 30) {
1469
+ const fiberKey = df.key;
1470
+ const sample = {
1471
+ name: getComponentName(df),
1472
+ key: fiberKey == null ? null : String(fiberKey),
1473
+ depth: branchStartDepth,
1474
+ count: 1,
1475
+ source: summarizeFiberSource(df),
1476
+ host: summarizeHostSubtree(df)
1477
+ };
1478
+ perCommit.unmountSamples.push(sample);
1479
+ nextBranch = { count: 1, sample };
1480
+ } else {
1481
+ nextBranch = null;
1482
+ }
1483
+ } else if (currentBranch && currentBranch.sample) {
1484
+ currentBranch.sample.count++;
1485
+ }
1486
+ lastSeenHookState.delete(df);
1487
+ if (df.alternate) lastSeenHookState.delete(df.alternate);
1488
+ }
1489
+ if (df.child) collectDeletions(df.child, dfIsComponent, branchStartDepth + 1, nextBranch);
1490
+ if (df.sibling) collectDeletions(df.sibling, parentIsComponent, branchStartDepth, currentBranch);
1491
+ };
1492
+ const deleted = fiber.deletions[di];
1493
+ if (perCommit.unmountSamples == null) perCommit.unmountSamples = [];
1494
+ if (perCommit.unmountSamples.length < 30) {
1495
+ let labelFiber = deleted;
1496
+ if (!isComponentFiber(labelFiber.tag)) {
1497
+ const stack = [labelFiber];
1498
+ labelFiber = null;
1499
+ while (stack.length) {
1500
+ const f = stack.pop();
1501
+ if (isComponentFiber(f.tag)) {
1502
+ labelFiber = f;
1503
+ break;
1504
+ }
1505
+ if (f.child) stack.push(f.child);
1506
+ if (f.sibling) stack.push(f.sibling);
1507
+ }
1508
+ }
1509
+ if (labelFiber) {
1510
+ const counts = countUnmountedComponentSubtree(deleted);
1511
+ const fiberKey = labelFiber.key;
1512
+ perCommit.unmountSamples.push({
1513
+ name: getComponentName(labelFiber),
1514
+ key: fiberKey == null ? null : String(fiberKey),
1515
+ depth,
1516
+ count: counts.components,
1517
+ source: summarizeFiberSource(labelFiber),
1518
+ host: summarizeHostSubtree(deleted)
1519
+ });
1520
+ }
1521
+ }
1522
+ collectDeletions(deleted, false, depth, null);
1523
+ }
1524
+ }
1525
+ const isComponent = isComponentFiber(fiber.tag);
1526
+ let ownerSourceId = currentOwnerSourceId;
1527
+ if (isSuspenseFiber(fiber.tag)) {
1528
+ const suspenseChange = didSuspenseStateChange(fiber);
1529
+ if (suspenseChange) {
1530
+ if (!perCommit.suspenseEvents) {
1531
+ perCommit.suspenseEvents = [];
1532
+ }
1533
+ const event = {
1534
+ depth,
1535
+ entered: suspenseChange.entered,
1536
+ left: suspenseChange.left,
1537
+ ...captureFibers && { fiber }
1538
+ };
1539
+ perCommit.suspenseEvents.push(event);
1540
+ if (enableSourceTracking) {
1541
+ const suspenseType = fiber.type;
1542
+ const sourceId = sourceIdManager.getSourceId(fiber, suspenseType, depth);
1543
+ const suspenseHook = {
1544
+ prevValue: suspenseChange.left ? "suspended" : null,
1545
+ currentValue: suspenseChange.entered ? "suspended" : null,
1546
+ info: suspenseChange.entered ? "suspense:entered-fallback" : "suspense:left-fallback"
1547
+ };
1548
+ perCommit.selfUpdated.set(sourceId, {
1549
+ type: suspenseType,
1550
+ depth,
1551
+ count: 1,
1552
+ changedHookIndices: [0],
1553
+ changedHooks: [suspenseHook],
1554
+ fiber: captureFibers ? fiber : void 0,
1555
+ isUpdater: true
1556
+ });
1557
+ ownerSourceId = sourceId;
1558
+ }
1559
+ }
1560
+ }
1561
+ if (isComponent) {
1562
+ const type = fiber.type;
1563
+ if (didFiberUpdate(fiber)) {
1564
+ perCommit.committedComponentCount++;
1565
+ }
1566
+ if (!didFiberUpdate(fiber)) {
1567
+ if (fiber.child) visit(fiber.child, depth + 1, ownerSourceId);
1568
+ if (fiber.sibling) visit(fiber.sibling, depth, currentOwnerSourceId);
1569
+ return;
1570
+ }
1571
+ if (!ignoreSet.has(type) && !isReactInternalComponentType(type)) {
1572
+ const componentSourceId = sourceIdManager.getSourceId(fiber, type, depth);
1573
+ const parentFiber = fiber.return && isComponentFiber(fiber.return.tag) ? fiber.return : null;
1574
+ const parentSourceId = parentFiber ? sourceIdManager.getSourceId(parentFiber, parentFiber.type, depth - 1) : null;
1575
+ const existingRenderEvidenceIndex = perCommit.rendered.findIndex(
1576
+ (entry) => entry.sourceId === componentSourceId
1577
+ );
1578
+ const initialRenderEvidence = {
1579
+ sourceId: componentSourceId,
1580
+ name: getComponentName(fiber),
1581
+ depth,
1582
+ parentSourceId,
1583
+ ownerSourceId,
1584
+ isSelfSource: false,
1585
+ propChanges: fiber.alternate ? diffProps(fiber.alternate.memoizedProps, fiber.memoizedProps) : [],
1586
+ hasPropChanges: false,
1587
+ trasherCounts: { effects: 0, callbacks: 0, memos: 0 }
1588
+ };
1589
+ initialRenderEvidence.hasPropChanges = initialRenderEvidence.propChanges.length > 0;
1590
+ if (existingRenderEvidenceIndex >= 0) {
1591
+ perCommit.rendered[existingRenderEvidenceIndex] = initialRenderEvidence;
1592
+ } else {
1593
+ perCommit.rendered.push(initialRenderEvidence);
1594
+ }
1595
+ if (enableAmplifiers) {
1596
+ let hook = fiber.memoizedState;
1597
+ let hookIndex = 0;
1598
+ while (hook) {
1599
+ const ms = hook.memoizedState;
1600
+ if (Array.isArray(ms) && ms.length === 2 && Array.isArray(ms[1])) {
1601
+ const result = ms[0];
1602
+ if (result != null && (typeof result === "object" || typeof result === "function")) {
1603
+ valueProvenance.set(result, {
1604
+ type,
1605
+ hookIndex,
1606
+ depth,
1607
+ hookType: typeof result === "function" ? "useCallback" : "useMemo"
1608
+ });
1609
+ }
1610
+ }
1611
+ hook = hook.next;
1612
+ hookIndex++;
1613
+ }
1614
+ }
1615
+ let currentHookSnapshot = null;
1616
+ if (enableSourceTracking) {
1617
+ currentHookSnapshot = captureHookStateSnapshot(fiber);
1618
+ }
1619
+ let isRealSource = false;
1620
+ let changedIndices = [];
1621
+ let changedHooks = [];
1622
+ if (enableSourceTracking) {
1623
+ const inMemoizedUpdaters = memoizedUpdaters ? memoizedUpdaters.has(fiber) || fiber.alternate && memoizedUpdaters.has(fiber.alternate) : null;
1624
+ if (memoizedUpdaters) {
1625
+ if (inMemoizedUpdaters) {
1626
+ isRealSource = true;
1627
+ const selfUpdateResult = didFiberSelfUpdate(fiber);
1628
+ if (selfUpdateResult && selfUpdateResult.actionable) {
1629
+ changedIndices = selfUpdateResult.changedIndices;
1630
+ changedHooks = selfUpdateResult.changedHooks;
1631
+ }
1632
+ }
1633
+ } else {
1634
+ const changes = detectChangesFromLastSeen(fiber, currentHookSnapshot);
1635
+ if (changes.changedIndices.length > 0) {
1636
+ isRealSource = true;
1637
+ changedIndices = changes.changedIndices;
1638
+ changedHooks = changes.changedHooks;
1639
+ }
1640
+ }
1641
+ lastSeenHookState.set(fiber, currentHookSnapshot);
1642
+ if (fiber.alternate) {
1643
+ lastSeenHookState.set(fiber.alternate, currentHookSnapshot);
1644
+ }
1645
+ }
1646
+ if (isRealSource && changedIndices.length > 0) {
1647
+ const sourceId = componentSourceId;
1648
+ perCommit.sourceFibers.add(fiber);
1649
+ if (fiber.alternate) {
1650
+ perCommit.sourceFibers.add(fiber.alternate);
1651
+ }
1652
+ const existing = perCommit.selfUpdated.get(sourceId);
1653
+ const updateData = {
1654
+ type,
1655
+ depth,
1656
+ count: (existing?.count || 0) + 1,
1657
+ changedHookIndices: changedIndices,
1658
+ changedHooks,
1659
+ fiber: captureFibers ? fiber : void 0,
1660
+ isUpdater: true
1661
+ };
1662
+ perCommit.selfUpdated.set(sourceId, updateData);
1663
+ perCommit.selfFibers.set(fiber, depth);
1664
+ ownerSourceId = sourceId;
1665
+ }
1666
+ const isSourceItself = perCommit.sourceFibers.has(fiber);
1667
+ const isNonPreventable = (fiber.flags & (Placement | Hydrating)) !== 0;
1668
+ if (isComponent && fiber.alternate && didFiberUpdate(fiber) && ownerSourceId && !isSourceItself && !isNonPreventable) {
1669
+ perCommit.blastBySourceId.set(
1670
+ ownerSourceId,
1671
+ (perCommit.blastBySourceId.get(ownerSourceId) || 0) + 1
1672
+ );
1673
+ const currentMax = perCommit.depthBySourceId.get(ownerSourceId) || 0;
1674
+ if (depth > currentMax) {
1675
+ perCommit.depthBySourceId.set(ownerSourceId, depth);
1676
+ }
1677
+ perCommit.totalCollateral++;
1678
+ }
1679
+ const renderedEntry = perCommit.rendered.find((entry) => entry.sourceId === componentSourceId);
1680
+ if (renderedEntry) {
1681
+ renderedEntry.ownerSourceId = ownerSourceId;
1682
+ renderedEntry.isSelfSource = isRealSource && changedIndices.length > 0;
1683
+ }
1684
+ if (enableAmplifiers && didFiberUpdate(fiber) && fiber.alternate) {
1685
+ const thrashResult = detectHookThrashing(
1686
+ fiber.alternate.memoizedState,
1687
+ fiber.memoizedState,
1688
+ effectThrashMode,
1689
+ valueProvenance
1690
+ );
1691
+ const totalThrashing = thrashResult.effects.length + thrashResult.callbacks.length + thrashResult.memos.length;
1692
+ if (totalThrashing > 0) {
1693
+ if (thrashMode === "counts") {
1694
+ if (state.thrashCounts) {
1695
+ state.thrashCounts.affectedComponents.add(type);
1696
+ state.thrashCounts.totalHooks += totalThrashing;
1697
+ }
1698
+ } else if (thrashMode === "full") {
1699
+ const allThrashedHooks = [
1700
+ ...thrashResult.effects,
1701
+ ...thrashResult.callbacks,
1702
+ ...thrashResult.memos
1703
+ ];
1704
+ const existing = perCommit.thrashing.get(type);
1705
+ const thrashData = {
1706
+ instanceCount: (existing?.instanceCount || 0) + 1,
1707
+ hookCount: (existing?.hookCount || 0) + totalThrashing,
1708
+ lastAmplification: {
1709
+ depth,
1710
+ thrashedHooks: allThrashedHooks,
1711
+ fiber: captureFibers ? fiber : void 0
1712
+ }
1713
+ };
1714
+ perCommit.thrashing.set(type, thrashData);
1715
+ const amplifierSourceId = sourceIdManager.getSourceId(fiber, type, depth);
1716
+ const existingLinked = perCommit.linkedThrashing.get(amplifierSourceId);
1717
+ const effects = (existingLinked?.effects || 0) + thrashResult.effects.length;
1718
+ const callbacks = (existingLinked?.callbacks || 0) + thrashResult.callbacks.length;
1719
+ const memos = (existingLinked?.memos || 0) + thrashResult.memos.length;
1720
+ perCommit.linkedThrashing.set(amplifierSourceId, {
1721
+ type,
1722
+ depth,
1723
+ effects,
1724
+ callbacks,
1725
+ memos,
1726
+ // If this fiber is a source, causedBySourceId is null (self-caused)
1727
+ // If this fiber is collateral, ownerSourceId is who caused it
1728
+ causedBySourceId: isRealSource ? null : ownerSourceId,
1729
+ lastAmplification: {
1730
+ depth,
1731
+ thrashedHooks: allThrashedHooks,
1732
+ fiber: captureFibers ? fiber : void 0
1733
+ }
1734
+ });
1735
+ const renderedEntry2 = perCommit.rendered.find((entry) => entry.sourceId === amplifierSourceId);
1736
+ if (renderedEntry2) {
1737
+ renderedEntry2.trasherCounts = { effects, callbacks, memos };
1738
+ }
1739
+ perCommit.effectCount += thrashResult.effects.length;
1740
+ perCommit.memoCount += thrashResult.memos.length;
1741
+ perCommit.callbackCount += thrashResult.callbacks.length;
1742
+ }
1743
+ }
1744
+ }
1745
+ }
1746
+ }
1747
+ if (fiber.child) visit(fiber.child, depth + 1, ownerSourceId);
1748
+ if (fiber.sibling) visit(fiber.sibling, depth, currentOwnerSourceId);
1749
+ }
1750
+ visit(rootFiber);
1751
+ state.commits++;
1752
+ for (const [sourceId, data] of perCommit.selfUpdated.entries()) {
1753
+ const { type, depth, count, changedHookIndices, fiber } = data;
1754
+ let s = state.sources.get(sourceId);
1755
+ if (!s) {
1756
+ s = {
1757
+ type,
1758
+ depth,
1759
+ renderCount: 0,
1760
+ causedUpdates: 0,
1761
+ collateralUpdates: 0,
1762
+ maxCollateralUpdates: 0,
1763
+ maxPenetrationDepth: 0,
1764
+ updateHistory: {},
1765
+ collateralByCommit: {},
1766
+ // Per-commit collateral for this source
1767
+ debugHookIndices: changedHookIndices.slice()
1768
+ };
1769
+ state.sources.set(sourceId, s);
1770
+ }
1771
+ s.renderCount++;
1772
+ s.debugHookIndices = changedHookIndices.slice();
1773
+ const historyWithUpdate = pushUpdateHistory(
1774
+ s.updateHistory,
1775
+ state.commits,
1776
+ changedHookIndices,
1777
+ data.changedHooks
1778
+ );
1779
+ s.updateHistory = trimHistoryToSize(historyWithUpdate, historySize);
1780
+ const thisCommitEntry = s.updateHistory[state.commits];
1781
+ const isRepeat = thisCommitEntry === "repeat";
1782
+ if (!isRepeat && data.isUpdater) {
1783
+ s.causedUpdates += count;
1784
+ const blast = perCommit.blastBySourceId.get(sourceId) || 0;
1785
+ s.collateralUpdates += blast;
1786
+ if (blast > s.maxCollateralUpdates) s.maxCollateralUpdates = blast;
1787
+ s.collateralByCommit[state.commits] = blast;
1788
+ const penetrationDepth = perCommit.depthBySourceId.get(sourceId) || 0;
1789
+ if (penetrationDepth > s.maxPenetrationDepth) {
1790
+ s.maxPenetrationDepth = penetrationDepth;
1791
+ }
1792
+ }
1793
+ if (captureFibers && fiber) {
1794
+ s.fiber = fiber;
1795
+ }
1796
+ }
1797
+ if (enableAmplifiers && thrashMode === "full") {
1798
+ for (const [type, data] of perCommit.thrashing.entries()) {
1799
+ let s = state.amplifiers.get(type);
1800
+ if (!s) {
1801
+ s = {
1802
+ renderCount: 0,
1803
+ instancesTotal: 0,
1804
+ maxInstancesPerCommit: 0,
1805
+ hooksTotal: 0,
1806
+ maxHooksPerCommit: 0,
1807
+ effectCount: 0,
1808
+ callbackCount: 0,
1809
+ memoCount: 0,
1810
+ lastAmplification: { depth: 0, thrashedHooks: [] }
1811
+ };
1812
+ state.amplifiers.set(type, s);
1813
+ }
1814
+ s.renderCount++;
1815
+ s.instancesTotal += data.instanceCount;
1816
+ s.hooksTotal += data.hookCount;
1817
+ if (data.instanceCount > s.maxInstancesPerCommit)
1818
+ s.maxInstancesPerCommit = data.instanceCount;
1819
+ if (data.hookCount > s.maxHooksPerCommit) s.maxHooksPerCommit = data.hookCount;
1820
+ data.lastAmplification.thrashedHooks.forEach((hook) => {
1821
+ if (hook.hookType === "effect") s.effectCount++;
1822
+ else if (hook.hookType === "callback") s.callbackCount++;
1823
+ else if (hook.hookType === "memo") s.memoCount++;
1824
+ });
1825
+ s.lastAmplification = data.lastAmplification;
1826
+ }
1827
+ for (const [sourceId, data] of perCommit.linkedThrashing.entries()) {
1828
+ let as = state.amplifierSources.get(sourceId);
1829
+ if (!as) {
1830
+ as = {
1831
+ type: data.type,
1832
+ depth: data.depth,
1833
+ effectCount: 0,
1834
+ callbackCount: 0,
1835
+ memoCount: 0,
1836
+ hooksTotal: 0,
1837
+ commitIds: [],
1838
+ byCommit: {},
1839
+ causedBySourceId: data.causedBySourceId,
1840
+ lastAmplification: { depth: 0, thrashedHooks: [] }
1841
+ };
1842
+ state.amplifierSources.set(sourceId, as);
1843
+ }
1844
+ as.effectCount += data.effects;
1845
+ as.callbackCount += data.callbacks;
1846
+ as.memoCount += data.memos;
1847
+ as.hooksTotal += data.effects + data.callbacks + data.memos;
1848
+ if (!as.commitIds.includes(state.commits)) {
1849
+ as.commitIds.push(state.commits);
1850
+ }
1851
+ as.byCommit[state.commits] = {
1852
+ effects: data.effects,
1853
+ callbacks: data.callbacks,
1854
+ memos: data.memos
1855
+ };
1856
+ if (data.causedBySourceId !== null) {
1857
+ as.causedBySourceId = data.causedBySourceId;
1858
+ }
1859
+ as.lastAmplification = data.lastAmplification;
1860
+ }
1861
+ }
1862
+ if (perCommit.suspenseEvents && perCommit.suspenseEvents.length > 0) {
1863
+ state.suspenseByCommit[state.commits] = perCommit.suspenseEvents;
1864
+ }
1865
+ if (perCommit.rendered.length > 0) {
1866
+ state.renderedByCommit[state.commits] = perCommit.rendered;
1867
+ }
1868
+ state.commitStats[state.commits] = [
1869
+ perCommit.totalCollateral,
1870
+ perCommit.selfUpdated.size,
1871
+ // Number of sources in this commit
1872
+ 0,
1873
+ // restarts - will be updated after the adapter's delta calculation
1874
+ perCommit.committedComponentCount,
1875
+ // performedWork - components with PerformedWork flag
1876
+ perCommit.effectCount,
1877
+ perCommit.memoCount,
1878
+ perCommit.callbackCount,
1879
+ perCommit.contextUpdates,
1880
+ perCommit.unmountedComponents,
1881
+ perCommit.unmountedSubtrees
1882
+ ];
1883
+ if (perCommit.unmountSamples && perCommit.unmountSamples.length > 0) {
1884
+ state.unmountsByCommit[state.commits] = perCommit.unmountSamples;
1885
+ }
1886
+ return perCommit.committedComponentCount;
1887
+ }
1888
+ function resetState() {
1889
+ state.commits = 0;
1890
+ state.sources.clear();
1891
+ state.amplifiers.clear();
1892
+ state.amplifierSources.clear();
1893
+ if (state.thrashCounts) {
1894
+ state.thrashCounts.totalHooks = 0;
1895
+ state.thrashCounts.affectedComponents.clear();
1896
+ }
1897
+ state.memoizedUpdatersAvailable = false;
1898
+ state.commitsWithMemoizedUpdaters = 0;
1899
+ for (const key of Object.keys(state.commitStats)) {
1900
+ delete state.commitStats[Number(key)];
1901
+ }
1902
+ for (const key of Object.keys(state.metricsByCommit)) {
1903
+ delete state.metricsByCommit[Number(key)];
1904
+ }
1905
+ for (const key of Object.keys(state.commitTimings)) {
1906
+ delete state.commitTimings[Number(key)];
1907
+ }
1908
+ for (const key of Object.keys(state.suspenseByCommit)) {
1909
+ delete state.suspenseByCommit[Number(key)];
1910
+ }
1911
+ for (const key of Object.keys(state.unmountsByCommit)) {
1912
+ delete state.unmountsByCommit[Number(key)];
1913
+ }
1914
+ for (const key of Object.keys(state.renderedByCommit)) {
1915
+ delete state.renderedByCommit[Number(key)];
1916
+ }
1917
+ interactionCapture.reset();
1918
+ state.lastMetricsSnapshot = captureMetricsSnapshot();
1919
+ state.lastCommitEndTime = performance.now();
1920
+ sourceIdManager.clear();
1921
+ }
1922
+ const commitSurface = createCommitSurfaceOwnership();
1923
+ let devtoolsHookPrev;
1924
+ let devtoolsUnmountPrev;
1925
+ let devtoolsHookCreatedByUs = false;
1926
+ let pendingDevtoolsUnmounts = { components: 0, subtrees: 0 };
1927
+ let pendingDevtoolsUnmountSamples = [];
1928
+ let integrationMode = "none";
1929
+ function countUnmountedComponentSubtree(fiber, parentIsComponent = false) {
1930
+ if (!fiber) return { components: 0, subtrees: 0 };
1931
+ const isComponent = isComponentFiber(fiber.tag);
1932
+ let components = isComponent ? 1 : 0;
1933
+ let subtrees = isComponent && !parentIsComponent ? 1 : 0;
1934
+ if (fiber.child) {
1935
+ const child = countUnmountedComponentSubtree(fiber.child, isComponent);
1936
+ components += child.components;
1937
+ subtrees += child.subtrees;
1938
+ }
1939
+ if (fiber.sibling) {
1940
+ const sibling = countUnmountedComponentSubtree(fiber.sibling, parentIsComponent);
1941
+ components += sibling.components;
1942
+ subtrees += sibling.subtrees;
1943
+ }
1944
+ return { components, subtrees };
1945
+ }
1946
+ function summarizeHostSubtree(fiber) {
1947
+ const tags = /* @__PURE__ */ new Map();
1948
+ const testIds = /* @__PURE__ */ new Map();
1949
+ const roles = /* @__PURE__ */ new Map();
1950
+ const ariaLabels = [];
1951
+ const textHints = [];
1952
+ let elements = 0;
1953
+ let text = 0;
1954
+ const add = (map, key) => {
1955
+ if (typeof key !== "string" || !key) return;
1956
+ map.set(key, (map.get(key) || 0) + 1);
1957
+ };
1958
+ const addHint = (arr, value, max = 6) => {
1959
+ if (typeof value !== "string") return;
1960
+ const trimmed = value.replace(/\s+/g, " ").trim();
1961
+ if (!trimmed || arr.includes(trimmed)) return;
1962
+ arr.push(trimmed.length > 80 ? `${trimmed.slice(0, 77)}\u2026` : trimmed);
1963
+ if (arr.length > max) arr.length = max;
1964
+ };
1965
+ const top = (map) => Array.from(map.entries()).sort((a, b) => b[1] - a[1]).slice(0, 6);
1966
+ const walk = (f) => {
1967
+ if (!f) return;
1968
+ if (f.tag === 5) {
1969
+ elements++;
1970
+ const props = f.memoizedProps || {};
1971
+ add(tags, typeof f.type === "string" ? f.type : "host");
1972
+ add(testIds, props["data-testid"]);
1973
+ add(roles, props.role);
1974
+ addHint(ariaLabels, props["aria-label"]);
1975
+ addHint(ariaLabels, props.title);
1976
+ addHint(textHints, typeof props.children === "string" ? props.children : null);
1977
+ } else if (f.tag === 6) {
1978
+ text++;
1979
+ addHint(textHints, f.memoizedProps);
1980
+ }
1981
+ walk(f.child);
1982
+ walk(f.sibling);
1983
+ };
1984
+ walk(fiber);
1985
+ return { elements, text, tagsTop: top(tags), testIdsTop: top(testIds), rolesTop: top(roles), ariaLabels, textHints };
1986
+ }
1987
+ function recordMetricsEvidence(commitId, committedComponents, before, after) {
1988
+ if (!metricsAdapter) return;
1989
+ const { delta, restarts } = metricsAdapter.measure(before, after, committedComponents, commitId);
1990
+ state.metricsByCommit[commitId] = delta;
1991
+ if (state.commitStats[commitId]) {
1992
+ state.commitStats[commitId][CS_RESTARTS] = restarts;
1993
+ }
1994
+ }
1995
+ function handleCommit(root, metricsDeltaAvailable) {
1996
+ debugRender.install();
1997
+ const commitHookStartTime = performance.now();
1998
+ const timeSinceLastCommit = commitHookStartTime - state.lastCommitEndTime;
1999
+ const metricsBefore = state.lastMetricsSnapshot || captureMetricsSnapshot();
2000
+ const memoizedUpdaters = root.memoizedUpdaters;
2001
+ const committedComponents = recordTree(root.current, memoizedUpdaters || null);
2002
+ const currentStats = state.commitStats[state.commits];
2003
+ if (currentStats && (pendingDevtoolsUnmounts.components > 0 || pendingDevtoolsUnmounts.subtrees > 0)) {
2004
+ const recordTreeSawDeletions = (currentStats[CS_UNMOUNTED_COMPONENTS] || 0) > 0;
2005
+ if (!recordTreeSawDeletions) {
2006
+ currentStats[CS_UNMOUNTED_COMPONENTS] = pendingDevtoolsUnmounts.components;
2007
+ currentStats[CS_UNMOUNTED_SUBTREES] = pendingDevtoolsUnmounts.subtrees;
2008
+ }
2009
+ pendingDevtoolsUnmounts = { components: 0, subtrees: 0 };
2010
+ }
2011
+ if (pendingDevtoolsUnmountSamples.length > 0) {
2012
+ const existing = state.unmountsByCommit[state.commits];
2013
+ if (existing && existing.length > 0) {
2014
+ } else {
2015
+ state.unmountsByCommit[state.commits] = pendingDevtoolsUnmountSamples;
2016
+ }
2017
+ pendingDevtoolsUnmountSamples = [];
2018
+ }
2019
+ const metricsAfter = captureMetricsSnapshot();
2020
+ if (metricsDeltaAvailable && metricsBefore && metricsAfter) {
2021
+ recordMetricsEvidence(state.commits, committedComponents, metricsBefore, metricsAfter);
2022
+ }
2023
+ state.lastMetricsSnapshot = metricsAfter;
2024
+ const commitHookEndTime = performance.now();
2025
+ interactionCapture.linkRecent(state.commits, commitHookEndTime);
2026
+ state.commitTimings[state.commits] = {
2027
+ timeSinceLastCommit,
2028
+ detectorOverhead: commitHookEndTime - commitHookStartTime
2029
+ };
2030
+ state.lastCommitEndTime = commitHookEndTime;
2031
+ return committedComponents;
2032
+ }
2033
+ return {
2034
+ /**
2035
+ * Start profiling React commits.
2036
+ *
2037
+ * Integration model is OBSERVED at runtime, not declared at install time.
2038
+ *
2039
+ * 'devtools' — DevTools `onCommitFiberRoot` is the canonical entry
2040
+ * point. We define the hook if absent, augment (chain)
2041
+ * if present. On every commit we capture the fiber tree.
2042
+ *
2043
+ * 'adapter:<id>' — Promoted from 'devtools' the first time the commit
2044
+ * metrics adapter's host callback actually fires for a
2045
+ * real commit. From then on the adapter is the
2046
+ * authoritative processor and the DevTools surface is
2047
+ * chained-observer-only. The adapter is preferred when
2048
+ * alive because it carries render-counter deltas
2049
+ * (restart math, metricsByCommit).
2050
+ *
2051
+ * 'none' — Only when no DevTools hook surface can be created and
2052
+ * no adapter surface is reachable. Practically never
2053
+ * hit, since the content-script ships a
2054
+ * devtools-hook-shim before react-dom.
2055
+ *
2056
+ * Double-invocation safety:
2057
+ * Once an adapter callback has fired, DevTools remains chained for
2058
+ * existing tooling but does not capture commits; the adapter alone calls
2059
+ * `handleCommit`. Without a live adapter, DevTools is the capture
2060
+ * surface. This avoids duplicate capture without relying on per-root
2061
+ * timing heuristics.
2062
+ *
2063
+ * Side effects:
2064
+ * - Defines `__REACT_DEVTOOLS_GLOBAL_HOOK__` if it does not already exist
2065
+ * (minimal contract: `onCommitFiberRoot` only). React reads the hook at
2066
+ * module-eval time; the content-script shim handles ordering.
2067
+ * - Asks the adapter (when one is registered) to route its host commit
2068
+ * callback here, *if and when* the host calls it.
2069
+ *
2070
+ * Safe to call multiple times (idempotent: chains skipped when our wrappers
2071
+ * are already in place).
2072
+ */
2073
+ install() {
2074
+ interactionCapture.install();
2075
+ const devtoolsGlobal = globalThis;
2076
+ metricsAdapter = resolveMetricsAdapter();
2077
+ state.lastMetricsSnapshot = captureMetricsSnapshot();
2078
+ let hook = devtoolsGlobal.__REACT_DEVTOOLS_GLOBAL_HOOK__;
2079
+ if (!hook) {
2080
+ hook = {};
2081
+ devtoolsGlobal.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
2082
+ devtoolsHookCreatedByUs = true;
2083
+ } else {
2084
+ devtoolsHookCreatedByUs = false;
2085
+ }
2086
+ devtoolsHookPrev = typeof hook.onCommitFiberRoot === "function" ? hook.onCommitFiberRoot : void 0;
2087
+ devtoolsUnmountPrev = typeof hook.onCommitFiberUnmount === "function" ? hook.onCommitFiberUnmount : void 0;
2088
+ const devtoolsHandler = (...args) => {
2089
+ if (devtoolsHookPrev) {
2090
+ try {
2091
+ devtoolsHookPrev(...args);
2092
+ } catch (_) {
2093
+ }
2094
+ }
2095
+ const root = args[1];
2096
+ if (!root) return;
2097
+ if (commitSurface.adapterObserved && metricsAdapter) {
2098
+ if (!metricsAdapter.isAttached()) {
2099
+ metricsAdapter.reattach();
2100
+ const metricsBefore2 = state.lastMetricsSnapshot || captureMetricsSnapshot();
2101
+ const committedComponents2 = handleCommit(
2102
+ root,
2103
+ /* metricsDeltaAvailable */
2104
+ false
2105
+ );
2106
+ commitSurface.recordDevtools(root, { commitId: state.commits, committedComponents: committedComponents2, metricsBefore: metricsBefore2 });
2107
+ return;
2108
+ }
2109
+ return;
2110
+ }
2111
+ const metricsBefore = state.lastMetricsSnapshot || captureMetricsSnapshot();
2112
+ const committedComponents = handleCommit(
2113
+ root,
2114
+ /* metricsDeltaAvailable */
2115
+ false
2116
+ );
2117
+ commitSurface.recordDevtools(root, { commitId: state.commits, committedComponents, metricsBefore });
2118
+ };
2119
+ hook.onCommitFiberRoot = devtoolsHandler;
2120
+ const devtoolsUnmountHandler = (...args) => {
2121
+ if (devtoolsUnmountPrev) {
2122
+ try {
2123
+ devtoolsUnmountPrev(...args);
2124
+ } catch (_) {
2125
+ }
2126
+ }
2127
+ const fiber = args[1];
2128
+ if (!fiber) return;
2129
+ const counts = countUnmountedComponentSubtree(fiber);
2130
+ pendingDevtoolsUnmounts.components += counts.components;
2131
+ pendingDevtoolsUnmounts.subtrees += counts.subtrees;
2132
+ if (counts.components > 0 && pendingDevtoolsUnmountSamples.length < 30) {
2133
+ let labelFiber = fiber;
2134
+ if (!isComponentFiber(labelFiber.tag)) {
2135
+ const stack = [labelFiber];
2136
+ labelFiber = null;
2137
+ while (stack.length) {
2138
+ const f = stack.pop();
2139
+ if (isComponentFiber(f.tag)) {
2140
+ labelFiber = f;
2141
+ break;
2142
+ }
2143
+ if (f.child) stack.push(f.child);
2144
+ if (f.sibling) stack.push(f.sibling);
2145
+ }
2146
+ }
2147
+ if (labelFiber) {
2148
+ const fiberKey = labelFiber.key;
2149
+ pendingDevtoolsUnmountSamples.push({
2150
+ name: getComponentName(labelFiber),
2151
+ key: fiberKey == null ? null : String(fiberKey),
2152
+ depth: null,
2153
+ count: counts.components,
2154
+ source: summarizeFiberSource(labelFiber),
2155
+ host: summarizeHostSubtree(fiber)
2156
+ });
2157
+ }
2158
+ }
2159
+ lastSeenHookState.delete(fiber);
2160
+ if (fiber.alternate) lastSeenHookState.delete(fiber.alternate);
2161
+ };
2162
+ hook.onCommitFiberUnmount = devtoolsUnmountHandler;
2163
+ integrationMode = "devtools";
2164
+ commitSurface.reset();
2165
+ const adapter = metricsAdapter;
2166
+ if (adapter) {
2167
+ const adapterMode = `adapter:${adapter.id}`;
2168
+ adapter.attach((root) => {
2169
+ const previousCapture = commitSurface.observeAdapter(root);
2170
+ if (integrationMode !== adapterMode) {
2171
+ integrationMode = adapterMode;
2172
+ }
2173
+ if (previousCapture) {
2174
+ const metricsAfter = captureMetricsSnapshot();
2175
+ if (previousCapture.metricsBefore && metricsAfter) {
2176
+ recordMetricsEvidence(previousCapture.commitId, previousCapture.committedComponents, previousCapture.metricsBefore, metricsAfter);
2177
+ }
2178
+ state.lastMetricsSnapshot = metricsAfter;
2179
+ } else {
2180
+ handleCommit(
2181
+ root,
2182
+ /* metricsDeltaAvailable */
2183
+ true
2184
+ );
2185
+ }
2186
+ });
2187
+ }
2188
+ },
2189
+ /**
2190
+ * Stop profiling React commits. Does not clear accumulated state.
2191
+ * Call reset() to clear state.
2192
+ *
2193
+ * Restores the DevTools hook to its prior state:
2194
+ * - hook was created by us → delete the hook entirely
2195
+ * - hook had a prior onCommitFiberRoot → restore it
2196
+ * - hook had no handler → delete the property
2197
+ *
2198
+ * Asks the adapter to remove its host callback only if it is still ours
2199
+ * (don't stomp a concurrent installer).
2200
+ */
2201
+ uninstall() {
2202
+ metricsAdapter?.detach();
2203
+ const devtoolsGlobal = globalThis;
2204
+ const hook = devtoolsGlobal.__REACT_DEVTOOLS_GLOBAL_HOOK__;
2205
+ if (hook) {
2206
+ if (devtoolsHookCreatedByUs) {
2207
+ delete devtoolsGlobal.__REACT_DEVTOOLS_GLOBAL_HOOK__;
2208
+ } else {
2209
+ if (devtoolsHookPrev) {
2210
+ hook.onCommitFiberRoot = devtoolsHookPrev;
2211
+ } else {
2212
+ delete hook.onCommitFiberRoot;
2213
+ }
2214
+ if (devtoolsUnmountPrev) {
2215
+ hook.onCommitFiberUnmount = devtoolsUnmountPrev;
2216
+ } else {
2217
+ delete hook.onCommitFiberUnmount;
2218
+ }
2219
+ }
2220
+ }
2221
+ debugRender.uninstall();
2222
+ interactionCapture.uninstall();
2223
+ commitSurface.reset();
2224
+ pendingDevtoolsUnmounts = { components: 0, subtrees: 0 };
2225
+ pendingDevtoolsUnmountSamples = [];
2226
+ devtoolsHookPrev = void 0;
2227
+ devtoolsUnmountPrev = void 0;
2228
+ devtoolsHookCreatedByUs = false;
2229
+ integrationMode = "none";
2230
+ },
2231
+ /**
2232
+ * Clear all accumulated profiling state. Useful for starting fresh measurements.
2233
+ * Does not uninstall the profiler - call uninstall() separately if needed.
2234
+ */
2235
+ reset() {
2236
+ resetState();
2237
+ debugRender.reset();
2238
+ },
2239
+ /**
2240
+ * Generate a report of top update sources and thrashing components.
2241
+ * @param topN - Number of top sources to include (default: 10)
2242
+ * @param sortBy - Sort thrashers by 'effect', 'callback', 'memo', or 'total' (default: 'effect')
2243
+ * @param options - Report filtering options (e.g., minCollateral to hide low-impact sources)
2244
+ * @returns Report containing sources, blast radius, and optional thrashing data
2245
+ */
2246
+ report(topN = 10, sortBy = "effect", reportOptions) {
2247
+ return buildReport(state, thrashMode, topN, sortBy, reportOptions, {
2248
+ effectThrashMode,
2249
+ captureFibers,
2250
+ historySize,
2251
+ detectionLevel,
2252
+ integrationMode
2253
+ });
2254
+ },
2255
+ /**
2256
+ * Raw passive hook events captured for a component (debug surface).
2257
+ */
2258
+ _debugRawHookEvents(componentName) {
2259
+ const components = debugRender.allComponents();
2260
+ const events = componentName === "*" ? [] : debugRender.rawEvents(componentName);
2261
+ return {
2262
+ ok: true,
2263
+ componentName,
2264
+ status: debugRender.status(),
2265
+ components,
2266
+ events
2267
+ };
2268
+ },
2269
+ _debugInjectHookEventsForTest(componentName, events) {
2270
+ if (typeof debugRender.recordEventForTest !== "function") return;
2271
+ for (const event of events) {
2272
+ debugRender.recordEventForTest({ ...event, componentName });
2273
+ }
2274
+ },
2275
+ _debugSetSourceForTest(sourceId, source) {
2276
+ state.sources.set(sourceId, source);
2277
+ },
2278
+ debugHook(sourceId, hookIndex, windowSize = 3) {
2279
+ let source = state.sources.get(sourceId);
2280
+ let resolvedSourceId = sourceId;
2281
+ if (!source) {
2282
+ for (const [internalId, candidate] of state.sources.entries()) {
2283
+ const candidateHumanId = `${getComponentDisplayName(candidate.type)}#${String(internalId).match(/#(\d+)@/)?.[1] || "1"}@${candidate.depth}`;
2284
+ if (candidateHumanId === sourceId) {
2285
+ source = candidate;
2286
+ resolvedSourceId = internalId;
2287
+ break;
2288
+ }
2289
+ }
2290
+ }
2291
+ if (!source) return { ok: false, error: "sourceId not found", sourceId };
2292
+ const componentName = getComponentDisplayName(source.type);
2293
+ let events = debugRender.rawEvents(componentName);
2294
+ let resolvedComponentName = componentName;
2295
+ if (events.length === 0) {
2296
+ const candidates = debugRender.allComponents();
2297
+ const matching = candidates.find((name) => name === componentName || name.includes(componentName) || componentName.includes(name));
2298
+ if (matching) {
2299
+ resolvedComponentName = matching;
2300
+ events = debugRender.rawEvents(matching);
2301
+ }
2302
+ }
2303
+ const sourceHookIndices = Array.isArray(source.debugHookIndices) ? source.debugHookIndices : [];
2304
+ const targetIndex = Math.floor(hookIndex);
2305
+ const eventIndex = sourceHookIndices[targetIndex] ?? targetIndex;
2306
+ const target = events[eventIndex];
2307
+ const radius = Math.max(0, Math.floor(windowSize));
2308
+ if (!target) {
2309
+ return {
2310
+ ok: false,
2311
+ error: "hook event not found",
2312
+ sourceId: resolvedSourceId,
2313
+ requestedSourceId: sourceId,
2314
+ componentName,
2315
+ resolvedComponentName,
2316
+ hookIndex: targetIndex,
2317
+ eventIndex,
2318
+ sourceHookIndices,
2319
+ eventCount: events.length,
2320
+ status: debugRender.status(),
2321
+ caveat: "hook ancestry comes from passive stack capture during real renders; component-name stack parsing can be ambiguous in bundled/minified code"
2322
+ };
2323
+ }
2324
+ const neighborhood = events.map((event, index) => ({ event, index })).filter(({ index }) => Math.abs(index - eventIndex) <= radius).map(({ event, index }) => ({
2325
+ hookIndex: sourceHookIndices.indexOf(index) >= 0 ? sourceHookIndices.indexOf(index) : index,
2326
+ eventIndex: index,
2327
+ primitive: event.primitive,
2328
+ parents: parentFramesForEvent(event, componentName)
2329
+ }));
2330
+ return {
2331
+ ok: true,
2332
+ sourceId: resolvedSourceId,
2333
+ requestedSourceId: sourceId,
2334
+ componentName,
2335
+ resolvedComponentName,
2336
+ hookIndex: targetIndex,
2337
+ eventIndex,
2338
+ sourceHookIndices,
2339
+ primitive: target.primitive,
2340
+ parents: parentFramesForEvent(target, componentName),
2341
+ neighborhood,
2342
+ eventCount: events.length,
2343
+ status: debugRender.status(),
2344
+ caveat: "hook ancestry comes from passive stack capture during real renders; source-local hook indices are mapped onto the captured component event stream"
2345
+ };
2346
+ },
2347
+ /**
2348
+ * Returns which commit-hook integration model is currently observed.
2349
+ * 'devtools' — DevTools hook is the active processor (no proven adapter callbacks).
2350
+ * 'adapter:<id>' — The adapter's host surface has fired; it owns the math now.
2351
+ * 'none' — Detector not installed.
2352
+ */
2353
+ getIntegrationMode() {
2354
+ return integrationMode;
2355
+ },
2356
+ /** Id of the commit metrics adapter resolved on install, or null. */
2357
+ getMetricsAdapterId() {
2358
+ return metricsAdapter?.id ?? null;
2359
+ }
2360
+ };
2361
+ }
2362
+ var DETECTION_LEVEL = {
2363
+ /** Level 1: Only commitStats (performedWork for restarts) - minimal overhead */
2364
+ LEVEL1_STATS: "stats",
2365
+ /** Level 2: Stats + source tracking (which components caused updates) */
2366
+ LEVEL2_SOURCES: "sources",
2367
+ /** Level 3a: Sources + aggregate amplifier counts only (meta.amplifiersMetrics) */
2368
+ LEVEL3_AMPLIFIER_METRICS: "amplifierMetrics",
2369
+ /** Level 3b: Sources + full amplifier arrays (report.amplifiers, report.amplifierSources) */
2370
+ LEVEL3_AMPLIFIERS: "amplifiers"
2371
+ };
2372
+
2373
+ // src/plugins/atl/metrics-slot.ts
2374
+ var DEFAULT_METRICS_SLOT = "__ATL_REACT_METRICS";
2375
+ var METRICS_COUNTER_KEYS = [
2376
+ "mountWorkCount",
2377
+ "updateWorkCount",
2378
+ "commitWorkCount",
2379
+ "interruptedWorkCount",
2380
+ "priorityInterruptedWorkCount",
2381
+ "externalStoreRestartCount",
2382
+ "renderRootSyncCount",
2383
+ "renderRootConcurrentCount",
2384
+ "componentWorkCount",
2385
+ "componentMountWorkCount",
2386
+ "componentUpdateWorkCount",
2387
+ "componentRenderWithHooksCount",
2388
+ "finishClassComponentCount",
2389
+ "domWorkCount",
2390
+ "totalTickCount",
2391
+ "totalCommitCount",
2392
+ "contextUpdateCount",
2393
+ "legacyContextUpdateCount"
2394
+ ];
2395
+ var ACCESSOR_KEYS = [...METRICS_COUNTER_KEYS, "totalRestarts", "totalPerformedWork"];
2396
+ function createZeroMetrics() {
2397
+ const metrics = {};
2398
+ for (const key of METRICS_COUNTER_KEYS) metrics[key] = 0;
2399
+ metrics.setStateCount = 0;
2400
+ return metrics;
2401
+ }
2402
+ function isAllZero(value) {
2403
+ if (!value || typeof value !== "object") return true;
2404
+ const record = value;
2405
+ for (const key of ACCESSOR_KEYS) if ((record[key] || 0) !== 0) return false;
2406
+ return true;
2407
+ }
2408
+ function isSlotHostLocked(target, slotName = DEFAULT_METRICS_SLOT) {
2409
+ const existing = Object.getOwnPropertyDescriptor(target, slotName);
2410
+ return !!existing && (existing.configurable === false || typeof existing.get === "function");
2411
+ }
2412
+ function protectMetricsSlot(target, metrics, slotName = DEFAULT_METRICS_SLOT) {
2413
+ try {
2414
+ const existing = Object.getOwnPropertyDescriptor(target, slotName);
2415
+ if (existing && existing.configurable === false) return;
2416
+ Object.defineProperty(target, slotName, { configurable: true, enumerable: true, writable: true, value: metrics });
2417
+ } catch {
2418
+ try {
2419
+ target[slotName] = metrics;
2420
+ } catch {
2421
+ }
2422
+ }
2423
+ }
2424
+ function seedMetricsSlot(target, slotName = DEFAULT_METRICS_SLOT) {
2425
+ const metrics = createZeroMetrics();
2426
+ try {
2427
+ target[slotName] = metrics;
2428
+ } catch {
2429
+ }
2430
+ protectMetricsSlot(target, metrics, slotName);
2431
+ return metrics;
2432
+ }
2433
+ function installMetricsAccessor(target, slotName = DEFAULT_METRICS_SLOT) {
2434
+ try {
2435
+ const existing = Object.getOwnPropertyDescriptor(target, slotName);
2436
+ if (existing && existing.configurable === false) return false;
2437
+ let live = existing && typeof existing.get === "function" ? target[slotName] : existing?.value;
2438
+ if (!live || typeof live !== "object") {
2439
+ live = { onCommitFiberRoot: null };
2440
+ for (const key of ACCESSOR_KEYS) live[key] = 0;
2441
+ }
2442
+ Object.defineProperty(target, slotName, {
2443
+ configurable: true,
2444
+ enumerable: true,
2445
+ get() {
2446
+ return live;
2447
+ },
2448
+ set(next) {
2449
+ if (next === void 0 || next === null || isAllZero(next)) return;
2450
+ live = next;
2451
+ }
2452
+ });
2453
+ return true;
2454
+ } catch {
2455
+ return false;
2456
+ }
2457
+ }
2458
+
2459
+ // src/plugins/atl/commit-metrics-adapter.ts
2460
+ var ATL_DISPATCHER_FRAME = "atlDispatcherHook";
2461
+ var num = (snapshot, key) => snapshot[key] ?? 0;
2462
+ var diff = (before, after, key) => num(after, key) - num(before, key);
2463
+ function calculateATLDelta(before, after) {
2464
+ return {
2465
+ // Basic work counters
2466
+ mountWorkCount: diff(before, after, "mountWorkCount"),
2467
+ updateWorkCount: diff(before, after, "updateWorkCount"),
2468
+ commitWorkCount: diff(before, after, "commitWorkCount"),
2469
+ performedWorkCount: diff(before, after, "totalPerformedWork"),
2470
+ // Interruption/restart counters
2471
+ interruptedWorkCount: diff(before, after, "interruptedWorkCount"),
2472
+ externalStoreRestartCount: diff(before, after, "externalStoreRestartCount"),
2473
+ priorityInterruptedWorkCount: diff(before, after, "priorityInterruptedWorkCount"),
2474
+ renderRootSyncCount: diff(before, after, "renderRootSyncCount"),
2475
+ renderRootConcurrentCount: diff(before, after, "renderRootConcurrentCount"),
2476
+ // Scheduler
2477
+ ticksCount: diff(before, after, "totalTickCount"),
2478
+ // Component work counters
2479
+ componentWorkCount: diff(before, after, "componentWorkCount"),
2480
+ componentMountWorkCount: diff(before, after, "componentMountWorkCount"),
2481
+ componentUpdateWorkCount: diff(before, after, "componentUpdateWorkCount"),
2482
+ componentRenderWithHooksCount: diff(before, after, "componentRenderWithHooksCount"),
2483
+ finishClassComponentCount: diff(before, after, "finishClassComponentCount"),
2484
+ // DOM work
2485
+ domWorkCount: diff(before, after, "domWorkCount"),
2486
+ // Context updates
2487
+ contextUpdateCount: diff(before, after, "contextUpdateCount"),
2488
+ legacyContextUpdateCount: diff(before, after, "legacyContextUpdateCount"),
2489
+ // Hook-metric delta (react-dom patch; optional). Omit when neither snapshot
2490
+ // carries a numeric setStateCount so we never emit a bogus 0 on unpatched hosts.
2491
+ // TODO: Require finite values on both sides or model a counter reset
2492
+ // explicitly. Treating a missing/wiped side as zero can emit a negative delta,
2493
+ // and typeof NaN === 'number' lets a poisoned counter enter the report.
2494
+ ...typeof after.setStateCount === "number" || typeof before.setStateCount === "number" ? { setStateCount: diff(before, after, "setStateCount") } : {}
2495
+ };
2496
+ }
2497
+ function createATLCommitMetricsAdapter(options = {}) {
2498
+ const slotName = options.slotName ?? DEFAULT_METRICS_SLOT;
2499
+ const target = () => options.target ?? globalThis;
2500
+ const slot = () => {
2501
+ const value = target()[slotName];
2502
+ return value && typeof value === "object" ? value : null;
2503
+ };
2504
+ let ourHandler;
2505
+ return {
2506
+ id: "atl",
2507
+ frameFilters: [ATL_DISPATCHER_FRAME],
2508
+ prePage() {
2509
+ installMetricsAccessor(target(), slotName);
2510
+ },
2511
+ prepare() {
2512
+ try {
2513
+ if (!isSlotHostLocked(target(), slotName)) seedMetricsSlot(target(), slotName);
2514
+ } catch {
2515
+ }
2516
+ },
2517
+ isAvailable: () => !!slot(),
2518
+ snapshot() {
2519
+ const live = slot();
2520
+ return live ? { ...live } : null;
2521
+ },
2522
+ measure(before, after, committedComponents, commitId) {
2523
+ const delta = calculateATLDelta(before, after);
2524
+ delta.performedWorkCount = committedComponents;
2525
+ const rendered = (delta.componentRenderWithHooksCount ?? 0) + (delta.finishClassComponentCount ?? 0);
2526
+ const restarts = Math.max(0, rendered - committedComponents);
2527
+ if (committedComponents > rendered && rendered > 0) {
2528
+ console.warn(
2529
+ `[Avalanche] Commit ${commitId}: committedComponents (${committedComponents}) > atlTotal (${rendered}). This indicates a counting bug. Using 0 restarts.`
2530
+ );
2531
+ }
2532
+ const live = slot();
2533
+ if (live) {
2534
+ live.totalRestarts = (live.totalRestarts ?? 0) + restarts;
2535
+ live.totalPerformedWork = (live.totalPerformedWork ?? 0) + committedComponents;
2536
+ }
2537
+ return { delta, restarts };
2538
+ },
2539
+ attach(onCommit) {
2540
+ const live = slot();
2541
+ if (!live) return false;
2542
+ const handler = (_rendererID, root) => onCommit(root);
2543
+ live.onCommitFiberRoot = handler;
2544
+ ourHandler = handler;
2545
+ return true;
2546
+ },
2547
+ isAttached() {
2548
+ const live = slot();
2549
+ return !!live && !!ourHandler && live.onCommitFiberRoot === ourHandler;
2550
+ },
2551
+ reattach() {
2552
+ const fresh = seedMetricsSlot(target(), slotName);
2553
+ if (ourHandler) fresh.onCommitFiberRoot = ourHandler;
2554
+ },
2555
+ detach() {
2556
+ const live = slot();
2557
+ if (live && ourHandler && live.onCommitFiberRoot === ourHandler) live.onCommitFiberRoot = void 0;
2558
+ ourHandler = void 0;
2559
+ }
2560
+ };
2561
+ }
2562
+ var atlCommitMetricsAdapter = createATLCommitMetricsAdapter();
2563
+ export {
2564
+ DETECTION_LEVEL,
2565
+ atlCommitMetricsAdapter,
2566
+ createATLCommitMetricsAdapter,
2567
+ createAvalancheDetector,
2568
+ registerCommitMetricsAdapter
2569
+ };
2570
+ //# sourceMappingURL=avalanche.js.map