fix #432: allocation-free oracle fingerprint sort — the ~6 MB/frame diagnostics tax

Second instance of the #429 (ad695589) boxing-comparer defect class, this
time in the measurement harness rather than production:

ACDREAM_AUTOMATION_ARTIFACT_DIR (with retained-UI screenshots) constructs
CurrentRenderSceneOracle, whose presence as partition observer re-enables
the G5-retired legacy InteriorEntityPartition every frame with per-entity
fingerprinting. Complete() then sorts one fingerprint per RESIDENT entity
(~60k across the streaming window), and the comparer's first key bound
x.ProjectionClass.CompareTo(y.ProjectionClass) to Enum.CompareTo(object),
boxing both operands. The 3-value enum almost always ties, so the boxing
ran on essentially every comparison: a measured ~6.2 MB and ~14 ms per
frame, everywhere — not town-specific and not view-triggered, which is
also why it appeared to "latch" (the resident set drives it, not the view).

Comparing the underlying integral value keeps the identical order. Hermetic
gate: one warmed observed partition of 20,000 entities allocated 15,876,088
bytes before, and passes a <64 KiB bound after
(OracleObservedPartitionAllocationTests).

Ordinary play never constructs the oracle, so no player-visible behavior
changes; what changes is that captures taken with the automation artifact
directory set are no longer taxed. The #429 acceptance data is unaffected
(owner drives and the deciding A/B arms ran with the artifact dir null).

The temporary [pview-alloc] attribution probe that localized this is
retired in the same commit; the gate test now guards the defect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-24 10:10:46 +02:00
parent b217a292bb
commit 92999b0101
3 changed files with 180 additions and 18 deletions

View file

@ -0,0 +1,65 @@
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.Core.World;
namespace AcDream.App.Tests.Rendering;
public sealed class OracleObservedPartitionAllocationTests
{
// #432: the automation-artifact harness constructs CurrentRenderSceneOracle,
// which re-enables the G5-retired legacy partition every frame WITH the
// oracle observing every resident entity. A warmed observed partition must
// not allocate per frame — the fingerprint walk and its sort run over the
// whole streaming window's resident entity set (~60k live), so any
// per-comparison or per-entity transient multiplies into MB/frame and
// poisons every measurement taken with ACDREAM_AUTOMATION_ARTIFACT_DIR set.
[Fact]
internal void AWarmedObservedPartitionAllocatesNearZero()
{
const int EntityCount = 20_000;
var entities = new List<WorldEntity>(EntityCount);
for (int i = 0; i < EntityCount; i++)
{
// Scramble ids so the fingerprint sort does real work instead of
// consuming pre-sorted input.
uint id = unchecked((uint)i * 2654435761u);
entities.Add(new WorldEntity
{
Id = id,
SourceGfxObjOrSetupId = 0x01000000u + (id & 0xFFFu),
Position = new Vector3(i % 192, i / 192f, 0f),
Rotation = Quaternion.Identity,
MeshRefs = new[] { new MeshRef(0x01000001u, Matrix4x4.Identity) },
});
}
var landblockEntries = new[]
{
(LandblockId: 0xA9B4FFFFu,
AabbMin: Vector3.Zero,
AabbMax: new Vector3(192f, 192f, 100f),
Entities: (IReadOnlyList<WorldEntity>)entities,
AnimatedById: (IReadOnlyDictionary<uint, WorldEntity>?)null),
};
var visibleCells = new HashSet<uint>();
var result = new InteriorEntityPartition.Result();
var oracle = new CurrentRenderSceneOracle();
for (int warm = 0; warm < 3; warm++)
{
InteriorEntityPartition.Partition(
result, visibleCells, landblockEntries, oracle);
}
long before = GC.GetAllocatedBytesForCurrentThread();
InteriorEntityPartition.Partition(
result, visibleCells, landblockEntries, oracle);
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.True(
allocated < 64 * 1024,
$"warmed observed partition allocated {allocated} bytes for "
+ $"{EntityCount} entities");
}
}