Skip to content

perf(FragmentsModels): bound the invisible-tile cache budget per worker - #286

Open
rihokirss wants to merge 1 commit into
ThatOpen:mainfrom
rihokirss:perf/c4b-per-worker-tile-budget
Open

perf(FragmentsModels): bound the invisible-tile cache budget per worker#286
rihokirss wants to merge 1 commit into
ThatOpen:mainfrom
rihokirss:perf/c4b-per-worker-tile-budget

Conversation

@rihokirss

Copy link
Copy Markdown
Contributor

Part of #279 — the invisible-tile cache budget.

VirtualTilesController._graphicMemoryConsumed is a per-worker static (module scope inside one worker), but ViewManager hands every worker the global GPU.estimateCapacity(). So N workers each believe they may cache the whole budget: memoryOverflow() never trips, and since getShouldDeleteTile is invisible && noHighlight && memoryOverflow, invisible tiles are in practice never evicted. Two changes:

  • divide the threshold by the number of workers currently hosting a model (new FragmentsConnection.activeThreadCount);
  • cap estimateCapacity() at 1 GB — the screen-size heuristic (width × height × dpr² × 200) reaches ~6.6 GB on a 4K hidpi display. The budget only bounds the cache of invisible tiles; visible geometry is never evicted, so the worst case of a bounded cache is re-uploading tiles when the camera comes back to a previously culled area.

Measurements

Headless Chromium, Radeon 780M (Mesa), 1920×1080, 16 cores → the worker pool caps at 13. Medians of 3 runs. Scenes: 8 models (one 38 MB .frag building model converted from IFC, plus seven interior models of the same building) and 16 models (that plus three more disciplines). Each candidate was measured directly after an unpatched baseline run, because this box throttles: the same unpatched build measured 35 minutes later reports +39 % render time on the 16-model scene while the tile counts stay identical to within 0.5 %. Counts below are therefore exact; frame-time deltas are only meaningful against the adjacent baseline.

8 models 16 models
render ms, idle / orbit −0.4 % / +0.4 % −0.8 % / +0.5 %
FPS, idle / orbit +0.6 % / −0.5 % +2.4 % / −0.3 %
tile meshes + GPU geometries alive after orbiting 1 874 → 1 704 (−9 %) 2 550 → 1 970 (−23 %)
settle after orbit +3.6 % +7.8 %

On top of #283 (the same two runs back to back, which is how they would ship together): tiles and geometries −9.9 % / −23.0 %, frame time neutral to slightly better, group-load wall time −5 % on the 8-model scene. The one real cost appears there: settle after a large camera move on the 16-model scene goes 740 → 962 ms, because evicted tiles have to be rebuilt when the camera returns. That is the trade this knob is for.

An older measurement of the same change on 13 models after 10 s of orbiting: tile meshes 3 872 → 1 595, GPU geometries 1 838 → 876, main-thread JS heap 148 → 47 MB.

Screenshots of six fixed camera poses are pixel-identical to the baseline.

One thing this PR does not fix

VirtualTilesController.setupView calls VirtualMemoryController.setCapacity(view.meshThreshold), but meshThreshold is assigned nowhere in the repository — git grep meshThreshold returns exactly that one line. So the call is setCapacity(undefined), which hits the value === this._capacity early return (both undefined) and is a permanent no-op: the LRU stays at deviceMemory × 100 MB per worker and never learns the app's budget. Wiring it to the (now divided) graphicThreshold would change the cache size materially and deserves its own measurement, so I left it out rather than bundling it here. Happy to do it as a follow-up if you want it.


I first assumed this needed #283 merged before it made sense, and said so there; re-measuring with an interleaved baseline showed it is neutral on frame time on its own, so the two are independent and can go in either order.

🤖 Generated with Claude Code

VirtualTilesController tracks graphic memory in a static counter, which
is per worker — but the graphicThreshold sent from the main thread was
the full global GPU estimate. With the worker pool distributing N
models over N workers, each worker independently allowed the full
budget, so the cache of invisible tiles could grow to N x capacity and
eviction effectively never happened. On top of that, the screen-size
heuristic (width x height x dpr^2 x 200) explodes on hidpi displays: a
4K screen at devicePixelRatio 2 yields ~6.6 GB per worker.

Changes:
- The main thread divides the global estimate by the number of active
  workers (new FragmentsConnection.activeThreadCount) so the combined
  cache stays within one global budget regardless of model count.
- GPU.estimateCapacity() is capped at 1 GB. The budget only bounds the
  cache of invisible tiles — visible geometry is never evicted — so the
  cost of a tighter cap is at most some re-uploads when the camera
  returns to a previously culled area.

Measured with 13 real IFC-derived models after 10 s of orbiting
(headless Chromium): tile meshes 3872 -> 1595, GPU geometries
1838 -> 876, main-thread JS heap 148 MB -> 47 MB, with no change in
settle time after the camera stops (~65 ms both).
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