Skip to content

Commit 89c6dea

Browse files
authored
Merge pull request #20 from rian-be/develop
Feat: BenchmarkDotNet suites for render and player pipelines
2 parents a678c41 + 67b0732 commit 89c6dea

21 files changed

Lines changed: 1310 additions & 1 deletion
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\ChangeTrace.csproj" />
12+
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
13+
</ItemGroup>
14+
15+
</Project>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using BenchmarkDotNet.Attributes;
2+
using ChangeTrace.Player.Playback;
3+
4+
namespace ChangeTrace.Benchmarks.Player;
5+
6+
/// <summary>
7+
/// Benchmarks low-level timeline cursor navigation.
8+
/// </summary>
9+
/// <remarks>
10+
/// Measures binary seeking and forward draining over synthetic timeline events.
11+
/// These operations are used by player seeking and playback tick event emission.
12+
/// </remarks>
13+
[MemoryDiagnoser]
14+
[InProcess]
15+
[MinIterationTime(250)]
16+
public class EventCursorBenchmarks
17+
{
18+
private PlayerBenchmarkFixture _fixture = null!;
19+
private EventCursor _cursor = null!;
20+
21+
/// <summary>
22+
/// Number of synthetic timeline events used by the cursor.
23+
/// </summary>
24+
[Params(1_000, 10_000, 100_000)]
25+
public int EventCount { get; set; }
26+
27+
/// <summary>
28+
/// Creates deterministic player benchmark state for the current event count.
29+
/// </summary>
30+
[GlobalSetup]
31+
public void Setup()
32+
{
33+
_fixture = PlayerBenchmarkFixture.Create(EventCount);
34+
_cursor = _fixture.CreateCursor();
35+
}
36+
37+
/// <summary>
38+
/// Seeks to the middle of the event stream using cursor binary search.
39+
/// </summary>
40+
[Benchmark]
41+
public int SeekToMiddle()
42+
{
43+
_cursor.SeekTo(EventCount / 2.0);
44+
return _cursor.Index;
45+
}
46+
47+
/// <summary>
48+
/// Drains all events forward from a fresh cursor.
49+
/// </summary>
50+
[Benchmark]
51+
public int DrainForwardAll()
52+
{
53+
_cursor.ResetToStart();
54+
return _cursor.DrainForward(EventCount).Count;
55+
}
56+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using BenchmarkDotNet.Attributes;
2+
using ChangeTrace.Core.Models;
3+
using ChangeTrace.Player.Playback;
4+
5+
namespace ChangeTrace.Benchmarks.Player;
6+
7+
/// <summary>
8+
/// Benchmarks player seek operations.
9+
/// </summary>
10+
/// <remarks>
11+
/// Measures absolute and relative seek paths, including virtual clock snapping,
12+
/// cursor repositioning, and progress calculation.
13+
/// </remarks>
14+
[MemoryDiagnoser]
15+
[InProcess]
16+
[MinIterationTime(250)]
17+
public class SeekableTimelineBenchmarks
18+
{
19+
private PlayerBenchmarkFixture _fixture = null!;
20+
private SeekableTimeline _seekable = null!;
21+
private Timestamp _middle = default;
22+
23+
/// <summary>
24+
/// Number of synthetic timeline events used by seek benchmarks.
25+
/// </summary>
26+
[Params(1_000, 10_000, 100_000)]
27+
public int EventCount { get; set; }
28+
29+
/// <summary>
30+
/// Creates deterministic player benchmark state for the current event count.
31+
/// </summary>
32+
[GlobalSetup]
33+
public void Setup()
34+
{
35+
_fixture = PlayerBenchmarkFixture.Create(EventCount);
36+
_seekable = _fixture.CreateSeekable();
37+
_middle = Timestamp.Create(EventCount / 2).Value;
38+
}
39+
40+
/// <summary>
41+
/// Seeks to the middle of the timeline using an absolute timestamp.
42+
/// </summary>
43+
[Benchmark]
44+
public double SeekToMiddle()
45+
{
46+
_seekable.Seek(_middle);
47+
return _seekable.Progress;
48+
}
49+
50+
/// <summary>
51+
/// Seeks relative to the current position and returns progress.
52+
/// </summary>
53+
[Benchmark]
54+
public double SeekRelativeSmallDelta()
55+
{
56+
_seekable.SeekRelative(17);
57+
return _seekable.Progress;
58+
}
59+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using BenchmarkDotNet.Attributes;
2+
3+
namespace ChangeTrace.Benchmarks.Player;
4+
5+
/// <summary>
6+
/// Benchmarks creation of fully wired timeline players.
7+
/// </summary>
8+
/// <remarks>
9+
/// Covers factory wiring, timeline duration calculation, timeline normalization,
10+
/// cursor creation, seekable timeline creation, and transport setup.
11+
/// </remarks>
12+
[MemoryDiagnoser]
13+
[InProcess]
14+
[MinIterationTime(250)]
15+
public class TimelinePlayerFactoryBenchmarks
16+
{
17+
private PlayerBenchmarkFixture _fixture = null!;
18+
19+
/// <summary>
20+
/// Number of synthetic timeline events used to create the player.
21+
/// </summary>
22+
[Params(1_000, 10_000, 100_000)]
23+
public int EventCount { get; set; }
24+
25+
/// <summary>
26+
/// Creates deterministic player benchmark state for the current event count.
27+
/// </summary>
28+
[GlobalSetup]
29+
public void Setup()
30+
=> _fixture = PlayerBenchmarkFixture.Create(EventCount);
31+
32+
/// <summary>
33+
/// Creates and disposes a fully wired timeline player.
34+
/// </summary>
35+
[Benchmark]
36+
public double CreateTimelinePlayer()
37+
{
38+
using var player = _fixture.CreatePlayerFactory()
39+
.Create(PlayerBenchmarkFixture.CreateTimeline(EventCount));
40+
41+
return player.DurationSeconds;
42+
}
43+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using BenchmarkDotNet.Attributes;
2+
3+
namespace ChangeTrace.Benchmarks.Player;
4+
5+
/// <summary>
6+
/// Benchmarks single-event stepping through a timeline.
7+
/// </summary>
8+
/// <remarks>
9+
/// Measures the cost of repeatedly calling the player stepper until all events have
10+
/// been emitted. This covers cursor movement and virtual clock position snapping.
11+
/// </remarks>
12+
[MemoryDiagnoser]
13+
[InProcess]
14+
[MinIterationTime(250)]
15+
public class TimelineStepperBenchmarks
16+
{
17+
private PlayerBenchmarkFixture _fixture = null!;
18+
19+
/// <summary>
20+
/// Number of synthetic timeline events stepped through by the benchmark.
21+
/// </summary>
22+
[Params(1_000, 10_000, 100_000)]
23+
public int EventCount { get; set; }
24+
25+
/// <summary>
26+
/// Creates deterministic player benchmark state for the current event count.
27+
/// </summary>
28+
[GlobalSetup]
29+
public void Setup()
30+
=> _fixture = PlayerBenchmarkFixture.Create(EventCount);
31+
32+
/// <summary>
33+
/// Steps forward through all events in a fresh stepper.
34+
/// </summary>
35+
[Benchmark]
36+
public int StepForwardThroughAllEvents()
37+
{
38+
var stepper = _fixture.CreateStepper();
39+
var moved = 0;
40+
41+
while (stepper.StepForward().IsSuccess)
42+
moved++;
43+
44+
return moved;
45+
}
46+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using ChangeTrace.Core.Diagnostics;
2+
using ChangeTrace.Core.Events;
3+
using ChangeTrace.Core.Events.Info;
4+
using ChangeTrace.Core.Models;
5+
using ChangeTrace.Core.Timelines;
6+
using ChangeTrace.Player.Factory;
7+
using ChangeTrace.Player.Interfaces;
8+
using ChangeTrace.Player.Playback;
9+
10+
namespace ChangeTrace.Benchmarks.Player;
11+
12+
/// <summary>
13+
/// Shared deterministic fixture for player benchmarks.
14+
/// </summary>
15+
/// <remarks>
16+
/// Creates synthetic timelines and player components without starting the timer-driven
17+
/// playback transport, keeping benchmarks deterministic and CPU-bound.
18+
/// </remarks>
19+
internal sealed class PlayerBenchmarkFixture
20+
{
21+
private readonly IReadOnlyList<TraceEvent> _events;
22+
23+
private PlayerBenchmarkFixture(
24+
int eventCount,
25+
Timeline timeline,
26+
IReadOnlyList<TraceEvent> events)
27+
{
28+
EventCount = eventCount;
29+
Timeline = timeline;
30+
_events = events;
31+
}
32+
33+
/// <summary>
34+
/// Number of synthetic timeline events represented by the fixture.
35+
/// </summary>
36+
public int EventCount { get; }
37+
38+
/// <summary>
39+
/// Synthetic timeline used by factory-level player benchmarks.
40+
/// </summary>
41+
public Timeline Timeline { get; }
42+
43+
/// <summary>
44+
/// Builds a player benchmark fixture with normalized event playback times.
45+
/// </summary>
46+
/// <param name="eventCount">Number of synthetic timeline events to create.</param>
47+
public static PlayerBenchmarkFixture Create(int eventCount)
48+
{
49+
var timeline = CreateTimeline(eventCount);
50+
TimelineNormalizer.Normalize(timeline, targetDurationSeconds: eventCount);
51+
return new PlayerBenchmarkFixture(eventCount, timeline, timeline.Events.ToArray());
52+
}
53+
54+
/// <summary>
55+
/// Creates a fresh event cursor over the fixture events.
56+
/// </summary>
57+
public EventCursor CreateCursor()
58+
=> new(_events);
59+
60+
/// <summary>
61+
/// Creates a fresh stepper using a new cursor and virtual clock.
62+
/// </summary>
63+
public TimelineStepper CreateStepper()
64+
=> new(CreateCursor(), CreateClock());
65+
66+
/// <summary>
67+
/// Creates a seekable timeline over a new cursor and virtual clock.
68+
/// </summary>
69+
public SeekableTimeline CreateSeekable()
70+
=> new(CreateClock(), CreateCursor(), EventCount);
71+
72+
/// <summary>
73+
/// Creates a player factory with no-op diagnostics.
74+
/// </summary>
75+
public TimelinePlayerFactory CreatePlayerFactory()
76+
=> new(new NoopDiagnosticsProvider());
77+
78+
/// <summary>
79+
/// Creates a raw synthetic timeline.
80+
/// </summary>
81+
/// <param name="eventCount">Number of events to create.</param>
82+
public static Timeline CreateTimeline(int eventCount)
83+
{
84+
var repository = RepositoryId.Create("bench", "player").Value;
85+
var timeline = new Timeline(repository);
86+
var actor = ActorName.Create("benchmark-user").Value;
87+
const long baseUnix = 1_700_000_000;
88+
89+
for (var i = 0; i < eventCount; i++)
90+
{
91+
var timestamp = Timestamp.Create(baseUnix + i).Value;
92+
timeline.AddEvent(new TraceEvent(
93+
new TraceEventCore(
94+
timestamp,
95+
actor,
96+
$"src/module-{i % 128}/file-{i}.cs")));
97+
}
98+
99+
return timeline;
100+
}
101+
102+
private static VirtualClock CreateClock()
103+
=> new(initialSpeed: 1.0, acceleration: 1.0);
104+
105+
private sealed class NoopDiagnosticsProvider : IDiagnosticsProvider
106+
{
107+
/// <inheritdoc />
108+
public MemoryMetrics GetMemoryMetrics()
109+
=> new(0, 0, 0);
110+
111+
/// <inheritdoc />
112+
public int[] GetGcCollections()
113+
=> [0, 0, 0];
114+
115+
/// <inheritdoc />
116+
public RuntimeMetrics GetRuntimeMetrics()
117+
=> new(0, 0, 0, 0);
118+
119+
/// <inheritdoc />
120+
public IReadOnlyDictionary<string, double> GetCustomMetrics()
121+
=> new Dictionary<string, double>();
122+
123+
/// <inheritdoc />
124+
public void RecordMetric(string key, double value)
125+
{
126+
}
127+
128+
/// <inheritdoc />
129+
public void RecordEvent(string category, string label)
130+
{
131+
}
132+
133+
/// <inheritdoc />
134+
public IReadOnlyList<KeyValuePair<string, int>> GetTopEvents(string category, int count)
135+
=> [];
136+
}
137+
}

Benchmarks/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
using BenchmarkDotNet.Running;
2+
3+
BenchmarkSwitcher
4+
.FromAssembly(typeof(Program).Assembly)
5+
.Run(args);

0 commit comments

Comments
 (0)