Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 deletions apps/benchmarks/src/benchmark/package-sizes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('independent package-size report', () => {
}
});

it('bounds the accepted coverage-capability growth from its pre-coverage baseline', () => {
it('bounds accumulated renderer growth from the pre-coverage baseline', () => {
const coverageGrowth = {
'browser-core': {
rawBytes: { baseline: 324_269, maximumGrowth: 16_000 },
Expand All @@ -86,10 +86,10 @@ describe('independent package-size report', () => {
brotliBytes: { baseline: 173_552, maximumGrowth: 7_000 },
},
'bitmap-runtime-js': {
rawBytes: { baseline: 361_809, maximumGrowth: 20_500 },
minifiedBytes: { baseline: 271_005, maximumGrowth: 13_000 },
gzipBytes: { baseline: 78_673, maximumGrowth: 2_800 },
brotliBytes: { baseline: 60_857, maximumGrowth: 2_350 },
rawBytes: { baseline: 361_809, maximumGrowth: 25_500 },
minifiedBytes: { baseline: 271_005, maximumGrowth: 15_750 },
gzipBytes: { baseline: 78_673, maximumGrowth: 3_450 },
brotliBytes: { baseline: 60_857, maximumGrowth: 2_950 },
},
'mtsdf-baker-wasm': {
rawBytes: { baseline: 534_709, maximumGrowth: 18_500 },
Expand All @@ -104,10 +104,10 @@ describe('independent package-size report', () => {
brotliBytes: { baseline: 4_176, maximumGrowth: 800 },
},
'mtsdf-runtime-js': {
rawBytes: { baseline: 370_255, maximumGrowth: 19_750 },
minifiedBytes: { baseline: 275_271, maximumGrowth: 12_500 },
gzipBytes: { baseline: 79_993, maximumGrowth: 2_800 },
brotliBytes: { baseline: 62_081, maximumGrowth: 2_300 },
rawBytes: { baseline: 370_255, maximumGrowth: 25_650 },
minifiedBytes: { baseline: 275_271, maximumGrowth: 15_600 },
gzipBytes: { baseline: 79_993, maximumGrowth: 3_600 },
brotliBytes: { baseline: 62_081, maximumGrowth: 3_050 },
},
} as const;
const fields = ['rawBytes', 'minifiedBytes', 'gzipBytes', 'brotliBytes'] as const;
Expand All @@ -122,6 +122,32 @@ describe('independent package-size report', () => {
}
});

it('bounds retained-capacity growth from the warm-publication baseline', () => {
const retainedCapacityGrowth = {
'bitmap-runtime-js': {
baseline: { rawBytes: 382_060, minifiedBytes: 283_898, gzipBytes: 81_435, brotliBytes: 63_146 },
maximumGrowth: { rawBytes: 5_200, minifiedBytes: 2_850, gzipBytes: 700, brotliBytes: 650 },
},
'mtsdf-runtime-js': {
baseline: { rawBytes: 389_761, minifiedBytes: 287_629, gzipBytes: 82_721, brotliBytes: 64_286 },
maximumGrowth: { rawBytes: 6_100, minifiedBytes: 3_200, gzipBytes: 850, brotliBytes: 825 },
},
'slug-runtime-js': {
baseline: { rawBytes: 390_276, minifiedBytes: 286_600, gzipBytes: 82_730, brotliBytes: 64_271 },
maximumGrowth: { rawBytes: 9_400, minifiedBytes: 5_050, gzipBytes: 1_300, brotliBytes: 1_275 },
},
} as const;
const fields = ['rawBytes', 'minifiedBytes', 'gzipBytes', 'brotliBytes'] as const;
for (const [id, expectation] of Object.entries(retainedCapacityGrowth)) {
const entry = report.entries.find((candidate) => candidate.id === id);
expect(entry?.status).toBe('measured');
if (entry?.status !== 'measured') throw new Error(`Missing measured size entry: ${id}`);
for (const field of fields) {
expect(entry[field] - expectation.baseline[field]).toBeLessThanOrEqual(expectation.maximumGrowth[field]);
}
}
});

it('keeps the lazy validator out of the initial browser-core measurement', () => {
const core = report.entries.find((candidate) => candidate.id === 'browser-core');
const validator = report.entries.find((candidate) => candidate.id === 'font-validator-js');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
{
"id": "package-sizes",
"path": "apps/benchmarks/src/generated/package-sizes.json",
"sha256": "2e7af35a43c9723209db882d67991a257a06133b3908369b12a8c9692554eb8b",
"sha256": "03038d8c419a7732e80b5ac092eab31362ac05322a207ffe5336f7d9db8bda3b",
"bytes": 6825
},
{
Expand Down
30 changes: 15 additions & 15 deletions apps/benchmarks/src/generated/package-sizes.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,33 +76,33 @@
"label": "Bitmap runtime JS graph",
"status": "measured",
"format": "javascript",
"sha256": "557bdc43359c60abf24af30464e07185d0c291d1e2ff3fd3e8ec131c85e5364b",
"rawBytes": 382060,
"minifiedBytes": 283898,
"gzipBytes": 81435,
"brotliBytes": 63146
"sha256": "4298138fcc2ff76e028a7e00489c32c62704706d5d769127c70105ed56e20049",
"rawBytes": 387215,
"minifiedBytes": 286677,
"gzipBytes": 82062,
"brotliBytes": 63744
},
{
"id": "mtsdf-runtime-js",
"label": "MTSDF runtime JS graph",
"status": "measured",
"format": "javascript",
"sha256": "0bf8890daaf4e203ea8bee82a278a504055738525c1b7be26280c91e77caf131",
"rawBytes": 389761,
"minifiedBytes": 287629,
"gzipBytes": 82721,
"brotliBytes": 64286
"sha256": "1b978b5594e963e8fae7323ea8bcbfe3d1fab7b2fd108b4e6e83ef44cb46483b",
"rawBytes": 395799,
"minifiedBytes": 290777,
"gzipBytes": 83499,
"brotliBytes": 65048
},
{
"id": "slug-runtime-js",
"label": "Slug runtime JS graph",
"status": "measured",
"format": "javascript",
"sha256": "6fd4b4c4350ad3085c1d29546de66c3961bb460a9d77bbe854155b6e1f559cd3",
"rawBytes": 390276,
"minifiedBytes": 286600,
"gzipBytes": 82730,
"brotliBytes": 64271
"sha256": "8ecdc71de5ff5db1e864e12c460ca9836dc39ae90a8e0a6bd9c5ddfc58a95b0f",
"rawBytes": 399585,
"minifiedBytes": 291576,
"gzipBytes": 83968,
"brotliBytes": 65475
},
{
"id": "bitmap-baker-wasm",
Expand Down
11 changes: 9 additions & 2 deletions apps/benchmarks/src/renderer/react-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ async function runReconciliation(resources: ReactTextResources): Promise<TargetR
const drawCount = countDraws(core);
const paintCount = countUniquePaints(core);
if (drawCount !== 1 || paintCount !== 2) {
throw new Error('React Text did not preserve its nested-span draw and paint contract');
throw new Error(
`React Text did not preserve its nested-span draw and paint contract: ${drawCount} draws, ${paintCount} paints`,
);
}
const hash = hashParagraphLayout(restoredLayout);
return {
Expand Down Expand Up @@ -265,7 +267,12 @@ function countUniquePaints(object: CoreText): number {
if (!(child instanceof THREE.Mesh)) return;
const colors = child.geometry.getAttribute('bitmapColor');
if (colors === undefined) return;
for (let instance = 0; instance < colors.count; instance += 1) {
const instanceCount =
child.geometry instanceof THREE.InstancedBufferGeometry ? child.geometry.instanceCount : colors.count;
if (instanceCount > colors.count) {
throw new Error(`React Text submits ${instanceCount} instances from a ${colors.count}-entry paint buffer`);
}
for (let instance = 0; instance < instanceCount; instance += 1) {
paints.add(
[colors.getX(instance), colors.getY(instance), colors.getZ(instance), colors.getW(instance)].join(','),
);
Expand Down
1 change: 1 addition & 0 deletions docs/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 2026-08-01

- **Bounded retained raster capacity** — Completed roadmap item 10.3 by giving Bitmap, MTSDF, and Slug deterministic 25% instance slack capped at 256 glyphs, separate logical draw counts, complete in-place parallel-field replacement, transactional overflow/topology replacement, and shared 32-instance dirty buckets with an eight-range full-upload fallback. Unconsumed Three.js update ranges carry into later stages. Focused tests prove arbitrary glyph replacement, shrink, exact-capacity growth, retained object/buffer identity, abort preservation, overflow disposal, Bitmap/Slug page-run changes, and color-only preservation; existing Text and Slug lifecycle suites remain green. Browser core, every baker host, and every Wasm artifact remain byte-identical. Optional Bitmap, MTSDF, and Slug closures grow by 5,155/2,779/627/598, 6,038/3,148/778/762, and 9,309/4,976/1,238/1,204 raw/minified/gzip/Brotli bytes, respectively, without changing existing absolute budgets.
- **Warm Presentation publication and continuity** — Published resident `Text` replacements from the Three.js matrix-update lifecycle and removed Icon Grid's hide-and-await recycling path: pooled tiles retain their old complete glyph until one coherent warm assignment is staged and published. Ordinary Text Ladder playback keeps visible content while the timed demo alone owns its authored off-screen exit. The persistent host now ignores identical resize requests and keeps its provider-owned canvas attached across replacement-effect handoffs; benchmark DPR no longer keys a subtree remount. Split the DPR-2 evidence lanes so headless pixel sampling cannot flash a visible GPU canvas while the headful timed demo runs without screenshots. Bitmap, MTSDF, and Slug each complete all seven sequential workloads with visible pixels and one renderer; the headful timed sequence completes at 60.02 Icon Grid FPS, returns to Off-axis / 3D, and emits no warnings. A focused regression hides the zero-vertex screen grid during the temporary 1×1 pre-layout surface instead of submitting an empty WebGPU draw.
- **Lifecycle-owned warm publication** — Added synchronous cache peeks for registered font loads, initialized shapers, and current decoded raster resources. Resident `Text` updates now shape, lay out, plan paint, and stage without crossing a Promise boundary, retain the previous generation until Three.js object traversal, and publish before raster children. React Suspense continues to own genuinely cold preparation; the React integration performs no consumer `ready` wait and explicitly invalidates its R3F root after core updates. Asynchronous plugin preparation is carried forward under one abort controller rather than probed and restarted. Adversarial findings led to shared staging logic, paint-only layout/page reuse, abort propagation across multi-font preparation, valid replacement preservation after superseded-font disposal, both Three matrix-update entry points, and defensive sibling traversal after a plugin violates the infallible-commit contract. The complete browser conformance lane proves 3/3 exact R3F reconciliations through explicit renderer traversal and 3/3 exact 68-frame Advanced Shaping timelines with five cold observations, 63 warm lifecycle publications, and zero warm readiness waits per sample; the packed consumer remains exact. Against the preceding transaction layer, browser core grows by 8,070 raw / 5,005 minified / 966 gzip / 795 Brotli bytes; Bitmap, MTSDF, and Slug runtime closures grow by 8,064/4,998/1,055/755, 8,064/4,998/1,065/808, and 8,064/4,997/973/657 bytes respectively. Baker hosts and Wasm remain byte-identical.
- **Renderer-neutral raster transaction** — Started Milestone 10 by replacing the optional Bitmap-only retained-update seam plus separate build/repaint methods with one required `stageBatch` lifecycle for every raster. The portable batch contract no longer imports Three.js; the Three-backed `Text` adapter validates attachment separately. Bitmap, MTSDF, and Slug now share stage/commit/abort ownership, and focused evidence proves success, injected failure, stale abort, idempotent stage transitions, and preservation of the live scene. Against the closed Milestone 6/8 head, browser core grows by 1,747 raw / 1,053 minified / 207 gzip / 144 Brotli bytes; Bitmap, MTSDF, and Slug runtime closures grow by 2,100/1,253/261/253, 2,783/1,524/345/272, and 2,102/1,256/274/274 bytes respectively. Baker hosts and Wasm remain byte-identical. Only the browser-core raw/minified absolute ceilings and the pre-coverage caps for closures containing the new lifecycle advance; compressed absolute ceilings remain unchanged.
Expand Down
6 changes: 3 additions & 3 deletions docs/packages/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Provides the shared interactive and automated benchmark product sur
resource: ../../apps/benchmarks
workspace_package: '@pmndrs/text-benchmarks'
documentation_type: reference
source_digest: 'sha256:e7c99c8d10158636b5eb15a053e1d34ff2f16fa19470e06396a1b46f61897332'
source_digest: 'sha256:de43027a0a1d5e34671a28cc375eee89d8889cab7b2924c970de5a411bf95a51'
tags: [package, benchmarks, react, vite, product-e2e]
sources:
- id: manifest
Expand Down Expand Up @@ -64,7 +64,7 @@ sources:
title: Realtime comparison product probe
generated:
by: openai-codex/gpt-5.6
at: '2026-08-01T15:28:00Z'
at: '2026-08-01T20:19:48Z'
---

# Package reference: `@pmndrs/text-benchmarks`
Expand Down Expand Up @@ -187,7 +187,7 @@ The browser product also carries the React 19 subpath proofs. A shared registry

The initial deterministic browser probe is admitted with a checked-in record: 100 executions across 10 fresh GPU-friendly Chromium/Vite lifecycles, zero retries/failures, unique causal completion identities, and wrong-expectation plus withheld-completion negative controls. Probe exit status and every parsed lifecycle/environment field are validated before publication. Browser scripts navigate only through DOM readiness and then wait on the product's own completion promise or visible state; they do not use network-idle heuristics. Exact contract comparison rejects non-finite numbers, exotic objects, key-order differences, and missing or additional fields without JSON coercion. The current live probe executes the exact TSL graph on asserted WebGPU and forced WebGL2 backends before paragraph measurement, positioned-layout, bidi/policy, CJK, and mobile Playwright flows. This proves a real GPU shader workload while reserving the rendered-font claim for item 6.1.

The final lifecycle remediation revises the reviewed browser-core raw ceiling from 330,000 to 331,000 bytes. Independent pre-coverage caps advance only for browser core and the Bitmap/MTSDF runtime closures that contain it, retaining 13–120 bytes of headroom over the current values. Every absolute minified/compressed budget and every baker-host and Wasm ceiling remains unchanged.
Roadmap item 10.3 leaves browser core, every baker host, and every Wasm artifact byte-identical. Relative to the warm-publication baseline, bounded retained capacity adds 5,155 raw / 2,779 minified / 627 gzip / 598 Brotli bytes to the optional Bitmap closure, 6,038 / 3,148 / 778 / 762 to MTSDF, and 9,309 / 4,976 / 1,238 / 1,204 to Slug. A dedicated regression bounds those increments independently from the accumulated pre-coverage baseline. All three remain below the existing absolute 425,000 raw / 325,000 minified / 95,000 gzip / 75,000 Brotli renderer ceilings, so no absolute runtime, baker-host, or Wasm budget changes.

## Package scripts

Expand Down
Loading