WebCodecs Census docs

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

SignalMeans
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

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.