v153 · performance · profiling

Markers on a self-profiling sample

The JS Self-Profiling API lets a page sample its own call stacks on real user devices. What it does not tell you is what the browser was doing at each sample — so a function that allocates heavily, one that forces layout, and one that just computes all produce the same trace. A marker field names the activity behind each sample.

concepts

  1. Profile something, for real

    Four workloads that spend their time in different parts of the engine, each profiled here with a real Profiler. The trace is the browser's, and so is every number derived from it.

  2. Four workloads, one profile

    Profile a numeric loop, an allocation storm, a layout thrash and a style recalculation, side by side. The traces come out the same shape — which is the problem, stated as a measurement.

  3. The field itself, and the header it needs

    Whether a sample carries marker here, what values the proposal defines, and the Document-Policy without which none of this runs at all — including what the failure looks like.

why it shipped

A self-profile is a list of samples, each pointing at a stack. The proposal describes traces with gaps between stacks that cannot be interpreted; what these pages measured on Chrome 150 is the other half of the same problem, and the more common one. Every sample had a stack — including the samples taken while the engine was collecting garbage or running a forced layout, because the function that triggered that work is still on the stack while it happens.

So the time is not missing, it is misattributed: it lands on your function and looks like your function. Profiling four deliberately different workloads here produces four traces with the same two frames and shares within a few points of each other — measured, all six pairs indistinguishable. Adding a marker to each sample — script, gc, style, layout, paint, other — is what tells those four apart.

the API

const profiler = new Profiler({ sampleInterval: 10, maxBufferSize: 10000 });
// … do the work you want to measure …
const trace = await profiler.stop();

for (const sample of trace.samples) {
  // stackId is absent when there was no JavaScript on the stack.
  // marker says what the browser was doing instead.
  console.log(sample.timestamp, sample.stackId, sample.marker);
}

The constructor throws unless the document was served with Document-Policy: js-profiling. That is not a detail — it is why a profiling feature cannot be demonstrated from a static file.

enabling it now

These pages are served with Document-Policy: js-profiling, so the Profiler constructor works and every profile shown is real. Measured on the Chrome 150 used to build them: a 300ms profile produced about 30 samples whose keys were stackId and timestamp — no marker field on any of them. Nearly every sample had a stack too — across repeated runs of all four workloads the largest number without one was a single sample in a layout-thrashing run.

# the response header these pages are served with
Document-Policy: js-profiling

The marker field itself has no flag on this build; --enable-experimental-web-platform-features was measured making no difference. So the demos report what the trace contains and will show the field the moment a browser adds it.

references