Skip to content

Show the city at ~25s instead of ~3min on a big cold load - #150

Merged
thalida merged 5 commits into
mainfrom
feat/issue-147-layout-stem-perf
Aug 8, 2026
Merged

Show the city at ~25s instead of ~3min on a big cold load#150
thalida merged 5 commits into
mainfrom
feat/issue-147-layout-stem-perf

Conversation

@thalida

@thalida thalida commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Cold-loading a big repo took ~3 minutes with nothing on screen. The city is now visible at ~25s, and the scan keeps streaming behind it.

All numbers are torvalds/linux (93,455 files, ~1.43M commits), scanned with no_cache=true against a clone already on disk, same machine.

Before / after

Milestone Before After
First manifest on the wire 143.8s, 358.3 MB 2.8s, 38.6 MB
Real building heights streamed 169.3s 21.7s
City visible (overlay lifts) ~170s ~25s
Full manifest incl. commits 169.3s 171.5s

What changed

The git history walk no longer blocks the first paint. It was ~135s of the load and ran before anything else, but the packer only needs structure + per-file byte size (exactly what layout_signature hashes). The scan now emits in three stages: skeleton (structure, fs dates, no commits) → metadata (real per-file lines, the emit worth revealing on) → complete (history: commits, git dates). _apply_git_dates overlays history dates onto the tree and its directory rollups when the walk finishes; both signatures are date-free, so the frontend keeps the layout it packed from the skeleton.

The skeleton carries no commits. They're 317.9 MB of the 358.5 MB payload (89%) and nothing the skeleton draws reads them. First payload drops 358 MB → 38.6 MB, and the commit-derived passes in _wrap_manifest (compute_repo_stats, _annotate_same_day_totals over 1.43M commits) run once instead of twice.

Each manifest declares pending stages (["metadata","history"]["history"][]), so consumers know which fields are provisional instead of guessing. Cache schema bumped to v23.

The overlay lifts on real heights, not on stream end. The pump records the applied manifest's pending on SCAN_PROGRESS (written after the apply, so it can never run ahead of REBUILD_STATUS); the overlay reaction holds while metadata is owed, lifts when the real-heights paint lands, and stays down while history streams behind the live city. Trees + timeline pop in when history arrives (~170s).

Redundant work removed. The signature stat-walk of all 93k files now runs only when there's a cache to check (a no_cache scan paid for it twice — scan_tree computes the same digest as it walks). _build_tree's pending_entries.pop(0) was O(entries²) per directory; now a cursor. isMirrorInvariant rejects asymmetric extents in O(n) before its O(n²) pair search (128ms → 35ms at 93k — the one layout-solver win that survived measurement; the issue records everything that didn't).

Verification

  • API: 369 passed, incl. a new guard that the skeleton emits before _collect_git_history runs (verified red without the reordering) and that skeleton/final layout_signature match.
  • Frontend: 2877 passed, incl. reveal-ordering tests and a regression test for the repo-switch case (a previous repo's finished manifest must not skip the overlay).
  • Layout + decoration goldens: unchanged (bit-identical output).
  • End-to-end timeline above reproduced with the branch's API serving the real Linux clone.

Out of scope

The complete manifest is still 358 MB because it carries all 1.43M commits; slimming that changes the timeline/decorations contract — split out as its own issue.

Closes #147

🤖 Generated with Claude Code

thalida and others added 5 commits August 8, 2026 13:55
A subtree grows away from its origin along the mirrored axis, so its rect
list can never be symmetric about 0 — but isMirrorInvariant only learned
that after an O(n²) scan over up to ~1000 rects. An extent check settles
those cases in one pass, and hoisting the mirrored coordinates out of the
inner loop cuts the remaining work.

93k-file layout: isMirrorInvariant 128ms → 35ms.

Refs #147

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On linux the history walk is ~135s of a ~180s cold load, and it ran before
the tree walk, so the first manifest waited on all of it. Nothing the
skeleton draws needs it: the packer keys off structure and per-file size,
which is exactly what layout_signature hashes.

The walk now records filesystem dates, the skeleton goes out right after
it (~10s), and _apply_git_dates overlays history onto the tree and its
directory rollups when that walk finishes. The skeleton also carries no
commits, which are 318 MB of the 358 MB linux payload and which nothing in
it reads — so the commit-derived passes in _wrap_manifest, previously run
once per manifest, now happen once.

The signature pre-walk moves behind the cache check too: it stat-walks the
whole tree to build a key that scan_tree computes anyway, so a no-cache
scan paid for the walk twice.

Refs #147

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The city app moved under app/src/, constants/ is gone, and layoutV4.ts no
longer exists — layout now lives in app/src/city/layout/. The bit-identical
snapshot contract still holds, so it moves with the path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The overlay waited for the whole stream, so a finished city sat hidden
behind the git history walk — minutes of it on a big repo, for data that
only draws trees and the timeline.

Per-file metadata now runs before the history walk and gets its own emit,
so the scan streams structure, then real building heights, then history.
Each manifest declares what is still to come in `pending`, and the overlay
lifts once a manifest with no pending metadata has painted.

On linux that puts the city on screen at roughly 30s instead of ~170s,
with trees and the timeline filling in when history lands.

Refs #147

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gnal

The reveal check read MANIFEST, which still holds the previous repo's
finished manifest when a new load starts — so on any repo switch the
"metadata resolved" test passed instantly, the reveal latched, and the
overlay never showed. Two latches and a misnamed helper were papering
over that stale read.

The pump now records the applied manifest's `pending` on SCAN_PROGRESS,
written after the apply so it can never run ahead of REBUILD_STATUS, and
absent until this load's first partial applies so nothing stale can leak
in. The overlay reaction is back to one state bit: hold while the applied
manifest still owes metadata, lift when the real-heights paint lands,
stay down while history streams behind the live city.

Refs #147

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thalida thalida linked an issue Aug 8, 2026 that may be closed by this pull request
@thalida
thalida merged commit 44dad1c into main Aug 8, 2026
1 check passed
@thalida
thalida deleted the feat/issue-147-layout-stem-perf branch August 8, 2026 20:59
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.

Perf: cut the first-load wait on a big repo (3 min on Linux)

1 participant