zgui
Performance

The cost model

What each kind of change costs, what that cost is proportional to, and the measured numbers behind every claim.

The framework's central claim is that cost follows change. This page states what each kind of change costs, why, and what the repository measures it at. It assumes the guide and reads better after Architecture overview.

How to read the numbers

Every figure here comes from docs/performance.md in the repository, which is generated by cargo xtask perf rather than typed in. A number that moved moved that document with it.

That file is regenerated on every run, so the exact figures below drift by a few per cent from one run to the next. Read them as the order of magnitude and the shape of the cost. The band and the budget are the stable part, and the generated file is always the current answer.

Unless a row says otherwise, a measurement was taken headless, on a virtual clock, with a null renderer, over the gallery document at 1 851 boxes and 2 363 fragments.

Each measurement carries a band and, sometimes, a budget.

  • A band is a baseline and a tolerance. A duration is a property of the machine as much as of the code, so a band has to be wide.
  • A budget is what the design was supposed to cost. It does not move with the measurement. It is the statement a band cannot make: that a number is not merely no worse than it was.
  • A count band has no tolerance at all, because a count is a property of the design. It reads the same on a slow machine, a fast one and under a debugger.

The counts are the sharper half of the suite. Every defect these cases exist for was the same kind of thing: a stage doing the whole document's work for one element's change. That shows up in a count long before it shows up in a time.

The table

What changedMeasuredBandBudget
Nothing, one turn of a parked loop0.07 µs2.00
Nothing, frames drawn0.000.000.00 met
One pointer crossing, 1 000-row table207.78 µs266.00500.00 met
…primitives emitted by it41.56200.00200.00 met
One class toggled on one element11.34 µs15.12
One keystroke: one edit, one paragraph, one box301.39 µs407.40
One turn over an untouched but animating window23.31 µs30.80
One scroll translation frame44.18 µs61.601000.00 met
…restyles, relayouts, hit-index rebuilds0 / 0 / 00 each0 met
One scroll recycle frame, 10 000-row virtual list1021.42 µs1442.001500.00 met
One frame of the framework's own scroll animation973.26 µs854.00
One window resize step7.36 ms10.25
Cold start to first frame103.31 ms140.00250.00 met

Every measurement with a budget met it.

Idle

Zero frames. A still document draws nothing. idle.frames has a ceiling of zero, so one frame here is the whole defect.

A turn of the parked loop — the loop waking, finding nothing to do, and parking again — is 0.07 µs.

An animating window is a different thing. A CSS animation keeps the loop at the output's refresh rate, and one such turn over an otherwise untouched 1 851-box document is 23.31 µs.

The loop parks with one of three policies: block until something arrives, block until a deadline, or spin. A deadline already in the past collapses to a plain block, because installing it would produce a loop that wakes instantly and repeats for ever.

Hover, and any single class toggle

Cost: proportional to the ink of the elements whose style changed.

The path, in order:

The input system finds two elements: the one the pointer left and the one it entered.

Those two are restyled. Nothing else owes a restyle.

The paint key moves, so each owes REPAINT and nothing more. No relayout is implied.

The fragment pass enters those two boxes.

Damage is their ink. The emit walk reaches only fragments that intersect it.

Measured over a 1 000-row table at 120 Hz crossings: 207.78 µs per crossing, emitting 41.56 primitives per frame against a ceiling of 200.

A control measurement over the 1 851-box gallery is sharper still: one element restyled, 11 of 2 363 fragments diffed, 0 rebuilt, 2 relaid out, 2 primitives emitted, damage of 2 628 pixels — 0.16 % of the surface — and one frame (docs/perf/gallery-interactions.md).

No box is rebuilt. No glyph is rasterised. A click that toggles one class on one element is 11.34 µs.

A signal write that changes one text node

Cost: one paragraph reshaped, one box repainted.

Measured as kitchen.keystroke at 301.39 µs over 1 851 boxes. The work is exactly what the name implies: one edit, one paragraph shaped again, one box painted again.

The dominant term is text shaping, not the document mutation. Changing a number from 41 to 42 costs a paragraph shape; changing a colour does not.

"One paragraph" is not a constant cost. Shaping is proportional to the generated text bytes, and line construction and glyph placement are proportional to the paragraph's output. A large text replacement can therefore dominate one frame even though only one paragraph needs shaping. TextBytesShaped records that input volume beside TextShaped.

The flattened context computes its ParagraphKey once, so layout probes at several widths do not hash the large string again. The old shaped version remains an inactive cache entry until LRU eviction needs space. Crossing the shaping-cache level removes inactive versions and does not cause a whole-document reshape or relayout.

A hole that produces the same string writes nothing. Text nodes remember their last value, so the document is not touched and nothing below is invalidated. A closure that runs and produces an unchanged result costs the closure and no more.

A colour change

Cost: repaint only. Layout is not entered.

A declaration classified as a repaint cost yields empty layout damage, so the element owes REPAINT from the paint-key comparison and nothing else. The box tree, the layout, the fragment geometry and the shaped text are all still valid and are all kept.

This is why the guide keeps saying that changing a colour costs less than changing a width. It is not a rule of thumb; the style engine classifies the two differently and the second one reaches two more stages.

A size change

Cost: relayout of the box, its descendants, and its ancestors up to the first whose size does not depend on it.

A geometry cost yields RELAYOUT_BOX, plus text bits narrowed by the element's own shaping key. The box tree is kept. Marking throws away cached layouts along the path to the root, the layout gate then fails, and a pass runs over the dirty region.

No box is rebuilt. The difference between "relayout" and "rebuild" is the whole distinction here.

Adding or removing a list row

Cost: the boxes of that element, spliced. Not the document.

A structural change marks REBUILD_BOX on that element and on its subtree, and the box builder splices that element's boxes rather than rebuilding the tree.

Measured through the scroll scenario's recycle frames: one row shift rebuilds 8 boxes, relays out 29 nodes, and emits 404 primitives. The 8 and the 29 are the row that arrived. The 404 are the viewport, and they are what a recycle frame's remaining cost is.

A recycle frame over a 10 000-row virtualised list is 1021.42 µs.

Scrolling

There are two kinds of scroll frame, and they cost very different things.

A translation frame

Cost: 0 restyles, 0 relayouts, 0 hit-index rebuilds. All three are ceiling-zero gates, so any one of them becoming non-zero fails the suite.

Moving content changes no computed style and changes no size. Four mechanisms service the frame:

  • the fragment differ offsets a moved subtree instead of composing it again;
  • the hit index is carried with the content and settled once, never rebuilt from it.
  • the runtime verifies that one opaque, unobscured port moved rigidly by whole device pixels;
  • the renderer copies valid pixels in its persistent target and draws only exposed bands and other damage.

If the port cannot be shifted safely, the same frame uses normal damage and drawing. Measured at 44.18 µs when the shift path applies.

The per-box cost of the offsetting walk has been split (docs/perf/glide-split.md, on an unvirtualised probe): the whole glide pass is 107.6 ns per box, of which the fused walk is 73.1 — traversal 12.4, memory faulted in 39.3, rectangle and clip arithmetic 21.6, hit entries and moved marks 13.6 — plus a deferred hit-index settle at 35.5 ns per box. The verdict was 0.30 arithmetic to 0.70 index, stable to ±0.01 over five runs.

A recycle frame

A frame in which rows enter or leave the viewport. That is the 1021.42 µs case above.

An animated scroll

One frame of the framework's own glide over the gallery is 973.26 µs.

Resizing the window

Cost: everything. A resize genuinely invalidates every box and damages the whole surface.

Measured at 7.36 ms over 1 851 boxes. This is the one interaction the design does not make cheap, because there is nothing to keep.

Two mitigations exist:

Pacing. A configure that arrives less than one frame of the output after the last resize frame is recorded and not answered. The obligation stays pending and is discharged at the merged deadline. Measured on a real desktop (docs/perf/resize.md):

75 Hz output, beforeafter240 Hz output, beforeafter
configures delivered per second400314403529
frames drawn per second40075.1402233
surface configures per second40075.1403234
frames drawing a superseded size0000
command to the frame that drew it, p502.3 ms6.4 ms2.8 ms3.0 ms

The added latency is bounded by one frame of the output, and it buys back four fifths of the pipeline runs, swap-chain rebuilds and buffer commits on a 75 Hz output.

Idempotence. A resize that repeats the extent runs no frame at all. The repository asserts this by name.

Cold start

103.31 ms to the first frame, headless. On screen with a real graphics device it is 216–247 ms; the difference is the device, deliberately excluded from the gated number.

What is inside it: font enumeration, sheet parsing, the first cascade, the first layout with no cached measurement, and the first emission of every primitive.

A scale-factor change

Cost: misses in both caches that are keyed on device size.

A glyph key carries the size it was rasterised at, and a paragraph key carries the ratio it was shaped at. Moving a window to a display with a different scale factor therefore misses both, and every box's cached layout is thrown away.

Measured on the gallery (docs/perf/scale.md): its 483 placed glyphs cost 218–230 rasterisations on every move to a ratio it had not been at before, and zero on the move back to 1.0, because the entries for 1.0 were still there.

Where a frame's cost actually is

Primitive counts per scenario, from the same generated document:

ScenarioEmittedCulledBounds-tree inserts
idle000
hover storm9 975241 9169 975
scroll121 02029 242121 020
cold start8444 575844
kitchen sink114 169171 683114 169

The hover row is the one to read twice. Over the whole storm, 96 % of primitives considered were culled against the damage rectangles before reaching the renderer.

Wall-clock budgets

A second suite runs in release, behind a feature flag, and asserts absolute durations against generous ceilings. Its purpose is not to detect a machine two times slower; it is to detect a stage going back to doing the whole document's work.

InteractionCeilingMeasured on the maintainer's machine
a click3 ms0.058 ms, over a 136-box document with 28 lines of text
a hover pair3 ms0.066 ms
a resize step8 ms0.78 ms

The ceilings are roughly fifty times the measurement, because what they are looking for is the regression that took a click back to 36 ms by rebuilding the box tree.

Counts are asserted beside the times:

  • a click places zero glyphs and rasterises zero;
  • a hover places zero and rasterises zero;
  • a resize re-places the whole document, as it must, and rasterises fewer than a tenth of the document's glyphs over 32 full repaints;
  • a click runs exactly three frames — move, press, release — and not more.

The one-line summary

ChangeReachesProportional to
nothingnothing
a colour, a shadow, a border colourrestyle, paint, drawthe ink of the changed elements
a class togglerestyle, paint, drawthe same
a text valuerestyle, text, layout, paint, drawthat paragraph's bytes, lines and glyphs
a width, a padding, a gaprestyle, layout, paint, drawthe subtree, up to the first fixed ancestor
a row inserted or removedbox tree, layout, paint, drawthat row, plus the viewport
a scrollfragment offset, paint replaythe boxes in the viewport
a window resizeeverythingthe whole document
a scale-factor changeeverything, plus both device-keyed cachesthe whole document

Next

On this page