Custom columns for benchmarking

This commit is contained in:
Simon Lübeß
2025-08-17 03:01:41 +02:00
parent 8f51d1c626
commit 7df651103b
4 changed files with 260 additions and 67 deletions
+4 -1
View File
@@ -6,4 +6,7 @@ Name = "GlitchyEngine.Benchmark"
StartupObject = "GlitchyEngine.Benchmark.BenchmarkApp"
[Configs.Debug.Win64]
DebugWorkingDirectory = "D:\\Development\\Projects\\Beef\\GlitchyEngine\\GlitchyEditor"
DebugWorkingDirectory = "d:\\Development\\Projects\\Beef\\GlitchyEngine\\GlitchyEditor"
[Configs.Release.Win64]
DebugWorkingDirectory = "d:\\Development\\Projects\\Beef\\GlitchyEngine\\GlitchyEditor"
+29 -1
View File
@@ -99,6 +99,7 @@ class BasicBenchmark
}
benchmark.Name = scope $"Trivial Entity {entityCount}";
benchmark.RunInfo["EntityCount"] = Variant.Create(entityCount);
benchmark.Run(resultCollector);
for (Entity e in references)
@@ -128,6 +129,7 @@ class BasicBenchmark
}
benchmark.Name = scope $"Large Entity {entityCount}";
benchmark.RunInfo["EntityCount"] = Variant.Create(entityCount);
benchmark.Run(resultCollector);
for (Entity e in references)
@@ -147,7 +149,33 @@ class BasicBenchmark
Shutdown();
String output = scope .();
resultCollector.PrintStats(output);
List<IResultColumn> columns = scope List<IResultColumn>(BenchmarkResult.DefaultColumns);
columns.Add(scope IResultColumn() {
public static String ColumnName => "Per Entity";
public String Name => ColumnName;
private static List<StringView> _dependsOn = new List<StringView>(){MeanTimeResultColumn.ColumnName} ~ delete _;
public Span<StringView> DependsOn => _dependsOn;
public TimeSpan Calculate(BenchmarkResult.BenchmarkRun runToCalculate, BenchmarkResult allResults)
{
if (runToCalculate.RunInfo.TryGetValue("EntityCount", let entityCount))
{
TimeSpan meanTime = runToCalculate.ResultColumnValue[MeanTimeResultColumn.ColumnName];
if (entityCount.TryGet<int>(let count))
{
return TimeSpan(meanTime.Ticks / (int)count);
}
}
return default;
}
});
resultCollector.PrintStats(output, customColumns: columns);
File.WriteAllText("test.log", output);
Console.WriteLine(output);
+180 -48
View File
@@ -3,9 +3,66 @@ using System.Diagnostics;
using System.Linq;
using System.Collections;
using System.Collections;
using GlitchyEngine.Collections;
using static GlitchyEngine.Benchmark.BenchmarkResult;
namespace GlitchyEngine.Benchmark;
public interface IResultColumn
{
String Name => ColumnName;
static String ColumnName { get; }
Span<StringView> DependsOn => null;
TimeSpan Calculate(BenchmarkRun runToCalculate, BenchmarkResult allResults);
}
class TotalTimeResultColumn : IResultColumn
{
public static String ColumnName => "Total";
public String Name => ColumnName;
public TimeSpan Calculate(BenchmarkResult.BenchmarkRun runToCalculate, BenchmarkResult allRuns)
{
return runToCalculate.RunDurations.Sum();
}
}
class MeanTimeResultColumn : IResultColumn
{
public static String ColumnName => "Mean";
public String Name => ColumnName;
private static List<StringView> _dependsOn = new List<StringView>(){TotalTimeResultColumn.ColumnName} ~ delete _;
public Span<StringView> DependsOn => _dependsOn;
public TimeSpan Calculate(BenchmarkResult.BenchmarkRun runToCalculate, BenchmarkResult allRuns)
{
return TimeSpan(runToCalculate.ResultColumnValue[TotalTimeResultColumn.ColumnName].Ticks / runToCalculate.RunCount);
}
}
class StdDevResultColumn : IResultColumn
{
public static String ColumnName => "Std";
public String Name => ColumnName;
private static List<StringView> _dependsOn = new List<StringView>(){MeanTimeResultColumn.ColumnName} ~ delete _;
public Span<StringView> DependsOn => _dependsOn;
public TimeSpan Calculate(BenchmarkResult.BenchmarkRun runToCalculate, BenchmarkResult allRuns)
{
TimeSpan meanTime = runToCalculate.ResultColumnValue[MeanTimeResultColumn.ColumnName];
int64 variance = runToCalculate.RunDurations.Select(scope (d) => {
TimeSpan delta = d - meanTime;
return (delta * delta).Ticks / runToCalculate.RunCount;
}).Sum();
return TimeSpan((int64)Math.Sqrt((double)variance));
}
}
class BenchmarkResult
{
public class BenchmarkRun
@@ -15,9 +72,13 @@ class BenchmarkResult
public int RunCount => RunDurations.Count;
public TimeSpan TotalRunTime { get; private set; }
public TimeSpan MeanRunTime { get; private set; }
public TimeSpan RuntimeStdDev { get; private set; }
public append Dictionary<StringView, TimeSpan> ResultColumnValue = .();
public append Dictionary<StringView, Variant> RunInfo = .() ~ {
for (var v in _.Values)
{
v.Dispose();
}
};
[AllowAppend]
public this(StringView name, int runCount)
@@ -28,18 +89,6 @@ class BenchmarkResult
Name = appendedName;
RunDurations = appendedRunDurations;
}
public void CalculateStats()
{
TotalRunTime = RunDurations.Sum();
MeanRunTime = TimeSpan(TotalRunTime.Ticks / RunCount);
int64 variance = RunDurations.Select(scope (d) => {
TimeSpan delta = d - MeanRunTime;
return (delta * delta).Ticks / RunCount;
}).Sum();
RuntimeStdDev = TimeSpan((int64)Math.Sqrt((double)variance));
}
}
private append List<BenchmarkRun> _runData ~ ClearAndDeleteItems!(_);
@@ -67,7 +116,7 @@ class BenchmarkResult
private TimeUnit FigureOutUnits<T>(T timeData) where T : concrete, IEnumerable<TimeSpan>
{
TimeSpan shortestMeanTime = _runData.Select(scope (s) => s.MeanRunTime).Min();
/*TimeSpan shortestMeanTime = _runData.Select(scope (s) => s.MeanRunTime).Min();
if (shortestMeanTime.TotalDays > 2)
{
@@ -85,7 +134,7 @@ class BenchmarkResult
{
return .Seconds;
}
else
else*/
{
return .Milliseconds;
}
@@ -108,52 +157,139 @@ class BenchmarkResult
}
}
public void PrintStats(String outString, bool printHeader = true, TimeUnit? forcedTimeUnit = null)
typealias DagColumnNode = TreeNode<IResultColumn>;
typealias ColDepth = (IResultColumn Column, int Depth);
private void BuildDag(Span<IResultColumn> customColumns, List<IResultColumn> outColumnCalculationPlan)
{
for (BenchmarkRun run in _runData)
Dictionary<StringView, ColDepth> nameToColumns = scope .();
for (IResultColumn column in customColumns)
{
run.CalculateStats();
nameToColumns.TryAdd(column.Name, (column, -1));
}
TimeUnit printedUnit = forcedTimeUnit ?? FigureOutUnits(_runData.Select(scope (s) => s.MeanRunTime));
for (ref ColDepth entry in ref nameToColumns.Values)
{
int CalculateDepth(ColDepth columnWithDepth)
{
if (columnWithDepth.Depth != -1)
return columnWithDepth.Depth;
var widths = (NameColumn: 8, TotalTimeColumn: 7, MeanTimeColumn: 7, StdDevColumn: 7);
if (columnWithDepth.Column.DependsOn.IsEmpty)
{
return 0;
}
int maxParentDepth = -1;
for (StringView parentName in columnWithDepth.Column.DependsOn)
{
if (nameToColumns.TryGetRef(parentName, let _, let parentEntry))
{
int currentParentsDepth = CalculateDepth(*parentEntry);
parentEntry.Depth = currentParentsDepth;
maxParentDepth = Math.Max(maxParentDepth, currentParentsDepth);
}
}
return maxParentDepth + 1;
}
entry.Depth = CalculateDepth(entry);
}
List<ColDepth> columnsToSort = new List<ColDepth>(nameToColumns.Values);
defer delete columnsToSort;
columnsToSort.Sort(scope (a, b) => a.Depth <=> b.Depth);
outColumnCalculationPlan.AddRange(columnsToSort.Select(scope (a) => a.Column));
}
private void CalculateStats(Span<IResultColumn> customColumns)
{
List<IResultColumn> orderedColumns = scope List<IResultColumn>();
BuildDag(customColumns, orderedColumns);
for (IResultColumn columnToCalculate in orderedColumns)
{
for (BenchmarkRun runToCalculate in _runData)
{
runToCalculate.ResultColumnValue[columnToCalculate.Name] = columnToCalculate.Calculate(runToCalculate, this);
}
}
}
public static List<IResultColumn> DefaultColumns = new List<IResultColumn>(){
new TotalTimeResultColumn(), new MeanTimeResultColumn(), new StdDevResultColumn()} ~ DeleteContainerAndItems!(_);
public void PrintStats(String outString, bool printHeader = true, TimeUnit forcedTimeUnit = .Milliseconds, Span<IResultColumn> customColumns = null)
{
var customColumns;
if (customColumns.IsEmpty)
customColumns = DefaultColumns;
CalculateStats(customColumns);
//TODO: TimeUnit printedUnit = forcedTimeUnit ?? FigureOutUnits(_runData.Select(scope (s) => s.MeanRunTime));
TimeUnit printedUnit = forcedTimeUnit;
int nameColumnWidth = 0;
int[] columnWidths = scope int[customColumns.Length];
// Prints all numbers into a buffer, calculates column widths
SimpleStringList preparedStrings = scope .();
for (BenchmarkRun run in _runData)
{
// Print Name
widths.NameColumn = Math.Max(widths.NameColumn, run.Name.Length);
nameColumnWidth = Math.Max(nameColumnWidth, run.Name.Length);
// Print Total Time
StringView totalRunTimeView = preparedStrings.Add(scope (s) => PrintTimeSpan(run.TotalRunTime, printedUnit, s));
widths.TotalTimeColumn = Math.Max(widths.TotalTimeColumn, totalRunTimeView.Length);
// Print Total Time
StringView meanRunTimeView = preparedStrings.Add(scope (s) => PrintTimeSpan(run.MeanRunTime, printedUnit, s));
widths.MeanTimeColumn = Math.Max(widths.MeanTimeColumn, meanRunTimeView.Length);
// Print Standard Deviation
StringView stdDevView = preparedStrings.Add(scope (s) => PrintTimeSpan(run.RuntimeStdDev, printedUnit, s));
widths.StdDevColumn = Math.Max(widths.StdDevColumn, stdDevView.Length);
for (IResultColumn column in customColumns)
{
StringView printedValue = preparedStrings.Add(scope (s) => PrintTimeSpan(run.ResultColumnValue[column.Name], printedUnit, s));
columnWidths[@column.Index] = Math.Max(columnWidths[@column.Index], printedValue.Length);
}
}
String lineFormat = scope $"| {{, -{widths.NameColumn}}} | {{, {widths.TotalTimeColumn}}} | {{, {widths.MeanTimeColumn}}} | {{, {widths.StdDevColumn}}} |\n";
if (printHeader)
{
nameColumnWidth = Math.Max(nameColumnWidth, "Benchmark".Length);
int lengthBefore = outString.Length;
outString.AppendF(lineFormat, Formatter.Center("Benchmark", widths.NameColumn), Formatter.Center("Total", widths.TotalTimeColumn),
Formatter.Center("Mean", widths.MeanTimeColumn), Formatter.Center("Std", widths.StdDevColumn));
outString.Append("|");
outString.AppendF($" {Formatter.Center("Benchmark", nameColumnWidth)} |");
for (IResultColumn column in customColumns)
{
columnWidths[@column.Index] = Math.Max(columnWidths[@column.Index], column.Name.Length);
outString.AppendF($" {Formatter.Center(column.Name, columnWidths[@column.Index])} |");
}
outString.Append('\n');
// Print dashed line to separate header
int lineLength = outString.Length - lengthBefore;
outString.Append('-', lineLength - 1);
outString.Append('\n');
}
int preparedStringIndex = 0;
for (BenchmarkRun run in _runData)
{
outString.AppendF(lineFormat, run.Name, preparedStrings[preparedStringIndex++], preparedStrings[preparedStringIndex++], preparedStrings[preparedStringIndex++]);
outString.Append("|");
outString.AppendF($" {Formatter.LeftAlign(run.Name, nameColumnWidth)} |");
for (int i < customColumns.Length)
{
outString.AppendF($" {Formatter.RightAlign(preparedStrings[preparedStringIndex++], columnWidths[i])} |");
}
outString.Append('\n');
}
}
}
@@ -162,6 +298,8 @@ class Benchmark
{
private append String _name = .();
public append Dictionary<StringView, Variant> RunInfo = .();
public StringView Name
{
get => _name;
@@ -177,6 +315,8 @@ class Benchmark
public void Run(BenchmarkResult resultsCollector, int warmupRuns = 10, int runs = 100)
{
let resultData = resultsCollector.NewRun(Name, runs);
for (var kv in RunInfo)
resultData.RunInfo[kv.key] = kv.value;
BeforeAll?.Invoke();
@@ -187,8 +327,6 @@ class Benchmark
{
Console.Write($"Warmup run {i + 1}/{warmupRuns}...");
Console.CursorLeft = 0;
//TimeSpan duration = DoRun();
//Console.WriteLine($"Warmup run {i + 1} finished after {duration.TotalMilliseconds}ms.");
}
watch.Stop();
Console.WriteLine($"Warmup finished after {watch.Elapsed.TotalMilliseconds}ms");
@@ -201,14 +339,8 @@ class Benchmark
watch.Restart();
resultData.RunDurations[i] = DoRun();
watch.Stop();
//Console.WriteLine($"Run {i + 1}: {_runDurations[i]} (total: {watch.Elapsed.TotalMilliseconds}ms).");
}
resultData.CalculateStats();
Console.WriteLine(scope $"Finished: Total ({runs} runs) {resultData.TotalRunTime.TotalMilliseconds}ms | Mean: {resultData.MeanRunTime.TotalMilliseconds}ms | Std Dev: {resultData.RuntimeStdDev.TotalMilliseconds}ms");
AfterAll?.Invoke();
}
+42 -12
View File
@@ -4,22 +4,39 @@ namespace GlitchyEngine.Benchmark;
static class Formatter
{
public static CenterFormatter<T> Center<T>(T value, int width, bool leftAlignWhenUneven = true)
public static AlignFormatter<T> LeftAlign<T>(T value, int width)
{
return CenterFormatter<T>(value, width, leftAlignWhenUneven);
return AlignFormatter<T>(value, width, .Left);
}
public struct CenterFormatter<T> : IFormattable
public static AlignFormatter<T> RightAlign<T>(T value, int width)
{
return AlignFormatter<T>(value, width, .Right);
}
public static AlignFormatter<T> Center<T>(T value, int width, bool leftAlignWhenUneven = true)
{
return AlignFormatter<T>(value, width, .Center(leftAlignWhenUneven));
}
public enum Align
{
case Left;
case Center(bool LeftAlignWhenUneven);
case Right;
}
public struct AlignFormatter<T> : IFormattable
{
private T _value;
private int _width;
private bool _leftAlignWhenUneven;
private Align _align;
public this(T value, int width, bool leftAlignWhenUneven = true)
public this(T value, int width, Align align)
{
_value = value;
_width = width;
_leftAlignWhenUneven = leftAlignWhenUneven;
_align = align;
}
public void ToString(String outString, String format, IFormatProvider formatProvider)
@@ -35,13 +52,26 @@ static class Formatter
int missingSpaceCount = _width - buffer.Length;
int widthAfterLeftPad = _width - missingSpaceCount / 2;
// If the number of spaces is uneven and we want to left align (have more space right) we need to pad one less on the left
if (_leftAlignWhenUneven && missingSpaceCount % 2 == 1)
widthAfterLeftPad -= 1;
switch (_align)
{
case .Left:
buffer.PadRight(_width, ' ');
case .Center(bool leftAlignWhenUneven):
int widthAfterLeftPad = _width - missingSpaceCount / 2;
// If the number of spaces is uneven and we want to left align (have more space right) we need to pad one less on the left
if (leftAlignWhenUneven && missingSpaceCount % 2 == 1)
widthAfterLeftPad -= 1;
buffer..PadLeft(widthAfterLeftPad).PadRight(_width, ' ');
case .Right:
buffer.PadLeft(_width, ' ');
}
buffer..PadLeft(widthAfterLeftPad);
outString..Append(buffer).PadRight(_width, ' ');
outString.Append(buffer);
}
public override void ToString(String strBuffer)
{
ToString(strBuffer, null, null);
}
}
}