perf(streaming): item 3 bought nothing, item 5 removed dead weight -- both worth knowing
Two WORK-NEXT items, one null result and one confirmed removal. Item 4 untouched.
ITEM 3 -- CANCEL OBSOLETE WORK AND PRIORITISE THE FLOOR: NULL RESULT
Clean -nullrhi, diagnostics off, 800 cm/s, 200 m of travel:
before after
request->ready p95 0.369117 s 0.367612 s
queue wait p95 0.066949 s 0.064483 s
generation p95 0.287926 s 0.283642 s
p95 travel distance 2.952938 m 2.940894 m
obsolete aborts 0 12
Flat. Aborting obsolete tiles, sorting from the true pawn position and prioritising the
occupied/next tile changed request-to-ready by 0.4%.
⭐ AND THE NULL RESULT IS ITSELF THE FINDING. Look at the split: queue wait is 65 ms of a
368 ms p95; generation is 284 ms. **The queue was never the problem, so reordering it
cannot help.** Only 12 tiles in a 200 m run were obsolete enough to abort. The floor is
late because generation is slow, not because work is badly ordered -- item 3 establishes
that by exclusion, which is worth more than the 0.4%.
The machinery is kept: it is correct, it costs nothing, and it will matter once
generation is fast enough for ordering to be the binding constraint.
ITEM 5 -- VERDICT CACHE REMOVED. It was pure overhead.
Clean moving-session audit:
outer calls/probes/hits 2,109 / 2,109 / 0
nested calls/probes/hits 19,904 / 19,904 / 0
enclosing hits 0
stores 15,877, evictions 15,365
⇒ It stored 15,877 proofs, evicted 15,365 of them, and hit ZERO times. A moving player
visits unique regions; the cache was filling and thrashing a 512-entry table under a
global lock for no benefit. Same-binary A/B: request p95 0.367645 vs 0.365978,
generation p95 0.285559 vs 0.285992 -- neutral within noise, so its cost was the lock and
the linear scans rather than time we can see.
Removed: the global cache, its lock, the entry table, the invalidation path, the
cache-only profiler fields and the synthetic probe. The proof classifier stays.
⚠️ I asked for this cache two days ago on the strength of a commandlet measurement that
re-visited the same region repeatedly. That access pattern is nothing like a moving
player, and I should have said so when specifying it.
VERIFIED BY ME: build 0 errors; export 0.563 / 0.565 / 0.566 s (35de59b was 0.545-0.563,
flat as expected); geometry SHA-256 b3e5f4c3..9b377 UNCHANGED; capability 20,830 /
10,909, connected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
35de59bac5
commit
cf0319d480
12 files changed
+660
-514
No files matched your search
+24
-12
@@ -557,10 +557,16 @@ ApplyModification` (BlueprintCallable) + `EditorCarveSphere/EditorFillSphere` (C
|
||||
driven by `EditorBrush*` props.
|
||||
|
||||
### 8.10 Performance invariants (DON'T regress)
|
||||
- **Streaming** (`UpdateChunksAroundPosition`): rebuild/cull the desired set ONLY when the
|
||||
player crosses a chunk boundary (`LastUpdateCenter`); use the `DesiredSet` TSet for the cull;
|
||||
idle via `bAllChunksLoaded`. Stationary player ≈ free. (Old per-frame O(loaded×desired) scan = 22ms.)
|
||||
- **LOD** changes HOT-SWAP (`LoadChunk` only, never unload-first) → no holes. LOD
|
||||
- **Streaming** (`UpdateChunksAroundPosition`): rebuild/cull the desired set only when the player
|
||||
crosses a level-0 chunk boundary, an anchor moves, or a rebuild is forced; use the stamped
|
||||
desired map for the cull and idle via `bAllChunksLoaded`. On every rebuild, pending requests whose
|
||||
keys left the desired set are marked obsolete through a per-request atomic token. Workers only
|
||||
read that token (and `bShuttingDown`/generation pause), abort between expensive stages, and send
|
||||
the result through the normal MPSC queue; the game thread reads the token again before applying.
|
||||
`FChunkResult::Epoch` still guards the generation epoch. Desired work is sorted from the true pawn
|
||||
position, with the occupied/support LOD0 tile and the next tile along the heading in an absolute
|
||||
high-priority prefix. Stationary player ≈ free. (Old per-frame O(loaded×desired) scan = 22ms.)
|
||||
- **LOD** changes HOT-SWAP (`LoadTile` only, never unload-first) → no holes. LOD
|
||||
reconciliation lives in the PERSISTENT per-frame submit loop (same loop as new-chunk loads),
|
||||
NOT as a one-shot on the boundary-cross frame — a one-shot drops every chunk past the task
|
||||
budget and strands it at a stale LOD. Idle (`bAllChunksLoaded`) only when a full scan finds
|
||||
@@ -688,9 +694,13 @@ driven by `EditorBrush*` props.
|
||||
tile is replaced by several finer tiles, so the old center-owner
|
||||
check (`ReplacementLoaded`) dropped it as soon as the ONE tile over its centre loaded → the not-yet-
|
||||
ready edges flashed a hole; the full-coverage check keeps the old tile at its current resolution until
|
||||
the better mesh is wholly in, then swaps. In-flight (pending) tiles are NEVER cancelled on a rebuild —
|
||||
they finish, apply, and are culled later if no longer desired. Collision level-0 only; water level-0
|
||||
only; shadows off for level≥2. **Decorations are NO LONGER tied to tiles** — they stream on a fixed world
|
||||
the better mesh is wholly in, then swaps. In-flight requests are cancelled when their key leaves the
|
||||
desired set: the game thread flips the request token, the worker returns an aborted result, and the
|
||||
queue consumer drops it even if cancellation raced the final enqueue. A result must also match the
|
||||
current generation epoch and desired membership before it can apply. `TransitionHold` therefore
|
||||
protects only already-applied geometry during a LOD transition; obsolete work never becomes loaded
|
||||
geometry. Collision level-0 only; water level-0 only; shadows off for level≥2. **Decorations are NO
|
||||
LONGER tied to tiles** — they stream on a fixed world
|
||||
grid by distance (§8.5), so they don't pop on LOD swaps. Settings: `VoxelSettings::ClipRadius` (full-res near radius, tiles/level),
|
||||
`MaxClipLevel` (far reach). **NEAR-FIELD GEN COST levers** (`LoadTile`): levels `< FullResClipLevels`
|
||||
mesh at full `CHUNK_SIZE` cells (≈35³ `GetDensityAt` incl. margin ring), coarser levels at
|
||||
@@ -756,11 +766,13 @@ driven by `EditorBrush*` props.
|
||||
(stale stamp — removed + returned). The cull then considers ONLY leavers + `TransitionHold`
|
||||
(tiles kept by load-before-unload from earlier crossings) instead of re-scanning EVERY loaded
|
||||
tile per crossing — that scan was the measured ~1.6 ms/crossing `CullTiles` spike (2026-07-05
|
||||
trace, plus `BuildDesiredTiles` 0.66 ms on the same frames). In-flight tiles that finish while
|
||||
no longer desired are caught at apply time (`ProcessPendingChunks` adds them to the hold);
|
||||
`UnloadTile` drops hold entries; the settled cull (everything loaded) keeps its full scan as
|
||||
the safety net. `DesiredPending` for the overlap test is now built LAZILY (only when an
|
||||
in-range transition candidate exists). **The hold is re-evaluated on a ROTATING BUDGET**
|
||||
trace, plus `BuildDesiredTiles` 0.66 ms on the same frames). Pending requests that become leavers
|
||||
are marked obsolete before replacement submission; `ProcessPendingChunks` reads the token, drops
|
||||
the aborted result, and does not put it in the hold. `ApplyTileResult` independently rejects a
|
||||
wrong generation epoch or undesired key. `UnloadTile` drops hold entries; the settled cull
|
||||
(everything loaded) keeps its full scan as the safety net. `DesiredPending` for the overlap test is
|
||||
now built LAZILY (only when an in-range transition candidate exists). **The hold is re-evaluated on
|
||||
a ROTATING BUDGET**
|
||||
(`TransitionHoldQueue` + cursor, ~256 tiles/crossing): re-scanning the whole hold each
|
||||
crossing degenerates back to the O(loaded) scan whenever streaming never settles (measured
|
||||
2.47 ms/crossing in the first packaged capture) — keeping a tile a few crossings longer is
|
||||
|
||||
Reference in new issue
Block a user