Verification Checkpoints

Reference environment: Chrome for Testing 149.0.7827.55, arm64 macOS. Ranges are the pass condition; exact bytes are machine-specific.


Checkpoint 1 — Closures retain contexts, not variables

npm run closures
  variant                            | retained heap
  A  no sibling references it        |    0.01MB
  B  unused sibling references it    |   38.17MB
  C  sibling + explicit big = null   |    0.01MB

Pass: B is at least 100× A. A and C are within noise of each other (< 0.5 MB). Fail — all three near zero: SIZE too small, or the engine optimised the arrays away. Raise SIZE/N. Fail — A is also large: you are retaining something else; check that __held is reset between variants.


Checkpoint 2 — The JS heap does not see DOM

npm run detached
  removed from DOM, JS refs held   |           5000 |        507.8KB |  0.84MB
  JS refs released                 |              0 |          0.0KB |  0.74MB

Pass: detached count goes 0 → ROWS → 0, while the JS-heap column changes by a much smaller amount than the detached-bytes column. Fail — detached always 0: the snapshot has no detachedness field (older V8). lib/heap.mjs reports hasDetachednessField; check it. Fall back to name-prefix matching only if false.

This checkpoint is the one to internalise: the instrument selects the conclusion.


Checkpoint 3 — Listener lifecycle, three variants

npm run listeners
  anonymous listener, never removed    |          23.00MB |            300
  removeEventListener in destroy()     |           0.08MB |              0
  AbortController signal + abort()     |           0.09MB |              0

Pass: the anonymous variant retains ≥ 20× the other two and shows CYCLES detached nodes while the other two show 0. Fail — all three show the same detached count: page isolation is broken; each variant must run on a fresh page. This exact bug occurred while building the module.


Checkpoint 4 — WeakMap weakens keys

npm run weakmap
  Map        |          38.34MB |                      2000
  WeakMap    |           0.09MB |                         0

Pass: Map retains ≥ 100× the WeakMap variant, and detached counts differ by ~N.


Checkpoint 5 — Slope and linearity together

npm run growth
clean  ▁▁▁▁▁▁▁█████  slope 0.001 MB/cycle   R² 0.742
leaky  ▁▁▂▃▃▄▅▆▆▇██  slope 1.836 MB/cycle   R² 1.000

Pass: leaky slope ≥ 0.5 MB/cycle with R² ≥ 0.95; clean slope < 0.05 MB/cycle. Note the clean R² is high — around 0.74 here — and that is the point. If your clean R² comes out low, run it again; over a short series it will sometimes be high. The checkpoint is that you can state why a high R² on the clean run is not alarming (the slope is at zero magnitude).


Checkpoint 6 — You can name the retainer path

Not a script. Open web/leaky-app.html, take two snapshots around a repeated action, use the Comparison view, and for each of the four seeded leaks write down:

  1. the object class that grew,
  2. the retainer path from a GC root,
  3. the single edge you would break to fix it,
  4. whether breaking that edge is a local fix or an architectural one.

Pass: four retainer paths written down, each naming a specific root. "It's a closure" is not a retainer path. window → appState.cache → Map → {…} → HTMLDivElement is.


Module completion

All six checkpoints, and:

  • steps/06-principal-review.md answered in writing
  • One leak found in a real codebase you work on, or a documented statement of why none was found and what you checked — the method transfers or it was not learned
  • ../fe-00-roadmap/docs/learning-log.md updated with any prediction you got wrong
  • A soak-test sketch: what you would assert, at what cadence, in CI (fe-33 picks this up)