docs #429: root cause — 30-77MB/frame LINQ allocation in streamed-mesh completion on the render thread; fix plan + acceptance protocol

Local commit for the implementation handoff; push withheld per owner
direction until the fix session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-23 21:46:05 +02:00
parent ca4bae776c
commit 5b9d0260bb

View file

@ -148,6 +148,35 @@ of running per arm (`artifacts/owner-gate/player-present-429-packON.csv`
frames spend their extra time (the pack's CPU stage profiler names frames spend their extra time (the pack's CPU stage profiler names
stages) and where the player position is sampled relative to it. stages) and where the player position is sampled relative to it.
**ROOT CAUSE FOUND 2026-08-23 (frame-history run, 33,350 frames with stage
attribution — `ACDREAM_FRAME_PROF=1` + `ACDREAM_FRAME_HISTORY`):** of 708
stall frames (>12 ms), 701 allocate 30-77 MB IN THAT SINGLE FRAME (normal
frames: ~22 KB, p99 94 KB). The time and the allocation sit on the render
path outside the tracked stages. The bursts arrive in ~8-frame clusters
during movement — the streaming MESH COMPLETION path: each frame completes
up to `MaxCompletionsPerFrame` (quality High = 4) newly streamed meshes on
the render thread, and each completion in
`ObjectMeshManager.UploadGfxObjMeshData` (~line 2043) runs LINQ chains —
`TextureBatches.Values.SelectMany(...).Select(b => b.Indices.ToArray())
.ToArray()`, plus the retained pick-support copies
`CPUPositions = Vertices.Select(v => v.Position).ToArray()` and
`CPUIndices = ...SelectMany(...).SelectMany(...).ToArray()` — megabytes of
enumerator/intermediate-List garbage per mesh, tens of MB per frame.
Gen0 runs 70-145 collections per 5 s during movement (vs ~1/6 s idle).
**Fix shape (a bounded slice, not a quickie — this is the production mesh
pipeline):**
1. De-LINQ the conversion: direct pre-sized loops for the index batches and
the CPU pick copies (sizes are known up front from the batch counts).
2. Consider byte-budgeted completions (4 huge EnvCell meshes is not the
same frame cost as 4 fence posts) and/or moving the CPU-side conversion
onto the existing mesh-preparation scheduler thread so the render thread
only adopts finished arrays.
3. Allocation-gate test in the I1 style: a completion of a representative
mesh set must allocate near its retained-copy size, not multiples of it.
The pack-ON player-jump phase question remains as the second defect but
becomes mostly moot once the stalls themselves shrink.
**Next probes (in order):** **Next probes (in order):**
1. `ACDREAM_DUMP_MOTION=1` + a temporary inbound-position log for the 1. `ACDREAM_DUMP_MOTION=1` + a temporary inbound-position log for the
LOCAL guid: does ACE send position sets for the local player every LOCAL guid: does ACE send position sets for the local player every