maniator / maniator/verticopolis
[Bug]: [P2] Per-frame congestion() rebuild is a render-path full-tower scan; memoize spatialCongestionByFloor + functionalParkingSet
Nobody has claimed this yet.
- 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:
- rescans all
tower.unitswithcensusCount(u)per unit to buildpopByFloor(25k units on a real endgame save), - runs the un-memoized
functionalParkingSetflood fill (src/engine/tower/routing.ts:135-162), whose own doc comment assumes its callers keep it off the frame path, - 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
spatialCongestionByFloorandfunctionalParkingSetcompute 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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