Skip to content

feat: paragraph-scoped synchronous measurement with candidate adoption (11.17) - #75

Draft
thejustinwalsh wants to merge 10 commits into
docs/uikit-measure-guidancefrom
feat/measure-entry
Draft

feat: paragraph-scoped synchronous measurement with candidate adoption (11.17)#75
thejustinwalsh wants to merge 10 commits into
docs/uikit-measure-guidancefrom
feat/measure-entry

Conversation

@thejustinwalsh

@thejustinwalsh thejustinwalsh commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Closes roadmap 11.17: paragraph-scoped synchronous measurement as one retained speculative session transaction — no third result buffer, no revision burn, no publication flips — with next-frame candidate adoption. Recorded as decision D-253.

What a measurement paid before

A measure-only update ran the complete session pipeline: per-paragraph preparation across every paragraph, full policy gather and render-plan compilation, publication packing, the A/B result flip with a generation bump, and a revision advance the renderer never consumed — which forced the next real frame into a checkpoint rebuild. That last hazard was the largest hidden cost for reactive integrations (uikit-style measure-per-frame resize).

The layers

  1. Engine entrypmndrs_text_engine_measure_paragraph(session, request, len, paragraphId) reuses the update request layout, prepares the queried paragraph only, and copies the header + semantic table into the inactive result slot under a host lease: no gather, no plan, no publish, no fence acknowledgment.
  2. Retained speculative transaction — the prepared pending state persists; sequential queries extend it while committed revision, lifecycle fingerprint, and per-paragraph text/style input fingerprints match. Geometry-only follow-ups re-run just the geometry/flow/positioning tail over the retained shaping; identities reserve linearly from transaction high-water marks.
  3. Candidate adoption — the committing frame fingerprint-compares its taken batches per paragraph and adopts matching pending state together with its reserved stable glyph identities; a prefix match re-runs only the geometry tail; anything else prepares cold. The raw-wasm regression proves the frame commits the exact ids the query reported.
  4. Host fast path — core-host session.measureParagraph; Text.measureLayout() routes geometry-only queries through it. A Three-level regression drives four constraint changes: zero full engine updates, unchanged publication generation, non-checkpoint committing frame. The uikit doc replaces its planned-cost note with landed guidance (round-up rule intact).
  5. Evidence — a measure-query benchmark lane beside column-resize (same alternating widths, 22,000-glyph corpus): 1.815 ms median / 1.930 ms p95 / 3.0 % RSD with zero publication bytes, vs the full update's 2.996 ms / 4.483 ms / 21.7 % — the first width-change lane under the 4 ms p95 objective.

Safety

Mutation-checked Rust regressions pin retention, extension, cold rebuild (results identical to fresh preparation), and candidate-paragraph reclamation; an unadopted transaction drops leave-committed, so committed state never observes a query. Interleaved lane A/Bs show no update hot-path delta — frames without a transaction pay one Option check. Net shaper wasm across the stack: +9,413 raw bytes before the follow-up stack's registry flattening banks −9,433.

…ssion

The measure-path cost anatomy, the layer-1 entry contract (paragraph-
scoped synchronous measurement into the inactive result slot without
publication, revision burn, or fence side effects), the retained-
transaction and adoption layers, and the proof obligations. The raw-
Wasm regression is in place and red on the missing measureParagraph
export; the text.md digest rides the extended test harness.
…vidence

The workerRasterKinds routing (0d93370) changed the ABI-embedding JS bundles
and the runtime-bake host/Worker without refreshing the reviewed package-size
evidence. Regenerate it at that baseline so the next wasm change carries only
its own delta.
…g (11.17 layer 1)

pmndrs_text_engine_measure_paragraph reuses the update request layout with the
queried paragraph as an ABI argument. It validates, speculatively prepares, and
emits measurement records for that paragraph only, then terminates
leave-committed: no gather, no plan compile, no A/B flip, no
publication-generation bump, no revision burn, and no renderer-fence
acknowledgment, so the next real frame proceeds from pre-measure revisions
without a checkpoint rebuild. Query results ride the inactive result slot under
a host lease; the plan validator never sees them. The shared per-paragraph
record emission moves to append_paragraph_measurement so update and measure
paths stay one implementation. Shaper wasm grows 5,806 raw / 2,869 Brotli
bytes; the ABI functions table adds 62 raw bytes per embedding JS bundle.
…ial queries (11.17 layer 2)

Measure queries no longer abort their pending state at exit. The session keeps
one SpeculativeTransaction — committed revision, lifecycle fingerprint,
generation, and linear identity high-water marks — and each paragraph records
the text/style input fingerprints its pending prefix was prepared from. A
follow-up query whose fingerprints match re-runs only geometry, flow, and
positioning over the retained prefix through the extracted
prepare_geometry_and_layout tail; identities extend linearly instead of rolling
back between queries. Any mismatch rebuilds cold with results identical to
fresh preparation (generation records the rebuild), and an ordinary frame drops
the transaction leave-committed at entry, so committed state never observes a
query. Mutation-checked Rust regressions pin retention, extension, cold
rebuild, and candidate-paragraph reclamation; the raw-Wasm regression extends
to sequential queries under a real shaper. Shaper wasm grows 2,148 raw / 929
Brotli bytes.
… (11.17 layer 3)

A frame whose lifecycle input matches the retained transaction continues its
identity counters from the reserved high-water marks and, per paragraph,
fingerprint-compares its taken text/style/geometry batches against the
speculative pending state: a full match skips preparation entirely and reports
positioned_changed by the same formula prepare would have used; a prefix match
re-runs only the geometry/flow/positioning tail; anything else prepares cold
through the unchanged stage-abort discipline. The lifecycle fingerprint now
folds in the semantic implicit-paragraph id so a query and a frame addressing
the same structure fingerprint equally. The raw-Wasm regression proves the
contract end to end: identities reserve linearly across two sequential queries
and the committing frame reports exactly the stable glyph ids the adopted query
promised. Update-lane A/B (22k glyphs, interleaved) shows no hot-path delta;
frames without a transaction pay only an Option check. Shaper wasm grows 1,459
raw / 216 Brotli bytes.
…uery (11.17 layer 4)

The core host gains session.measureParagraph beside update: it stages the same
request layout, calls the synchronous engine measure, and decodes the result
from the inactive slot under the host lease with revisions, publication
generation, and the renderer fence untouched. Text.measureLayout uses it when
the only pending work is a geometry change on the measured text and every
other paragraph is quiescent, building exactly the upsert, constraint, and
region bytes the next frame will send so the engine's retained transaction
answers the query and the committing frame adopts it. A Three-level regression
drives four constraint changes through measureLayout and proves zero full
engine updates, an unchanged publication generation, and a non-checkpoint
committing frame; the uikit integration document replaces its planned-cost
note with the landed fast-path guidance and keeps the round-up rule intact.
…e update (11.17 layer 5)

The rust-layout benchmark gains a measure-query lane that answers the
column-resize lane's exact alternating widths through
pmndrs_text_engine_measure_paragraph. At the 22,000-glyph corpus it measures
1.815 ms median / 1.930 ms p95 / 3.0% RSD with zero publication bytes, beside
the full update's 2.996 ms median / 4.483 ms p95 / 21.7% RSD in the same run —
the first width-change lane under the 4 ms p95 objective, recorded as an
authenticated fixture. Roadmap 11.17 closes and D-253 records the retained
speculative transaction design and its adoption contract.
@github-actions

Copy link
Copy Markdown

size-limit report 📦

Path Size
Core JS (gzip) 18.72 KB (+0.13% 🔺)
Shaper Wasm (gzip) 423.35 KB (+1.17% 🔺)
Three.js adapter JS (gzip) 61.41 KB (+0.76% 🔺)
Inter font · Bitmap (gzip) 545.26 KB (0%)
Inter font · MTSDF (gzip) 6.48 MB (0%)
Inter font · Slug (gzip) 604.06 KB (0%)
Font Awesome icons · Bitmap (gzip) 439.53 KB (0%)
Font Awesome icons · MTSDF (gzip) 6.89 MB (0%)
Font Awesome icons · Slug (gzip) 642.64 KB (0%)
Font validator JS (gzip) 134.24 KB (0%)
Runtime bake host JS (gzip) 5.4 KB (0%)
Runtime bake Worker JS (gzip) 142.47 KB (0%)
Font baker JS (gzip) 2.41 KB (0%)
Font baker Wasm (gzip) 379.63 KB (0%)
Bitmap baker JS (gzip) 4.66 KB (0%)
Bitmap baker Wasm (gzip) 228.3 KB (0%)
MTSDF baker JS (gzip) 5.39 KB (0%)
MTSDF baker Wasm (gzip) 209.22 KB (0%)
Slug baker JS (gzip) 4.01 KB (0%)
Slug baker Wasm (gzip) 181.24 KB (0%)

…om a clean tree

The static CI gates have been red since the columns slice: oxfmt rejects
trailing whitespace the editor later stripped locally, the measure-entry test
helper shadowed its inner geometry box under no-shadow, and every source
digest generated since was computed over a worktree whose bytes differed from
the committed tree by exactly that unstaged whitespace — CI hashes the commit,
the generator hashes the disk. Formatting is applied, the helper renames its
outer box, and the digests are re-derived from a tree that matches what CI
checks out.
@thejustinwalsh thejustinwalsh changed the title feat/measure entry feat: paragraph-scoped synchronous measurement with candidate adoption (11.17) Aug 13, 2026
…e tier

The measure tier grew three artifacts past ceilings that were priced
before it: the renderer-neutral core by +2,481 raw / +1,258 minified /
+211 gzip (synchronous measure entry, retained speculative transaction,
host fast path) on top of +1,786 raw / +919 minified of external-raster
routing growth whose evidence refold rides this branch; the shaper wasm
by +9,413 raw / +5,028 gzip / +4,014 Brotli (speculative measure
transactions, candidate adoption, the paragraph query path); and the
Three adapter by +7,096 raw / +3,604 minified (frame adoption and the
measureLayout fast path). Ceilings move to the measured actuals with the
usual tight headroom and the established cross-host compression margin
for the wasm; the CI gate measured the JS bundles byte-identical across
linux-x64 and darwin. The integer-units slices above ride within the
remaining headroom (+360 raw at their tip, wasm net -4,228).
The gate's fresh build at the routing branch measured three-runtime at
370,521 raw / 241,995 minified, so +1,786 raw / +914 minified of the
Three adapter growth belongs to the external-raster routing, not the
11.17 tier; the tier's own share is +5,310 raw / +2,690 minified. The
ceilings are unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant