acdream/tests/AcDream.App.Tests/FrameStatsBufferTests.cs
Erik 4b44a15286 fix(pipeline): MP0 - profiler toggle-path hygiene + Max() seed (review follow-up)
Three review finds, all in the runtime-toggle path or edge math (the
steady-state path was confirmed correct):

1. GPU stale-slot across toggle-off/on: the disabled branch now
   Disposes the GpuFrameTimer (and nulls it) instead of Stop()-ing and
   keeping it — a kept instance would poll slots left pending from
   BEFORE the pause on re-enable and report temporally stale GPU
   samples. The re-enable branch's existing null guard rebuilds the
   ring fresh. Dispose is safe there: the branch runs at the top of
   OnRender with the GL context current.

2. Stale stage accumulation across mid-stage toggle-off: a StageScope
   disposed after the flag flips still calls EndStage, leaving a
   partial delta in _stageAccumTicks. The re-baseline branch now
   Array.Clear()s it alongside the GC re-baselining.

3. FrameStatsBuffer.Max() seeded with 0, clamping all-negative windows
   (the alloc channel can go negative if the boundary ever crosses
   threads) and disagreeing with Percentile(). Now seeds from the
   first live sample after the empty check (slots [0.._count) are
   always the live window regardless of ring wraparound); empty still
   returns 0 to match Percentile. TDD: Max_AllNegative_ReturnsTrueMax
   failed (returned 0) before the fix, passes after.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 19:26:28 +02:00

59 lines
1.6 KiB
C#

using AcDream.App.Diagnostics;
using Xunit;
namespace AcDream.App.Tests;
public class FrameStatsBufferTests
{
[Fact]
public void Percentiles_OnKnownDistribution_AreExact()
{
var buf = new FrameStatsBuffer(capacity: 100);
// 1..100 µs — p50 = 50, p95 = 95, p99 = 99, max = 100.
for (long i = 1; i <= 100; i++) buf.Push(i);
Assert.Equal(50, buf.Percentile(0.50));
Assert.Equal(95, buf.Percentile(0.95));
Assert.Equal(99, buf.Percentile(0.99));
Assert.Equal(100, buf.Max());
}
[Fact]
public void Push_PastCapacity_KeepsOnlyNewestWindow()
{
var buf = new FrameStatsBuffer(capacity: 4);
foreach (long v in new long[] { 1000, 1000, 1000, 1000, 1, 2, 3, 4 })
buf.Push(v);
// The four 1000s were overwritten; window is {1,2,3,4}.
Assert.Equal(4, buf.Count);
Assert.Equal(4, buf.Max());
Assert.Equal(2, buf.Percentile(0.50));
}
[Fact]
public void Percentile_Empty_ReturnsZero()
{
var buf = new FrameStatsBuffer(capacity: 8);
Assert.Equal(0, buf.Percentile(0.95));
Assert.Equal(0, buf.Max());
Assert.Equal(0, buf.Count);
}
[Fact]
public void Reset_ClearsWindow()
{
var buf = new FrameStatsBuffer(capacity: 8);
buf.Push(5); buf.Push(7);
buf.Reset();
Assert.Equal(0, buf.Count);
Assert.Equal(0, buf.Percentile(0.5));
}
[Fact]
public void Max_AllNegative_ReturnsTrueMax()
{
var buf = new FrameStatsBuffer(capacity: 4);
buf.Push(-5); buf.Push(-2); buf.Push(-9);
Assert.Equal(-2, buf.Max());
}
}