Skip to content

perf(viewer): give the initial wall build a 48 ms frame budget - #834

Open
RIRuhAI wants to merge 1 commit into
pascalorg:mainfrom
RIRuhAI:ruh/perf-initial-build-time-budget
Open

perf(viewer): give the initial wall build a 48 ms frame budget#834
RIRuhAI wants to merge 1 commit into
pascalorg:mainfrom
RIRuhAI:ruh/perf-initial-build-time-budget

Conversation

@RIRuhAI

@RIRuhAI RIRuhAI commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

#800 lifted the 8-walls-per-frame cap during the initial wall build but kept the 8 ms time budget, which exists to protect a drag. The initial build has no gesture to protect, and each of its frames also pays a full render of the still-unbatched scene (the level wall batch merges only once a level stops changing), so every extra frame spent draining walls costs a whole scene render.

This gives the initial build its own budget, 48 ms (about three display frames, still two orders of magnitude under a perceptible freeze), checked between walls exactly as before. The interactive tiers (8 ms / 8 walls) are unchanged, and a heavy wall (≥ 6 openings) still takes its own frame. shouldDeferWallRebuild gains an optional initialBuild flag so the test can pin both tiers, and wiki/architecture/systems.md records the budget.

Measurement caveat: the numbers come from an equivalent change (a 48 ms / 128-wall tier for 64+ pending walls) applied to the published @pascal-app/viewer@1.0.0-beta.5, which predates #800 and has no initial-build state; this PR is narrower and applies the wider budget only inside #800's initial build. On a 1,600-wall scene in Chrome (WebGPU on Metal) that change halved time to a ready model in an interleaved A/B (17.6/18.3 s → 8.2/8.8 s) and cut main-thread long-task time roughly 3× (~12 s → 4 s). Not re-taken on this source; happy to re-run on your fixture.

How to test

  1. bun test --cwd packages/viewer src/systems/wall/ — new case in wall-progressive-budget.test.ts; the lifecycle probes now simulate 24 ms per wall (was 4) so the budget exit is still exercised, and the recorded span expectation moves 28 → 68 accordingly. Full viewer suite: 311 pass.
  2. Load a scene with 1,000+ walls with ?perf; __pascalPerf.batchStats().wallDrain shows walls consumed per frame rising from ~6 to ~35 during initial build, and first-clean arriving in a fraction of the frames.
  3. Drag a wall after load: the 8 ms / 8-wall behaviour is unchanged.

A constant is the smallest change. A frame-time-adaptive budget (spend as long as the last render took) would self-tune but needs a render-time signal in the wall system; glad to follow up either way.

Screenshots / screen recording

No visual change; timing only.

Checklist

  • I've tested this locally with bun dev — verified through the package test suites, tsc --build and bun check instead; no editor session was run for this change
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

🤖 Generated with Claude Code


Note

Low Risk
Timing-only change scoped to initial-build hydration; interactive 8 ms / 8-wall limits and heavy-wall rules are unchanged.

Overview
Initial wall hydration now drains dirty walls with a 48 ms per-frame time budget (checked between walls, still no 8-walls/frame cap), while interactive progressive rebuilds keep 8 ms and 8 walls/frame. The 8 ms limit exists to protect drags; initial load has no gesture and pays a full unbatched render each frame, so spending more wall work per frame reduces total frames to readiness.

wallRebuildExitReason / shouldDeferWallRebuild take an optional initialBuild flag to select the budget; heavy walls (≥6 openings) still defer to their own frame. Tests lock both tiers and bump simulated rebuild cost so budget-exit scenarios still match the wider allowance; architecture docs record the split.

Reviewed by Cursor Bugbot for commit d61e07c. Bugbot is set up for automated code reviews on this repo. Configure here.

pascalorg#800 lifted the 8-walls-per-frame cap during initial build but kept the 8 ms
time budget, which is tuned to protect a drag. Initial build has no gesture to
protect, and each of its frames also pays a full render of the still-unbatched
scene (the level wall batch only merges once a level stops changing). Draining
under a 48 ms budget (about three display frames, still two orders of magnitude
under a perceptible freeze) cuts the frames spent rendering an unbatched scene
several-fold. The interactive 8 ms / 8-wall tiers are unchanged, and a heavy
(>= 6 openings) wall still takes its own frame.

Indicative measurement: an equivalent change (a 48 ms / 128-wall tier for 64+
pending walls) applied to the published @pascal-app/viewer 1.0.0-beta.5 —
which predates pascalorg#800 and has no initial-build state — on a 1,600-wall scene in
Chrome (WebGPU on Metal) halved time to a ready model in an interleaved A/B
(17.6/18.3 s -> 8.2/8.8 s) and cut main-thread long-task time ~3x. This PR
applies the wider budget only inside pascalorg#800's initial build; the numbers were
not re-taken on this source.

shouldDeferWallRebuild gains an optional initialBuild flag so the test pins
both tiers; the lifecycle probes simulate 24 ms per wall (was 4) so the budget
exit is still exercised; the systems wiki records the new budget.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pascal

pascal Bot commented Sep 10, 2026

Copy link
Copy Markdown

I hit an error while handling your request (Model unavailable on AI Gateway free tier: Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted…).

Please try again, rephrase, or reach out if it keeps failing.

Error id: 9afeb7d8-0d20-4886-bc87-8ffc58cdcfdb

@Aymericr Aymericr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The code is focused, but the evidence does not yet establish this 48 ms budget on the code being merged. The reported A/B used beta.5 plus a different 48 ms / 128-wall rule; this branch applies an uncapped 48 ms tier inside the newer #800 lifecycle. The test pins the constant and control flow, but cannot tell us whether current main improves time-to-ready without creating visible 50+ ms long tasks (the check occurs between walls, so an individual rebuild can overshoot the budget). Please rerun the real Chrome A/B on current source with the same scene and report at least time to first-clean/ready, maximum main-thread task, wallDrain frames/walls per frame, and one post-load drag check. I’m adding ready-for-human because choosing a deliberate multi-frame main-thread budget is a maintainer performance/UX tradeoff once those measurements are available.

@Aymericr Aymericr added the ready-for-human Needs human input during execution label Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Needs human input during execution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants