Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
51abbf1
refactorings to better test logfilereader
Hirogen Apr 24, 2026
61991d8
readonly struct for GC Pressure
Hirogen Apr 24, 2026
492394a
PluginRegistry was not correctly initialised for the tests
Hirogen Apr 24, 2026
e01e4bb
benchmarks
Hirogen Apr 24, 2026
3e2fa36
chore: update plugin hashes [skip ci]
github-actions[bot] Apr 26, 2026
43cc405
restructure tests for better organisation
Hirogen Apr 26, 2026
4a6a838
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
Hirogen Apr 26, 2026
c3202f0
chore: update plugin hashes [skip ci]
github-actions[bot] Apr 26, 2026
205c2b9
more Benchmarks
Hirogen Apr 26, 2026
571e9a3
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
Hirogen Apr 26, 2026
9b33824
chore: update plugin hashes [skip ci]
github-actions[bot] Apr 26, 2026
c02bb1d
BlockAllocator implemented
Apr 27, 2026
b90992f
chore: update plugin hashes [skip ci]
github-actions[bot] Apr 27, 2026
d7f7942
small changes
Apr 27, 2026
f153014
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
Apr 27, 2026
fab05cd
chore: update plugin hashes [skip ci]
github-actions[bot] Apr 27, 2026
ca43748
more optimisations and ArrayPool that works with GUI
Apr 29, 2026
63089f0
updates on LogBuffer
Apr 29, 2026
6d39dd2
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
Apr 29, 2026
1488bc6
chore: update plugin hashes [skip ci]
github-actions[bot] Apr 29, 2026
77e748a
extracting more interfaces from the LogWindow and LogfileReader, and …
Apr 30, 2026
c60ac30
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
Apr 30, 2026
c01453f
update review
Apr 30, 2026
145af1c
missing diagnostics if
Apr 30, 2026
092fb44
chore: update plugin hashes [skip ci]
github-actions[bot] Apr 30, 2026
a6faee1
update
May 4, 2026
e1c032d
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
May 4, 2026
6462e7a
chore: update plugin hashes [skip ci]
github-actions[bot] May 4, 2026
ad26847
add gitignore and json stuff
May 4, 2026
9dae529
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
May 4, 2026
212cca3
chore: update plugin hashes [skip ci]
github-actions[bot] May 4, 2026
a10e8f3
update columnizers
May 4, 2026
e11187b
merge conflict
May 4, 2026
b7e045a
chore: update plugin hashes [skip ci]
github-actions[bot] May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
/[Ll]og/
/[Ll]ogs/

# Visual Studio 2015/2017 cache/options directory
.vs/
Expand Down
191 changes: 191 additions & 0 deletions src/LogExpert.Benchmarks/BufferIndexBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
using BenchmarkDotNet.Attributes;

using ColumnizerLib;

using LogExpert.Benchmarks.Support;
using LogExpert.Core.Classes.Log.Buffers;

namespace LogExpert.Benchmarks;

[MemoryDiagnoser]
[RankColumn]
public class BufferIndexBenchmarks : IDisposable
{
private BufferIndex _index = null!;
private int _totalLines;

private bool _disposed;

[Params(100, 1_000, 10_000)]
public int BufferCount { get; set; }

private const int LINES_PER_BUFFER = 500;

[GlobalSetup]
public void Setup ()
{
_index = new BufferIndex(BufferCount, LINES_PER_BUFFER);
_totalLines = BufferCount * LINES_PER_BUFFER;

var fakeFileInfo = new FakeLogFileInfo();

using (var writeLock = _index.AcquireWriteLock())
Comment thread
Hirogen marked this conversation as resolved.
Dismissed
{
for (int i = 0; i < BufferCount; i++)
{
var buffer = new LogBuffer(fakeFileInfo, LINES_PER_BUFFER)
{
StartLine = i * LINES_PER_BUFFER
};

for (int j = 0; j < LINES_PER_BUFFER; j++)
{
buffer.AddLine(new LogLine($"line {i * LINES_PER_BUFFER + j}".AsMemory(), i * LINES_PER_BUFFER + j), 0);
}

_index.Add(buffer);
}
}

// Validate setup
var snapshot = _index.CreateSnapshot();
if (snapshot.BufferCount != BufferCount)
{
throw new InvalidOperationException($"Setup failed: expected {BufferCount} buffers, got {snapshot.BufferCount}");
}
}

[GlobalCleanup]
public void Cleanup () => _index.Dispose();

/// <summary>
/// Simulates tail-follow: reading the last 1000 lines sequentially.
/// Should hit Layer 0 (thread-local cache) ~99% of the time.
/// </summary>
[Benchmark(Baseline = true)]
public LogBuffer? SequentialAccess ()
{
using var readlock = _index.AcquireReadLock();
LogBuffer? last = null;
var start = Math.Max(0, _totalLines - 1000);
for (int i = start; i < _totalLines; i++)
{
var logBufferEntry = _index.TryFindBuffer(i);
if (logBufferEntry.Found)
{
last = logBufferEntry.Buffer;
}
}

return last;
}

/// <summary>
/// Simulates search/goto: deterministic stride across the full file.
/// Co-prime stride visits buffers in non-sequential, non-repeating order.
/// Exercises Layers 2 and 3 heavily.
/// </summary>
[Benchmark]
public LogBuffer? StrideAccess ()
{
using var readLock = _index.AcquireReadLock();
LogBuffer? last = null;
var stride = _totalLines / 3 + 1;
var lineNum = 0;
for (int i = 0; i < 1000; i++)
{
var logBufferEntry = _index.TryFindBuffer(lineNum);
if (logBufferEntry.Found)
{
last = logBufferEntry.Buffer;
}

lineNum = (lineNum + stride) % _totalLines;
}

return last;
}

/// <summary>
/// Worst case for Layer 0: always crossing buffer boundaries.
/// Exercises Layer 1 (adjacent prediction).
/// </summary>
[Benchmark]
public LogBuffer? BoundaryAccess ()
{
using var readLock = _index.AcquireReadLock();
LogBuffer? last = null;

for (int i = 0; i < 1000; i++)
{
int lineNum = i * (_totalLines / 1000);
var logBufferEntry = _index.TryFindBuffer(lineNum);
if (logBufferEntry.Found)
{
last = logBufferEntry.Buffer;
}
}

return last;
}

/// <summary>
/// Simulates UI scrolling: page-sized jumps forward through the file.
/// 50-line pages with 3x page jumps (fast scroll drag).
/// Exercises Layer 0 within pages and Layers 1-2 on transitions.
/// </summary>
[Benchmark]
public LogBuffer? ScrollAccess ()
{
using var readLock = _index.AcquireReadLock();
LogBuffer? last = null;
const int pageSize = 50;
const int pageJump = pageSize * 3;
var pageStart = 0;

for (int page = 0; page < 20 && pageStart < _totalLines; page++)
{
var pageEnd = Math.Min(pageStart + pageSize, _totalLines);
for (int line = pageStart; line < pageEnd; line++)
{
var logBufferEntry = _index.TryFindBuffer(line);
if (logBufferEntry.Found)
{
last = logBufferEntry.Buffer;
}
}

pageStart += pageJump;
}

return last;
}

/// <summary>
/// Measures LRU eviction cost at current scale.
/// </summary>
[Benchmark]
public void EvictAndRepopulate ()
{
_index.EvictLeastRecentlyUsed();
}

public void Dispose ()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose (bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_index?.Dispose();
}

_disposed = true;
}
}
}
170 changes: 170 additions & 0 deletions src/LogExpert.Benchmarks/BufferIndexContentionBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;

using ColumnizerLib;

using LogExpert.Benchmarks.Support;
using LogExpert.Core.Classes.Log.Buffers;

namespace LogExpert.Benchmarks;

/// <summary>
/// Measures ReaderWriterLockSlim contention under concurrent read load.
/// Compares single-threaded throughput against N concurrent readers
/// to determine if RWLS is a bottleneck worth optimizing.
/// </summary>
[MemoryDiagnoser]
[ThreadingDiagnoser] // Reports lock contention + thread pool stats
[RankColumn]
public class BufferIndexContentionBenchmarks : IDisposable
{
private BufferIndex _index = null!;
private int _totalLines;
private bool _disposed;

private const int BUFFERS = 10_000;
private const int LINES_PER_BUFFER = 500;
private const int READS_PER_TASK = 1_000;

[GlobalSetup]
public void Setup ()
{
_index = new BufferIndex(BUFFERS, LINES_PER_BUFFER);
_totalLines = BUFFERS * LINES_PER_BUFFER;

var fakeFileInfo = new FakeLogFileInfo();
using var writeLock = _index.AcquireWriteLock();
for (int i = 0; i < BUFFERS; i++)
{
var buffer = new LogBuffer(fakeFileInfo, LINES_PER_BUFFER)
{
StartLine = i * LINES_PER_BUFFER
};
for (int j = 0; j < LINES_PER_BUFFER; j++)
{
buffer.AddLine(
new LogLine($"line {i * LINES_PER_BUFFER + j}".AsMemory(),
i * LINES_PER_BUFFER + j), 0);
}
_index.Add(buffer);
}
}

/// <summary>
/// Single-threaded baseline: sequential reads under one read lock.
/// This is the ideal throughput ceiling.
/// </summary>
[Benchmark(Baseline = true)]
public int SingleThreadedReads ()
{
int found = 0;
using var readLock = _index.AcquireReadLock();
var start = Math.Max(0, _totalLines - READS_PER_TASK);
for (int i = start; i < _totalLines; i++)
{
if (_index.TryFindBuffer(i).Found)
{
found++;
}
}

return found;
}

/// <summary>
/// N concurrent readers each acquiring their own read lock.
/// If RWLS has no contention, throughput ≈ N × single-threaded.
/// </summary>
[Benchmark]
[Arguments(2)]
[Arguments(4)]
[Arguments(8)]
[Arguments(12)]
public int ConcurrentReads (int threadCount)
{
var total = 0;
_ = Parallel.For(0, threadCount, _ =>
{
int found = 0;
using var readLock = _index.AcquireReadLock();
var start = Math.Max(0, _totalLines - READS_PER_TASK);
for (int i = start; i < _totalLines; i++)
{
if (_index.TryFindBuffer(i).Found)
{
found++;
}
}
_ = Interlocked.Add(ref total, found);
});
return total;
}

/// <summary>
/// Simulates production: N readers + 1 writer (tail-follow append).
/// Writer acquires write lock briefly every ~1000 reads.
/// This is the realistic contention scenario.
/// </summary>
[Benchmark]
[Arguments(4)]
[Arguments(8)]
public int ConcurrentReadsWithWriter (int readerCount)
{
using var cts = new CancellationTokenSource();
var total = 0;

// Writer task: periodically takes write lock (simulates new buffer append)
var writerTask = Task.Run(() =>
{
while (!cts.Token.IsCancellationRequested)
{
using var writeLock = _index.AcquireWriteLock();
// Simulate brief write work (no actual mutation to keep state clean)
Thread.SpinWait(100);
}
});

// Reader tasks
_ = Parallel.For(0, readerCount, _ =>
{
int found = 0;
using var readLock = _index.AcquireReadLock();
var start = Math.Max(0, _totalLines - READS_PER_TASK);
for (int i = start; i < _totalLines; i++)
{
if (_index.TryFindBuffer(i).Found)
{
found++;
}
}

_ = Interlocked.Add(ref total, found);
});

cts.Cancel();
writerTask.Wait();
return total;
}

[GlobalCleanup]
public void Cleanup () => _index.Dispose();

public void Dispose ()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose (bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_index?.Dispose();
}

_disposed = true;
}
}
}
1 change: 1 addition & 0 deletions src/LogExpert.Benchmarks/LogExpert.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

<ItemGroup>
<ProjectReference Include="..\LogExpert.Core\LogExpert.Core.csproj" />
<ProjectReference Include="..\PluginRegistry\LogExpert.PluginRegistry.csproj" />
</ItemGroup>

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