From 200f19ce475e465e5c37657ce0ddcefe5e51addb Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 29 Jul 2026 03:49:13 +0200 Subject: [PATCH] test(app): put every strict-zero site on the probe (#250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first commit converted the four members the issue named and left the other sites alone, reasoning that none had been observed failing. A 20-run complete-solution baseline disproved that within minutes: run 2 LiveEntityRuntimeTests.AnimationView_HotSpatialTraversal… run 14 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization… run 18 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization… run 19 CurrentRenderSceneOracleTests.SurfaceOverrideFingerprint… Both new names are the same shape as the four — one warm call, then a thousand-iteration loop inside the measured window — and neither had been recorded anywhere. "Not observed failing" only ever meant "not yet observed", and leaving known-shape sites in place would have guaranteed the acceptance gate failed. Run 19 is the sharper lesson: the issue named `SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing`, and the first commit converted a *different* test in that same file, so the actually-named member was still on the old shape. Matching by file was not matching by test. Every strict-zero site in the assembly is now on the probe — ten tests. Two came out stricter rather than merely steadier: `StaticRenderProjectionJournalTests` was measuring a synchronise whose journal does **not** coalesce. Repeating it grew the journal by 1,000 entries per call — 192,000 by the end of a probe run — so the steady state the test claimed to measure did not exist and the single-call window had been hiding it. Its step is now the whole frame cycle, synchronise *and* drain, which puts `DrainTo` inside the measured window for the first time and asserts the journal ends empty. `RetailInboundEventDispatcherTests` asserted a hard-coded 1,001 callbacks. It now counts its own dispatches and pins the callback count against that, so the assertion still proves the fast path ran the callback every time without being coupled to a loop bound that no longer exists. Left alone deliberately: the four sites asserting a tolerance rather than zero — `CellViewDedupTests` and `PortalProjectionTests`. Their ceilings already absorb this noise and none has flaked; changing a bound in either direction is a separate decision from fixing a measurement. Worth noting that `PortalProjectionTests`' ceiling exists explicitly to tolerate "a tiered-JIT/ArrayPool bookkeeping transition ... to the first measured batch", which is exactly what the probe removes, so it could probably be tightened to zero now — recorded in the issue rather than done here. Solution build 0 warnings / 0 errors; App suite 3,941 passed / 3 skipped. Co-Authored-By: Claude Fable 5 --- docs/ISSUES.md | 42 ++++++++++++++----- .../CurrentRenderSceneOracleTests.cs | 15 ++++--- .../EquippedChildProjectionWithdrawalTests.cs | 9 ++-- .../RenderFrameRouteOwnerSelectorTests.cs | 12 +++--- .../StaticRenderProjectionJournalTests.cs | 37 +++++++++++----- ...ackedProjectionClassificationCacheTests.cs | 14 ++----- .../GpuWorldStateRenderTraversalTests.cs | 16 +++---- .../UI/Layout/UiTextLayoutCacheTests.cs | 10 ++--- .../World/LiveEntityRuntimeTests.cs | 22 +++++----- .../RetailInboundEventDispatcherTests.cs | 21 ++++++---- 10 files changed, 116 insertions(+), 82 deletions(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index d6731107..cf81dc07 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -750,17 +750,39 @@ cost reads as zero; a cost every tenth call is caught; and the stated limit — the batch must cover the period — is pinned rather than left as prose. Without those, a later edit could quietly make the whole family unfailable. -**Not yet done:** twelve further `GetAllocatedBytesForCurrentThread` sites in -`AcDream.App.Tests` still use the hand-rolled shape (`CellViewDedupTests`, -`EquippedChildProjectionWithdrawalTests`, `PortalProjectionTests`, -`RenderFrameRouteOwnerSelectorTests`, `StaticRenderProjectionJournalTests`, -`PackedProjectionClassificationCacheTests`, +**The family was larger than four.** The first attempt converted only the four +members the issue named and left the rest, on the grounds that none had been +observed failing. A 20-run complete-solution baseline immediately disproved +that: `LiveEntityRuntimeTests.AnimationView_HotSpatialTraversalDoesNot` +`AllocateAfterWarmup` failed in run 2 and +`StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization_Reuses` +`RetainedJournalStorage` in runs 14 and 18 — both the same shape, neither +previously recorded. "Not observed failing" only ever meant "not yet observed". + +So **every strict-zero site in the assembly is now on the probe** — ten tests: +the four named members plus `LiveEntityRuntimeTests`, +`StaticRenderProjectionJournalTests`, `RenderFrameRouteOwnerSelectorTests`, `GpuWorldStateRenderTraversalTests`, `UiTextLayoutCacheTests`, -`LiveEntityRuntimeTests`, `RetailInboundEventDispatcherTests`, and a second -site in `CurrentRenderSceneOracleTests`). None has been observed failing. -Each needs its own repeatability analysis — several mutate state or consume -monotonic sequences — so they were left alone rather than converted blind. -Adopt the probe when one is next touched, or immediately if it flakes. +`RetailInboundEventDispatcherTests`, `PackedProjectionClassificationCacheTests` +and `EquippedChildProjectionWithdrawalTests`. + +Two of those got stricter rather than merely steadier. +`StaticRenderProjectionJournalTests` turned out to be measuring a synchronise +whose journal **does not coalesce** — repeating it grew the journal by 1,000 +entries per call, so the steady state it claimed to test did not exist. Its step +is now the whole frame cycle, synchronise *and* drain, which puts `DrainTo` +inside the measured window for the first time. +`RetailInboundEventDispatcherTests` now counts its own dispatches and pins the +callback count against them, where before it asserted a hard-coded 1,001. + +**Deliberately not converted:** the four sites that assert a *tolerance* rather +than zero — `CellViewDedupTests` (two, `<= 256`) and `PortalProjectionTests` +(two, `<= 1_024` and `<= 4_096`). Their ceilings already absorb the noise this +issue is about, none has been observed failing, and touching their bounds in +either direction is a separate decision. `PortalProjectionTests` is worth +revisiting: its ceiling exists explicitly to tolerate "a tiered-JIT/ArrayPool +bookkeeping transition ... to the first measured batch", which is precisely what +the probe removes, so it could likely be tightened to zero on the probe now. --- diff --git a/tests/AcDream.App.Tests/Rendering/CurrentRenderSceneOracleTests.cs b/tests/AcDream.App.Tests/Rendering/CurrentRenderSceneOracleTests.cs index 04d4bd16..4583b08d 100644 --- a/tests/AcDream.App.Tests/Rendering/CurrentRenderSceneOracleTests.cs +++ b/tests/AcDream.App.Tests/Rendering/CurrentRenderSceneOracleTests.cs @@ -484,18 +484,17 @@ public sealed class CurrentRenderSceneOracleTests CurrentRenderSceneOracle.CreateSurfaceOverrideFingerprint( overrides); RenderSceneHash128 actual = default; - long before = GC.GetAllocatedBytesForCurrentThread(); - for (int iteration = 0; iteration < 10_000; iteration++) - { - actual = + // #250: the measured window was a 10,000-iteration loop written inline, + // which is exactly the shape on-stack replacement rewrites mid-flight, + // on this thread, inside the window. + ZeroAllocationProbe.AssertAllocatesNothing( + "CurrentRenderSceneOracle.CreateSurfaceOverrideFingerprint", + () => actual = CurrentRenderSceneOracle.CreateSurfaceOverrideFingerprint( - overrides); - } + overrides)); - long allocated = GC.GetAllocatedBytesForCurrentThread() - before; Assert.Equal(expected, actual); - Assert.Equal(0, allocated); } private static CurrentRenderProjectionFingerprint CaptureSingle( diff --git a/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs b/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs index 52ae83ce..61ff1ca2 100644 --- a/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs +++ b/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs @@ -310,12 +310,11 @@ public sealed class EquippedChildProjectionWithdrawalTests fixture.Controller.Tick(); fixture.Controller.ReconcileSpatialMutations(); - long before = GC.GetAllocatedBytesForCurrentThread(); - for (int i = 0; i < 1_000; i++) - fixture.Controller.ReconcileSpatialMutations(); - long allocated = GC.GetAllocatedBytesForCurrentThread() - before; + // #250: the measured window was a 1,000-iteration loop written inline. + ZeroAllocationProbe.AssertAllocatesNothing( + "LiveEntityController.ReconcileSpatialMutations", + () => fixture.Controller.ReconcileSpatialMutations()); - Assert.Equal(0L, allocated); Assert.Equal(0, fixture.Controller.LastReconcilePoseCompositionVisits); } diff --git a/tests/AcDream.App.Tests/Rendering/RenderFrameRouteOwnerSelectorTests.cs b/tests/AcDream.App.Tests/Rendering/RenderFrameRouteOwnerSelectorTests.cs index 2f79c133..5a8b5810 100644 --- a/tests/AcDream.App.Tests/Rendering/RenderFrameRouteOwnerSelectorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RenderFrameRouteOwnerSelectorTests.cs @@ -90,13 +90,13 @@ public sealed class RenderFrameRouteOwnerSelectorTests var owners = new HashSet(); owners.EnsureCapacity(16); - SelectOwners(owners, in view); - long before = GC.GetAllocatedBytesForCurrentThread(); - for (int iteration = 0; iteration < 1_000; iteration++) - SelectOwners(owners, in view); - long allocated = GC.GetAllocatedBytesForCurrentThread() - before; + // #250: warmed once, then measured a 1,000-iteration loop written + // inline — the shape on-stack replacement rewrites mid-measurement. + RenderFrameView captured = view; + ZeroAllocationProbe.AssertAllocatesNothing( + "RenderFrameRouteOwnerSelector.SelectOwners", + () => SelectOwners(owners, in captured)); - Assert.Equal(0, allocated); Assert.Equal([50u], owners); exchange.Release(in view); } diff --git a/tests/AcDream.App.Tests/Rendering/StaticRenderProjectionJournalTests.cs b/tests/AcDream.App.Tests/Rendering/StaticRenderProjectionJournalTests.cs index 1ddbbda7..88675706 100644 --- a/tests/AcDream.App.Tests/Rendering/StaticRenderProjectionJournalTests.cs +++ b/tests/AcDream.App.Tests/Rendering/StaticRenderProjectionJournalTests.cs @@ -358,19 +358,34 @@ public sealed class StaticRenderProjectionJournalTests } statics.SynchronizeActiveAnimatedSources(animated); journal.DrainTo(scene); - for (int index = 0; index < animated.Length; index++) - { - animated[index].Rotation = Quaternion.CreateFromAxisAngle( - Vector3.UnitZ, - 0.5f); - } - long before = GC.GetAllocatedBytesForCurrentThread(); + // #250: one warm call is not enough to have paid the callee tree's + // first-call costs. The step is the whole steady-state frame cycle — + // advance the rotations, synchronise, drain — rather than a bare + // synchronise, for two reasons. The rotations must actually change on + // every invocation or the synchronise early-outs and the probe measures + // nothing; and the journal does not coalesce, so without the drain it + // grows by `count` entries per invocation and the thing under test + // stops being steady state at all. Draining also makes this stricter + // than the original: `DrainTo` is now inside the measured window. + float angle = 0.5f; + ZeroAllocationProbe.AssertAllocatesNothing( + "StaticRenderProjectionJournal animated synchronise and drain", + () => + { + angle += 0.01f; + for (int index = 0; index < animated.Length; index++) + { + animated[index].Rotation = Quaternion.CreateFromAxisAngle( + Vector3.UnitZ, + angle); + } - statics.SynchronizeActiveAnimatedSources(animated); + statics.SynchronizeActiveAnimatedSources(animated); + journal.DrainTo(scene); + }); - long allocated = GC.GetAllocatedBytesForCurrentThread() - before; - Assert.Equal(count, journal.Count); - Assert.True(allocated == 0, $"Allocated {allocated:N0} bytes."); + // Retained storage: the cycle leaves nothing behind to grow. + Assert.Equal(0, journal.Count); } private static GpuLandblockSpatialPublication Publication( diff --git a/tests/AcDream.App.Tests/Rendering/Wb/PackedProjectionClassificationCacheTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/PackedProjectionClassificationCacheTests.cs index b0176135..95e829bb 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/PackedProjectionClassificationCacheTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/PackedProjectionClassificationCacheTests.cs @@ -171,16 +171,10 @@ public sealed class PackedProjectionClassificationCacheTests cache.CompleteRebuild(entry, reusableAcrossFrames: true); cache.EndFrame(); - for (int i = 0; i < 100; i++) - Hit(cache, generation, id, identity); - - long before = GC.GetAllocatedBytesForCurrentThread(); - for (int i = 0; i < 1_000; i++) - Hit(cache, generation, id, identity); - long allocated = - GC.GetAllocatedBytesForCurrentThread() - before; - - Assert.Equal(0, allocated); + // #250: the measured window was a 1,000-iteration loop written inline. + ZeroAllocationProbe.AssertAllocatesNothing( + "PackedProjectionClassificationCache warm cache hit", + () => Hit(cache, generation, id, identity)); } private static void Hit( diff --git a/tests/AcDream.App.Tests/Streaming/GpuWorldStateRenderTraversalTests.cs b/tests/AcDream.App.Tests/Streaming/GpuWorldStateRenderTraversalTests.cs index a1bf84e1..f6e516aa 100644 --- a/tests/AcDream.App.Tests/Streaming/GpuWorldStateRenderTraversalTests.cs +++ b/tests/AcDream.App.Tests/Streaming/GpuWorldStateRenderTraversalTests.cs @@ -84,15 +84,15 @@ public sealed class GpuWorldStateRenderTraversalTests var bounds = state.LandblockBounds; _ = entries.Count; _ = bounds.Count; - long before = GC.GetAllocatedBytesForCurrentThread(); - for (int iteration = 0; iteration < 1_000; iteration++) - { - _ = state.LandblockEntries.Count; - _ = state.LandblockBounds.Count; - } - long allocated = GC.GetAllocatedBytesForCurrentThread() - before; + // #250: the measured window was a 1,000-iteration loop written inline. + ZeroAllocationProbe.AssertAllocatesNothing( + "GpuWorldState landblock collection access", + () => + { + _ = state.LandblockEntries.Count; + _ = state.LandblockBounds.Count; + }); - Assert.Equal(0, allocated); Assert.Same(entries, state.LandblockEntries); Assert.Same(bounds, state.LandblockBounds); diff --git a/tests/AcDream.App.Tests/UI/Layout/UiTextLayoutCacheTests.cs b/tests/AcDream.App.Tests/UI/Layout/UiTextLayoutCacheTests.cs index 4a097bf0..a6f9ce9b 100644 --- a/tests/AcDream.App.Tests/UI/Layout/UiTextLayoutCacheTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/UiTextLayoutCacheTests.cs @@ -69,11 +69,9 @@ public sealed class UiTextLayoutCacheTests Func> provider = cache.Provider; _ = provider(); - long before = GC.GetAllocatedBytesForCurrentThread(); - for (int i = 0; i < 1_000; i++) - _ = provider(); - long allocated = GC.GetAllocatedBytesForCurrentThread() - before; - - Assert.Equal(0L, allocated); + // #250: the measured window was a 1,000-iteration loop written inline. + ZeroAllocationProbe.AssertAllocatesNothing( + "UiTextLayoutCache stable provider poll", + () => _ = provider()); } } diff --git a/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs b/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs index 1f43e606..e462ad0b 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs @@ -556,18 +556,20 @@ public sealed class LiveEntityRuntimeTests view.CopySpatialIdsTo(ids); foreach (KeyValuePair _ in view) { } + // #250: the old shape warmed once and then measured a 1,000-iteration + // loop written inline, which is precisely the shape on-stack + // replacement rewrites mid-flight, on this thread, inside the window. int visits = 0; - long before = GC.GetAllocatedBytesForCurrentThread(); - for (int i = 0; i < 1_000; i++) - { - view.CopySpatialIdsTo(ids); - foreach (KeyValuePair _ in view) - visits++; - } - long allocated = GC.GetAllocatedBytesForCurrentThread() - before; + ZeroAllocationProbe.AssertAllocatesNothing( + "LiveEntityAnimationRuntimeView spatial traversal", + () => + { + view.CopySpatialIdsTo(ids); + foreach (KeyValuePair _ in view) + visits++; + }); - Assert.Equal(0, allocated); - Assert.Equal(1_000, visits); + Assert.True(visits > 0); Assert.Equal(entity.Id, Assert.Single(ids)); } diff --git a/tests/AcDream.App.Tests/World/RetailInboundEventDispatcherTests.cs b/tests/AcDream.App.Tests/World/RetailInboundEventDispatcherTests.cs index 4377edae..299c1291 100644 --- a/tests/AcDream.App.Tests/World/RetailInboundEventDispatcherTests.cs +++ b/tests/AcDream.App.Tests/World/RetailInboundEventDispatcherTests.cs @@ -99,14 +99,19 @@ public sealed class RetailInboundEventDispatcherTests static (target, amount) => target.Value += amount; dispatcher.Run(counter, 1, increment); - long before = GC.GetAllocatedBytesForCurrentThread(); - for (int i = 0; i < 1_000; i++) - { - dispatcher.Run(counter, 1, increment); - } - long allocated = GC.GetAllocatedBytesForCurrentThread() - before; + // #250: the measured window was a 1,000-iteration loop written inline. + int dispatches = 0; + ZeroAllocationProbe.AssertAllocatesNothing( + "RetailInboundEventDispatcher.Run state fast path", + () => + { + dispatches++; + dispatcher.Run(counter, 1, increment); + }); - Assert.Equal(0, allocated); - Assert.Equal(1_001, counter.Value); + // The fast path really ran the callback on every dispatch — once for + // the priming call above, then once per probe invocation. + Assert.True(dispatches > 0); + Assert.Equal(dispatches + 1, counter.Value); } }