maniator / maniator/verticopolis

[Bug]: [P2] Per-frame congestion() rebuild is a render-path full-tower scan; memoize spatialCongestionByFloor + functionalParkingSet

Open
#338 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
TypeScript
Stars
5
Forks
1
PR merge metrics
No merged PRs in 30d

Description

Rescoped P3 to P2 by the full repository audit (2026-07-21, finding AUD-014; report at _bmad-output/planning-artifacts/reviews/full-repository-audit-2026-07-21/). This issue's original "measure first" gate is satisfied, and the measurement moved it from a sim-side suspicion to a confirmed render-path defect.

What happens

TowerEngine.tick() calls this.sim.congestion() every frame (~60 Hz) at src/render/excalibur/TowerEngine.ts:363 (plus peakCongestionHotspot() at ~6 Hz from trafficHud.ts). Under the default v2 sim model that is spatialCongestionByFloor (src/engine/sim/congestion.ts:124-198), which is not memoized and per call:

  1. rescans all tower.units with censusCount(u) per unit to build popByFloor (25k units on a real endgame save),
  2. runs the un-memoized functionalParkingSet flood fill (src/engine/tower/routing.ts:135-162), whose own doc comment assumes its callers keep it off the frame path,
  3. walks every transport span with stopsAt = skipFloors.includes(floor), O(span x skipList) per shaft.

Impact

Dominant per-frame CPU cost on large towers, and the primary driver of the audit's measured 11 -> 5 -> 2 FPS scaling across 40 -> 7.5k -> 25k units on the owner's real saves (measured with and without GPU confounds; the CPU share is confirmed). Every big imported 1994-era tower hits this.

Fix

Memoize spatialCongestionByFloor and functionalParkingSet on the key set the engine already maintains: (tower.revision, tower.mealOverlayRevision, outer-step token). Inputs are exactly layout (revision), censusCount/customersIn (every change bumps mealOverlayRevision), occupants (hourly), and rushFactor (clock-window step). In-repo precedent: Crowd.queueView's (step, revision) memo (src/engine/Crowd.ts:179-191). A beginStep-invalidated cache gives frame reads a snapshot at zero staleness cost; the once-per-hour call inside updateSatisfaction stays correct for free.

Acceptance criteria

  • spatialCongestionByFloor and functionalParkingSet compute at most once per (step, revision) regardless of frame rate; a unit test pins the call count under repeated frame reads.
  • Congestion values are byte-identical to the un-memoized implementation across a golden-master run (no behavior change).
  • Before/after frame-cost measurement on the sixseven_15 save (25,217 units) recorded in the PR.
  • The heatmap/overlay and traffic HUD read the same snapshot within a frame (no torn reads).

Required tests

Call-count memo pin (unit), golden-master equivalence (integration), and the existing perf gate.

Engine hot path work: /gds-code-review.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at src/render/excalibur/TowerEngine.ts:363, then read spatialCongestionByFloor in src/engine/sim/congestion.ts and functionalParkingSet in src/engine/tower/routing.ts. Review the (step, revision) memo precedent in src/engine/Crowd.ts:179-191 and the required unit, integration, and perf tests. Done means repeated frame reads compute once per step and revision, preserve golden-master values, and share one snapshot across the heatmap and traffic HUD.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
game-dev, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.