perf(FragmentsModels): bound the invisible-tile cache budget per worker - #286
Open
rihokirss wants to merge 1 commit into
Open
perf(FragmentsModels): bound the invisible-tile cache budget per worker#286rihokirss wants to merge 1 commit into
rihokirss wants to merge 1 commit into
Conversation
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).
This was referenced Sep 4, 2026
perf(FragmentsModels): skip view refresh when the view is unchanged and coalesce forced updates
#283
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #279 — the invisible-tile cache budget.
VirtualTilesController._graphicMemoryConsumedis a per-worker static (module scope inside one worker), butViewManagerhands every worker the globalGPU.estimateCapacity(). So N workers each believe they may cache the whole budget:memoryOverflow()never trips, and sincegetShouldDeleteTileisinvisible && noHighlight && memoryOverflow, invisible tiles are in practice never evicted. Two changes:FragmentsConnection.activeThreadCount);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
.fragbuilding 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.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.setupViewcallsVirtualMemoryController.setCapacity(view.meshThreshold), butmeshThresholdis assigned nowhere in the repository —git grep meshThresholdreturns exactly that one line. So the call issetCapacity(undefined), which hits thevalue === this._capacityearly return (bothundefined) and is a permanent no-op: the LRU stays atdeviceMemory × 100 MBper worker and never learns the app's budget. Wiring it to the (now divided)graphicThresholdwould 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