@react-perfscope/react 0.7.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +17 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -121,7 +121,14 @@ function ensureHookInstalled() {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
};
|
|
124
|
-
|
|
124
|
+
if (hook !== existing) {
|
|
125
|
+
try {
|
|
126
|
+
g[HOOK_KEY] = hook;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.warn("[react-perfscope] could not install the DevTools hook:", err);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
125
132
|
ourHook = hook;
|
|
126
133
|
ourCommitWrapper = hook.onCommitFiberRoot;
|
|
127
134
|
ourUnmountWrapper = hook.onCommitFiberUnmount ?? null;
|
|
@@ -295,6 +302,7 @@ function createRenderCollector() {
|
|
|
295
302
|
};
|
|
296
303
|
let unsubscribe = null;
|
|
297
304
|
let commitId = 0;
|
|
305
|
+
let warnedNoDurations = false;
|
|
298
306
|
function onCommit(root) {
|
|
299
307
|
if (!active) return;
|
|
300
308
|
const at = performance.now();
|
|
@@ -307,7 +315,14 @@ function createRenderCollector() {
|
|
|
307
315
|
if (!didPerformWork(fiber)) return;
|
|
308
316
|
const name = fiberComponentName(fiber);
|
|
309
317
|
if (!name) return;
|
|
310
|
-
const
|
|
318
|
+
const hasDuration = typeof fiber.actualDuration === "number";
|
|
319
|
+
if (!hasDuration && !warnedNoDurations) {
|
|
320
|
+
warnedNoDurations = true;
|
|
321
|
+
console.warn(
|
|
322
|
+
"[react-perfscope] render durations unavailable \u2014 React did not profile this root. Make sure react-perfscope loads before react-dom (import it first, or use the Vite/webpack plugin), and use a development or profiling build of react-dom. Renders will be reported with duration 0."
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const duration = hasDuration ? fiber.actualDuration : 0;
|
|
311
326
|
const { reason, changedProps } = classifyRenderReason(fiber);
|
|
312
327
|
members.push({
|
|
313
328
|
kind: "render",
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/devtools-hook.ts","../src/fiber-walker.ts","../src/attribution.ts","../src/render-reason.ts","../src/render-collector.ts","../src/leak-collector.ts"],"sourcesContent":["export * from './types'\nexport { installDevToolsHook, uninstallDevToolsHook, onFiberUnmount } from './devtools-hook'\nexport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nexport { resolveComponentFromElement } from './attribution'\nexport { createRenderCollector } from './render-collector'\nexport { createLeakCollector } from './leak-collector'\n","import type { MinimalFiber, ReactDevToolsHook } from './types'\n\ntype CommitListener = (root: { current: MinimalFiber }, rendererId: number) => void\ntype UnmountListener = (fiber: MinimalFiber, rendererId: number) => void\n\nconst HOOK_KEY = '__REACT_DEVTOOLS_GLOBAL_HOOK__'\n\ninterface GlobalWithHook {\n [HOOK_KEY]?: ReactDevToolsHook\n}\n\ntype UnmountHandler = (rendererId: number, fiber: MinimalFiber) => void\n\nconst listeners = new Set<CommitListener>()\nconst unmountListeners = new Set<UnmountListener>()\nlet ourHook: ReactDevToolsHook | null = null\n// Our installed wrappers and the handlers they chain to. The chained handlers\n// are ALSO captured per-wrapper in closures below — wrappers must never read\n// this mutable module state, or an uninstall→reinstall cycle makes the old\n// wrapper chain to itself (infinite recursion on every commit). These module\n// copies exist only so uninstall can restore the originals onto the hook.\nlet ourCommitWrapper: ReactDevToolsHook['onCommitFiberRoot'] | null = null\nlet ourUnmountWrapper: UnmountHandler | null = null\nlet restoreCommit: ReactDevToolsHook['onCommitFiberRoot'] | null = null\nlet restoreUnmount: UnmountHandler | null = null\n\nfunction ensureHookInstalled(): void {\n const g = globalThis as GlobalWithHook\n // Already ours and still installed — nothing to do.\n if (ourHook && g[HOOK_KEY] === ourHook) return\n\n const existing = g[HOOK_KEY]\n const chainedOriginal =\n existing && existing !== ourHook && typeof existing.onCommitFiberRoot === 'function'\n ? existing.onCommitFiberRoot\n : null\n const existingUnmount =\n existing && existing !== ourHook\n ? (existing as { onCommitFiberUnmount?: unknown }).onCommitFiberUnmount\n : null\n const chainedUnmount =\n typeof existingUnmount === 'function' ? (existingUnmount as UnmountHandler) : null\n\n const hook: ReactDevToolsHook = existing && existing !== ourHook ? existing : {}\n // React's injectInternals() checks for supportsFiber and calls inject() to\n // get a renderer ID. Without these, React stores injectedHook = null and\n // never calls onCommitFiberRoot on subsequent commits.\n if (!hook.supportsFiber) {\n hook.supportsFiber = true\n }\n if (typeof hook.inject !== 'function') {\n let _nextRendererId = 1\n hook.inject = () => _nextRendererId++\n }\n // Additional fields that React Refresh (@vitejs/plugin-react) accesses on\n // the hook. Without these it throws \"Cannot read properties of undefined\n // (reading 'forEach')\" at injectIntoGlobalHook. We default each to a safe\n // empty/noop value; if the real React DevTools extension installs later\n // it will overwrite these.\n if (!hook.renderers) {\n ;(hook as Record<string, unknown>).renderers = new Map()\n }\n if (typeof (hook as Record<string, unknown>)['on'] !== 'function') {\n ;(hook as Record<string, unknown>)['on'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['off'] !== 'function') {\n ;(hook as Record<string, unknown>)['off'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['emit'] !== 'function') {\n ;(hook as Record<string, unknown>)['emit'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['sub'] !== 'function') {\n ;(hook as Record<string, unknown>)['sub'] = () => () => {}\n }\n if (typeof (hook as Record<string, unknown>)['checkDCE'] !== 'function') {\n ;(hook as Record<string, unknown>)['checkDCE'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onPostCommitFiberRoot'] !== 'function') {\n ;(hook as Record<string, unknown>)['onPostCommitFiberRoot'] = () => {}\n }\n hook.onCommitFiberRoot = (rendererId, root, priorityLevel) => {\n if (chainedOriginal) {\n try {\n chainedOriginal(rendererId, root, priorityLevel)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools hook threw:', err)\n }\n }\n for (const cb of listeners) {\n try {\n cb(root, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] commit listener threw:', err)\n }\n }\n }\n // React calls this for every fiber it unmounts (dev builds). We fan out to\n // unmount listeners (the leak collector) and chain any pre-existing handler\n // (e.g. the React DevTools extension), mirroring onCommitFiberRoot.\n ;(hook as { onCommitFiberUnmount?: UnmountHandler }).onCommitFiberUnmount =\n (rendererId, fiber) => {\n if (chainedUnmount) {\n try {\n chainedUnmount(rendererId, fiber)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools unmount hook threw:', err)\n }\n }\n for (const cb of unmountListeners) {\n try {\n cb(fiber, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] unmount listener threw:', err)\n }\n }\n }\n g[HOOK_KEY] = hook\n ourHook = hook\n ourCommitWrapper = hook.onCommitFiberRoot\n ourUnmountWrapper = (hook as { onCommitFiberUnmount?: UnmountHandler }).onCommitFiberUnmount ?? null\n restoreCommit = chainedOriginal\n restoreUnmount = chainedUnmount\n}\n\nexport function installDevToolsHook(listener: CommitListener): () => void {\n ensureHookInstalled()\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Subscribe to fiber unmounts. React invokes `onCommitFiberUnmount` for each\n * unmounted fiber in dev builds; the leak collector uses this to track which\n * component instances were torn down. Returns an unsubscribe function.\n */\nexport function onFiberUnmount(listener: UnmountListener): () => void {\n ensureHookInstalled()\n unmountListeners.add(listener)\n return () => {\n unmountListeners.delete(listener)\n }\n}\n\n/**\n * Clears all listeners, detaches our wrappers from the global hook, and\n * restores any pre-existing handlers we chained to (e.g. the React DevTools\n * extension). Does NOT remove the hook object from globalThis — react-dom\n * captured it at module load, so it must stay. Callers that want a fully\n * clean slate must also `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`.\n */\nexport function uninstallDevToolsHook(): void {\n listeners.clear()\n unmountListeners.clear()\n if (ourHook) {\n // Only restore if our wrapper is still the installed handler — someone\n // (e.g. a late-loading DevTools extension) may have replaced it since.\n if (ourHook.onCommitFiberRoot === ourCommitWrapper) {\n if (restoreCommit) ourHook.onCommitFiberRoot = restoreCommit\n else delete ourHook.onCommitFiberRoot\n }\n const h = ourHook as { onCommitFiberUnmount?: UnmountHandler }\n if (h.onCommitFiberUnmount === ourUnmountWrapper) {\n if (restoreUnmount) h.onCommitFiberUnmount = restoreUnmount\n else delete h.onCommitFiberUnmount\n }\n }\n ourHook = null\n ourCommitWrapper = null\n ourUnmountWrapper = null\n restoreCommit = null\n restoreUnmount = null\n}\n","import type { MinimalFiber } from './types'\n\nconst MEMO_TYPE = Symbol.for('react.memo')\nconst FORWARD_REF_TYPE = Symbol.for('react.forward_ref')\n\ntype WrapperType = {\n $$typeof: symbol\n type?: unknown\n render?: unknown\n}\n\nfunction namedFunctionName(value: unknown): string | null {\n if (typeof value !== 'function') return null\n const fn = value as { displayName?: string; name?: string }\n if (typeof fn.displayName === 'string' && fn.displayName.length > 0) return fn.displayName\n if (typeof fn.name === 'string' && fn.name.length > 0) return fn.name\n return null\n}\n\n/**\n * Return the component name for a fiber.\n *\n * Host components (DOM tags) return their tag string. Function and class\n * components return their displayName or name. Memo and forwardRef wrappers\n * are unwrapped to their inner component. Unknown shapes return null.\n */\nexport function fiberComponentName(fiber: MinimalFiber | null): string | null {\n if (!fiber) return null\n const type = fiber.type\n if (typeof type === 'string') return type\n if (typeof type === 'function') return namedFunctionName(type)\n if (type && typeof type === 'object') {\n const wrapper = type as WrapperType\n if (wrapper.$$typeof === MEMO_TYPE) {\n return namedFunctionName(wrapper.type)\n }\n if (wrapper.$$typeof === FORWARD_REF_TYPE) {\n return namedFunctionName(wrapper.render)\n }\n }\n return null\n}\n\ninterface WalkOptions {\n /**\n * Maximum number of fibers to visit. Prevents runaway traversal on very\n * deep trees. Default 10_000.\n */\n stopAt?: number\n /**\n * Predicate deciding whether to descend into a fiber's children. Returning\n * false prunes the entire subtree (the fiber itself is still visited). Used\n * to skip subtrees that did no work this commit. Defaults to always descend.\n */\n descend?: (fiber: MinimalFiber) => boolean\n}\n\n/**\n * Walk a fiber subtree depth-first, invoking `visit` for every fiber with its\n * depth relative to `root` (root is depth 0). Returns early once `stopAt`\n * fibers have been visited. When `descend` returns false for a fiber, its\n * children are skipped entirely.\n */\nexport function walkChangedFibers(\n root: MinimalFiber,\n visit: (fiber: MinimalFiber, depth: number) => void,\n opts: WalkOptions = {}\n): void {\n const max = opts.stopAt ?? 10_000\n const descend = opts.descend ?? (() => true)\n let count = 0\n let depth = 0\n let node: MinimalFiber | null = root\n while (node) {\n visit(node, depth)\n count++\n if (count >= max) return\n if (node.child && descend(node)) {\n node = node.child\n depth++\n continue\n }\n while (node && !node.sibling) {\n if (node === root) return\n node = node.return\n depth--\n }\n if (!node || node === root) return\n node = node.sibling\n }\n}\n","import type { MinimalFiber } from './types'\nimport { fiberComponentName } from './fiber-walker'\n\nconst FIBER_KEY_PATTERNS = ['__reactFiber$', '__reactInternalInstance$']\n\nfunction findFiberOnElement(el: HTMLElement): MinimalFiber | null {\n for (const key of Object.keys(el)) {\n for (const pattern of FIBER_KEY_PATTERNS) {\n if (key.startsWith(pattern)) {\n return (el as HTMLElement & Record<string, MinimalFiber>)[key] ?? null\n }\n }\n }\n return null\n}\n\n/**\n * Walk up from the fiber attached to `el` until we find one whose `type` is\n * a function or class component (not a host tag string). Returns that\n * component's display name. If no component is found, returns the host\n * tag name of the starting fiber. If no fiber is attached, returns null.\n */\nexport function resolveComponentFromElement(el: HTMLElement): string | null {\n const start = findFiberOnElement(el)\n if (!start) return null\n let node: MinimalFiber | null = start\n while (node) {\n if (typeof node.type === 'function' || (node.type && typeof node.type === 'object')) {\n const name = fiberComponentName(node)\n if (name) return name\n }\n node = node.return\n }\n // Nothing but host fibers above — return the host tag name of the starting fiber.\n return fiberComponentName(start)\n}\n","import type { MinimalFiber } from './types'\n\n/** React's `PerformedWork` flag — set on a fiber whose render actually ran. */\nexport const PERFORMED_WORK = 0b1\n\nexport type RenderReason = 'mount' | 'state' | 'props' | 'parent'\n\nexport interface RenderReasonResult {\n reason: RenderReason\n changedProps?: string[]\n}\n\nexport function didPerformWork(fiber: MinimalFiber | null): boolean {\n if (!fiber || typeof fiber.flags !== 'number') return false\n return (fiber.flags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Should the walk descend into this fiber's children when hunting for renders?\n *\n * React bubbles the `PerformedWork` bit of every descendant into a fiber's\n * `subtreeFlags`, and (unlike `flags`) resets it whenever the reconciler\n * re-clones the fiber. So a numeric `subtreeFlags` without the bit reliably\n * means \"no descendant rendered this commit\" — we can stop here and avoid\n * stale leaves whose own `flags` never got cleared.\n *\n * When `subtreeFlags` is absent (React < 18) we cannot prune safely, so we\n * descend and fall back to the full-tree walk.\n */\nexport function subtreeMightHaveRendered(fiber: MinimalFiber): boolean {\n if (typeof fiber.subtreeFlags !== 'number') return true\n return (fiber.subtreeFlags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Walk up `.return` to the nearest *component* fiber, skipping host (DOM)\n * fibers. A component's immediate `.return` is usually a host fiber (the\n * `<div>` it's nested in), not its parent component — so to ask \"did my\n * parent component render?\" we have to climb past the host nodes first.\n */\nexport function nearestComponentAncestor(fiber: MinimalFiber): MinimalFiber | null {\n let p = fiber.return\n while (p) {\n // Function/class components have function types; forwardRef and\n // non-simple memo wrappers have object types — all are components\n // (host fibers are strings). Mirrors the check in attribution.ts.\n if (typeof p.type === 'function' || (p.type && typeof p.type === 'object')) return p\n p = p.return\n }\n return null\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n\n/** Shallow diff of two prop bags. Returns the keys whose values differ. */\nexport function changedPropKeys(prev: unknown, next: unknown): string[] {\n if (prev === next) return []\n const p = asObject(prev)\n const n = asObject(next)\n const keys = new Set([...Object.keys(p), ...Object.keys(n)])\n const changed: string[] = []\n for (const k of keys) {\n if (p[k] !== n[k]) changed.push(k)\n }\n return changed\n}\n\n/**\n * Classify WHY a fiber that performed work this commit rendered.\n *\n * - `mount` — first render (no previous fiber).\n * - `props` — at least one prop changed since the previous render.\n * - `state` — props are identical AND the parent did not render, so the\n * update originated here (own state/force update). This is the\n * root of a render cascade.\n * - `parent` — props are identical but the parent rendered, so this only\n * re-rendered because of its parent. This is the avoidable\n * \"wrap me in memo\" case the developer is hunting for.\n *\n * Assumes the caller only invokes this for fibers that actually performed\n * work (see `didPerformWork`).\n */\nexport function classifyRenderReason(fiber: MinimalFiber): RenderReasonResult {\n if (!fiber.alternate) return { reason: 'mount' }\n const changed = changedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps)\n if (changed.length > 0) return { reason: 'props', changedProps: changed }\n if (didPerformWork(nearestComponentAncestor(fiber))) return { reason: 'parent' }\n return { reason: 'state' }\n}\n","import type { Collector, RenderSignal, Signal } from '@react-perfscope/core'\nimport { installDevToolsHook } from './devtools-hook'\nimport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nimport { classifyRenderReason, didPerformWork, subtreeMightHaveRendered } from './render-reason'\nimport type { MinimalFiber } from './types'\n\n/** The cascade root of a commit: the component whose own state/mount started\n * it. Falls back to the shallowest member. */\nfunction cascadeRoot(members: RenderSignal[]): RenderSignal {\n let shallowest = members[0]!\n for (const m of members) {\n if (m.reason === 'state') return m\n if (m.depth < shallowest.depth) shallowest = m\n }\n return members.find((m) => m.reason === 'mount') ?? shallowest\n}\n\nexport function createRenderCollector(): Collector {\n let active = false\n let emit: (signal: Signal) => void = () => {}\n let unsubscribe: (() => void) | null = null\n let commitId = 0\n\n function onCommit(root: { current: MinimalFiber }) {\n if (!active) return\n const at = performance.now()\n const id = commitId++\n const members: RenderSignal[] = []\n walkChangedFibers(\n root.current,\n (fiber, depth) => {\n // Skip host (DOM) fibers — we only want function/class components in\n // render reports.\n if (typeof fiber.type === 'string') return\n // Only report fibers that actually re-ran their render this commit.\n // A bailed-out fiber (e.g. memo with equal props) keeps its place in\n // the tree but did no work, so reporting it would be noise.\n if (!didPerformWork(fiber)) return\n const name = fiberComponentName(fiber)\n if (!name) return\n const duration = typeof fiber.actualDuration === 'number' ? fiber.actualDuration : 0\n const { reason, changedProps } = classifyRenderReason(fiber)\n members.push({\n kind: 'render',\n at,\n component: name,\n reason,\n duration,\n commitId: id,\n depth,\n ...(changedProps ? { changedProps } : {}),\n })\n },\n // Prune subtrees that did no work this commit. Without this, a leaf that\n // mounted long ago keeps its stale PerformedWork flag and gets reported\n // as a phantom render on every unrelated commit.\n { descend: subtreeMightHaveRendered }\n )\n if (members.length === 0) return\n // One coalesced signal per commit. The root describes the cascade; the\n // duration is the commit's total render time.\n const root2 = cascadeRoot(members)\n // fiber.actualDuration is inclusive of descendants, so summing every member\n // double-counts nested render time. Members arrive in pre-order with depth,\n // so sum only the \"forest roots\" — members with no rendered ancestor — whose\n // inclusive durations already cover their subtrees. This matches what React's\n // own Profiler reports for the commit.\n let total = 0\n const ancestorDepths: number[] = []\n for (const m of members) {\n while (ancestorDepths.length > 0 && ancestorDepths[ancestorDepths.length - 1]! >= m.depth) {\n ancestorDepths.pop()\n }\n if (ancestorDepths.length === 0) total += m.duration\n ancestorDepths.push(m.depth)\n }\n emit({\n kind: 'render',\n at,\n component: root2.component,\n reason: root2.reason,\n duration: total,\n commitId: id,\n depth: root2.depth,\n ...(root2.changedProps ? { changedProps: root2.changedProps } : {}),\n members,\n count: members.length,\n })\n }\n\n return {\n kind: 'render',\n activate(emitFn) {\n emit = emitFn\n active = true\n if (unsubscribe) return\n unsubscribe = installDevToolsHook(onCommit)\n },\n deactivate() {\n active = false\n // Keep the global hook listener attached — installDevToolsHook is\n // idempotent and removing it on every deactivate would cost us the\n // ability to re-attach cleanly under the test reset hooks. The\n // `active` flag gates emission.\n },\n }\n}\n","import type { Collector, LeakSample, LeakSuspect, RecordingResult } from '@react-perfscope/core'\nimport { analyzeLeakTrend } from '@react-perfscope/core'\nimport { onFiberUnmount } from './devtools-hook'\nimport { fiberComponentName } from './fiber-walker'\nimport type { MinimalFiber } from './types'\n\n/** How often to sample per-component retained counts. Matches the heap sampler\n * — cheap (reading a small map) and fine-grained enough to resolve the floor. */\nconst SAMPLE_INTERVAL_MS = 250\n/** Per-component cap on retained-count samples. */\nconst MAX_SAMPLES = 4000\n\nexport interface LeakCollector extends Collector {\n finalize(result: RecordingResult): Promise<RecordingResult>\n}\n\nfunction maybeGc(): void {\n // Only present when Chrome is launched with --js-flags=--expose-gc. When\n // available, a nudge makes the final retained count accurate instead of\n // upper-bounded by GC lag. A no-op otherwise — the trend over the recording\n // is the real signal.\n const gc = (globalThis as { gc?: () => void }).gc\n if (typeof gc === 'function') {\n try {\n gc()\n } catch {\n /* ignore */\n }\n }\n}\n\n/**\n * Detects component-level memory leaks: components whose instances were\n * unmounted but stayed retained (not garbage-collected), with a retained count\n * that climbed across the recording.\n *\n * Mechanism (all in-page, no DevTools protocol): React calls\n * `onCommitFiberUnmount` for each unmounted fiber. For component fibers we\n * register the fiber in a FinalizationRegistry (keyed by component name) and\n * bump an `unmounted` counter; when the fiber is later collected the registry\n * callback bumps a `collected` counter. `retained = unmounted − collected` is\n * sampled over time and fed to {@link analyzeLeakTrend}. A component whose\n * retained floor keeps rising is flagged.\n *\n * Limitation: identifies WHICH component leaks and HOW MANY instances, not the\n * retainer chain (who holds them) — that needs a heap snapshot, unavailable to\n * in-page JS. No-ops when FinalizationRegistry is unavailable.\n */\nexport function createLeakCollector(): LeakCollector {\n const unmounted = new Map<string, number>()\n const collected = new Map<string, number>()\n const series = new Map<string, LeakSample[]>()\n let registry: FinalizationRegistry<string> | null = null\n let unsubscribe: (() => void) | null = null\n let timer: ReturnType<typeof setInterval> | null = null\n let active = false\n\n function bump(map: Map<string, number>, key: string): void {\n map.set(key, (map.get(key) ?? 0) + 1)\n }\n\n function retainedFor(name: string): number {\n return (unmounted.get(name) ?? 0) - (collected.get(name) ?? 0)\n }\n\n function sample(): void {\n const at = performance.now()\n for (const name of unmounted.keys()) {\n let arr = series.get(name)\n if (!arr) {\n arr = []\n series.set(name, arr)\n }\n arr.push({ at, retained: retainedFor(name) })\n if (arr.length > MAX_SAMPLES) arr.splice(0, arr.length - MAX_SAMPLES)\n }\n }\n\n function onUnmount(fiber: MinimalFiber): void {\n if (!active) return\n // Only component fibers — host (DOM tag) fibers churn constantly and aren't\n // \"component leaks\". Anonymous components (no resolvable name) are skipped.\n if (typeof fiber.type === 'string') return\n const name = fiberComponentName(fiber)\n if (!name) return\n bump(unmounted, name)\n registry?.register(fiber as object, name)\n }\n\n return {\n kind: 'leak',\n activate() {\n // Re-activation without an intervening deactivate would overwrite (and\n // orphan) the running interval and the unmount subscription.\n if (active) return\n unmounted.clear()\n collected.clear()\n series.clear()\n if (typeof FinalizationRegistry === 'undefined') return // unsupported\n active = true\n registry = new FinalizationRegistry<string>((name) => {\n bump(collected, name)\n })\n unsubscribe = onFiberUnmount(onUnmount)\n timer = setInterval(sample, SAMPLE_INTERVAL_MS)\n },\n deactivate() {\n active = false\n if (timer != null) {\n clearInterval(timer)\n timer = null\n }\n if (unsubscribe) {\n unsubscribe()\n unsubscribe = null\n }\n sample() // final reading at stop\n },\n async finalize(result) {\n if (unmounted.size === 0) return result\n // Best-effort: collect now and let the registry's cleanup callbacks flush\n // so the final retained counts aren't inflated by GC lag.\n maybeGc()\n await new Promise((r) => setTimeout(r, 0))\n\n const suspects: LeakSuspect[] = []\n for (const name of unmounted.keys()) {\n const samples = series.get(name) ?? []\n const trend = analyzeLeakTrend(samples)\n const retained = retainedFor(name)\n if (trend?.leaking && retained > 0) {\n suspects.push({\n component: name,\n unmounted: unmounted.get(name) ?? 0,\n retained,\n retainedSlopePerMin: trend.slopePerMin,\n })\n }\n }\n if (suspects.length === 0) return result\n suspects.sort((a, b) => b.retained - a.retained)\n return { ...result, leakSuspects: suspects }\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/devtools-hook.ts","../src/fiber-walker.ts","../src/attribution.ts","../src/render-reason.ts","../src/render-collector.ts","../src/leak-collector.ts"],"sourcesContent":["export * from './types'\nexport { installDevToolsHook, uninstallDevToolsHook, onFiberUnmount } from './devtools-hook'\nexport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nexport { resolveComponentFromElement } from './attribution'\nexport { createRenderCollector } from './render-collector'\nexport { createLeakCollector } from './leak-collector'\n","import type { MinimalFiber, ReactDevToolsHook } from './types'\n\ntype CommitListener = (root: { current: MinimalFiber }, rendererId: number) => void\ntype UnmountListener = (fiber: MinimalFiber, rendererId: number) => void\n\nconst HOOK_KEY = '__REACT_DEVTOOLS_GLOBAL_HOOK__'\n\ninterface GlobalWithHook {\n [HOOK_KEY]?: ReactDevToolsHook\n}\n\ntype UnmountHandler = (rendererId: number, fiber: MinimalFiber) => void\n\nconst listeners = new Set<CommitListener>()\nconst unmountListeners = new Set<UnmountListener>()\nlet ourHook: ReactDevToolsHook | null = null\n// Our installed wrappers and the handlers they chain to. The chained handlers\n// are ALSO captured per-wrapper in closures below — wrappers must never read\n// this mutable module state, or an uninstall→reinstall cycle makes the old\n// wrapper chain to itself (infinite recursion on every commit). These module\n// copies exist only so uninstall can restore the originals onto the hook.\nlet ourCommitWrapper: ReactDevToolsHook['onCommitFiberRoot'] | null = null\nlet ourUnmountWrapper: UnmountHandler | null = null\nlet restoreCommit: ReactDevToolsHook['onCommitFiberRoot'] | null = null\nlet restoreUnmount: UnmountHandler | null = null\n\nfunction ensureHookInstalled(): void {\n const g = globalThis as GlobalWithHook\n // Already ours and still installed — nothing to do.\n if (ourHook && g[HOOK_KEY] === ourHook) return\n\n const existing = g[HOOK_KEY]\n const chainedOriginal =\n existing && existing !== ourHook && typeof existing.onCommitFiberRoot === 'function'\n ? existing.onCommitFiberRoot\n : null\n const existingUnmount =\n existing && existing !== ourHook\n ? (existing as { onCommitFiberUnmount?: unknown }).onCommitFiberUnmount\n : null\n const chainedUnmount =\n typeof existingUnmount === 'function' ? (existingUnmount as UnmountHandler) : null\n\n const hook: ReactDevToolsHook = existing && existing !== ourHook ? existing : {}\n // React's injectInternals() checks for supportsFiber and calls inject() to\n // get a renderer ID. Without these, React stores injectedHook = null and\n // never calls onCommitFiberRoot on subsequent commits.\n if (!hook.supportsFiber) {\n hook.supportsFiber = true\n }\n if (typeof hook.inject !== 'function') {\n let _nextRendererId = 1\n hook.inject = () => _nextRendererId++\n }\n // Additional fields that React Refresh (@vitejs/plugin-react) accesses on\n // the hook. Without these it throws \"Cannot read properties of undefined\n // (reading 'forEach')\" at injectIntoGlobalHook. We default each to a safe\n // empty/noop value; if the real React DevTools extension installs later\n // it will overwrite these.\n if (!hook.renderers) {\n ;(hook as Record<string, unknown>).renderers = new Map()\n }\n if (typeof (hook as Record<string, unknown>)['on'] !== 'function') {\n ;(hook as Record<string, unknown>)['on'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['off'] !== 'function') {\n ;(hook as Record<string, unknown>)['off'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['emit'] !== 'function') {\n ;(hook as Record<string, unknown>)['emit'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['sub'] !== 'function') {\n ;(hook as Record<string, unknown>)['sub'] = () => () => {}\n }\n if (typeof (hook as Record<string, unknown>)['checkDCE'] !== 'function') {\n ;(hook as Record<string, unknown>)['checkDCE'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onPostCommitFiberRoot'] !== 'function') {\n ;(hook as Record<string, unknown>)['onPostCommitFiberRoot'] = () => {}\n }\n hook.onCommitFiberRoot = (rendererId, root, priorityLevel) => {\n if (chainedOriginal) {\n try {\n chainedOriginal(rendererId, root, priorityLevel)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools hook threw:', err)\n }\n }\n for (const cb of listeners) {\n try {\n cb(root, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] commit listener threw:', err)\n }\n }\n }\n // React calls this for every fiber it unmounts (dev builds). We fan out to\n // unmount listeners (the leak collector) and chain any pre-existing handler\n // (e.g. the React DevTools extension), mirroring onCommitFiberRoot.\n ;(hook as { onCommitFiberUnmount?: UnmountHandler }).onCommitFiberUnmount =\n (rendererId, fiber) => {\n if (chainedUnmount) {\n try {\n chainedUnmount(rendererId, fiber)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools unmount hook threw:', err)\n }\n }\n for (const cb of unmountListeners) {\n try {\n cb(fiber, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] unmount listener threw:', err)\n }\n }\n }\n // Only assign when we built a fresh hook. When we reused the existing hook\n // object (e.g. the React DevTools extension's), it was mutated in place —\n // and the extension installs the global as a getter-only, non-configurable\n // property, so assigning to it would throw a TypeError.\n if (hook !== existing) {\n try {\n g[HOOK_KEY] = hook\n } catch (err) {\n // Getter-only global that yielded no usable hook object: we cannot\n // install, and react-dom will never see our hook either. Fail soft.\n console.warn('[react-perfscope] could not install the DevTools hook:', err)\n return\n }\n }\n ourHook = hook\n ourCommitWrapper = hook.onCommitFiberRoot\n ourUnmountWrapper = (hook as { onCommitFiberUnmount?: UnmountHandler }).onCommitFiberUnmount ?? null\n restoreCommit = chainedOriginal\n restoreUnmount = chainedUnmount\n}\n\nexport function installDevToolsHook(listener: CommitListener): () => void {\n ensureHookInstalled()\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Subscribe to fiber unmounts. React invokes `onCommitFiberUnmount` for each\n * unmounted fiber in dev builds; the leak collector uses this to track which\n * component instances were torn down. Returns an unsubscribe function.\n */\nexport function onFiberUnmount(listener: UnmountListener): () => void {\n ensureHookInstalled()\n unmountListeners.add(listener)\n return () => {\n unmountListeners.delete(listener)\n }\n}\n\n/**\n * Clears all listeners, detaches our wrappers from the global hook, and\n * restores any pre-existing handlers we chained to (e.g. the React DevTools\n * extension). Does NOT remove the hook object from globalThis — react-dom\n * captured it at module load, so it must stay. Callers that want a fully\n * clean slate must also `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`\n * (impossible when the React DevTools extension installed it — the extension\n * defines the property non-configurable).\n */\nexport function uninstallDevToolsHook(): void {\n listeners.clear()\n unmountListeners.clear()\n if (ourHook) {\n // Only restore if our wrapper is still the installed handler — someone\n // (e.g. a late-loading DevTools extension) may have replaced it since.\n if (ourHook.onCommitFiberRoot === ourCommitWrapper) {\n if (restoreCommit) ourHook.onCommitFiberRoot = restoreCommit\n else delete ourHook.onCommitFiberRoot\n }\n const h = ourHook as { onCommitFiberUnmount?: UnmountHandler }\n if (h.onCommitFiberUnmount === ourUnmountWrapper) {\n if (restoreUnmount) h.onCommitFiberUnmount = restoreUnmount\n else delete h.onCommitFiberUnmount\n }\n }\n ourHook = null\n ourCommitWrapper = null\n ourUnmountWrapper = null\n restoreCommit = null\n restoreUnmount = null\n}\n","import type { MinimalFiber } from './types'\n\nconst MEMO_TYPE = Symbol.for('react.memo')\nconst FORWARD_REF_TYPE = Symbol.for('react.forward_ref')\n\ntype WrapperType = {\n $$typeof: symbol\n type?: unknown\n render?: unknown\n}\n\nfunction namedFunctionName(value: unknown): string | null {\n if (typeof value !== 'function') return null\n const fn = value as { displayName?: string; name?: string }\n if (typeof fn.displayName === 'string' && fn.displayName.length > 0) return fn.displayName\n if (typeof fn.name === 'string' && fn.name.length > 0) return fn.name\n return null\n}\n\n/**\n * Return the component name for a fiber.\n *\n * Host components (DOM tags) return their tag string. Function and class\n * components return their displayName or name. Memo and forwardRef wrappers\n * are unwrapped to their inner component. Unknown shapes return null.\n */\nexport function fiberComponentName(fiber: MinimalFiber | null): string | null {\n if (!fiber) return null\n const type = fiber.type\n if (typeof type === 'string') return type\n if (typeof type === 'function') return namedFunctionName(type)\n if (type && typeof type === 'object') {\n const wrapper = type as WrapperType\n if (wrapper.$$typeof === MEMO_TYPE) {\n return namedFunctionName(wrapper.type)\n }\n if (wrapper.$$typeof === FORWARD_REF_TYPE) {\n return namedFunctionName(wrapper.render)\n }\n }\n return null\n}\n\ninterface WalkOptions {\n /**\n * Maximum number of fibers to visit. Prevents runaway traversal on very\n * deep trees. Default 10_000.\n */\n stopAt?: number\n /**\n * Predicate deciding whether to descend into a fiber's children. Returning\n * false prunes the entire subtree (the fiber itself is still visited). Used\n * to skip subtrees that did no work this commit. Defaults to always descend.\n */\n descend?: (fiber: MinimalFiber) => boolean\n}\n\n/**\n * Walk a fiber subtree depth-first, invoking `visit` for every fiber with its\n * depth relative to `root` (root is depth 0). Returns early once `stopAt`\n * fibers have been visited. When `descend` returns false for a fiber, its\n * children are skipped entirely.\n */\nexport function walkChangedFibers(\n root: MinimalFiber,\n visit: (fiber: MinimalFiber, depth: number) => void,\n opts: WalkOptions = {}\n): void {\n const max = opts.stopAt ?? 10_000\n const descend = opts.descend ?? (() => true)\n let count = 0\n let depth = 0\n let node: MinimalFiber | null = root\n while (node) {\n visit(node, depth)\n count++\n if (count >= max) return\n if (node.child && descend(node)) {\n node = node.child\n depth++\n continue\n }\n while (node && !node.sibling) {\n if (node === root) return\n node = node.return\n depth--\n }\n if (!node || node === root) return\n node = node.sibling\n }\n}\n","import type { MinimalFiber } from './types'\nimport { fiberComponentName } from './fiber-walker'\n\nconst FIBER_KEY_PATTERNS = ['__reactFiber$', '__reactInternalInstance$']\n\nfunction findFiberOnElement(el: HTMLElement): MinimalFiber | null {\n for (const key of Object.keys(el)) {\n for (const pattern of FIBER_KEY_PATTERNS) {\n if (key.startsWith(pattern)) {\n return (el as HTMLElement & Record<string, MinimalFiber>)[key] ?? null\n }\n }\n }\n return null\n}\n\n/**\n * Walk up from the fiber attached to `el` until we find one whose `type` is\n * a function or class component (not a host tag string). Returns that\n * component's display name. If no component is found, returns the host\n * tag name of the starting fiber. If no fiber is attached, returns null.\n */\nexport function resolveComponentFromElement(el: HTMLElement): string | null {\n const start = findFiberOnElement(el)\n if (!start) return null\n let node: MinimalFiber | null = start\n while (node) {\n if (typeof node.type === 'function' || (node.type && typeof node.type === 'object')) {\n const name = fiberComponentName(node)\n if (name) return name\n }\n node = node.return\n }\n // Nothing but host fibers above — return the host tag name of the starting fiber.\n return fiberComponentName(start)\n}\n","import type { MinimalFiber } from './types'\n\n/** React's `PerformedWork` flag — set on a fiber whose render actually ran. */\nexport const PERFORMED_WORK = 0b1\n\nexport type RenderReason = 'mount' | 'state' | 'props' | 'parent'\n\nexport interface RenderReasonResult {\n reason: RenderReason\n changedProps?: string[]\n}\n\nexport function didPerformWork(fiber: MinimalFiber | null): boolean {\n if (!fiber || typeof fiber.flags !== 'number') return false\n return (fiber.flags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Should the walk descend into this fiber's children when hunting for renders?\n *\n * React bubbles the `PerformedWork` bit of every descendant into a fiber's\n * `subtreeFlags`, and (unlike `flags`) resets it whenever the reconciler\n * re-clones the fiber. So a numeric `subtreeFlags` without the bit reliably\n * means \"no descendant rendered this commit\" — we can stop here and avoid\n * stale leaves whose own `flags` never got cleared.\n *\n * When `subtreeFlags` is absent (React < 18) we cannot prune safely, so we\n * descend and fall back to the full-tree walk.\n */\nexport function subtreeMightHaveRendered(fiber: MinimalFiber): boolean {\n if (typeof fiber.subtreeFlags !== 'number') return true\n return (fiber.subtreeFlags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Walk up `.return` to the nearest *component* fiber, skipping host (DOM)\n * fibers. A component's immediate `.return` is usually a host fiber (the\n * `<div>` it's nested in), not its parent component — so to ask \"did my\n * parent component render?\" we have to climb past the host nodes first.\n */\nexport function nearestComponentAncestor(fiber: MinimalFiber): MinimalFiber | null {\n let p = fiber.return\n while (p) {\n // Function/class components have function types; forwardRef and\n // non-simple memo wrappers have object types — all are components\n // (host fibers are strings). Mirrors the check in attribution.ts.\n if (typeof p.type === 'function' || (p.type && typeof p.type === 'object')) return p\n p = p.return\n }\n return null\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n\n/** Shallow diff of two prop bags. Returns the keys whose values differ. */\nexport function changedPropKeys(prev: unknown, next: unknown): string[] {\n if (prev === next) return []\n const p = asObject(prev)\n const n = asObject(next)\n const keys = new Set([...Object.keys(p), ...Object.keys(n)])\n const changed: string[] = []\n for (const k of keys) {\n if (p[k] !== n[k]) changed.push(k)\n }\n return changed\n}\n\n/**\n * Classify WHY a fiber that performed work this commit rendered.\n *\n * - `mount` — first render (no previous fiber).\n * - `props` — at least one prop changed since the previous render.\n * - `state` — props are identical AND the parent did not render, so the\n * update originated here (own state/force update). This is the\n * root of a render cascade.\n * - `parent` — props are identical but the parent rendered, so this only\n * re-rendered because of its parent. This is the avoidable\n * \"wrap me in memo\" case the developer is hunting for.\n *\n * Assumes the caller only invokes this for fibers that actually performed\n * work (see `didPerformWork`).\n */\nexport function classifyRenderReason(fiber: MinimalFiber): RenderReasonResult {\n if (!fiber.alternate) return { reason: 'mount' }\n const changed = changedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps)\n if (changed.length > 0) return { reason: 'props', changedProps: changed }\n if (didPerformWork(nearestComponentAncestor(fiber))) return { reason: 'parent' }\n return { reason: 'state' }\n}\n","import type { Collector, RenderSignal, Signal } from '@react-perfscope/core'\nimport { installDevToolsHook } from './devtools-hook'\nimport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nimport { classifyRenderReason, didPerformWork, subtreeMightHaveRendered } from './render-reason'\nimport type { MinimalFiber } from './types'\n\n/** The cascade root of a commit: the component whose own state/mount started\n * it. Falls back to the shallowest member. */\nfunction cascadeRoot(members: RenderSignal[]): RenderSignal {\n let shallowest = members[0]!\n for (const m of members) {\n if (m.reason === 'state') return m\n if (m.depth < shallowest.depth) shallowest = m\n }\n return members.find((m) => m.reason === 'mount') ?? shallowest\n}\n\nexport function createRenderCollector(): Collector {\n let active = false\n let emit: (signal: Signal) => void = () => {}\n let unsubscribe: (() => void) | null = null\n let commitId = 0\n let warnedNoDurations = false\n\n function onCommit(root: { current: MinimalFiber }) {\n if (!active) return\n const at = performance.now()\n const id = commitId++\n const members: RenderSignal[] = []\n walkChangedFibers(\n root.current,\n (fiber, depth) => {\n // Skip host (DOM) fibers — we only want function/class components in\n // render reports.\n if (typeof fiber.type === 'string') return\n // Only report fibers that actually re-ran their render this commit.\n // A bailed-out fiber (e.g. memo with equal props) keeps its place in\n // the tree but did no work, so reporting it would be noise.\n if (!didPerformWork(fiber)) return\n const name = fiberComponentName(fiber)\n if (!name) return\n const hasDuration = typeof fiber.actualDuration === 'number'\n if (!hasDuration && !warnedNoDurations) {\n // React only records actualDuration when the root was created in\n // ProfileMode: the DevTools hook must exist before createRoot(),\n // and production react-dom (non-profiling build) never records it.\n // Without this warning every render reports a believable-looking\n // 0ms.\n warnedNoDurations = true\n console.warn(\n '[react-perfscope] render durations unavailable — React did not profile this root. ' +\n 'Make sure react-perfscope loads before react-dom (import it first, or use the ' +\n 'Vite/webpack plugin), and use a development or profiling build of react-dom. ' +\n 'Renders will be reported with duration 0.'\n )\n }\n const duration = hasDuration ? (fiber.actualDuration as number) : 0\n const { reason, changedProps } = classifyRenderReason(fiber)\n members.push({\n kind: 'render',\n at,\n component: name,\n reason,\n duration,\n commitId: id,\n depth,\n ...(changedProps ? { changedProps } : {}),\n })\n },\n // Prune subtrees that did no work this commit. Without this, a leaf that\n // mounted long ago keeps its stale PerformedWork flag and gets reported\n // as a phantom render on every unrelated commit.\n { descend: subtreeMightHaveRendered }\n )\n if (members.length === 0) return\n // One coalesced signal per commit. The root describes the cascade; the\n // duration is the commit's total render time.\n const root2 = cascadeRoot(members)\n // fiber.actualDuration is inclusive of descendants, so summing every member\n // double-counts nested render time. Members arrive in pre-order with depth,\n // so sum only the \"forest roots\" — members with no rendered ancestor — whose\n // inclusive durations already cover their subtrees. This matches what React's\n // own Profiler reports for the commit.\n let total = 0\n const ancestorDepths: number[] = []\n for (const m of members) {\n while (ancestorDepths.length > 0 && ancestorDepths[ancestorDepths.length - 1]! >= m.depth) {\n ancestorDepths.pop()\n }\n if (ancestorDepths.length === 0) total += m.duration\n ancestorDepths.push(m.depth)\n }\n emit({\n kind: 'render',\n at,\n component: root2.component,\n reason: root2.reason,\n duration: total,\n commitId: id,\n depth: root2.depth,\n ...(root2.changedProps ? { changedProps: root2.changedProps } : {}),\n members,\n count: members.length,\n })\n }\n\n return {\n kind: 'render',\n activate(emitFn) {\n emit = emitFn\n active = true\n if (unsubscribe) return\n unsubscribe = installDevToolsHook(onCommit)\n },\n deactivate() {\n active = false\n // Keep the global hook listener attached — installDevToolsHook is\n // idempotent and removing it on every deactivate would cost us the\n // ability to re-attach cleanly under the test reset hooks. The\n // `active` flag gates emission.\n },\n }\n}\n","import type { Collector, LeakSample, LeakSuspect, RecordingResult } from '@react-perfscope/core'\nimport { analyzeLeakTrend } from '@react-perfscope/core'\nimport { onFiberUnmount } from './devtools-hook'\nimport { fiberComponentName } from './fiber-walker'\nimport type { MinimalFiber } from './types'\n\n/** How often to sample per-component retained counts. Matches the heap sampler\n * — cheap (reading a small map) and fine-grained enough to resolve the floor. */\nconst SAMPLE_INTERVAL_MS = 250\n/** Per-component cap on retained-count samples. */\nconst MAX_SAMPLES = 4000\n\nexport interface LeakCollector extends Collector {\n finalize(result: RecordingResult): Promise<RecordingResult>\n}\n\nfunction maybeGc(): void {\n // Only present when Chrome is launched with --js-flags=--expose-gc. When\n // available, a nudge makes the final retained count accurate instead of\n // upper-bounded by GC lag. A no-op otherwise — the trend over the recording\n // is the real signal.\n const gc = (globalThis as { gc?: () => void }).gc\n if (typeof gc === 'function') {\n try {\n gc()\n } catch {\n /* ignore */\n }\n }\n}\n\n/**\n * Detects component-level memory leaks: components whose instances were\n * unmounted but stayed retained (not garbage-collected), with a retained count\n * that climbed across the recording.\n *\n * Mechanism (all in-page, no DevTools protocol): React calls\n * `onCommitFiberUnmount` for each unmounted fiber. For component fibers we\n * register the fiber in a FinalizationRegistry (keyed by component name) and\n * bump an `unmounted` counter; when the fiber is later collected the registry\n * callback bumps a `collected` counter. `retained = unmounted − collected` is\n * sampled over time and fed to {@link analyzeLeakTrend}. A component whose\n * retained floor keeps rising is flagged.\n *\n * Limitation: identifies WHICH component leaks and HOW MANY instances, not the\n * retainer chain (who holds them) — that needs a heap snapshot, unavailable to\n * in-page JS. No-ops when FinalizationRegistry is unavailable.\n */\nexport function createLeakCollector(): LeakCollector {\n const unmounted = new Map<string, number>()\n const collected = new Map<string, number>()\n const series = new Map<string, LeakSample[]>()\n let registry: FinalizationRegistry<string> | null = null\n let unsubscribe: (() => void) | null = null\n let timer: ReturnType<typeof setInterval> | null = null\n let active = false\n\n function bump(map: Map<string, number>, key: string): void {\n map.set(key, (map.get(key) ?? 0) + 1)\n }\n\n function retainedFor(name: string): number {\n return (unmounted.get(name) ?? 0) - (collected.get(name) ?? 0)\n }\n\n function sample(): void {\n const at = performance.now()\n for (const name of unmounted.keys()) {\n let arr = series.get(name)\n if (!arr) {\n arr = []\n series.set(name, arr)\n }\n arr.push({ at, retained: retainedFor(name) })\n if (arr.length > MAX_SAMPLES) arr.splice(0, arr.length - MAX_SAMPLES)\n }\n }\n\n function onUnmount(fiber: MinimalFiber): void {\n if (!active) return\n // Only component fibers — host (DOM tag) fibers churn constantly and aren't\n // \"component leaks\". Anonymous components (no resolvable name) are skipped.\n if (typeof fiber.type === 'string') return\n const name = fiberComponentName(fiber)\n if (!name) return\n bump(unmounted, name)\n registry?.register(fiber as object, name)\n }\n\n return {\n kind: 'leak',\n activate() {\n // Re-activation without an intervening deactivate would overwrite (and\n // orphan) the running interval and the unmount subscription.\n if (active) return\n unmounted.clear()\n collected.clear()\n series.clear()\n if (typeof FinalizationRegistry === 'undefined') return // unsupported\n active = true\n registry = new FinalizationRegistry<string>((name) => {\n bump(collected, name)\n })\n unsubscribe = onFiberUnmount(onUnmount)\n timer = setInterval(sample, SAMPLE_INTERVAL_MS)\n },\n deactivate() {\n active = false\n if (timer != null) {\n clearInterval(timer)\n timer = null\n }\n if (unsubscribe) {\n unsubscribe()\n unsubscribe = null\n }\n sample() // final reading at stop\n },\n async finalize(result) {\n if (unmounted.size === 0) return result\n // Best-effort: collect now and let the registry's cleanup callbacks flush\n // so the final retained counts aren't inflated by GC lag.\n maybeGc()\n await new Promise((r) => setTimeout(r, 0))\n\n const suspects: LeakSuspect[] = []\n for (const name of unmounted.keys()) {\n const samples = series.get(name) ?? []\n const trend = analyzeLeakTrend(samples)\n const retained = retainedFor(name)\n if (trend?.leaking && retained > 0) {\n suspects.push({\n component: name,\n unmounted: unmounted.get(name) ?? 0,\n retained,\n retainedSlopePerMin: trend.slopePerMin,\n })\n }\n }\n if (suspects.length === 0) return result\n suspects.sort((a, b) => b.retained - a.retained)\n return { ...result, leakSuspects: suspects }\n },\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -96,7 +96,9 @@ declare function onFiberUnmount(listener: UnmountListener): () => void;
|
|
|
96
96
|
* restores any pre-existing handlers we chained to (e.g. the React DevTools
|
|
97
97
|
* extension). Does NOT remove the hook object from globalThis — react-dom
|
|
98
98
|
* captured it at module load, so it must stay. Callers that want a fully
|
|
99
|
-
* clean slate must also `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__
|
|
99
|
+
* clean slate must also `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`
|
|
100
|
+
* (impossible when the React DevTools extension installed it — the extension
|
|
101
|
+
* defines the property non-configurable).
|
|
100
102
|
*/
|
|
101
103
|
declare function uninstallDevToolsHook(): void;
|
|
102
104
|
|
package/dist/index.d.ts
CHANGED
|
@@ -96,7 +96,9 @@ declare function onFiberUnmount(listener: UnmountListener): () => void;
|
|
|
96
96
|
* restores any pre-existing handlers we chained to (e.g. the React DevTools
|
|
97
97
|
* extension). Does NOT remove the hook object from globalThis — react-dom
|
|
98
98
|
* captured it at module load, so it must stay. Callers that want a fully
|
|
99
|
-
* clean slate must also `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__
|
|
99
|
+
* clean slate must also `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`
|
|
100
|
+
* (impossible when the React DevTools extension installed it — the extension
|
|
101
|
+
* defines the property non-configurable).
|
|
100
102
|
*/
|
|
101
103
|
declare function uninstallDevToolsHook(): void;
|
|
102
104
|
|
package/dist/index.js
CHANGED
|
@@ -88,7 +88,14 @@ function ensureHookInstalled() {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
};
|
|
91
|
-
|
|
91
|
+
if (hook !== existing) {
|
|
92
|
+
try {
|
|
93
|
+
g[HOOK_KEY] = hook;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.warn("[react-perfscope] could not install the DevTools hook:", err);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
92
99
|
ourHook = hook;
|
|
93
100
|
ourCommitWrapper = hook.onCommitFiberRoot;
|
|
94
101
|
ourUnmountWrapper = hook.onCommitFiberUnmount ?? null;
|
|
@@ -262,6 +269,7 @@ function createRenderCollector() {
|
|
|
262
269
|
};
|
|
263
270
|
let unsubscribe = null;
|
|
264
271
|
let commitId = 0;
|
|
272
|
+
let warnedNoDurations = false;
|
|
265
273
|
function onCommit(root) {
|
|
266
274
|
if (!active) return;
|
|
267
275
|
const at = performance.now();
|
|
@@ -274,7 +282,14 @@ function createRenderCollector() {
|
|
|
274
282
|
if (!didPerformWork(fiber)) return;
|
|
275
283
|
const name = fiberComponentName(fiber);
|
|
276
284
|
if (!name) return;
|
|
277
|
-
const
|
|
285
|
+
const hasDuration = typeof fiber.actualDuration === "number";
|
|
286
|
+
if (!hasDuration && !warnedNoDurations) {
|
|
287
|
+
warnedNoDurations = true;
|
|
288
|
+
console.warn(
|
|
289
|
+
"[react-perfscope] render durations unavailable \u2014 React did not profile this root. Make sure react-perfscope loads before react-dom (import it first, or use the Vite/webpack plugin), and use a development or profiling build of react-dom. Renders will be reported with duration 0."
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const duration = hasDuration ? fiber.actualDuration : 0;
|
|
278
293
|
const { reason, changedProps } = classifyRenderReason(fiber);
|
|
279
294
|
members.push({
|
|
280
295
|
kind: "render",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/devtools-hook.ts","../src/fiber-walker.ts","../src/attribution.ts","../src/render-reason.ts","../src/render-collector.ts","../src/leak-collector.ts"],"sourcesContent":["import type { MinimalFiber, ReactDevToolsHook } from './types'\n\ntype CommitListener = (root: { current: MinimalFiber }, rendererId: number) => void\ntype UnmountListener = (fiber: MinimalFiber, rendererId: number) => void\n\nconst HOOK_KEY = '__REACT_DEVTOOLS_GLOBAL_HOOK__'\n\ninterface GlobalWithHook {\n [HOOK_KEY]?: ReactDevToolsHook\n}\n\ntype UnmountHandler = (rendererId: number, fiber: MinimalFiber) => void\n\nconst listeners = new Set<CommitListener>()\nconst unmountListeners = new Set<UnmountListener>()\nlet ourHook: ReactDevToolsHook | null = null\n// Our installed wrappers and the handlers they chain to. The chained handlers\n// are ALSO captured per-wrapper in closures below — wrappers must never read\n// this mutable module state, or an uninstall→reinstall cycle makes the old\n// wrapper chain to itself (infinite recursion on every commit). These module\n// copies exist only so uninstall can restore the originals onto the hook.\nlet ourCommitWrapper: ReactDevToolsHook['onCommitFiberRoot'] | null = null\nlet ourUnmountWrapper: UnmountHandler | null = null\nlet restoreCommit: ReactDevToolsHook['onCommitFiberRoot'] | null = null\nlet restoreUnmount: UnmountHandler | null = null\n\nfunction ensureHookInstalled(): void {\n const g = globalThis as GlobalWithHook\n // Already ours and still installed — nothing to do.\n if (ourHook && g[HOOK_KEY] === ourHook) return\n\n const existing = g[HOOK_KEY]\n const chainedOriginal =\n existing && existing !== ourHook && typeof existing.onCommitFiberRoot === 'function'\n ? existing.onCommitFiberRoot\n : null\n const existingUnmount =\n existing && existing !== ourHook\n ? (existing as { onCommitFiberUnmount?: unknown }).onCommitFiberUnmount\n : null\n const chainedUnmount =\n typeof existingUnmount === 'function' ? (existingUnmount as UnmountHandler) : null\n\n const hook: ReactDevToolsHook = existing && existing !== ourHook ? existing : {}\n // React's injectInternals() checks for supportsFiber and calls inject() to\n // get a renderer ID. Without these, React stores injectedHook = null and\n // never calls onCommitFiberRoot on subsequent commits.\n if (!hook.supportsFiber) {\n hook.supportsFiber = true\n }\n if (typeof hook.inject !== 'function') {\n let _nextRendererId = 1\n hook.inject = () => _nextRendererId++\n }\n // Additional fields that React Refresh (@vitejs/plugin-react) accesses on\n // the hook. Without these it throws \"Cannot read properties of undefined\n // (reading 'forEach')\" at injectIntoGlobalHook. We default each to a safe\n // empty/noop value; if the real React DevTools extension installs later\n // it will overwrite these.\n if (!hook.renderers) {\n ;(hook as Record<string, unknown>).renderers = new Map()\n }\n if (typeof (hook as Record<string, unknown>)['on'] !== 'function') {\n ;(hook as Record<string, unknown>)['on'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['off'] !== 'function') {\n ;(hook as Record<string, unknown>)['off'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['emit'] !== 'function') {\n ;(hook as Record<string, unknown>)['emit'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['sub'] !== 'function') {\n ;(hook as Record<string, unknown>)['sub'] = () => () => {}\n }\n if (typeof (hook as Record<string, unknown>)['checkDCE'] !== 'function') {\n ;(hook as Record<string, unknown>)['checkDCE'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onPostCommitFiberRoot'] !== 'function') {\n ;(hook as Record<string, unknown>)['onPostCommitFiberRoot'] = () => {}\n }\n hook.onCommitFiberRoot = (rendererId, root, priorityLevel) => {\n if (chainedOriginal) {\n try {\n chainedOriginal(rendererId, root, priorityLevel)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools hook threw:', err)\n }\n }\n for (const cb of listeners) {\n try {\n cb(root, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] commit listener threw:', err)\n }\n }\n }\n // React calls this for every fiber it unmounts (dev builds). We fan out to\n // unmount listeners (the leak collector) and chain any pre-existing handler\n // (e.g. the React DevTools extension), mirroring onCommitFiberRoot.\n ;(hook as { onCommitFiberUnmount?: UnmountHandler }).onCommitFiberUnmount =\n (rendererId, fiber) => {\n if (chainedUnmount) {\n try {\n chainedUnmount(rendererId, fiber)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools unmount hook threw:', err)\n }\n }\n for (const cb of unmountListeners) {\n try {\n cb(fiber, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] unmount listener threw:', err)\n }\n }\n }\n g[HOOK_KEY] = hook\n ourHook = hook\n ourCommitWrapper = hook.onCommitFiberRoot\n ourUnmountWrapper = (hook as { onCommitFiberUnmount?: UnmountHandler }).onCommitFiberUnmount ?? null\n restoreCommit = chainedOriginal\n restoreUnmount = chainedUnmount\n}\n\nexport function installDevToolsHook(listener: CommitListener): () => void {\n ensureHookInstalled()\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Subscribe to fiber unmounts. React invokes `onCommitFiberUnmount` for each\n * unmounted fiber in dev builds; the leak collector uses this to track which\n * component instances were torn down. Returns an unsubscribe function.\n */\nexport function onFiberUnmount(listener: UnmountListener): () => void {\n ensureHookInstalled()\n unmountListeners.add(listener)\n return () => {\n unmountListeners.delete(listener)\n }\n}\n\n/**\n * Clears all listeners, detaches our wrappers from the global hook, and\n * restores any pre-existing handlers we chained to (e.g. the React DevTools\n * extension). Does NOT remove the hook object from globalThis — react-dom\n * captured it at module load, so it must stay. Callers that want a fully\n * clean slate must also `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`.\n */\nexport function uninstallDevToolsHook(): void {\n listeners.clear()\n unmountListeners.clear()\n if (ourHook) {\n // Only restore if our wrapper is still the installed handler — someone\n // (e.g. a late-loading DevTools extension) may have replaced it since.\n if (ourHook.onCommitFiberRoot === ourCommitWrapper) {\n if (restoreCommit) ourHook.onCommitFiberRoot = restoreCommit\n else delete ourHook.onCommitFiberRoot\n }\n const h = ourHook as { onCommitFiberUnmount?: UnmountHandler }\n if (h.onCommitFiberUnmount === ourUnmountWrapper) {\n if (restoreUnmount) h.onCommitFiberUnmount = restoreUnmount\n else delete h.onCommitFiberUnmount\n }\n }\n ourHook = null\n ourCommitWrapper = null\n ourUnmountWrapper = null\n restoreCommit = null\n restoreUnmount = null\n}\n","import type { MinimalFiber } from './types'\n\nconst MEMO_TYPE = Symbol.for('react.memo')\nconst FORWARD_REF_TYPE = Symbol.for('react.forward_ref')\n\ntype WrapperType = {\n $$typeof: symbol\n type?: unknown\n render?: unknown\n}\n\nfunction namedFunctionName(value: unknown): string | null {\n if (typeof value !== 'function') return null\n const fn = value as { displayName?: string; name?: string }\n if (typeof fn.displayName === 'string' && fn.displayName.length > 0) return fn.displayName\n if (typeof fn.name === 'string' && fn.name.length > 0) return fn.name\n return null\n}\n\n/**\n * Return the component name for a fiber.\n *\n * Host components (DOM tags) return their tag string. Function and class\n * components return their displayName or name. Memo and forwardRef wrappers\n * are unwrapped to their inner component. Unknown shapes return null.\n */\nexport function fiberComponentName(fiber: MinimalFiber | null): string | null {\n if (!fiber) return null\n const type = fiber.type\n if (typeof type === 'string') return type\n if (typeof type === 'function') return namedFunctionName(type)\n if (type && typeof type === 'object') {\n const wrapper = type as WrapperType\n if (wrapper.$$typeof === MEMO_TYPE) {\n return namedFunctionName(wrapper.type)\n }\n if (wrapper.$$typeof === FORWARD_REF_TYPE) {\n return namedFunctionName(wrapper.render)\n }\n }\n return null\n}\n\ninterface WalkOptions {\n /**\n * Maximum number of fibers to visit. Prevents runaway traversal on very\n * deep trees. Default 10_000.\n */\n stopAt?: number\n /**\n * Predicate deciding whether to descend into a fiber's children. Returning\n * false prunes the entire subtree (the fiber itself is still visited). Used\n * to skip subtrees that did no work this commit. Defaults to always descend.\n */\n descend?: (fiber: MinimalFiber) => boolean\n}\n\n/**\n * Walk a fiber subtree depth-first, invoking `visit` for every fiber with its\n * depth relative to `root` (root is depth 0). Returns early once `stopAt`\n * fibers have been visited. When `descend` returns false for a fiber, its\n * children are skipped entirely.\n */\nexport function walkChangedFibers(\n root: MinimalFiber,\n visit: (fiber: MinimalFiber, depth: number) => void,\n opts: WalkOptions = {}\n): void {\n const max = opts.stopAt ?? 10_000\n const descend = opts.descend ?? (() => true)\n let count = 0\n let depth = 0\n let node: MinimalFiber | null = root\n while (node) {\n visit(node, depth)\n count++\n if (count >= max) return\n if (node.child && descend(node)) {\n node = node.child\n depth++\n continue\n }\n while (node && !node.sibling) {\n if (node === root) return\n node = node.return\n depth--\n }\n if (!node || node === root) return\n node = node.sibling\n }\n}\n","import type { MinimalFiber } from './types'\nimport { fiberComponentName } from './fiber-walker'\n\nconst FIBER_KEY_PATTERNS = ['__reactFiber$', '__reactInternalInstance$']\n\nfunction findFiberOnElement(el: HTMLElement): MinimalFiber | null {\n for (const key of Object.keys(el)) {\n for (const pattern of FIBER_KEY_PATTERNS) {\n if (key.startsWith(pattern)) {\n return (el as HTMLElement & Record<string, MinimalFiber>)[key] ?? null\n }\n }\n }\n return null\n}\n\n/**\n * Walk up from the fiber attached to `el` until we find one whose `type` is\n * a function or class component (not a host tag string). Returns that\n * component's display name. If no component is found, returns the host\n * tag name of the starting fiber. If no fiber is attached, returns null.\n */\nexport function resolveComponentFromElement(el: HTMLElement): string | null {\n const start = findFiberOnElement(el)\n if (!start) return null\n let node: MinimalFiber | null = start\n while (node) {\n if (typeof node.type === 'function' || (node.type && typeof node.type === 'object')) {\n const name = fiberComponentName(node)\n if (name) return name\n }\n node = node.return\n }\n // Nothing but host fibers above — return the host tag name of the starting fiber.\n return fiberComponentName(start)\n}\n","import type { MinimalFiber } from './types'\n\n/** React's `PerformedWork` flag — set on a fiber whose render actually ran. */\nexport const PERFORMED_WORK = 0b1\n\nexport type RenderReason = 'mount' | 'state' | 'props' | 'parent'\n\nexport interface RenderReasonResult {\n reason: RenderReason\n changedProps?: string[]\n}\n\nexport function didPerformWork(fiber: MinimalFiber | null): boolean {\n if (!fiber || typeof fiber.flags !== 'number') return false\n return (fiber.flags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Should the walk descend into this fiber's children when hunting for renders?\n *\n * React bubbles the `PerformedWork` bit of every descendant into a fiber's\n * `subtreeFlags`, and (unlike `flags`) resets it whenever the reconciler\n * re-clones the fiber. So a numeric `subtreeFlags` without the bit reliably\n * means \"no descendant rendered this commit\" — we can stop here and avoid\n * stale leaves whose own `flags` never got cleared.\n *\n * When `subtreeFlags` is absent (React < 18) we cannot prune safely, so we\n * descend and fall back to the full-tree walk.\n */\nexport function subtreeMightHaveRendered(fiber: MinimalFiber): boolean {\n if (typeof fiber.subtreeFlags !== 'number') return true\n return (fiber.subtreeFlags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Walk up `.return` to the nearest *component* fiber, skipping host (DOM)\n * fibers. A component's immediate `.return` is usually a host fiber (the\n * `<div>` it's nested in), not its parent component — so to ask \"did my\n * parent component render?\" we have to climb past the host nodes first.\n */\nexport function nearestComponentAncestor(fiber: MinimalFiber): MinimalFiber | null {\n let p = fiber.return\n while (p) {\n // Function/class components have function types; forwardRef and\n // non-simple memo wrappers have object types — all are components\n // (host fibers are strings). Mirrors the check in attribution.ts.\n if (typeof p.type === 'function' || (p.type && typeof p.type === 'object')) return p\n p = p.return\n }\n return null\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n\n/** Shallow diff of two prop bags. Returns the keys whose values differ. */\nexport function changedPropKeys(prev: unknown, next: unknown): string[] {\n if (prev === next) return []\n const p = asObject(prev)\n const n = asObject(next)\n const keys = new Set([...Object.keys(p), ...Object.keys(n)])\n const changed: string[] = []\n for (const k of keys) {\n if (p[k] !== n[k]) changed.push(k)\n }\n return changed\n}\n\n/**\n * Classify WHY a fiber that performed work this commit rendered.\n *\n * - `mount` — first render (no previous fiber).\n * - `props` — at least one prop changed since the previous render.\n * - `state` — props are identical AND the parent did not render, so the\n * update originated here (own state/force update). This is the\n * root of a render cascade.\n * - `parent` — props are identical but the parent rendered, so this only\n * re-rendered because of its parent. This is the avoidable\n * \"wrap me in memo\" case the developer is hunting for.\n *\n * Assumes the caller only invokes this for fibers that actually performed\n * work (see `didPerformWork`).\n */\nexport function classifyRenderReason(fiber: MinimalFiber): RenderReasonResult {\n if (!fiber.alternate) return { reason: 'mount' }\n const changed = changedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps)\n if (changed.length > 0) return { reason: 'props', changedProps: changed }\n if (didPerformWork(nearestComponentAncestor(fiber))) return { reason: 'parent' }\n return { reason: 'state' }\n}\n","import type { Collector, RenderSignal, Signal } from '@react-perfscope/core'\nimport { installDevToolsHook } from './devtools-hook'\nimport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nimport { classifyRenderReason, didPerformWork, subtreeMightHaveRendered } from './render-reason'\nimport type { MinimalFiber } from './types'\n\n/** The cascade root of a commit: the component whose own state/mount started\n * it. Falls back to the shallowest member. */\nfunction cascadeRoot(members: RenderSignal[]): RenderSignal {\n let shallowest = members[0]!\n for (const m of members) {\n if (m.reason === 'state') return m\n if (m.depth < shallowest.depth) shallowest = m\n }\n return members.find((m) => m.reason === 'mount') ?? shallowest\n}\n\nexport function createRenderCollector(): Collector {\n let active = false\n let emit: (signal: Signal) => void = () => {}\n let unsubscribe: (() => void) | null = null\n let commitId = 0\n\n function onCommit(root: { current: MinimalFiber }) {\n if (!active) return\n const at = performance.now()\n const id = commitId++\n const members: RenderSignal[] = []\n walkChangedFibers(\n root.current,\n (fiber, depth) => {\n // Skip host (DOM) fibers — we only want function/class components in\n // render reports.\n if (typeof fiber.type === 'string') return\n // Only report fibers that actually re-ran their render this commit.\n // A bailed-out fiber (e.g. memo with equal props) keeps its place in\n // the tree but did no work, so reporting it would be noise.\n if (!didPerformWork(fiber)) return\n const name = fiberComponentName(fiber)\n if (!name) return\n const duration = typeof fiber.actualDuration === 'number' ? fiber.actualDuration : 0\n const { reason, changedProps } = classifyRenderReason(fiber)\n members.push({\n kind: 'render',\n at,\n component: name,\n reason,\n duration,\n commitId: id,\n depth,\n ...(changedProps ? { changedProps } : {}),\n })\n },\n // Prune subtrees that did no work this commit. Without this, a leaf that\n // mounted long ago keeps its stale PerformedWork flag and gets reported\n // as a phantom render on every unrelated commit.\n { descend: subtreeMightHaveRendered }\n )\n if (members.length === 0) return\n // One coalesced signal per commit. The root describes the cascade; the\n // duration is the commit's total render time.\n const root2 = cascadeRoot(members)\n // fiber.actualDuration is inclusive of descendants, so summing every member\n // double-counts nested render time. Members arrive in pre-order with depth,\n // so sum only the \"forest roots\" — members with no rendered ancestor — whose\n // inclusive durations already cover their subtrees. This matches what React's\n // own Profiler reports for the commit.\n let total = 0\n const ancestorDepths: number[] = []\n for (const m of members) {\n while (ancestorDepths.length > 0 && ancestorDepths[ancestorDepths.length - 1]! >= m.depth) {\n ancestorDepths.pop()\n }\n if (ancestorDepths.length === 0) total += m.duration\n ancestorDepths.push(m.depth)\n }\n emit({\n kind: 'render',\n at,\n component: root2.component,\n reason: root2.reason,\n duration: total,\n commitId: id,\n depth: root2.depth,\n ...(root2.changedProps ? { changedProps: root2.changedProps } : {}),\n members,\n count: members.length,\n })\n }\n\n return {\n kind: 'render',\n activate(emitFn) {\n emit = emitFn\n active = true\n if (unsubscribe) return\n unsubscribe = installDevToolsHook(onCommit)\n },\n deactivate() {\n active = false\n // Keep the global hook listener attached — installDevToolsHook is\n // idempotent and removing it on every deactivate would cost us the\n // ability to re-attach cleanly under the test reset hooks. The\n // `active` flag gates emission.\n },\n }\n}\n","import type { Collector, LeakSample, LeakSuspect, RecordingResult } from '@react-perfscope/core'\nimport { analyzeLeakTrend } from '@react-perfscope/core'\nimport { onFiberUnmount } from './devtools-hook'\nimport { fiberComponentName } from './fiber-walker'\nimport type { MinimalFiber } from './types'\n\n/** How often to sample per-component retained counts. Matches the heap sampler\n * — cheap (reading a small map) and fine-grained enough to resolve the floor. */\nconst SAMPLE_INTERVAL_MS = 250\n/** Per-component cap on retained-count samples. */\nconst MAX_SAMPLES = 4000\n\nexport interface LeakCollector extends Collector {\n finalize(result: RecordingResult): Promise<RecordingResult>\n}\n\nfunction maybeGc(): void {\n // Only present when Chrome is launched with --js-flags=--expose-gc. When\n // available, a nudge makes the final retained count accurate instead of\n // upper-bounded by GC lag. A no-op otherwise — the trend over the recording\n // is the real signal.\n const gc = (globalThis as { gc?: () => void }).gc\n if (typeof gc === 'function') {\n try {\n gc()\n } catch {\n /* ignore */\n }\n }\n}\n\n/**\n * Detects component-level memory leaks: components whose instances were\n * unmounted but stayed retained (not garbage-collected), with a retained count\n * that climbed across the recording.\n *\n * Mechanism (all in-page, no DevTools protocol): React calls\n * `onCommitFiberUnmount` for each unmounted fiber. For component fibers we\n * register the fiber in a FinalizationRegistry (keyed by component name) and\n * bump an `unmounted` counter; when the fiber is later collected the registry\n * callback bumps a `collected` counter. `retained = unmounted − collected` is\n * sampled over time and fed to {@link analyzeLeakTrend}. A component whose\n * retained floor keeps rising is flagged.\n *\n * Limitation: identifies WHICH component leaks and HOW MANY instances, not the\n * retainer chain (who holds them) — that needs a heap snapshot, unavailable to\n * in-page JS. No-ops when FinalizationRegistry is unavailable.\n */\nexport function createLeakCollector(): LeakCollector {\n const unmounted = new Map<string, number>()\n const collected = new Map<string, number>()\n const series = new Map<string, LeakSample[]>()\n let registry: FinalizationRegistry<string> | null = null\n let unsubscribe: (() => void) | null = null\n let timer: ReturnType<typeof setInterval> | null = null\n let active = false\n\n function bump(map: Map<string, number>, key: string): void {\n map.set(key, (map.get(key) ?? 0) + 1)\n }\n\n function retainedFor(name: string): number {\n return (unmounted.get(name) ?? 0) - (collected.get(name) ?? 0)\n }\n\n function sample(): void {\n const at = performance.now()\n for (const name of unmounted.keys()) {\n let arr = series.get(name)\n if (!arr) {\n arr = []\n series.set(name, arr)\n }\n arr.push({ at, retained: retainedFor(name) })\n if (arr.length > MAX_SAMPLES) arr.splice(0, arr.length - MAX_SAMPLES)\n }\n }\n\n function onUnmount(fiber: MinimalFiber): void {\n if (!active) return\n // Only component fibers — host (DOM tag) fibers churn constantly and aren't\n // \"component leaks\". Anonymous components (no resolvable name) are skipped.\n if (typeof fiber.type === 'string') return\n const name = fiberComponentName(fiber)\n if (!name) return\n bump(unmounted, name)\n registry?.register(fiber as object, name)\n }\n\n return {\n kind: 'leak',\n activate() {\n // Re-activation without an intervening deactivate would overwrite (and\n // orphan) the running interval and the unmount subscription.\n if (active) return\n unmounted.clear()\n collected.clear()\n series.clear()\n if (typeof FinalizationRegistry === 'undefined') return // unsupported\n active = true\n registry = new FinalizationRegistry<string>((name) => {\n bump(collected, name)\n })\n unsubscribe = onFiberUnmount(onUnmount)\n timer = setInterval(sample, SAMPLE_INTERVAL_MS)\n },\n deactivate() {\n active = false\n if (timer != null) {\n clearInterval(timer)\n timer = null\n }\n if (unsubscribe) {\n unsubscribe()\n unsubscribe = null\n }\n sample() // final reading at stop\n },\n async finalize(result) {\n if (unmounted.size === 0) return result\n // Best-effort: collect now and let the registry's cleanup callbacks flush\n // so the final retained counts aren't inflated by GC lag.\n maybeGc()\n await new Promise((r) => setTimeout(r, 0))\n\n const suspects: LeakSuspect[] = []\n for (const name of unmounted.keys()) {\n const samples = series.get(name) ?? []\n const trend = analyzeLeakTrend(samples)\n const retained = retainedFor(name)\n if (trend?.leaking && retained > 0) {\n suspects.push({\n component: name,\n unmounted: unmounted.get(name) ?? 0,\n retained,\n retainedSlopePerMin: trend.slopePerMin,\n })\n }\n }\n if (suspects.length === 0) return result\n suspects.sort((a, b) => b.retained - a.retained)\n return { ...result, leakSuspects: suspects }\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/devtools-hook.ts","../src/fiber-walker.ts","../src/attribution.ts","../src/render-reason.ts","../src/render-collector.ts","../src/leak-collector.ts"],"sourcesContent":["import type { MinimalFiber, ReactDevToolsHook } from './types'\n\ntype CommitListener = (root: { current: MinimalFiber }, rendererId: number) => void\ntype UnmountListener = (fiber: MinimalFiber, rendererId: number) => void\n\nconst HOOK_KEY = '__REACT_DEVTOOLS_GLOBAL_HOOK__'\n\ninterface GlobalWithHook {\n [HOOK_KEY]?: ReactDevToolsHook\n}\n\ntype UnmountHandler = (rendererId: number, fiber: MinimalFiber) => void\n\nconst listeners = new Set<CommitListener>()\nconst unmountListeners = new Set<UnmountListener>()\nlet ourHook: ReactDevToolsHook | null = null\n// Our installed wrappers and the handlers they chain to. The chained handlers\n// are ALSO captured per-wrapper in closures below — wrappers must never read\n// this mutable module state, or an uninstall→reinstall cycle makes the old\n// wrapper chain to itself (infinite recursion on every commit). These module\n// copies exist only so uninstall can restore the originals onto the hook.\nlet ourCommitWrapper: ReactDevToolsHook['onCommitFiberRoot'] | null = null\nlet ourUnmountWrapper: UnmountHandler | null = null\nlet restoreCommit: ReactDevToolsHook['onCommitFiberRoot'] | null = null\nlet restoreUnmount: UnmountHandler | null = null\n\nfunction ensureHookInstalled(): void {\n const g = globalThis as GlobalWithHook\n // Already ours and still installed — nothing to do.\n if (ourHook && g[HOOK_KEY] === ourHook) return\n\n const existing = g[HOOK_KEY]\n const chainedOriginal =\n existing && existing !== ourHook && typeof existing.onCommitFiberRoot === 'function'\n ? existing.onCommitFiberRoot\n : null\n const existingUnmount =\n existing && existing !== ourHook\n ? (existing as { onCommitFiberUnmount?: unknown }).onCommitFiberUnmount\n : null\n const chainedUnmount =\n typeof existingUnmount === 'function' ? (existingUnmount as UnmountHandler) : null\n\n const hook: ReactDevToolsHook = existing && existing !== ourHook ? existing : {}\n // React's injectInternals() checks for supportsFiber and calls inject() to\n // get a renderer ID. Without these, React stores injectedHook = null and\n // never calls onCommitFiberRoot on subsequent commits.\n if (!hook.supportsFiber) {\n hook.supportsFiber = true\n }\n if (typeof hook.inject !== 'function') {\n let _nextRendererId = 1\n hook.inject = () => _nextRendererId++\n }\n // Additional fields that React Refresh (@vitejs/plugin-react) accesses on\n // the hook. Without these it throws \"Cannot read properties of undefined\n // (reading 'forEach')\" at injectIntoGlobalHook. We default each to a safe\n // empty/noop value; if the real React DevTools extension installs later\n // it will overwrite these.\n if (!hook.renderers) {\n ;(hook as Record<string, unknown>).renderers = new Map()\n }\n if (typeof (hook as Record<string, unknown>)['on'] !== 'function') {\n ;(hook as Record<string, unknown>)['on'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['off'] !== 'function') {\n ;(hook as Record<string, unknown>)['off'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['emit'] !== 'function') {\n ;(hook as Record<string, unknown>)['emit'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['sub'] !== 'function') {\n ;(hook as Record<string, unknown>)['sub'] = () => () => {}\n }\n if (typeof (hook as Record<string, unknown>)['checkDCE'] !== 'function') {\n ;(hook as Record<string, unknown>)['checkDCE'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onPostCommitFiberRoot'] !== 'function') {\n ;(hook as Record<string, unknown>)['onPostCommitFiberRoot'] = () => {}\n }\n hook.onCommitFiberRoot = (rendererId, root, priorityLevel) => {\n if (chainedOriginal) {\n try {\n chainedOriginal(rendererId, root, priorityLevel)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools hook threw:', err)\n }\n }\n for (const cb of listeners) {\n try {\n cb(root, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] commit listener threw:', err)\n }\n }\n }\n // React calls this for every fiber it unmounts (dev builds). We fan out to\n // unmount listeners (the leak collector) and chain any pre-existing handler\n // (e.g. the React DevTools extension), mirroring onCommitFiberRoot.\n ;(hook as { onCommitFiberUnmount?: UnmountHandler }).onCommitFiberUnmount =\n (rendererId, fiber) => {\n if (chainedUnmount) {\n try {\n chainedUnmount(rendererId, fiber)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools unmount hook threw:', err)\n }\n }\n for (const cb of unmountListeners) {\n try {\n cb(fiber, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] unmount listener threw:', err)\n }\n }\n }\n // Only assign when we built a fresh hook. When we reused the existing hook\n // object (e.g. the React DevTools extension's), it was mutated in place —\n // and the extension installs the global as a getter-only, non-configurable\n // property, so assigning to it would throw a TypeError.\n if (hook !== existing) {\n try {\n g[HOOK_KEY] = hook\n } catch (err) {\n // Getter-only global that yielded no usable hook object: we cannot\n // install, and react-dom will never see our hook either. Fail soft.\n console.warn('[react-perfscope] could not install the DevTools hook:', err)\n return\n }\n }\n ourHook = hook\n ourCommitWrapper = hook.onCommitFiberRoot\n ourUnmountWrapper = (hook as { onCommitFiberUnmount?: UnmountHandler }).onCommitFiberUnmount ?? null\n restoreCommit = chainedOriginal\n restoreUnmount = chainedUnmount\n}\n\nexport function installDevToolsHook(listener: CommitListener): () => void {\n ensureHookInstalled()\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Subscribe to fiber unmounts. React invokes `onCommitFiberUnmount` for each\n * unmounted fiber in dev builds; the leak collector uses this to track which\n * component instances were torn down. Returns an unsubscribe function.\n */\nexport function onFiberUnmount(listener: UnmountListener): () => void {\n ensureHookInstalled()\n unmountListeners.add(listener)\n return () => {\n unmountListeners.delete(listener)\n }\n}\n\n/**\n * Clears all listeners, detaches our wrappers from the global hook, and\n * restores any pre-existing handlers we chained to (e.g. the React DevTools\n * extension). Does NOT remove the hook object from globalThis — react-dom\n * captured it at module load, so it must stay. Callers that want a fully\n * clean slate must also `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`\n * (impossible when the React DevTools extension installed it — the extension\n * defines the property non-configurable).\n */\nexport function uninstallDevToolsHook(): void {\n listeners.clear()\n unmountListeners.clear()\n if (ourHook) {\n // Only restore if our wrapper is still the installed handler — someone\n // (e.g. a late-loading DevTools extension) may have replaced it since.\n if (ourHook.onCommitFiberRoot === ourCommitWrapper) {\n if (restoreCommit) ourHook.onCommitFiberRoot = restoreCommit\n else delete ourHook.onCommitFiberRoot\n }\n const h = ourHook as { onCommitFiberUnmount?: UnmountHandler }\n if (h.onCommitFiberUnmount === ourUnmountWrapper) {\n if (restoreUnmount) h.onCommitFiberUnmount = restoreUnmount\n else delete h.onCommitFiberUnmount\n }\n }\n ourHook = null\n ourCommitWrapper = null\n ourUnmountWrapper = null\n restoreCommit = null\n restoreUnmount = null\n}\n","import type { MinimalFiber } from './types'\n\nconst MEMO_TYPE = Symbol.for('react.memo')\nconst FORWARD_REF_TYPE = Symbol.for('react.forward_ref')\n\ntype WrapperType = {\n $$typeof: symbol\n type?: unknown\n render?: unknown\n}\n\nfunction namedFunctionName(value: unknown): string | null {\n if (typeof value !== 'function') return null\n const fn = value as { displayName?: string; name?: string }\n if (typeof fn.displayName === 'string' && fn.displayName.length > 0) return fn.displayName\n if (typeof fn.name === 'string' && fn.name.length > 0) return fn.name\n return null\n}\n\n/**\n * Return the component name for a fiber.\n *\n * Host components (DOM tags) return their tag string. Function and class\n * components return their displayName or name. Memo and forwardRef wrappers\n * are unwrapped to their inner component. Unknown shapes return null.\n */\nexport function fiberComponentName(fiber: MinimalFiber | null): string | null {\n if (!fiber) return null\n const type = fiber.type\n if (typeof type === 'string') return type\n if (typeof type === 'function') return namedFunctionName(type)\n if (type && typeof type === 'object') {\n const wrapper = type as WrapperType\n if (wrapper.$$typeof === MEMO_TYPE) {\n return namedFunctionName(wrapper.type)\n }\n if (wrapper.$$typeof === FORWARD_REF_TYPE) {\n return namedFunctionName(wrapper.render)\n }\n }\n return null\n}\n\ninterface WalkOptions {\n /**\n * Maximum number of fibers to visit. Prevents runaway traversal on very\n * deep trees. Default 10_000.\n */\n stopAt?: number\n /**\n * Predicate deciding whether to descend into a fiber's children. Returning\n * false prunes the entire subtree (the fiber itself is still visited). Used\n * to skip subtrees that did no work this commit. Defaults to always descend.\n */\n descend?: (fiber: MinimalFiber) => boolean\n}\n\n/**\n * Walk a fiber subtree depth-first, invoking `visit` for every fiber with its\n * depth relative to `root` (root is depth 0). Returns early once `stopAt`\n * fibers have been visited. When `descend` returns false for a fiber, its\n * children are skipped entirely.\n */\nexport function walkChangedFibers(\n root: MinimalFiber,\n visit: (fiber: MinimalFiber, depth: number) => void,\n opts: WalkOptions = {}\n): void {\n const max = opts.stopAt ?? 10_000\n const descend = opts.descend ?? (() => true)\n let count = 0\n let depth = 0\n let node: MinimalFiber | null = root\n while (node) {\n visit(node, depth)\n count++\n if (count >= max) return\n if (node.child && descend(node)) {\n node = node.child\n depth++\n continue\n }\n while (node && !node.sibling) {\n if (node === root) return\n node = node.return\n depth--\n }\n if (!node || node === root) return\n node = node.sibling\n }\n}\n","import type { MinimalFiber } from './types'\nimport { fiberComponentName } from './fiber-walker'\n\nconst FIBER_KEY_PATTERNS = ['__reactFiber$', '__reactInternalInstance$']\n\nfunction findFiberOnElement(el: HTMLElement): MinimalFiber | null {\n for (const key of Object.keys(el)) {\n for (const pattern of FIBER_KEY_PATTERNS) {\n if (key.startsWith(pattern)) {\n return (el as HTMLElement & Record<string, MinimalFiber>)[key] ?? null\n }\n }\n }\n return null\n}\n\n/**\n * Walk up from the fiber attached to `el` until we find one whose `type` is\n * a function or class component (not a host tag string). Returns that\n * component's display name. If no component is found, returns the host\n * tag name of the starting fiber. If no fiber is attached, returns null.\n */\nexport function resolveComponentFromElement(el: HTMLElement): string | null {\n const start = findFiberOnElement(el)\n if (!start) return null\n let node: MinimalFiber | null = start\n while (node) {\n if (typeof node.type === 'function' || (node.type && typeof node.type === 'object')) {\n const name = fiberComponentName(node)\n if (name) return name\n }\n node = node.return\n }\n // Nothing but host fibers above — return the host tag name of the starting fiber.\n return fiberComponentName(start)\n}\n","import type { MinimalFiber } from './types'\n\n/** React's `PerformedWork` flag — set on a fiber whose render actually ran. */\nexport const PERFORMED_WORK = 0b1\n\nexport type RenderReason = 'mount' | 'state' | 'props' | 'parent'\n\nexport interface RenderReasonResult {\n reason: RenderReason\n changedProps?: string[]\n}\n\nexport function didPerformWork(fiber: MinimalFiber | null): boolean {\n if (!fiber || typeof fiber.flags !== 'number') return false\n return (fiber.flags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Should the walk descend into this fiber's children when hunting for renders?\n *\n * React bubbles the `PerformedWork` bit of every descendant into a fiber's\n * `subtreeFlags`, and (unlike `flags`) resets it whenever the reconciler\n * re-clones the fiber. So a numeric `subtreeFlags` without the bit reliably\n * means \"no descendant rendered this commit\" — we can stop here and avoid\n * stale leaves whose own `flags` never got cleared.\n *\n * When `subtreeFlags` is absent (React < 18) we cannot prune safely, so we\n * descend and fall back to the full-tree walk.\n */\nexport function subtreeMightHaveRendered(fiber: MinimalFiber): boolean {\n if (typeof fiber.subtreeFlags !== 'number') return true\n return (fiber.subtreeFlags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Walk up `.return` to the nearest *component* fiber, skipping host (DOM)\n * fibers. A component's immediate `.return` is usually a host fiber (the\n * `<div>` it's nested in), not its parent component — so to ask \"did my\n * parent component render?\" we have to climb past the host nodes first.\n */\nexport function nearestComponentAncestor(fiber: MinimalFiber): MinimalFiber | null {\n let p = fiber.return\n while (p) {\n // Function/class components have function types; forwardRef and\n // non-simple memo wrappers have object types — all are components\n // (host fibers are strings). Mirrors the check in attribution.ts.\n if (typeof p.type === 'function' || (p.type && typeof p.type === 'object')) return p\n p = p.return\n }\n return null\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n\n/** Shallow diff of two prop bags. Returns the keys whose values differ. */\nexport function changedPropKeys(prev: unknown, next: unknown): string[] {\n if (prev === next) return []\n const p = asObject(prev)\n const n = asObject(next)\n const keys = new Set([...Object.keys(p), ...Object.keys(n)])\n const changed: string[] = []\n for (const k of keys) {\n if (p[k] !== n[k]) changed.push(k)\n }\n return changed\n}\n\n/**\n * Classify WHY a fiber that performed work this commit rendered.\n *\n * - `mount` — first render (no previous fiber).\n * - `props` — at least one prop changed since the previous render.\n * - `state` — props are identical AND the parent did not render, so the\n * update originated here (own state/force update). This is the\n * root of a render cascade.\n * - `parent` — props are identical but the parent rendered, so this only\n * re-rendered because of its parent. This is the avoidable\n * \"wrap me in memo\" case the developer is hunting for.\n *\n * Assumes the caller only invokes this for fibers that actually performed\n * work (see `didPerformWork`).\n */\nexport function classifyRenderReason(fiber: MinimalFiber): RenderReasonResult {\n if (!fiber.alternate) return { reason: 'mount' }\n const changed = changedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps)\n if (changed.length > 0) return { reason: 'props', changedProps: changed }\n if (didPerformWork(nearestComponentAncestor(fiber))) return { reason: 'parent' }\n return { reason: 'state' }\n}\n","import type { Collector, RenderSignal, Signal } from '@react-perfscope/core'\nimport { installDevToolsHook } from './devtools-hook'\nimport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nimport { classifyRenderReason, didPerformWork, subtreeMightHaveRendered } from './render-reason'\nimport type { MinimalFiber } from './types'\n\n/** The cascade root of a commit: the component whose own state/mount started\n * it. Falls back to the shallowest member. */\nfunction cascadeRoot(members: RenderSignal[]): RenderSignal {\n let shallowest = members[0]!\n for (const m of members) {\n if (m.reason === 'state') return m\n if (m.depth < shallowest.depth) shallowest = m\n }\n return members.find((m) => m.reason === 'mount') ?? shallowest\n}\n\nexport function createRenderCollector(): Collector {\n let active = false\n let emit: (signal: Signal) => void = () => {}\n let unsubscribe: (() => void) | null = null\n let commitId = 0\n let warnedNoDurations = false\n\n function onCommit(root: { current: MinimalFiber }) {\n if (!active) return\n const at = performance.now()\n const id = commitId++\n const members: RenderSignal[] = []\n walkChangedFibers(\n root.current,\n (fiber, depth) => {\n // Skip host (DOM) fibers — we only want function/class components in\n // render reports.\n if (typeof fiber.type === 'string') return\n // Only report fibers that actually re-ran their render this commit.\n // A bailed-out fiber (e.g. memo with equal props) keeps its place in\n // the tree but did no work, so reporting it would be noise.\n if (!didPerformWork(fiber)) return\n const name = fiberComponentName(fiber)\n if (!name) return\n const hasDuration = typeof fiber.actualDuration === 'number'\n if (!hasDuration && !warnedNoDurations) {\n // React only records actualDuration when the root was created in\n // ProfileMode: the DevTools hook must exist before createRoot(),\n // and production react-dom (non-profiling build) never records it.\n // Without this warning every render reports a believable-looking\n // 0ms.\n warnedNoDurations = true\n console.warn(\n '[react-perfscope] render durations unavailable — React did not profile this root. ' +\n 'Make sure react-perfscope loads before react-dom (import it first, or use the ' +\n 'Vite/webpack plugin), and use a development or profiling build of react-dom. ' +\n 'Renders will be reported with duration 0.'\n )\n }\n const duration = hasDuration ? (fiber.actualDuration as number) : 0\n const { reason, changedProps } = classifyRenderReason(fiber)\n members.push({\n kind: 'render',\n at,\n component: name,\n reason,\n duration,\n commitId: id,\n depth,\n ...(changedProps ? { changedProps } : {}),\n })\n },\n // Prune subtrees that did no work this commit. Without this, a leaf that\n // mounted long ago keeps its stale PerformedWork flag and gets reported\n // as a phantom render on every unrelated commit.\n { descend: subtreeMightHaveRendered }\n )\n if (members.length === 0) return\n // One coalesced signal per commit. The root describes the cascade; the\n // duration is the commit's total render time.\n const root2 = cascadeRoot(members)\n // fiber.actualDuration is inclusive of descendants, so summing every member\n // double-counts nested render time. Members arrive in pre-order with depth,\n // so sum only the \"forest roots\" — members with no rendered ancestor — whose\n // inclusive durations already cover their subtrees. This matches what React's\n // own Profiler reports for the commit.\n let total = 0\n const ancestorDepths: number[] = []\n for (const m of members) {\n while (ancestorDepths.length > 0 && ancestorDepths[ancestorDepths.length - 1]! >= m.depth) {\n ancestorDepths.pop()\n }\n if (ancestorDepths.length === 0) total += m.duration\n ancestorDepths.push(m.depth)\n }\n emit({\n kind: 'render',\n at,\n component: root2.component,\n reason: root2.reason,\n duration: total,\n commitId: id,\n depth: root2.depth,\n ...(root2.changedProps ? { changedProps: root2.changedProps } : {}),\n members,\n count: members.length,\n })\n }\n\n return {\n kind: 'render',\n activate(emitFn) {\n emit = emitFn\n active = true\n if (unsubscribe) return\n unsubscribe = installDevToolsHook(onCommit)\n },\n deactivate() {\n active = false\n // Keep the global hook listener attached — installDevToolsHook is\n // idempotent and removing it on every deactivate would cost us the\n // ability to re-attach cleanly under the test reset hooks. The\n // `active` flag gates emission.\n },\n }\n}\n","import type { Collector, LeakSample, LeakSuspect, RecordingResult } from '@react-perfscope/core'\nimport { analyzeLeakTrend } from '@react-perfscope/core'\nimport { onFiberUnmount } from './devtools-hook'\nimport { fiberComponentName } from './fiber-walker'\nimport type { MinimalFiber } from './types'\n\n/** How often to sample per-component retained counts. Matches the heap sampler\n * — cheap (reading a small map) and fine-grained enough to resolve the floor. */\nconst SAMPLE_INTERVAL_MS = 250\n/** Per-component cap on retained-count samples. */\nconst MAX_SAMPLES = 4000\n\nexport interface LeakCollector extends Collector {\n finalize(result: RecordingResult): Promise<RecordingResult>\n}\n\nfunction maybeGc(): void {\n // Only present when Chrome is launched with --js-flags=--expose-gc. When\n // available, a nudge makes the final retained count accurate instead of\n // upper-bounded by GC lag. A no-op otherwise — the trend over the recording\n // is the real signal.\n const gc = (globalThis as { gc?: () => void }).gc\n if (typeof gc === 'function') {\n try {\n gc()\n } catch {\n /* ignore */\n }\n }\n}\n\n/**\n * Detects component-level memory leaks: components whose instances were\n * unmounted but stayed retained (not garbage-collected), with a retained count\n * that climbed across the recording.\n *\n * Mechanism (all in-page, no DevTools protocol): React calls\n * `onCommitFiberUnmount` for each unmounted fiber. For component fibers we\n * register the fiber in a FinalizationRegistry (keyed by component name) and\n * bump an `unmounted` counter; when the fiber is later collected the registry\n * callback bumps a `collected` counter. `retained = unmounted − collected` is\n * sampled over time and fed to {@link analyzeLeakTrend}. A component whose\n * retained floor keeps rising is flagged.\n *\n * Limitation: identifies WHICH component leaks and HOW MANY instances, not the\n * retainer chain (who holds them) — that needs a heap snapshot, unavailable to\n * in-page JS. No-ops when FinalizationRegistry is unavailable.\n */\nexport function createLeakCollector(): LeakCollector {\n const unmounted = new Map<string, number>()\n const collected = new Map<string, number>()\n const series = new Map<string, LeakSample[]>()\n let registry: FinalizationRegistry<string> | null = null\n let unsubscribe: (() => void) | null = null\n let timer: ReturnType<typeof setInterval> | null = null\n let active = false\n\n function bump(map: Map<string, number>, key: string): void {\n map.set(key, (map.get(key) ?? 0) + 1)\n }\n\n function retainedFor(name: string): number {\n return (unmounted.get(name) ?? 0) - (collected.get(name) ?? 0)\n }\n\n function sample(): void {\n const at = performance.now()\n for (const name of unmounted.keys()) {\n let arr = series.get(name)\n if (!arr) {\n arr = []\n series.set(name, arr)\n }\n arr.push({ at, retained: retainedFor(name) })\n if (arr.length > MAX_SAMPLES) arr.splice(0, arr.length - MAX_SAMPLES)\n }\n }\n\n function onUnmount(fiber: MinimalFiber): void {\n if (!active) return\n // Only component fibers — host (DOM tag) fibers churn constantly and aren't\n // \"component leaks\". Anonymous components (no resolvable name) are skipped.\n if (typeof fiber.type === 'string') return\n const name = fiberComponentName(fiber)\n if (!name) return\n bump(unmounted, name)\n registry?.register(fiber as object, name)\n }\n\n return {\n kind: 'leak',\n activate() {\n // Re-activation without an intervening deactivate would overwrite (and\n // orphan) the running interval and the unmount subscription.\n if (active) return\n unmounted.clear()\n collected.clear()\n series.clear()\n if (typeof FinalizationRegistry === 'undefined') return // unsupported\n active = true\n registry = new FinalizationRegistry<string>((name) => {\n bump(collected, name)\n })\n unsubscribe = onFiberUnmount(onUnmount)\n timer = setInterval(sample, SAMPLE_INTERVAL_MS)\n },\n deactivate() {\n active = false\n if (timer != null) {\n clearInterval(timer)\n timer = null\n }\n if (unsubscribe) {\n unsubscribe()\n unsubscribe = null\n }\n sample() // final reading at stop\n },\n async finalize(result) {\n if (unmounted.size === 0) return result\n // Best-effort: collect now and let the registry's cleanup callbacks flush\n // so the final retained counts aren't inflated by GC lag.\n maybeGc()\n await new Promise((r) => setTimeout(r, 0))\n\n const suspects: LeakSuspect[] = []\n for (const name of unmounted.keys()) {\n const samples = series.get(name) ?? []\n const trend = analyzeLeakTrend(samples)\n const retained = retainedFor(name)\n if (trend?.leaking && retained > 0) {\n suspects.push({\n component: name,\n unmounted: unmounted.get(name) ?? 0,\n retained,\n retainedSlopePerMin: trend.slopePerMin,\n })\n }\n }\n if (suspects.length === 0) return result\n suspects.sort((a, b) => b.retained - a.retained)\n return { ...result, leakSuspects: suspects }\n },\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-perfscope/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "React render collector for react-perfscope.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"sideEffects": false,
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@react-perfscope/core": "0.
|
|
29
|
+
"@react-perfscope/core": "1.0.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"react": "^18.3.0 || ^19.0.0",
|
|
@@ -51,6 +51,9 @@
|
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18"
|
|
56
|
+
},
|
|
54
57
|
"scripts": {
|
|
55
58
|
"build": "tsup && node scripts/prepend-dts-refs.mjs",
|
|
56
59
|
"typecheck": "tsc --noEmit"
|