Honest limits
What it cannot see
A leak detector that silently sees nothing reports a clean bill of health for an app that is losing every frame. That is worse than having no tool. So here is the blind-spot list, and the machinery that keeps a gap from passing as a pass.
How a gap is reported
| Signal | Means |
|---|---|
problems[] |
A patch step that failed in this context. Every step of
installCensus is wrapped separately and records its failure here
instead of throwing. Non-empty means the counts beside it are a floor, not a
total.
|
closedUnseen |
Something was closed here that the census never saw arrive. Usually a receive path the message scanner did not reach. It is a count of objects whose lifetime was only half observed. |
| skipped workers (patch mode) | Every worker the extension's patch mode could not wrap, with the reason, in the popup and in the census payload. |
If you add a code path that can silently observe less than it appears to, add the counter or
the problems[] entry that makes it visible. That is the project's one
non-negotiable rule.
Blind spots
Anything allocated before it installs
The census counts what it sees enter a context. Objects that already existed are invisible —
not counted live, and a later close() on one lands in
closedUnseen. This is the entire reason the CDP driver goes to the trouble of
two pauses per worker.
Encoded chunks
EncodedVideoChunk and EncodedAudioChunk have no
close() and hold no external resource, so they are not tracked as leakable.
Encoders emit them, which is why an encoder's output callback produces nothing
the census counts.
Frames from MediaStreamTrackProcessor
They reach you through a ReadableStream rather than a constructor or a codec
output callback, so they are not attributed to an allocation site. Closing one
shows up as closedUnseen rather than as a matched lifetime.
Objects buried deep in a message
The receive-side scanner walks 3 levels and 64 entries per
level, and descends only into arrays and plain object carriers — an object whose prototype is
a class is not walked into. This runs on every message and has to stay cheap. A frame buried
deeper, or held on a class instance, arrives uncounted and later surfaces as
closedUnseen.
Patch mode changes self.location
A worker wrapped by the extension's patch mode sees the loader blob URL as
self.location. Workers using import.meta.url are unaffected;
workers building paths from self.location are not. Patch mode also cannot cover
a worker that started before it installed, and cannot create a blob worker at all on a page
whose CSP omits blob: — which it reports rather than hides.
Exact mode cannot share a tab with DevTools
Chrome allows one debugger client per tab. Patch mode exists for when that trade is not worth it.
One context at a time, in-process
localCensus() covers only the context it runs in. Getting a worker's census
means running it inside that worker and carrying the result back. The CDP driver and the
extension do this by querying each target on its own session.
collectedUnclosed waits for the collector
It is reported by a FinalizationRegistry, which fires when the GC actually
collects the object. That is not on your schedule. A leak that has not been collected yet
shows up as live, not as collectedUnclosed — the two are the same
bug at different stages.
An age filter is exact only up to the census cap
minAgeMs decides the verdict as of v0.3.1, using the ages the census carries.
Those ages are capped at 256 per type, kept from the oldest end. Below saturation the count
is exact; at saturation — every kept age clears the threshold — the report states a lower
bound and carries the exact total beside it, because the objects past the cap are of unknown
age. It never under-reports, and it never claims more than it knows.
Before v0.3.1 the option filtered report.sites only, so it changed the printed
attribution without changing pass or fail. If you are pinned to an older release, use
allow instead.
Chrome, for the driver and the extension
The CDP driver and the extension are Chrome and Chromium only — they depend on the DevTools Protocol and on Chrome-specific worker pause behaviour. The core is plain JavaScript with no dependencies and runs anywhere WebCodecs does.
Undocumented behaviour, pinned deliberately
Injection depends on Chrome behaviour that is measured rather than specified: that workers
pause before their first line, that the beforeScriptExecution breakpoint
exists, and that codecs are absent at the earlier pause.
test/platform-assumptions.test.mjs asserts each of those and prints what it
measured, so a browser change is reported as a browser change. CI pins one Chrome version
for its blocking job and runs a separate scheduled job against stable — drift is news, not a
blocked pull request.
What it costs
Measured, not estimated: +5.6 µs per tracked allocation and ~284 bytes per live tracked object, the latter bounded by the size of the leak itself. At 60 fps that is 0.03% of a second. It only matters above roughly 100k allocations per second.
Suitable for development and CI. Not recommended enabled by default in production builds — not for speed, but because it patches global constructors and retains a stack per live object.
What it deliberately does not do
-
No network requests. The core makes none: no telemetry, no beacons, no
reporting. Verify with
grep -rE 'fetch\(|sendBeacon|WebSocket' node_modules/@motionvector/webcodecs-census/dist. - No frame or audio contents are read, copied or transmitted.
- No strong references. The census holds metadata and weak references only, so it cannot itself become the leak it is looking for.
- No runtime dependencies in the core package.
Things that look alarming and are the point
The census patches global constructors, keeps allocation stacks in memory, exposes
window.__webcodecsCensus to anything in the page, and the MCP server runs
arbitrary JavaScript in the page under test. CDP ports are unauthenticated by design. None
of that is incidental —
SECURITY.md
says which of it is deliberate, which is not, and how to report a problem.