Skip to content

perf(app): eliminate tab-switch stall with background refresh and instant reveal#24

Merged
simota merged 3 commits into
mainfrom
perf/tab-switch-reveal-stall
Jul 17, 2026
Merged

perf(app): eliminate tab-switch stall with background refresh and instant reveal#24
simota merged 3 commits into
mainfrom
perf/tab-switch-reveal-stall

Conversation

@simota

@simota simota commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

Switching to a tab with heavy output visibly froze for a frame: occluded windows stop redrawing, so the first frame after reveal paid a synchronous full-viewport rebuild (measured 93ms cold-atlas / 38ms warm at 200x60 vs a ~16ms frame budget — see the new headless bench).

  • Throttled background cache refresh — dirty occluded windows refresh the pane cache/atlas at most once per 250ms app-wide (fairness-rotated, pty-event-driven + one trailing wake-up; fully idle when quiet), without touching the 1x1 occluded swapchain. Refreshes that can't reuse the row cache incrementally are skipped to keep the event-loop cost bounded; their consumed damage is deferred via pane invalidation, never lost.
  • Instant reveal — the first redraw after Occluded(false) presents the cached instances and defers the (now mostly incremental) rebuild to an immediately scheduled follow-up frame, with strict reuse guards (live viewport, build-time viewport, pane set, atlas identity/eviction generation, atlas stability) and damage carry-forward on re-occlusion.

Reviewable commit-by-commit: renderer guards → baseline bench → app wiring.

Test plan

  • cargo test --workspace (959 noa-app + 124 noa-render lib + GPU suites, executed on a real adapter), cargo clippy no new warnings, cargo fmt --check clean
  • New regression pins for every reviewed hazard: viewport mismatch, pane-set change while occluded, shared-atlas eviction/instability, damage carry-forward (fast path, re-occlusion, mixed-pane skip), throttle/fairness/trailing wake-up decisions
  • Interactive: NOA_TAB_SWITCH_TRACE=1 cargo run -p noa — reveal lines show (fast-path, rows_rebuilt=0), bg-refresh lines show throttled background updates

simota added 3 commits July 17, 2026 18:16
…redictor

Groundwork for reusing cached pane instances across occlusion cycles:

- cached_frame_matches_viewport: a cached frame is reusable only when the
  live viewport, the viewport the layout was built against, the current
  pane set (PaneId + rect), and the shared glyph atlas identity/eviction
  generation all still match, and the frame is not atlas-unstable.
- invalidate_pane: clears a pane's row-cache key so its next rebuild is
  unconditionally full (reads every row from the live snapshot,
  independent of consumed row-dirty bits).
- pane_rebuild_would_be_full: read-only predictor replicating
  rebuild_pane_cached's exact full-rebuild decision; the shared
  frame_invalidation_key_matches extraction keeps the two from drifting.
Measures the cost a busy tab pays on its first frame after a long
occlusion (full viewport rebuild, cold vs warm glyph atlas) on a real
GPU adapter; skips when none is available. Baseline on Apple Silicon at
200x60: ~93ms cold / ~38ms warm vs a ~16ms frame budget — the numbers
motivating the background-refresh + instant-reveal design.
…tant reveal

Switching to a tab with heavy output froze for a frame: while occluded,
redraw() early-returns, so the pane render cache goes fully stale and
the first frame after reveal rebuilds the whole viewport synchronously
(93ms cold-atlas / 38ms warm-atlas at 200x60, per the baseline bench).

- Throttled background cache refresh: dirty occluded windows refresh the
  pane cache and glyph atlas at most once per 250ms globally (one window
  per interval, least-recently-refreshed first), driven by pty output
  events plus a one-shot trailing wake-up per throttle window (fully
  idle again once the dirty backlog drains), without touching the 1x1
  occluded swapchain. A refresh that could not reuse the row cache
  incrementally (e.g. the tab scrolled past a whole viewport) is skipped
  to keep the event-loop cost bounded; its panes are invalidated so the
  consumed row damage is deferred to the next full rebuild, never lost.
- Instant reveal: the first redraw after Occluded(false) presents the
  cached instances (when the renderer-side guards allow reuse) and
  defers the now mostly-incremental rebuild to an immediately scheduled
  follow-up frame. The fast-path frame stashes its captured snapshots so
  the follow-up rebuilds against the exact damage it consumed, then
  schedules one more redraw to pick up output that landed in between.
  Re-occluding before the follow-up frame invalidates the stashed
  panes' row caches so the consumed damage is never lost.
- NOA_TAB_SWITCH_TRACE=1 env-gated tracer logging the reveal-to-present
  breakdown and background refreshes.
@simota
simota merged commit 23a954e into main Jul 17, 2026
1 check passed
@simota
simota deleted the perf/tab-switch-reveal-stall branch July 17, 2026 09:45
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