{"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":[]}