namespace Sandbox.Services; public partial class BenchmarkSystem { internal class Sampler { public struct Result { public string Name { get; set; } public double Min { get; set; } public double Max { get; set; } public double Avg { get; set; } public double Sum { get; set; } /// /// The value below which 5% of the samples fall (the 5th percentile). /// public double P5 { get; set; } /// /// The value below which 95% of the samples fall (the 95th percentile). /// public double P95 { get; set; } /// /// The value below which 99% of the samples fall (the 99th percentile). /// public double P99 { get; set; } /// /// The value below which 99.9% of the samples fall (the 99.9th percentile). /// public double P99_9 { get; set; } /// /// This is the sum of all samples that exceeded the stuttering threshold. /// May not be very useful for non time based samples. /// public double Stuttering { get; set; } public double Count { get; set; } } public string Name { get; private set; } private List samples; public delegate double GetSampleValueDelegate(); private GetSampleValueDelegate getSampleValue; public delegate uint GetSampleKeyDelegate(); private GetSampleKeyDelegate getSampleKey; private uint lastSampleKey; public Sampler( string name, GetSampleValueDelegate getSampleValue, GetSampleKeyDelegate getSampleKey = null ) { //Debug.Assert( getSampleValue != null, "No action provided to sampler" ); this.getSampleValue = getSampleValue; this.getSampleKey = getSampleKey; samples = new List(); Name = name; } public void Update() { // for samplers that are a value tied to a key, skip the sample if the key has not changed // (e.g. don't record duplicate GPU frametimes for a frame, where frame # is the key) if ( getSampleKey != null ) { uint key = getSampleKey.Invoke(); uint lastKey = lastSampleKey; lastSampleKey = key; if ( key == lastKey ) return; } if ( getSampleValue != null ) AddSample( getSampleValue.Invoke() ); } public void Clear() { samples.Clear(); } public void AddSample( double value ) { samples.Add( value ); } public bool HasValues => samples is not null; public Result GetResults() { return new Result() { Name = Name, Min = samples.Count() > 0 ? samples.Min() : 0, Max = samples.Count() > 0 ? samples.Max() : 0, Avg = samples.Count() > 0 ? samples.Average() : 0, Sum = samples.Count() > 0 ? samples.Sum() : 0, P5 = Percentile( 5 ), P95 = Percentile( 95 ), P99 = Percentile( 99 ), P99_9 = Percentile( 99.9 ), Stuttering = Stuttering( stutteringFactor: 2.5 ), Count = samples.Count() }; } public string GetDisplayName() { return Name; } /// /// Calculates the specified percentile value from the collected samples. /// /// /// The percentile to compute (a value between 0 and 100). /// For example, passing 95 computes the 95th percentile. /// /// /// The value below which the given percentage of samples fall. /// private double Percentile( double percentile ) { var sortedSequence = samples.OrderBy( n => n ).ToList(); double position = (sortedSequence.Count + 1) * percentile / 100.0; int index = (int)position - 1; double fraction = position - Math.Floor( position ); if ( index + 1 < sortedSequence.Count ) { return sortedSequence[index] + fraction * (sortedSequence[index + 1] - sortedSequence[index]); } else { return sortedSequence[index]; } } /// /// Calculates the sum of all samples that exceeded the stuttering threshold. /// A sample is considered stuttering if it exceeds the moving average multiplied by the stuttering factor. /// /// /// The multiplier used to determine the stuttering threshold. /// A higher factor makes it less sensitive to spikes (default is typically 2.5). /// private double Stuttering( double stutteringFactor ) { var averageSequence = GetMovingAverage(); double stuttering = 0.0; for ( int i = 0; i < samples.Count; i++ ) { if ( samples[i] > stutteringFactor * averageSequence[i] ) { stuttering += samples[i]; } } return stuttering; } private IList GetMovingAverage() { int windowSize = Math.Max( 1, (int)(Math.Sqrt( samples.Count ) * 10) ); var movingAverage = new List( samples.Count ); for ( int i = 0; i < samples.Count; i++ ) { int start = Math.Max( 0, i - windowSize / 2 ); int end = Math.Min( samples.Count - 1, i + windowSize / 2 ); double sum = 0.0; for ( int j = start; j <= end; j++ ) { sum += samples[j]; } double average = sum / (end - start + 1); movingAverage.Add( average ); } return movingAverage; } } }