WebCodecs Census docs

Quickstart

Get a leak report

Three ways in, depending on whether you can edit the code being measured, want to drive a browser from a script, or want an agent to do it.

The one thing that matters

The census only counts what it sees enter a context. Anything allocated before it installs is invisible to it. Install as early as you can — which is exactly why the CDP driver exists: it gets in before a worker's first line, which nothing running inside the page can do.

1. From your own code

Reach for this when you can edit the app or the test harness.

npm install --save-dev @motionvector/webcodecs-census

Install it at the top of every context that touches media — the main thread and each worker:

import { installCensus, localCensus } from '@motionvector/webcodecs-census';

installCensus({ context: 'decoder-worker' });

Then ask what is still open:

const census = localCensus();
// {
//   live:              { VideoFrame: 58, VideoDecoder: 1 },
//   entered:           { 'VideoFrame:decoded': 238, 'VideoDecoder:constructed': 1 },
//   left:              { 'VideoFrame:closed': 179 },
//   leakSites:         [ { count: 58, type: 'VideoFrame', origin: 'decoded', stack, oldestAgeMs } ],
//   collectedUnclosed: { VideoFrame: 1 },
//   mediaElements:     { total: 4, stalled: 1, byReadyState: { 0: 1, 4: 3 } },
//   timeline:          [ … ],
//   problems:          [],
// }

localCensus() covers only the context it runs in. A worker's census has to be collected in that worker and carried back to whoever is asking — the CDP driver does this for you by querying each target on its own session.

2. From outside the app

No change to the code being measured, and workers are instrumented before their first line.

npm install --save-dev @motionvector/webcodecs-census-cdp
import { attach, launchChrome } from '@motionvector/webcodecs-census-cdp';
import { summarize } from '@motionvector/webcodecs-census';

const chrome = await launchChrome({ executablePath: CHROME });
const session = await attach({ browserURL: chrome.browserURL });

await session.navigate('http://localhost:5173/');
await session.evaluate('document.querySelector("#play").click()');
// …let it run…

console.log(summarize(await session.census()));

session.detach();
await chrome.kill();

To attach to a browser you already started with --remote-debugging-port, pass its endpoint instead:

const session = await attach({ browserURL: 'http://127.0.0.1:9222' });

Full options on the CDP driver page.

3. From an agent

Register the MCP server with whatever runs your agent:

{
  "mcpServers": {
    "webcodecs-census": {
      "command": "npx",
      "args": ["-y", "@motionvector/webcodecs-census-mcp"]
    }
  }
}
webcodecs_attach { executablePath: "/path/to/chrome", url: "http://localhost:5173/" }
→ Instrumented 3 context(s): page, worker, worker

webcodecs_census
→ worker (20s): VideoDecoder=1 VideoFrame=58
  1 VideoFrame garbage collected without close() — definitively leaked.

webcodecs_leak_sites { type: "VideoFrame" }
→ 58x VideoFrame — decoded, oldest 124609ms, in worker
      at PackagerWorker.setupDecoder (worker.js:1756:21)
      (frame emitted by this VideoDecoder)

Tool-by-tool detail on the MCP server page.

Reading the report

These mean different things, and the difference is worth internalising.

SignalWhat it means
live Still open right now. Could be a leak, could be a pipeline mid-flight. Judge it against how long they have been live and how many you expect.
collectedUnclosed Garbage collected while still open. The resource was held for the object's whole lifetime and nothing will ever release it. Not a heuristic — a leak.
overCloses (v0.3.0) A close() that threw, because something closed an already-closed codec. A lifecycle bug, not a leak — reported, and only fatal if you ask for it.

That is why collectedUnclosed fails a check for any tracked type, whatever you passed for types — and why an over-close does not. A codec the platform closed after an error is not counted as either. See the API reference.

Making it a test

import { expectNoLeakedFrames } from '@motionvector/webcodecs-census';

test('the editor releases every frame it decodes', async () => {
  await playThroughTimeline();
  expectNoLeakedFrames(await session.census());
});

It throws with the allocation sites attached. CI recipes covers running this against a real Chrome on a runner.

Next