-
Notifications
You must be signed in to change notification settings - Fork 184
Refactor LogFileReader for better testing and fix PluginRegistry initialization #571
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 61991d8
readonly struct for GC Pressure
Hirogen 492394a
PluginRegistry was not correctly initialised for the tests
Hirogen e01e4bb
benchmarks
Hirogen 3e2fa36
chore: update plugin hashes [skip ci]
github-actions[bot] 43cc405
restructure tests for better organisation
Hirogen 4a6a838
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
Hirogen c3202f0
chore: update plugin hashes [skip ci]
github-actions[bot] 205c2b9
more Benchmarks
Hirogen 571e9a3
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
Hirogen 9b33824
chore: update plugin hashes [skip ci]
github-actions[bot] c02bb1d
BlockAllocator implemented
b90992f
chore: update plugin hashes [skip ci]
github-actions[bot] d7f7942
small changes
f153014
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
fab05cd
chore: update plugin hashes [skip ci]
github-actions[bot] ca43748
more optimisations and ArrayPool that works with GUI
63089f0
updates on LogBuffer
6d39dd2
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
1488bc6
chore: update plugin hashes [skip ci]
github-actions[bot] 77e748a
extracting more interfaces from the LogWindow and LogfileReader, and …
c60ac30
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
c01453f
update review
145af1c
missing diagnostics if
092fb44
chore: update plugin hashes [skip ci]
github-actions[bot] a6faee1
update
e1c032d
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
6462e7a
chore: update plugin hashes [skip ci]
github-actions[bot] ad26847
add gitignore and json stuff
9dae529
Merge branch 'refactorings_logfile_reader' of https://github.com/LogE…
212cca3
chore: update plugin hashes [skip ci]
github-actions[bot] a10e8f3
update columnizers
e11187b
merge conflict
b7e045a
chore: update plugin hashes [skip ci]
github-actions[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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()) | ||
| { | ||
| 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
170
src/LogExpert.Benchmarks/BufferIndexContentionBenchmarks.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.