Skip to content

feat(fonts): bounded late-load reflow scheduler#3613

Merged
caio-pizzol merged 9 commits into
mainfrom
caio/font-late-load-scheduler
Jun 3, 2026
Merged

feat(fonts): bounded late-load reflow scheduler#3613
caio-pizzol merged 9 commits into
mainfrom
caio/font-late-load-scheduler

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol commented Jun 3, 2026

Stacked on caio/font-load-planner (PR #3612) — review that first; this diff is the scheduler only.

The face-aware planner (#3612) narrows loading to the faces a document renders. This adds the other half of making a large font pack safe: it stops a font-heavy document on a slow network from triggering a full re-measure on every late font wave.

When a required face loads after the gate's first-paint timeout, the document must re-measure so it stops rendering against a fallback. On a slow link a font-heavy doc's faces arrive in many waves (a scaling probe measured ~38 over ~103s for 40 fonts) — reflowing per wave is a re-measure storm. FontReadinessGate now batches late-loaded faces through a small scheduler.

Policy: leading flush + throttled trailing (cooldown). The first late face flushes after a short quiet window (coalescing the initial parallel batch). After any flush a cooldownMs window opens during which further arrivals are deferred; at its end one trailing flush drains them, then the cooldown reopens if more arrive. This bounds the flush rate to ~once per cooldown regardless of arrival spacing — unlike a plain debounce (one flush per wave when waves are spaced wider than the window) or a per-batch max-wait (the quiet flush resets it before it bites). Honest floor: arrivals spaced wider than cooldownMs reflow per arrival — you can't coalesce a wave that lands after the doc already corrected without delaying every correction by at least the gap, so cooldownMs is the max correction lag.

What changed

  • FontLateLoadReflowScheduler.ts: the quiet + cooldown policy; injectable timers; schedule() / flushNow() / cancel().
  • FontReadinessGate: #onLoadingDone records the changed required-face keys and calls scheduler.schedule(...); one flush does bump-epoch + invalidate + reflow. notifyFontConfigChanged reflows immediately and cancels any pending batch (so a stale batch can't fire a second reflow right after). dispose() cancels pending work so a torn-down editor never reflows.

Out of scope (unchanged)

  • First paint is still bounded by the 3s per-font gate timeout — the scheduler only governs after-the-fact corrections.
  • No public font API change; no incremental block-level re-measure; no font pack.

Verified

  • Repo typecheck clean. Unit tests: scheduler policy — burst coalescing, 40 arrivals 500ms apart → far fewer than 40 flushes (the spaced-wave bound), trailing-flush-after-cooldown, dedup, cancel, flushNow, fresh-batch-after-idle; gate integration — batched late loads → one reflow, irrelevant face → none, config-change cancels a pending batch (no double reflow), dispose cancels, notifyFontConfigChanged immediate. Run in CI.
  • Browser: with Carlito delayed past the gate timeout, first paint happens with fallback (not blocked) and the late load produces a single batched reflow.

When a required face loads after the gate's first-paint timeout the document
must re-measure, but on a slow network a font-heavy doc's faces arrive in many
waves (a probe saw ~38 over ~103s for 40 fonts) - reflowing per wave is a
re-measure storm. FontReadinessGate now batches late-loaded faces through a
small scheduler using a leading flush + throttled trailing (cooldown): the first
late face flushes after a short quiet window, then a cooldown bounds the flush
RATE to ~once per cooldown regardless of arrival spacing. Unlike a plain
debounce (one flush per wave when waves are spaced apart) or a per-batch
max-wait (the quiet flush resets it first), this actually bounds the slow,
spaced-out case; arrivals wider than the cooldown reflow per arrival (the
inherent floor / max correction lag). First paint is untouched (3s gate
timeout). notifyFontConfigChanged reflows immediately AND cancels any pending
batch (no double reflow); dispose cancels pending so a torn-down editor never
reflows.
@caio-pizzol caio-pizzol force-pushed the caio/font-late-load-scheduler branch from 6347e2d to 6fe87a9 Compare June 3, 2026 00:20
@caio-pizzol caio-pizzol marked this pull request as ready for review June 3, 2026 11:27
@caio-pizzol caio-pizzol requested a review from a team as a code owner June 3, 2026 11:27
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

Re-trigger cubic

Base automatically changed from caio/font-load-planner to main June 3, 2026 16:02
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

caio-pizzol and others added 3 commits June 3, 2026 13:21
… document swap

Review follow-ups on the bounded late-load reflow scheduler:
- Move the epoch bump + cache invalidation from the batched flush to the loadingdone
  handler so they fire immediately. Measure caches are keyed without the font epoch
  (fontMetricsCache: family|size|bold|italic), so deferring the explicit clear left a
  window where a concurrent re-measure could cache fallback metrics. Only the expensive
  reflow stays batched.
- Reset the gate's late-load state on documentReplaced (new resetForDocumentChange) so
  a flush armed under the old document cannot fire a spurious reflow against the new one.
- notifyFontConfigChanged now also clears the required face/family sets (shared
  #resetRequiredAndSeen), closing a latent redundant-reflow race.
- Wrap the scheduler flush in try/finally so the cooldown always arms and a timer
  callback never leaks an uncaught exception.
- Trim the unused flushNow / 'manual' surface; correct the epoch doc (measure caches are
  not epoch-keyed); make the "no loop" test drain the cooldown so it is not vacuous.
…ocument reset

Two follow-ups on the scheduler review:
- The scheduler flush ran in a try/finally, which armed the cooldown but still let a
  throwing flush escape the timer callback as an uncaught exception. Wrap it in
  try/catch/finally so a throw is swallowed (font readiness must not break layout) and
  the cooldown still arms. Add a test driving a throwing flush.
- resetForDocumentChange left #lastSummary set, so an empty/no-text new document would
  short-circuit to the prior document's load summary. Clear it in the reset helper; add
  a test.
@caio-pizzol caio-pizzol merged commit 02f35fa into main Jun 3, 2026
68 checks passed
@caio-pizzol caio-pizzol deleted the caio/font-late-load-scheduler branch June 3, 2026 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants