feat(pipeline): MP0 - FrameStatsBuffer ring/percentile core
Fixed-capacity ring buffer of long samples with nearest-rank percentile and max over the current window. Pure, allocation-free after construction. Foundation for the MP0 frame profiler (docs/superpowers/specs/2026-07-05-modern-pipeline-design.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
2757dda63a
commit
7d74c68c60
2 changed files with 115 additions and 0 deletions
64
src/AcDream.App/Diagnostics/FrameStatsBuffer.cs
Normal file
64
src/AcDream.App/Diagnostics/FrameStatsBuffer.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AcDream.App.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MP0 (2026-07-05) — fixed-capacity ring buffer of long samples
|
||||||
|
/// (microseconds or bytes) with percentile/max over the current window.
|
||||||
|
/// Pure and allocation-free after construction: <see cref="Percentile"/>
|
||||||
|
/// sorts into a preallocated scratch array, so the 5-second report path
|
||||||
|
/// allocates nothing. Not thread-safe — owned by the window loop thread.
|
||||||
|
/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FrameStatsBuffer
|
||||||
|
{
|
||||||
|
private readonly long[] _samples;
|
||||||
|
private readonly long[] _scratch;
|
||||||
|
private int _cursor;
|
||||||
|
private int _count;
|
||||||
|
|
||||||
|
public FrameStatsBuffer(int capacity)
|
||||||
|
{
|
||||||
|
if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
|
||||||
|
_samples = new long[capacity];
|
||||||
|
_scratch = new long[capacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _count;
|
||||||
|
|
||||||
|
public void Push(long value)
|
||||||
|
{
|
||||||
|
_samples[_cursor] = value;
|
||||||
|
_cursor = (_cursor + 1) % _samples.Length;
|
||||||
|
if (_count < _samples.Length) _count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_cursor = 0;
|
||||||
|
_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nearest-rank percentile over the current window: element at
|
||||||
|
/// ceil(q·n) in the ascending sort (1-based), 0 when empty.
|
||||||
|
/// </summary>
|
||||||
|
public long Percentile(double q)
|
||||||
|
{
|
||||||
|
if (_count == 0) return 0;
|
||||||
|
Array.Copy(_samples, _scratch, _count);
|
||||||
|
Array.Sort(_scratch, 0, _count);
|
||||||
|
int rank = (int)Math.Ceiling(q * _count); // 1-based nearest rank
|
||||||
|
if (rank < 1) rank = 1;
|
||||||
|
if (rank > _count) rank = _count;
|
||||||
|
return _scratch[rank - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public long Max()
|
||||||
|
{
|
||||||
|
long max = 0;
|
||||||
|
for (int i = 0; i < _count; i++)
|
||||||
|
if (_samples[i] > max) max = _samples[i];
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
tests/AcDream.App.Tests/FrameStatsBufferTests.cs
Normal file
51
tests/AcDream.App.Tests/FrameStatsBufferTests.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue