using Xunit; namespace ZymonicServices.Performance.Tests; public class PerformanceTests { [Fact] public void SnapshotReportsPercentilesAndResetsInterval() { var recorder = new ZymonicPerformanceRecorder(); foreach (int value in Enumerable.Range(1, 100)) { recorder.Record("operation", "ms", value); } var result = Assert.Single(recorder.TakeSnapshot()); Assert.Equal(100, result.Count); Assert.Equal(5050, result.Total); Assert.Equal(50.5, result.Median); Assert.Equal(95, result.P95); Assert.Equal(100, result.Maximum); Assert.Equal(100, result.Sampled); Assert.Empty(recorder.TakeSnapshot()); } [Fact] public void BoundedSamplesPreserveWholeIntervalCountTotalAndMaximum() { var recorder = new ZymonicPerformanceRecorder(); recorder.Record("operation", "ms", 10000); foreach (int value in Enumerable.Range(1, ZymonicPerformanceRecorder.SampleLimit)) { recorder.Record("operation", "ms", value); } var result = Assert.Single(recorder.TakeSnapshot()); Assert.Equal(513, result.Count); Assert.Equal(141328, result.Total); Assert.Equal(10000, result.Maximum); Assert.Equal(512, result.Sampled); Assert.Equal(256.5, result.Median); Assert.Equal(487, result.P95); } [Fact] public async Task ConcurrentRecordingAndSnapshotsDoNotLoseCounts() { var recorder = new ZymonicPerformanceRecorder(); var summaries = new List(); var writers = Task.Run(() => Parallel.For(0, 10000, _ => recorder.RecordCount("rows", 3))); while (!writers.IsCompleted) { summaries.AddRange(recorder.TakeSnapshot()); await Task.Yield(); } await writers; summaries.AddRange(recorder.TakeSnapshot()); Assert.Equal(10000, summaries.Sum(summary => summary.Count)); Assert.Equal(30000, summaries.Sum(summary => summary.Total)); } [Fact] public void CardinalityIsBoundedAndInvalidSamplesAreIgnored() { var recorder = new ZymonicPerformanceRecorder(); recorder.Record("invalid", "ms", double.NaN); recorder.Record("invalid", "ms", double.PositiveInfinity); recorder.RecordCount("invalid", -1); recorder.Record("invalid", "unknown", 10); Assert.Empty(recorder.TakeSnapshot()); foreach (int value in Enumerable.Range(0, 1000)) { recorder.RecordCount($"metric{value}", value); } Assert.Equal(ZymonicPerformanceRecorder.MetricLimit, recorder.TakeSnapshot().Count); } [Fact] public void MeasurementCanOnlyBeDisposedOnce() { var recorder = new ZymonicPerformanceRecorder(); var measurement = recorder.Measure("operation"); measurement.Dispose(); measurement.Dispose(); Assert.Equal(1, Assert.Single(recorder.TakeSnapshot()).Count); } [Fact] public void BlockedDispatcherOnlyHasOneOutstandingProbe() { var recorder = new ZymonicPerformanceRecorder(); var callbacks = new List(); var probe = new ZymonicDispatcherProbe(callbacks.Add); Parallel.For(0, 1000, _ => probe.Poll(recorder)); Assert.Single(callbacks)(); Assert.Equal("ui.dispatch", Assert.Single(recorder.TakeSnapshot()).Name); callbacks.Clear(); probe.Poll(recorder); Assert.Single(callbacks)(); Assert.Single(recorder.TakeSnapshot()); } [Fact] public void SuspensionDiscardsOldTimingWithoutQueuingMoreCallbacks() { var recorder = new ZymonicPerformanceRecorder(); var callbacks = new List(); var probe = new ZymonicDispatcherProbe(callbacks.Add); probe.Poll(recorder); probe.Invalidate(); probe.Poll(recorder); Assert.Single(callbacks)(); Assert.Empty(recorder.TakeSnapshot()); callbacks.Clear(); probe.Poll(recorder); Assert.Single(callbacks)(); Assert.Single(recorder.TakeSnapshot()); } [Fact] public void RejectedDispatcherPostDoesNotCrashOrLeaveProbePending() { int attempts = 0; var probe = new ZymonicDispatcherProbe(_ => { attempts++; throw new InvalidOperationException(); }); var recorder = new ZymonicPerformanceRecorder(); probe.Poll(recorder); probe.Poll(recorder); Assert.Equal(2, attempts); Assert.Empty(recorder.TakeSnapshot()); } }