Skip to content

Commit e01e4bb

Browse files
committed
benchmarks
1 parent 492394a commit e01e4bb

5 files changed

Lines changed: 240 additions & 10 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
using BenchmarkDotNet.Attributes;
2+
3+
using ColumnizerLib;
4+
5+
using LogExpert.Benchmarks.Support;
6+
using LogExpert.Core.Classes.Log;
7+
8+
namespace LogExpert.Benchmarks;
9+
10+
[MemoryDiagnoser]
11+
[RankColumn]
12+
public class BufferIndexBenchmarks
13+
{
14+
private BufferIndex _index = null!;
15+
private int _totalLines;
16+
17+
[Params(100, 1_000, 10_000)]
18+
public int BufferCount { get; set; }
19+
20+
private const int LinesPerBuffer = 500;
21+
22+
[GlobalSetup]
23+
public void Setup ()
24+
{
25+
_index = new BufferIndex(BufferCount, LinesPerBuffer);
26+
_totalLines = BufferCount * LinesPerBuffer;
27+
28+
var fakeFileInfo = new FakeLogFileInfo();
29+
30+
using (var _ = _index.AcquireWriteLock())
31+
{
32+
for (int i = 0; i < BufferCount; i++)
33+
{
34+
var buffer = new LogBuffer(fakeFileInfo, LinesPerBuffer)
35+
{
36+
StartLine = i * LinesPerBuffer
37+
};
38+
39+
for (int j = 0; j < LinesPerBuffer; j++)
40+
{
41+
buffer.AddLine(
42+
new LogLine($"line {i * LinesPerBuffer + j}".AsMemory(), i * LinesPerBuffer + j),
43+
0);
44+
}
45+
46+
_index.Add(buffer);
47+
}
48+
}
49+
50+
// Validate setup
51+
var snapshot = _index.CreateSnapshot();
52+
if (snapshot.BufferCount != BufferCount)
53+
{
54+
throw new InvalidOperationException($"Setup failed: expected {BufferCount} buffers, got {snapshot.BufferCount}");
55+
}
56+
}
57+
58+
[GlobalCleanup]
59+
public void Cleanup () => _index.Dispose();
60+
61+
/// <summary>
62+
/// Simulates tail-follow: reading the last 1000 lines sequentially.
63+
/// Should hit Layer 0 (thread-local cache) ~99% of the time.
64+
/// </summary>
65+
[Benchmark(Baseline = true)]
66+
public LogBuffer? SequentialAccess ()
67+
{
68+
using var _ = _index.AcquireReadLock();
69+
LogBuffer? last = null;
70+
var start = Math.Max(0, _totalLines - 1000);
71+
for (int i = start; i < _totalLines; i++)
72+
{
73+
var logBufferEntry = _index.TryFindBuffer(i);
74+
if (logBufferEntry.Found)
75+
{
76+
last = logBufferEntry.Buffer;
77+
}
78+
}
79+
return last;
80+
}
81+
82+
/// <summary>
83+
/// Simulates search/goto: deterministic stride across the full file.
84+
/// Co-prime stride visits buffers in non-sequential, non-repeating order.
85+
/// Exercises Layers 2 and 3 heavily.
86+
/// </summary>
87+
[Benchmark]
88+
public LogBuffer? StrideAccess ()
89+
{
90+
using var _ = _index.AcquireReadLock();
91+
LogBuffer? last = null;
92+
var stride = _totalLines / 3 + 1;
93+
var lineNum = 0;
94+
for (int i = 0; i < 1000; i++)
95+
{
96+
var logBufferEntry = _index.TryFindBuffer(lineNum);
97+
if (logBufferEntry.Found)
98+
{
99+
last = logBufferEntry.Buffer;
100+
}
101+
102+
lineNum = (lineNum + stride) % _totalLines;
103+
}
104+
105+
return last;
106+
}
107+
108+
/// <summary>
109+
/// Worst case for Layer 0: always crossing buffer boundaries.
110+
/// Exercises Layer 1 (adjacent prediction).
111+
/// </summary>
112+
[Benchmark]
113+
public LogBuffer? BoundaryAccess ()
114+
{
115+
using var _ = _index.AcquireReadLock();
116+
LogBuffer? last = null;
117+
118+
for (int i = 0; i < 1000; i++)
119+
{
120+
int lineNum = i * (_totalLines / 1000);
121+
var logBufferEntry = _index.TryFindBuffer(lineNum);
122+
if (logBufferEntry.Found)
123+
{
124+
last = logBufferEntry.Buffer;
125+
}
126+
}
127+
128+
return last;
129+
}
130+
131+
/// <summary>
132+
/// Simulates UI scrolling: page-sized jumps forward through the file.
133+
/// 50-line pages with 3x page jumps (fast scroll drag).
134+
/// Exercises Layer 0 within pages and Layers 1-2 on transitions.
135+
/// </summary>
136+
[Benchmark]
137+
public LogBuffer? ScrollAccess ()
138+
{
139+
using var _ = _index.AcquireReadLock();
140+
LogBuffer? last = null;
141+
const int pageSize = 50;
142+
const int pageJump = pageSize * 3;
143+
var pageStart = 0;
144+
145+
for (int page = 0; page < 20 && pageStart < _totalLines; page++)
146+
{
147+
var pageEnd = Math.Min(pageStart + pageSize, _totalLines);
148+
for (int line = pageStart; line < pageEnd; line++)
149+
{
150+
var logBufferEntry = _index.TryFindBuffer(line);
151+
if (logBufferEntry.Found)
152+
{
153+
last = logBufferEntry.Buffer;
154+
}
155+
}
156+
157+
pageStart += pageJump;
158+
}
159+
160+
return last;
161+
}
162+
163+
/// <summary>
164+
/// Measures LRU eviction cost at current scale.
165+
/// </summary>
166+
[Benchmark]
167+
public void EvictAndRepopulate ()
168+
{
169+
_index.EvictLeastRecentlyUsed();
170+
}
171+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using BenchmarkDotNet.Running;
2+
3+
namespace LogExpert.Benchmarks;
4+
5+
public static class Program
6+
{
7+
public static void Main (string[] args)
8+
{
9+
//_ = BenchmarkRunner.Run<StreamReaderBenchmarks>();
10+
_ = BenchmarkRunner.Run<BufferIndexBenchmarks>();
11+
}
12+
}
13+
14+
/*
15+
* Comment / Uncommen the benchmark to run, careful some can run longer
16+
* 1.) a dry run
17+
* dotnet run -c Release --job Dry --noOverwrite
18+
* 2.) a short run
19+
* dotnet run -c Release --job Short --noOverwrite
20+
* 3.) a full baseline run
21+
* dotnet run -c Release --noOverwrite
22+
*
23+
* The full baseline run generates a MD file
24+
* BenchmarkDotNet.Artifacts/results/*-report-github.md
25+
*
26+
* If changes are made with the LogfileReader / BufferIndex, always do a Benchmark to
27+
* verify no performance regression is introduced, especially with large files.
28+
*/

src/LogExpert.Benchmarks/StreamReaderBenchmarks.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System.Text;
22

33
using BenchmarkDotNet.Attributes;
4-
using BenchmarkDotNet.Running;
54

65
using LogExpert.Core.Classes.Log;
76
using LogExpert.Core.Entities;
@@ -150,12 +149,4 @@ private static void ReadAllLines (ILogStreamReader reader)
150149
// Consume the line
151150
}
152151
}
153-
}
154-
155-
public static class Program
156-
{
157-
public static void Main (string[] args)
158-
{
159-
_ = BenchmarkRunner.Run<StreamReaderBenchmarks>();
160-
}
161-
}
152+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using ColumnizerLib;
2+
3+
namespace LogExpert.Benchmarks.Support;
4+
5+
/// <summary>
6+
/// Minimal ILogFileInfo stub for benchmarks. No filesystem access.
7+
/// Wraps an in-memory byte array as the file content.
8+
/// </summary>
9+
internal sealed class FakeLogFileInfo : ILogFileInfo
10+
{
11+
private readonly byte[] _content;
12+
13+
public FakeLogFileInfo (string name = "fake.log", byte[]? content = null, long length = 1_000_000)
14+
{
15+
FullName = name;
16+
_content = content ?? [];
17+
Length = content?.Length ?? length;
18+
OriginalLength = Length;
19+
}
20+
21+
public string FullName { get; }
22+
public string FileName => Path.GetFileName(FullName);
23+
public string DirectoryName => Path.GetDirectoryName(FullName) ?? "";
24+
public char DirectorySeparatorChar => Path.DirectorySeparatorChar;
25+
public Uri Uri => new($"file:///{FullName}");
26+
public long Length { get; set; }
27+
public long OriginalLength { get; }
28+
public bool FileExists => true;
29+
public int PollInterval => 250;
30+
31+
public bool FileHasChanged () => false;
32+
public Stream OpenStream () => new MemoryStream(_content, writable: false);
33+
public ILogFileInfo GetRolloverInfo (string fileName) => new FakeLogFileInfo(fileName);
34+
}

src/LogExpert.Core/Classes/Log/BufferIndex.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
namespace LogExpert.Core.Classes.Log;
88

9+
/*
10+
* !IMPORTANT
11+
* Before and after changes are made run the BufferIndexBenchmarks for a baseline, so no performance regression is introduced
12+
* If changes are made to this class, please also review BufferIndexSnapshot and BufferShiftTest to ensure consistency and correctness.
13+
*/
14+
915
/// <summary>
1016
/// Thread-safe index that maps line numbers to <see cref="LogBuffer"/> instances with LRU eviction. This is the hot
1117
/// path — every GetLogLine call goes through here. Has zero file-I/O dependencies. Constructable with only integers for

0 commit comments

Comments
 (0)