Files
sbox-public/engine/Sandbox.Engine/Game/Services/Benchmark/BenchmarkSystem.Sampler.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

190 lines
5.0 KiB
C#

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; }
/// <summary>
/// The value below which 5% of the samples fall (the 5th percentile).
/// </summary>
public double P5 { get; set; }
/// <summary>
/// The value below which 95% of the samples fall (the 95th percentile).
/// </summary>
public double P95 { get; set; }
/// <summary>
/// The value below which 99% of the samples fall (the 99th percentile).
/// </summary>
public double P99 { get; set; }
/// <summary>
/// The value below which 99.9% of the samples fall (the 99.9th percentile).
/// </summary>
public double P99_9 { get; set; }
/// <summary>
/// This is the sum of all samples that exceeded the stuttering threshold.
/// May not be very useful for non time based samples.
/// </summary>
public double Stuttering { get; set; }
public double Count { get; set; }
}
public string Name { get; private set; }
private List<double> 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<double>();
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;
}
/// <summary>
/// Calculates the specified percentile value from the collected samples.
/// </summary>
/// <param name="percentile">
/// The percentile to compute (a value between 0 and 100).
/// For example, passing 95 computes the 95th percentile.
/// </param>
/// <returns>
/// The value below which the given percentage of samples fall.
/// </returns>
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];
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="stutteringFactor">
/// The multiplier used to determine the stuttering threshold.
/// A higher factor makes it less sensitive to spikes (default is typically 2.5).
/// </param>
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<double> GetMovingAverage()
{
int windowSize = Math.Max( 1, (int)(Math.Sqrt( samples.Count ) * 10) );
var movingAverage = new List<double>( 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;
}
}
}