Skip to content

Overhaul glyph grid loading and scroll pipeline - #161

Merged
kostyafarber merged 4 commits into
mainfrom
perf/glyph-grid-loading
Jul 26, 2026
Merged

Overhaul glyph grid loading and scroll pipeline#161
kostyafarber merged 4 commits into
mainfrom
perf/glyph-grid-loading

Conversation

@kostyafarber

Copy link
Copy Markdown
Collaborator

Summary

End-to-end rework of how the glyph grid loads, schedules, and presents glyphs, driven by layered instrumentation at every stage (Rust bridge → utility host → coordinator → store → grid). Measured start point: opening a ~340-glyph variable font produced 20+ concurrent window reads with 8–25s renderer read times and multi-second white-outs on scroll. End point: cold 306-glyph reads at 245ms, zero-contention scrolling, and a CJK grid (16.7k glyphs) that scrubs smoothly with bounded memory.

Rust / bridge

  • Structural component-divergence scan in shift-font: glyph_projection no longer resolves full component-closure geometry at every master to compare component structure — 306-glyph reads went from 2,160ms to 245ms (master_resolve 1,842ms → 13ms). Characterization tests pin exact_source_components across compatible, sparse, nested, and anchor-attachment cases.
  • getGlyphSnapshots unchanged in contract; GlyphComponents::from_layers now borrows its layer map.
  • New getGlyphPreviews(glyphIds, location): svg path + advance printed at an internal location — 5–15× lighter on the wire than full snapshots (measured 25–35KB/glyph snapshots vs 2–6KB previews on CJK).

Renderer model layer

  • Font.loadGlyphs batches with in-flight dedup via createBatchRequest; redundant settled() double-wait removed from the read path.
  • Glyph object index derived as a lazy tracked computed — snapshot apply cost flat (1–13ms) instead of accreting with residency (was 27→122ms and climbing).
  • GlyphPreviewCache: byte-budgeted (256MiB) location-keyed LRU; reads touch recency.

Grid

  • Two-lane loading: cheap previews chase the live scroll position (latest-window-wins); full models load only after the window holds still (120ms settle). Windows scrolled past are never fetched.
  • Viewport-outward preview warm-up (nextWarmupChunk) — no font-size cliff; large fonts warm a halo around the viewport under the byte budget.
  • Inverse-sticky pinned frame (compositor-enforced CSS sticky, per Pierre's technique): blank scroll regions are structurally impossible; atomic complete-frame display with per-cell placeholders; content-aware row memoization; deferred pending windows.

Housekeeping

  • Instrumentation removed in f6287fde; lessons + reconstruction guide (one git revert) in docs/architecture/glyph-pipeline-performance.md.
  • Formatting switched to oxfmt.

Test plan

  • pnpm check green (Rust workspace + 471 renderer tests, incl. new suites for GlyphPreviewCache, nextWarmupChunk, batchRequest, projection characterization, get_glyph_previews).
  • Manually validated in-app on a ~340-glyph variable font and a 16,742-glyph CJK TTF: first paint, wheel/trackpad/thumb scrolling, design-location scrubbing.
  • Known follow-ups tracked in vault tickets: projection invariants closeout (work counters, I3 short-circuit, no-component fast path), packed outline format spike, canvas grid surface.

🤖 Generated with Claude Code

- rework grid around virtualized rows with batched frame loads; switch
  TypeScript formatting to oxfmt
- batch glyph loads with in-flight dedup (createBatchRequest -> Font.loadGlyphs)
- detect projection component divergence structurally in shift-font:
  306-glyph cold reads 2,160ms -> 245ms; characterization tests pin
  exact_source_components across compatible/sparse/nested/anchor cases
- derive the glyph object index as a lazy tracked computed; snapshot apply
  cost flat instead of accreting with residency
- getGlyphPreviews bridge read: svg path + advance printed at a location,
  5-15x lighter on the wire than full snapshots
- two-lane grid loading: previews chase the live scroll position,
  full models load only after the window settles; latest-window-wins
- byte-budgeted GlyphPreviewCache (256MiB) with viewport-outward warm-up;
  no font-size cliff
- inverse-sticky pinned frame: compositor-enforced, blank scroll regions
  impossible; atomic complete-frame display with placeholder cells
- memoized rows with content-aware comparator; deferred pending windows
- temporary phase instrumentation across bridge, workspace host,
  coordinator, store, and grid (removal tracked in the projection ticket)

Baselines and design: vault notes "Reference - Glyph Pipeline Performance
Baselines 2026-07", "Ticket - Glyph Projection Location-Independence
Invariants", "Ticket - Atomic Glyph Grid Frames and Retained Glyph
Acquisition", "Research - Glyph Grid Native-Feel Rendering at 65k".
Strips the phase timers and console probes used to isolate the glyph
loading bottlenecks: projection_timing in shift-font, get_glyph_snapshots
timers in the bridge, workspace-host handler timers and the payload probe,
[glyph-rpc]/[glyph-apply]/[glyph-read]/[glyph-load]/[glyph-frame]/
[glyph-blank] renderer logs, and the first-frame-ready log.

Revert this commit to restore the full measurement chain; the reading
guide lives in docs/architecture/glyph-pipeline-performance.md and the
recorded baselines in the vault performance reference.
Comment thread apps/desktop/src/renderer/src/components/editor/Canvas.tsx
Comment thread apps/desktop/src/renderer/src/views/Editor.tsx
Comment thread docs/architecture/glyph-pipeline-performance.md Outdated
@kostyafarber
kostyafarber merged commit e694adb into main Jul 26, 2026
11 of 12 checks passed
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