Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

151. A frame can say what it did

Date: 2026-08-19

Status

Accepted. The counters that two performance investigations each had to invent from scratch, kept this time.

Context

hud’s stage breakdown (ADR-0146) says which part of a frame is expensive. Twice now the answer to why has been a count rather than a duration — the style cache missing on every element (ADR-0142), and a click invalidating the whole tree (ADR-0149) — and both times finding it meant compiling a counter into the renderer, running a purpose-built probe, and taking it out again.

Decision

-Dgoldberry.trace.frames=true logs one line per frame that did something.

frame 268 | build 0.010 style 8.636 layout 1.115 raster 1.304 ms
  | elements 125, built 0, resolved 3, invalidated 1
  | cascade 5.133 identity 0.956 motion 0.176 boxes 1.467
  | text 17 cached, 6 shaped | damage 3

The counts are what the timings cannot say. resolved 3 beside style 8.6 is a cascade that is slow per element rather than running too often; built 125 is a rebuild; subtree walks: column:ACTIVE -> 61 names the node and the state that threw a subtree away; 6 shaped is text being re-shaped that a cache should have held.

style is broken down further into the four things the render walk does — cascade, identity (restyle and the value comparison that keeps a style’s instance), motion, and boxes (the widgets’ own render, paragraph measurement included). That split is what separated “the cascade is slow” from “the cascade is running too often”, which are different bugs with the same symptom.

Quiet frames are skipped unless =all, because an idle loop at 60 fps otherwise writes a line a frame saying nothing happened, and the frames worth reading are the ones next to the click.

-Dgoldberry.trace.input=true is separate and loud: one line per node per pseudo-class the pointer or the keyboard sets. It is the other half of the same question — that one says what a frame cost, this says what asked for it.

Consequences

Free when off. Every counter sits behind one static final boolean read from a system property, which the JIT folds away entirely. Nothing allocates on a traced frame either, except the map naming subtree walks — and that is only touched when a subtree is actually thrown away.

A system property and not a log level, for ADR-0101’s reason: an isTraceEnabled() per element per frame would be a diagnostic measuring itself.

The timings inside style are taken with nanoTime per element, so a traced frame is slower than an untraced one by four timestamps a node. That is fine for finding a 10× problem and useless for finding a 10% one, and it is the honest limit of a counter you can leave in the shipping build.