Skip to content

Commit 205c2b9

Browse files
committed
more Benchmarks
1 parent 4a6a838 commit 205c2b9

14 files changed

Lines changed: 648 additions & 142 deletions

src/LogExpert.Benchmarks/BufferIndexBenchmarks.cs

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,38 @@ namespace LogExpert.Benchmarks;
99

1010
[MemoryDiagnoser]
1111
[RankColumn]
12-
public class BufferIndexBenchmarks
12+
public class BufferIndexBenchmarks : IDisposable
1313
{
1414
private BufferIndex _index = null!;
1515
private int _totalLines;
1616

17+
private bool _disposed;
18+
1719
[Params(100, 1_000, 10_000)]
1820
public int BufferCount { get; set; }
1921

20-
private const int LinesPerBuffer = 500;
22+
private const int LINES_PER_BUFFER = 500;
2123

2224
[GlobalSetup]
2325
public void Setup ()
2426
{
25-
_index = new BufferIndex(BufferCount, LinesPerBuffer);
26-
_totalLines = BufferCount * LinesPerBuffer;
27+
_index = new BufferIndex(BufferCount, LINES_PER_BUFFER);
28+
_totalLines = BufferCount * LINES_PER_BUFFER;
2729

2830
var fakeFileInfo = new FakeLogFileInfo();
2931

30-
using (var _ = _index.AcquireWriteLock())
32+
using (var writeLock = _index.AcquireWriteLock())
3133
{
3234
for (int i = 0; i < BufferCount; i++)
3335
{
34-
var buffer = new LogBuffer(fakeFileInfo, LinesPerBuffer)
36+
var buffer = new LogBuffer(fakeFileInfo, LINES_PER_BUFFER)
3537
{
36-
StartLine = i * LinesPerBuffer
38+
StartLine = i * LINES_PER_BUFFER
3739
};
3840

39-
for (int j = 0; j < LinesPerBuffer; j++)
41+
for (int j = 0; j < LINES_PER_BUFFER; j++)
4042
{
41-
buffer.AddLine(
42-
new LogLine($"line {i * LinesPerBuffer + j}".AsMemory(), i * LinesPerBuffer + j),
43-
0);
43+
buffer.AddLine(new LogLine($"line {i * LINES_PER_BUFFER + j}".AsMemory(), i * LINES_PER_BUFFER + j), 0);
4444
}
4545

4646
_index.Add(buffer);
@@ -65,7 +65,7 @@ public void Setup ()
6565
[Benchmark(Baseline = true)]
6666
public LogBuffer? SequentialAccess ()
6767
{
68-
using var _ = _index.AcquireReadLock();
68+
using var readlock = _index.AcquireReadLock();
6969
LogBuffer? last = null;
7070
var start = Math.Max(0, _totalLines - 1000);
7171
for (int i = start; i < _totalLines; i++)
@@ -76,6 +76,7 @@ public void Setup ()
7676
last = logBufferEntry.Buffer;
7777
}
7878
}
79+
7980
return last;
8081
}
8182

@@ -87,7 +88,7 @@ public void Setup ()
8788
[Benchmark]
8889
public LogBuffer? StrideAccess ()
8990
{
90-
using var _ = _index.AcquireReadLock();
91+
using var readLock = _index.AcquireReadLock();
9192
LogBuffer? last = null;
9293
var stride = _totalLines / 3 + 1;
9394
var lineNum = 0;
@@ -112,7 +113,7 @@ public void Setup ()
112113
[Benchmark]
113114
public LogBuffer? BoundaryAccess ()
114115
{
115-
using var _ = _index.AcquireReadLock();
116+
using var readLock = _index.AcquireReadLock();
116117
LogBuffer? last = null;
117118

118119
for (int i = 0; i < 1000; i++)
@@ -136,7 +137,7 @@ public void Setup ()
136137
[Benchmark]
137138
public LogBuffer? ScrollAccess ()
138139
{
139-
using var _ = _index.AcquireReadLock();
140+
using var readLock = _index.AcquireReadLock();
140141
LogBuffer? last = null;
141142
const int pageSize = 50;
142143
const int pageJump = pageSize * 3;
@@ -168,4 +169,23 @@ public void EvictAndRepopulate ()
168169
{
169170
_index.EvictLeastRecentlyUsed();
170171
}
172+
173+
public void Dispose ()
174+
{
175+
Dispose(true);
176+
GC.SuppressFinalize(this);
177+
}
178+
179+
protected virtual void Dispose (bool disposing)
180+
{
181+
if (!_disposed)
182+
{
183+
if (disposing)
184+
{
185+
_index?.Dispose();
186+
}
187+
188+
_disposed = true;
189+
}
190+
}
171191
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Diagnosers;
3+
4+
using ColumnizerLib;
5+
6+
using LogExpert.Benchmarks.Support;
7+
using LogExpert.Core.Classes.Log.Buffers;
8+
9+
namespace LogExpert.Benchmarks;
10+
11+
/// <summary>
12+
/// Measures ReaderWriterLockSlim contention under concurrent read load.
13+
/// Compares single-threaded throughput against N concurrent readers
14+
/// to determine if RWLS is a bottleneck worth optimizing.
15+
/// </summary>
16+
[MemoryDiagnoser]
17+
[ThreadingDiagnoser] // Reports lock contention + thread pool stats
18+
[RankColumn]
19+
public class BufferIndexContentionBenchmarks : IDisposable
20+
{
21+
private BufferIndex _index = null!;
22+
private int _totalLines;
23+
private bool _disposed;
24+
25+
private const int BUFFERS = 10_000;
26+
private const int LINES_PER_BUFFER = 500;
27+
private const int READS_PER_TASK = 1_000;
28+
29+
[GlobalSetup]
30+
public void Setup ()
31+
{
32+
_index = new BufferIndex(BUFFERS, LINES_PER_BUFFER);
33+
_totalLines = BUFFERS * LINES_PER_BUFFER;
34+
35+
var fakeFileInfo = new FakeLogFileInfo();
36+
using var writeLock = _index.AcquireWriteLock();
37+
for (int i = 0; i < BUFFERS; i++)
38+
{
39+
var buffer = new LogBuffer(fakeFileInfo, LINES_PER_BUFFER)
40+
{
41+
StartLine = i * LINES_PER_BUFFER
42+
};
43+
for (int j = 0; j < LINES_PER_BUFFER; j++)
44+
{
45+
buffer.AddLine(
46+
new LogLine($"line {i * LINES_PER_BUFFER + j}".AsMemory(),
47+
i * LINES_PER_BUFFER + j), 0);
48+
}
49+
_index.Add(buffer);
50+
}
51+
}
52+
53+
/// <summary>
54+
/// Single-threaded baseline: sequential reads under one read lock.
55+
/// This is the ideal throughput ceiling.
56+
/// </summary>
57+
[Benchmark(Baseline = true)]
58+
public int SingleThreadedReads ()
59+
{
60+
int found = 0;
61+
using var readLock = _index.AcquireReadLock();
62+
var start = Math.Max(0, _totalLines - READS_PER_TASK);
63+
for (int i = start; i < _totalLines; i++)
64+
{
65+
if (_index.TryFindBuffer(i).Found)
66+
{
67+
found++;
68+
}
69+
}
70+
71+
return found;
72+
}
73+
74+
/// <summary>
75+
/// N concurrent readers each acquiring their own read lock.
76+
/// If RWLS has no contention, throughput ≈ N × single-threaded.
77+
/// </summary>
78+
[Benchmark]
79+
[Arguments(2)]
80+
[Arguments(4)]
81+
[Arguments(8)]
82+
[Arguments(12)]
83+
public int ConcurrentReads (int threadCount)
84+
{
85+
var total = 0;
86+
_ = Parallel.For(0, threadCount, _ =>
87+
{
88+
int found = 0;
89+
using var readLock = _index.AcquireReadLock();
90+
var start = Math.Max(0, _totalLines - READS_PER_TASK);
91+
for (int i = start; i < _totalLines; i++)
92+
{
93+
if (_index.TryFindBuffer(i).Found)
94+
{
95+
found++;
96+
}
97+
}
98+
_ = Interlocked.Add(ref total, found);
99+
});
100+
return total;
101+
}
102+
103+
/// <summary>
104+
/// Simulates production: N readers + 1 writer (tail-follow append).
105+
/// Writer acquires write lock briefly every ~1000 reads.
106+
/// This is the realistic contention scenario.
107+
/// </summary>
108+
[Benchmark]
109+
[Arguments(4)]
110+
[Arguments(8)]
111+
public int ConcurrentReadsWithWriter (int readerCount)
112+
{
113+
using var cts = new CancellationTokenSource();
114+
var total = 0;
115+
116+
// Writer task: periodically takes write lock (simulates new buffer append)
117+
var writerTask = Task.Run(() =>
118+
{
119+
while (!cts.Token.IsCancellationRequested)
120+
{
121+
using var writeLock = _index.AcquireWriteLock();
122+
// Simulate brief write work (no actual mutation to keep state clean)
123+
Thread.SpinWait(100);
124+
}
125+
});
126+
127+
// Reader tasks
128+
_ = Parallel.For(0, readerCount, _ =>
129+
{
130+
int found = 0;
131+
using var readLock = _index.AcquireReadLock();
132+
var start = Math.Max(0, _totalLines - READS_PER_TASK);
133+
for (int i = start; i < _totalLines; i++)
134+
{
135+
if (_index.TryFindBuffer(i).Found)
136+
{
137+
found++;
138+
}
139+
}
140+
141+
_ = Interlocked.Add(ref total, found);
142+
});
143+
144+
cts.Cancel();
145+
writerTask.Wait();
146+
return total;
147+
}
148+
149+
[GlobalCleanup]
150+
public void Cleanup () => _index.Dispose();
151+
152+
public void Dispose ()
153+
{
154+
Dispose(true);
155+
GC.SuppressFinalize(this);
156+
}
157+
158+
protected virtual void Dispose (bool disposing)
159+
{
160+
if (!_disposed)
161+
{
162+
if (disposing)
163+
{
164+
_index?.Dispose();
165+
}
166+
167+
_disposed = true;
168+
}
169+
}
170+
}

src/LogExpert.Benchmarks/LogExpert.Benchmarks.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
<ItemGroup>
1818
<ProjectReference Include="..\LogExpert.Core\LogExpert.Core.csproj" />
19+
<ProjectReference Include="..\PluginRegistry\LogExpert.PluginRegistry.csproj" />
1920
</ItemGroup>
2021

2122
<!-- Exclude the shared AssemblyInfo.cs that Directory.Build.props tries to add -->

src/LogExpert.Benchmarks/Program.cs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,47 @@ namespace LogExpert.Benchmarks;
44

55
public static class Program
66
{
7+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Benchmarks")]
78
public static void Main (string[] args)
89
{
9-
//_ = BenchmarkRunner.Run<StreamReaderBenchmarks>();
10-
_ = BenchmarkRunner.Run<BufferIndexBenchmarks>();
10+
if (args == null || args.Length == 0)
11+
{
12+
Console.WriteLine("No benchmarks specified. Running all benchmarks...");
13+
14+
// Run all benchmarks if no arguments are provided
15+
_ = BenchmarkRunner.Run<StreamReaderBenchmarks>();
16+
_ = BenchmarkRunner.Run<BufferIndexBenchmarks>();
17+
_ = BenchmarkRunner.Run<ReadThroughputBenchmarks>();
18+
_ = BenchmarkRunner.Run<BufferIndexContentionBenchmarks>();
19+
}
20+
else
21+
{
22+
// Run specific benchmarks based on command-line arguments
23+
_ = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
24+
}
25+
26+
Console.WriteLine("Replace <benchmarkname> with the name of the benchmark you want to run, e.g. ");
27+
Console.WriteLine("StreamReaderBenchmarks: Benchmarks for stream readers");
28+
Console.WriteLine("ReadThroughputBenchmarks: Benchmarks for read throughput");
29+
Console.WriteLine("BufferIndexBenchmarks: Benchmarks for buffer index");
30+
Console.WriteLine("BufferIndexContentionBenchmarks: Benchmarks for buffer index contention");
31+
Console.WriteLine("Dry run:");
32+
Console.WriteLine("dotnet run -c Release -- --filter \"*<benchmarkname>*\" --job Dry --noOverwrite");
33+
Console.WriteLine("Short run:");
34+
Console.WriteLine("dotnet run -c Release -- --filter \"*<benchmarkname>*\" --job Short --noOverwrite");
35+
Console.WriteLine("Full baseline run:");
36+
Console.WriteLine("dotnet run -c Release -- --filter \"*<benchmarkname>*\" --noOverwrite");
1137
}
1238
}
1339

1440
/*
1541
* Comment / Uncommen the benchmark to run, careful some can run longer
1642
* 1.) a dry run
17-
* dotnet run -c Release --job Dry --noOverwrite
43+
* dotnet run -c Release -- --filter "StreamReaderBenchmarks" --job Dry --noOverwrite
1844
* 2.) a short run
19-
* dotnet run -c Release --job Short --noOverwrite
45+
* dotnet run -c Release -- --filter "StreamReaderBenchmarks" --job Short --noOverwrite
2046
* 3.) a full baseline run
21-
* dotnet run -c Release --noOverwrite
47+
* dotnet run -c Release -- --filter "StreamReaderBenchmarks" --noOverwrite
2248
*
2349
* The full baseline run generates a MD file
2450
* BenchmarkDotNet.Artifacts/results/*-report-github.md

0 commit comments

Comments
 (0)