perf(drivers): fixed-rate poll pacing with overrun counter; non-blocking unsubscribe

This commit is contained in:
Joseph Doherty
2026-08-16 04:26:20 -04:00
parent 684e59d871
commit 9cf400f1e8
9 changed files with 277 additions and 43 deletions
@@ -598,6 +598,106 @@ public sealed class PollGroupEngineTests
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(2));
}
// ---- Fixed-rate pacing + overrun counter + async unsubscribe (Task 11) ----
/// <summary>
/// A reader slower than the publishing interval must NOT push the cycle out to
/// (read + interval): the loop paces at a fixed RATE, so the post-poll delay is
/// <c>max(interval - elapsed, 0)</c> and the next poll starts immediately. The overrun is
/// counted and reported through the <c>onOverrun</c> callback.
/// </summary>
/// <remarks>
/// Numbers: interval 150 ms, reader 250 ms. Old (delay-after-completion) cycle = 400 ms →
/// ≈5 polls started in 2 s. Fixed-rate cycle = 250 ms → ≈8. The ≥7 assertion sits clear of
/// both the old shape's ceiling and CI jitter.
/// </remarks>
[Fact]
public async Task A_slow_reader_overrun_is_counted_and_the_next_poll_starts_promptly()
{
var readCount = 0;
async Task<IReadOnlyList<DataValueSnapshot>> Reader(IReadOnlyList<string> refs, CancellationToken ct)
{
Interlocked.Increment(ref readCount);
await Task.Delay(250, ct);
var now = DateTime.UtcNow;
return refs.Select(_ => new DataValueSnapshot(1, 0u, now, now)).ToList();
}
var overruns = new ConcurrentQueue<TimeSpan>();
await using var engine = new PollGroupEngine(
Reader,
(_, _, _) => { },
minInterval: TimeSpan.FromMilliseconds(50),
onOverrun: elapsed => overruns.Enqueue(elapsed));
var handle = engine.Subscribe(["X"], TimeSpan.FromMilliseconds(150));
await Task.Delay(2000, TestContext.Current.CancellationToken);
var observedReads = Volatile.Read(ref readCount);
var observedOverruns = engine.OverrunCount;
await engine.UnsubscribeAsync(handle);
observedReads.ShouldBeGreaterThanOrEqualTo(7);
observedOverruns.ShouldBeGreaterThanOrEqualTo(4);
overruns.Count.ShouldBeGreaterThanOrEqualTo(4);
overruns.ShouldAllBe(e => e > TimeSpan.FromMilliseconds(150));
}
/// <summary>
/// Falsification control for the overrun counter: a reader that completes well inside the
/// interval must never register an overrun (an unconditional increment would fail here).
/// </summary>
[Fact]
public async Task A_fast_reader_never_overruns()
{
var src = new FakeSource();
src.Values["X"] = 1;
var overruns = new ConcurrentQueue<TimeSpan>();
await using var engine = new PollGroupEngine(
src.ReadAsync,
(_, _, _) => { },
minInterval: TimeSpan.FromMilliseconds(50),
onOverrun: e => overruns.Enqueue(e));
var handle = engine.Subscribe(["X"], TimeSpan.FromMilliseconds(100));
await WaitForAsync(() => Volatile.Read(ref src.ReadCount) >= 4, TimeSpan.FromSeconds(3));
await engine.UnsubscribeAsync(handle);
Volatile.Read(ref src.ReadCount).ShouldBeGreaterThanOrEqualTo(4);
engine.OverrunCount.ShouldBe(0);
overruns.ShouldBeEmpty();
}
/// <summary>
/// The async teardown must be behaviourally identical to the sync one: the handle is known,
/// the loop stops, and no further callback fires. Mirrors
/// <see cref="Unsubscribe_halts_the_loop"/> through <c>UnsubscribeAsync</c>.
/// </summary>
[Fact]
public async Task UnsubscribeAsync_stops_callbacks_like_the_sync_path()
{
var src = new FakeSource();
src.Values["X"] = 1;
var events = new ConcurrentQueue<(ISubscriptionHandle, string, DataValueSnapshot)>();
await using var engine = new PollGroupEngine(src.ReadAsync,
(h, r, s) => events.Enqueue((h, r, s)));
var handle = engine.Subscribe(["X"], TimeSpan.FromMilliseconds(100));
await WaitForAsync(() => events.Count >= 1, TimeSpan.FromSeconds(1));
(await engine.UnsubscribeAsync(handle)).ShouldBeTrue();
var afterUnsub = events.Count;
engine.ActiveSubscriptionCount.ShouldBe(0);
src.Values["X"] = 999;
await Task.Delay(400, TestContext.Current.CancellationToken);
events.Count.ShouldBe(afterUnsub);
// An unknown handle is false through the async path too.
(await engine.UnsubscribeAsync(new DummyHandle())).ShouldBeFalse();
(await engine.UnsubscribeAsync(handle)).ShouldBeFalse();
}
private sealed record DummyHandle : ISubscriptionHandle
{
/// <summary>Gets a diagnostic identifier for this handle.</summary>