From 1e8882ffba629fb17ca5d7155cae56a40da40ed5 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Tue, 14 Apr 2026 17:20:29 +0200 Subject: [PATCH 01/24] optimisations --- .../Classes/Log/LogfileReader.cs | 65 ++++--------------- .../Log/PositionAwareStreamReaderLegacy.cs | 3 - src/LogExpert.Core/Classes/Util.cs | 44 +++++++++++-- .../Interfaces/ILogStreamReaderMemory.cs | 23 ++++--- .../Dialogs/HighlightDialog.Designer.cs | 4 +- .../Classes/CommandLine/CmdLineInt.cs | 1 - 6 files changed, 68 insertions(+), 72 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index ad2b43ce..f6a74964 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -30,6 +30,7 @@ public partial class LogfileReader : IAutoLogLineMemoryColumnizerCallback, IDisp private readonly ReaderType _readerType; private readonly int _maximumLineLength; + private readonly Lock _logBufferLock = new(); private readonly ReaderWriterLockSlim _bufferListLock = new(LockRecursionPolicy.SupportsRecursion); private readonly ReaderWriterLockSlim _disposeLock = new(LockRecursionPolicy.SupportsRecursion); private readonly ReaderWriterLockSlim _lruCacheDictLock = new(LockRecursionPolicy.SupportsRecursion); @@ -1203,7 +1204,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start try { using var fileStream = logFileInfo.OpenStream(); - using var reader = GetLogStreamReader(fileStream, EncodingOptions); + using var reader = GetLogStreamReader(fileStream, EncodingOptions) as ILogStreamReaderMemory; reader.Position = filePos; _fileLength = logFileInfo.Length; @@ -1236,7 +1237,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start } else { - logBuffer = _bufferList[_bufferList.Count - 1]; + logBuffer = _bufferList[^1]; if (!logBuffer.FileInfo.FullName.Equals(logFileInfo.FullName, StringComparison.Ordinal)) { @@ -1281,7 +1282,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start var droppedLines = logBuffer.PrevBuffersDroppedLinesSum; filePos = reader.Position; - var (success, lineMemory, wasDropped) = ReadLineMemory(reader as ILogStreamReaderMemory, logBuffer.StartLine + logBuffer.LineCount, logBuffer.StartLine + logBuffer.LineCount + droppedLines); + var (success, lineMemory, wasDropped) = ReadLineMemory(reader, logBuffer.StartLine + logBuffer.LineCount, logBuffer.StartLine + logBuffer.LineCount + droppedLines); while (success) { @@ -1294,7 +1295,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start { logBuffer.DroppedLinesCount += 1; droppedLines++; - (success, lineMemory, wasDropped) = ReadLineMemory(reader as ILogStreamReaderMemory, logBuffer.StartLine + logBuffer.LineCount, logBuffer.StartLine + logBuffer.LineCount + droppedLines); + (success, lineMemory, wasDropped) = ReadLineMemory(reader, logBuffer.StartLine + logBuffer.LineCount, logBuffer.StartLine + logBuffer.LineCount + droppedLines); continue; } @@ -1351,7 +1352,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start filePos = reader.Position; lineNum++; - (success, lineMemory, wasDropped) = ReadLineMemory(reader as ILogStreamReaderMemory, logBuffer.StartLine + logBuffer.LineCount, logBuffer.StartLine + logBuffer.LineCount + droppedLines); + (success, lineMemory, wasDropped) = ReadLineMemory(reader, logBuffer.StartLine + logBuffer.LineCount, logBuffer.StartLine + logBuffer.LineCount + droppedLines); } logBuffer.Size = filePos - logBuffer.StartPos; @@ -1619,9 +1620,8 @@ private void ReReadBuffer (LogBuffer logBuffer) #if DEBUG _logger.Info(CultureInfo.InvariantCulture, "re-reading buffer: {0}/{1}/{2}", logBuffer.StartLine, logBuffer.LineCount, logBuffer.FileInfo.FullName); #endif - try + lock (_logBufferLock) { - Monitor.Enter(logBuffer); Stream fileStream = null; try { @@ -1635,7 +1635,8 @@ private void ReReadBuffer (LogBuffer logBuffer) try { - var reader = GetLogStreamReader(fileStream, EncodingOptions); + //TODO LogStream Reader has to be changed to ILogStreamReaderMemory + var reader = GetLogStreamReader(fileStream, EncodingOptions) as ILogStreamReaderMemory; var filePos = logBuffer.StartPos; reader.Position = logBuffer.StartPos; @@ -1644,7 +1645,7 @@ private void ReReadBuffer (LogBuffer logBuffer) var dropCount = logBuffer.PrevBuffersDroppedLinesSum; logBuffer.ClearLines(); - var (success, lineMemory, wasDropped) = ReadLineMemory(reader as ILogStreamReaderMemory, logBuffer.StartLine + logBuffer.LineCount, logBuffer.StartLine + logBuffer.LineCount + dropCount); + var (success, lineMemory, wasDropped) = ReadLineMemory(reader, logBuffer.StartLine + logBuffer.LineCount, logBuffer.StartLine + logBuffer.LineCount + dropCount); while (success) { @@ -1656,7 +1657,7 @@ private void ReReadBuffer (LogBuffer logBuffer) if (wasDropped) { dropCount++; - (success, lineMemory, wasDropped) = ReadLineMemory(reader as ILogStreamReaderMemory, logBuffer.StartLine + logBuffer.LineCount, logBuffer.StartLine + logBuffer.LineCount + dropCount); + (success, lineMemory, wasDropped) = ReadLineMemory(reader, logBuffer.StartLine + logBuffer.LineCount, logBuffer.StartLine + logBuffer.LineCount + dropCount); continue; } @@ -1666,7 +1667,7 @@ private void ReReadBuffer (LogBuffer logBuffer) filePos = reader.Position; lineCount++; - (success, lineMemory, wasDropped) = ReadLineMemory(reader as ILogStreamReaderMemory, logBuffer.StartLine + logBuffer.LineCount, logBuffer.StartLine + logBuffer.LineCount + dropCount); + (success, lineMemory, wasDropped) = ReadLineMemory(reader, logBuffer.StartLine + logBuffer.LineCount, logBuffer.StartLine + logBuffer.LineCount + dropCount); } if (maxLinesCount != logBuffer.LineCount) @@ -1691,50 +1692,8 @@ private void ReReadBuffer (LogBuffer logBuffer) fileStream.Close(); } } - finally - { - Monitor.Exit(logBuffer); - } } - /// - /// Retrieves the log buffer that contains the specified line number. - /// - /// - /// The zero-based line number for which to retrieve the corresponding log buffer. Must be greater than or equal to - /// zero. - /// - /// - /// The instance that contains the specified line number, or if no - /// such buffer exists. - /// - // private LogBuffer GetBufferForLine (int lineNum) - // { - //#if DEBUG - // long startTime = Environment.TickCount; - //#endif - // LogBuffer logBuffer = null; - // AcquireBufferListReaderLock(); - - // var startIndex = 0; - // var count = _bufferList.Count; - // for (var i = startIndex; i < count; ++i) - // { - // logBuffer = _bufferList[i]; - // if (lineNum >= logBuffer.StartLine && lineNum < logBuffer.StartLine + logBuffer.LineCount) - // { - // UpdateLruCache(logBuffer); - // break; - // } - // } - //#if DEBUG - // long endTime = Environment.TickCount; - // //_logger.logDebug("getBufferForLine(" + lineNum + ") duration: " + ((endTime - startTime)) + " ms. Buffer start line: " + logBuffer.StartLine); - //#endif - // ReleaseBufferListReaderLock(); - // return logBuffer; - // } - /// /// Retrieves the log buffer that contains the specified line number. /// diff --git a/src/LogExpert.Core/Classes/Log/PositionAwareStreamReaderLegacy.cs b/src/LogExpert.Core/Classes/Log/PositionAwareStreamReaderLegacy.cs index d4149460..b30baefe 100644 --- a/src/LogExpert.Core/Classes/Log/PositionAwareStreamReaderLegacy.cs +++ b/src/LogExpert.Core/Classes/Log/PositionAwareStreamReaderLegacy.cs @@ -13,9 +13,6 @@ public class PositionAwareStreamReaderLegacy (Stream stream, EncodingOptions enc public override bool IsDisposed { get; protected set; } - #endregion - #region cTor - #endregion #region Public methods diff --git a/src/LogExpert.Core/Classes/Util.cs b/src/LogExpert.Core/Classes/Util.cs index 18137729..f8f29edd 100644 --- a/src/LogExpert.Core/Classes/Util.cs +++ b/src/LogExpert.Core/Classes/Util.cs @@ -9,12 +9,14 @@ namespace LogExpert.Core.Classes; -public class Util +public static class Util { #region Public methods public static string GetNameFromPath (string fileName) { + ArgumentNullException.ThrowIfNull(fileName, nameof(fileName)); + var i = fileName.LastIndexOf('\\'); if (i < 0) @@ -30,9 +32,10 @@ public static string GetNameFromPath (string fileName) return fileName[(i + 1)..]; } - //TODO Add Null Check (https://github.com/LogExperts/LogExpert/issues/403) public static string StripExtension (string fileName) { + ArgumentNullException.ThrowIfNull(fileName, nameof(fileName)); + var i = fileName.LastIndexOf('.'); if (i < 0) @@ -43,9 +46,10 @@ public static string StripExtension (string fileName) return fileName[..i]; } - //TODO Add Null Check (https://github.com/LogExperts/LogExpert/issues/403) public static string GetExtension (string fileName) { + ArgumentNullException.ThrowIfNull(fileName, nameof(fileName)); + var i = fileName.LastIndexOf('.'); return i < 0 || i >= fileName.Length - 1 @@ -174,6 +178,24 @@ public static int DamerauLevenshteinDistance (ReadOnlySpan source, ReadOnl public static unsafe int YetiLevenshtein (string s1, string s2) { + ArgumentNullException.ThrowIfNull(s1, nameof(s1)); + ArgumentNullException.ThrowIfNull(s2, nameof(s2)); + + if (ReferenceEquals(s1, s2)) + { + return 0; + } + + if (s1.Length == 0) + { + return s2.Length; + } + + if (s2.Length == 0) + { + return s1.Length; + } + fixed (char* p1 = s1) fixed (char* p2 = s2) { @@ -183,6 +205,19 @@ public static unsafe int YetiLevenshtein (string s1, string s2) public static unsafe int YetiLevenshtein (ReadOnlySpan s1, ReadOnlySpan s2) { + int len1 = s1.Length; + int len2 = s2.Length; + + if (len1 == 0) + { + return len2; + } + + if (len2 == 0) + { + return len1; + } + fixed (char* p1 = s1) fixed (char* p2 = s2) { @@ -208,7 +243,8 @@ public static unsafe int YetiLevenshtein (string s1, string s2, int substitution /// /// Cetin Sert, David Necas - /// Source Code + /// + /// Source Code /// /// /// diff --git a/src/LogExpert.Core/Interfaces/ILogStreamReaderMemory.cs b/src/LogExpert.Core/Interfaces/ILogStreamReaderMemory.cs index 4cd90053..31a07649 100644 --- a/src/LogExpert.Core/Interfaces/ILogStreamReaderMemory.cs +++ b/src/LogExpert.Core/Interfaces/ILogStreamReaderMemory.cs @@ -1,5 +1,6 @@ namespace LogExpert.Core.Interfaces; +//TODO ILogStreamReader function should be moved to ILogStreamReaderMemory, and ILogStreamReader should be removed, as it is not used anywhere else in the codebase. This will simplify the interface hierarchy and reduce confusion about which methods are available on which interfaces. public interface ILogStreamReaderMemory : ILogStreamReader { @@ -7,28 +8,32 @@ public interface ILogStreamReaderMemory : ILogStreamReader /// Attempts to read the next line from the stream. /// /// - /// When this method returns true, contains a representing the next line read from the stream. - /// The memory is only valid until the next call to or until is called. + /// When this method returns true, contains a representing the next line + /// read from the stream. The memory is only valid until the next call to or until + /// is called. /// /// - /// true if a line was successfully read; false if the end of the stream has been reached or no more lines are available. + /// true if a line was successfully read; false if the end of the stream has been reached or no more + /// lines are available. /// /// - /// The returned memory is only valid until the next call to or until is called. - /// This method is not guaranteed to be thread-safe; concurrent access should be synchronized externally. + /// The returned memory is only valid until the next call to or until + /// is called. This method is not guaranteed to be thread-safe; concurrent access should + /// be synchronized externally. /// bool TryReadLine (out ReadOnlyMemory lineMemory); /// - /// Returns the memory buffer previously obtained from to the underlying pool or resource manager. + /// Returns the memory buffer previously obtained from to the underlying pool or resource + /// manager. /// /// /// The instance previously obtained from . /// /// - /// Call this method when you are done processing the memory returned by to avoid memory leaks or resource retention. - /// Failing to call this method may result in increased memory usage. - /// It is safe to call this method multiple times for the same memory, but only the first call will have an effect. + /// Call this method when you are done processing the memory returned by to avoid memory + /// leaks or resource retention. Failing to call this method may result in increased memory usage. It is safe to + /// call this method multiple times for the same memory, but only the first call will have an effect. /// void ReturnMemory (ReadOnlyMemory memory); } diff --git a/src/LogExpert.UI/Dialogs/HighlightDialog.Designer.cs b/src/LogExpert.UI/Dialogs/HighlightDialog.Designer.cs index b729c003..af7d8866 100644 --- a/src/LogExpert.UI/Dialogs/HighlightDialog.Designer.cs +++ b/src/LogExpert.UI/Dialogs/HighlightDialog.Designer.cs @@ -379,7 +379,7 @@ private void InitializeComponent () colorBoxForeground.DrawMode = DrawMode.OwnerDrawFixed; colorBoxForeground.DropDownStyle = ComboBoxStyle.DropDownList; colorBoxForeground.FormattingEnabled = true; - colorBoxForeground.Items.AddRange(new object[] { Color.Black, Color.Black, Color.White, Color.Gray, Color.DarkGray, Color.Blue, Color.LightBlue, Color.DarkBlue, Color.Green, Color.LightGreen, Color.DarkGreen, Color.Olive, Color.Red, Color.Pink, Color.Purple, Color.IndianRed, Color.DarkCyan, Color.Yellow, Color.Black, Color.Black, Color.White, Color.Gray, Color.DarkGray, Color.Blue, Color.LightBlue, Color.DarkBlue, Color.Green, Color.LightGreen, Color.DarkGreen, Color.Olive, Color.Red, Color.Pink, Color.Purple, Color.IndianRed, Color.DarkCyan, Color.Yellow, Color.Black, Color.Black, Color.White, Color.Gray, Color.DarkGray, Color.Blue, Color.LightBlue, Color.DarkBlue, Color.Green, Color.LightGreen, Color.DarkGreen, Color.Olive, Color.Red, Color.Pink, Color.Purple, Color.IndianRed, Color.DarkCyan, Color.Yellow }); + colorBoxForeground.Items.AddRange(new object[] { Color.Black, Color.White, Color.Gray, Color.DarkGray, Color.Blue, Color.LightBlue, Color.DarkBlue, Color.Green, Color.LightGreen, Color.DarkGreen, Color.Olive, Color.Red, Color.Pink, Color.Purple, Color.IndianRed, Color.DarkCyan, Color.Yellow}); colorBoxForeground.Location = new Point(8, 63); colorBoxForeground.Margin = new Padding(4, 5, 4, 5); colorBoxForeground.Name = "colorBoxForeground"; @@ -394,7 +394,7 @@ private void InitializeComponent () colorBoxBackground.DrawMode = DrawMode.OwnerDrawFixed; colorBoxBackground.DropDownStyle = ComboBoxStyle.DropDownList; colorBoxBackground.FormattingEnabled = true; - colorBoxBackground.Items.AddRange(new object[] { Color.Black, Color.Black, Color.White, Color.Gray, Color.DarkGray, Color.Blue, Color.LightBlue, Color.DarkBlue, Color.Green, Color.LightGreen, Color.DarkGreen, Color.Olive, Color.Red, Color.Pink, Color.Purple, Color.IndianRed, Color.DarkCyan, Color.Yellow, Color.Black, Color.Black, Color.White, Color.Gray, Color.DarkGray, Color.Blue, Color.LightBlue, Color.DarkBlue, Color.Green, Color.LightGreen, Color.DarkGreen, Color.Olive, Color.Red, Color.Pink, Color.Purple, Color.IndianRed, Color.DarkCyan, Color.Yellow, Color.Black, Color.Black, Color.White, Color.Gray, Color.DarkGray, Color.Blue, Color.LightBlue, Color.DarkBlue, Color.Green, Color.LightGreen, Color.DarkGreen, Color.Olive, Color.Red, Color.Pink, Color.Purple, Color.IndianRed, Color.DarkCyan, Color.Yellow }); + colorBoxBackground.Items.AddRange(new object[] { Color.Black, Color.White, Color.Gray, Color.DarkGray, Color.Blue, Color.LightBlue, Color.DarkBlue, Color.Green, Color.LightGreen, Color.DarkGreen, Color.Olive, Color.Red, Color.Pink, Color.Purple, Color.IndianRed, Color.DarkCyan, Color.Yellow }); colorBoxBackground.Location = new Point(9, 140); colorBoxBackground.Margin = new Padding(4, 5, 4, 5); colorBoxBackground.Name = "colorBoxBackground"; diff --git a/src/LogExpert/Classes/CommandLine/CmdLineInt.cs b/src/LogExpert/Classes/CommandLine/CmdLineInt.cs index c9bfead4..897a3f3b 100644 --- a/src/LogExpert/Classes/CommandLine/CmdLineInt.cs +++ b/src/LogExpert/Classes/CommandLine/CmdLineInt.cs @@ -3,7 +3,6 @@ * */ -//TODO: Replace with https://github.com/commandlineparser/commandline //TODO: or with this https://github.com/natemcmaster/CommandLineUtils namespace LogExpert.Classes.CommandLine; From 85db9f062b066906cf69b1f77400b6470c51d635 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Tue, 14 Apr 2026 17:39:26 +0200 Subject: [PATCH 02/24] no force GC --- src/LogExpert.Core/Classes/Log/LogfileReader.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index f6a74964..f903508e 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -789,7 +789,7 @@ public void DeleteAllContent () return; } - _logger.Info(CultureInfo.InvariantCulture, "Deleting all log buffers for {0}. Used mem: {1:N0}", Util.GetNameFromPath(_fileName), GC.GetTotalMemory(true)); //TODO [Z] uh GC collect calls creepy + _logger.Info(CultureInfo.InvariantCulture, "Deleting all log buffers for {0}. Used mem: {1:N0}", Util.GetNameFromPath(_fileName), GC.GetTotalMemory(false)); AcquireBufferListWriterLock(); ClearBufferState(); AcquireLruCacheDictWriterLock(); @@ -809,9 +809,8 @@ public void DeleteAllContent () ReleaseDisposeWriterLock(); ReleaseLRUCacheDictWriterLock(); ReleaseBufferListWriterLock(); - GC.Collect(); _contentDeleted = true; - _logger.Info(CultureInfo.InvariantCulture, "Deleting complete. Used mem: {0:N0}", GC.GetTotalMemory(true)); //TODO [Z] uh GC collect calls creepy + _logger.Info(CultureInfo.InvariantCulture, "Deleting complete. Used mem: {0:N0}", GC.GetTotalMemory(false)); } /// From 39d83b6056f269e0c43a6a1c0e7c1bf34e707047 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Tue, 14 Apr 2026 20:14:24 +0200 Subject: [PATCH 03/24] fontsize is now a int --- src/LogExpert.Core/Config/Preferences.cs | 2 +- src/LogExpert.UI/Dialogs/SettingsDialog.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/LogExpert.Core/Config/Preferences.cs b/src/LogExpert.Core/Config/Preferences.cs index a8aaa13d..1bc5abc0 100644 --- a/src/LogExpert.Core/Config/Preferences.cs +++ b/src/LogExpert.Core/Config/Preferences.cs @@ -164,7 +164,7 @@ public List HilightGroupList public string FontName { get; set; } = "Courier New"; - public float FontSize { get; set; } = 9; + public int FontSize { get; set; } = 9; public List HighlightMaskList { get; set; } = []; } \ No newline at end of file diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.cs index 27768b28..52be393d 100644 --- a/src/LogExpert.UI/Dialogs/SettingsDialog.cs +++ b/src/LogExpert.UI/Dialogs/SettingsDialog.cs @@ -124,9 +124,9 @@ private void FillDialog () Preferences.FontName = DEFAULT_FONT_NAME; } - if (Math.Abs(Preferences.FontSize) < 0.1) + if (Preferences.FontSize <= 0) { - Preferences.FontSize = 9.0f; + Preferences.FontSize = 9; } FillPortableMode(); @@ -263,7 +263,7 @@ private void FillPortableMode () private void DisplayFontName () { - labelFont.Text = $"{Preferences.FontName} {(int)Preferences.FontSize}"; + labelFont.Text = $"{Preferences.FontName} {Preferences.FontSize}"; labelFont.Font = new Font(new FontFamily(Preferences.FontName), Preferences.FontSize); } From 31eec44227858be6549ecc9cc990cdc46ab67fb5 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Tue, 14 Apr 2026 21:08:39 +0200 Subject: [PATCH 04/24] float from the fontdialog --- src/LogExpert.Core/Classes/Log/LogBuffer.cs | 15 +- .../Classes/Log/LogfileReader.cs | 293 ++++++++++++------ src/LogExpert.UI/Dialogs/SettingsDialog.cs | 2 +- 3 files changed, 212 insertions(+), 98 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/LogBuffer.cs b/src/LogExpert.Core/Classes/Log/LogBuffer.cs index ae34615d..63a37203 100644 --- a/src/LogExpert.Core/Classes/Log/LogBuffer.cs +++ b/src/LogExpert.Core/Classes/Log/LogBuffer.cs @@ -11,13 +11,12 @@ public class LogBuffer private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); #if DEBUG - private readonly IList _filePositions = []; // file position for every line + private readonly List _filePositions; // file position for every line #endif - private readonly List _lineList = []; + private readonly List _lineList; private int MAX_LINES = 500; - private long _size; #endregion @@ -31,6 +30,10 @@ public LogBuffer (ILogFileInfo fileInfo, int maxLines) { FileInfo = fileInfo; MAX_LINES = maxLines; + _lineList = new(MAX_LINES); +#if DEBUG + _filePositions = new(MAX_LINES); +#endif } #endregion @@ -43,18 +46,18 @@ public long Size { set { - _size = value; + field = value; #if DEBUG if (_filePositions.Count > 0) { - if (_size < _filePositions[_filePositions.Count - 1] - StartPos) + if (field < _filePositions[^1] - StartPos) { _logger.Error("LogBuffer overall Size must be greater than last line file position!"); } } #endif } - get => _size; + get; } public int EndLine => StartLine + LineCount; diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index f903508e..69d6c4ce 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Globalization; using System.Runtime.InteropServices; using System.Text; @@ -41,21 +42,22 @@ public partial class LogfileReader : IAutoLogLineMemoryColumnizerCallback, IDisp private List _bufferList; private bool _contentDeleted; - private DateTime _lastProgressUpdate = DateTime.MinValue; + private long _lastProgressUpdate; private long _fileLength; private Task _garbageCollectorTask; private Task _monitorTask; private bool _isDeleted; - private bool _isFailModeCheckCallPending; - private bool _isFastFailOnGetLogLine; - private bool _isLineCountDirty = true; + + private int _totalLineCount; private IList _logFileInfoList = []; private Dictionary _lruCacheDict; private bool _shouldStop; private bool _disposed; private ILogFileInfo _watchedILogFileInfo; + private volatile bool _isFailModeCheckCallPending; + private volatile bool _isFastFailOnGetLogLine; private volatile int _lastBufferIndex = -1; #endregion @@ -151,36 +153,8 @@ private LogfileReader (string[] fileNames, EncodingOptions encodingOptions, bool /// public int LineCount { - get - { - if (_isLineCountDirty) - { - field = 0; - if (_bufferListLock.IsReadLockHeld || _bufferListLock.IsWriteLockHeld) - { - foreach (var buffer in _bufferList) - { - field += buffer.LineCount; - } - } - else - { - AcquireBufferListReaderLock(); - foreach (var buffer in _bufferList) - { - field += buffer.LineCount; - } - - ReleaseBufferListReaderLock(); - } - - _isLineCountDirty = false; - } - - return field; - } - - private set; + get => Volatile.Read(ref _totalLineCount); + private set => Interlocked.Exchange(ref _totalLineCount, value); } /// @@ -250,7 +224,7 @@ private EncodingOptions EncodingOptions //TODO: Make this private public void ReadFiles () { - _lastProgressUpdate = DateTime.MinValue; + _lastProgressUpdate = 0; FileSize = 0; LineCount = 0; //this.lastReturnedLine = ""; @@ -317,7 +291,6 @@ public int ShiftBuffers () ClearBufferState(); var offset = 0; - _isLineCountDirty = true; lock (_monitor) { @@ -462,6 +435,14 @@ public int ShiftBuffers () _logger.Info(CultureInfo.InvariantCulture, "ShiftBuffers() end. offset={0}", offset); + var newTotal = 0; + foreach (var buf in _bufferList) + { + newTotal += buf.LineCount; + } + + _ = Interlocked.Exchange(ref _totalLineCount, newTotal); + ReleaseBufferListWriterLock(); return offset; @@ -565,16 +546,38 @@ public async Task GetLogLineMemoryWithWait (int lineNum) if (!_isFastFailOnGetLogLine) { - var task = Task.Run(() => GetLogLineMemoryInternal(lineNum)); - if (task.Wait(WAIT_TIME)) + // Fast path: if the buffer is in memory, skip the thread-pool hop entirely + bool canFastPath = false; + AcquireBufferListReaderLock(); + try + { + var logBuffer = GetBufferForLine(lineNum); + canFastPath = logBuffer is { IsDisposed: false }; + } + finally + { + ReleaseBufferListReaderLock(); + } + + if (canFastPath) { - result = await task.ConfigureAwait(false); + result = GetLogLineMemoryInternal(lineNum).Result; _isFastFailOnGetLogLine = false; } else { - _isFastFailOnGetLogLine = true; - _logger.Debug(CultureInfo.InvariantCulture, "No result after {0}ms. Returning .", WAIT_TIME); + // Slow path: buffer disposed or not found — use Task.Run with timeout + var task = Task.Run(() => GetLogLineMemoryInternal(lineNum).AsTask()); + if (task.Wait(WAIT_TIME)) + { + result = await task.ConfigureAwait(false); + _isFastFailOnGetLogLine = false; + } + else + { + _isFastFailOnGetLogLine = true; + _logger.Debug(CultureInfo.InvariantCulture, "No result after {0}ms. Returning .", WAIT_TIME); + } } } else @@ -626,19 +629,16 @@ public int GetNextMultiFileLine (int lineNum) { var result = -1; AcquireBufferListReaderLock(); - var logBuffer = GetBufferForLine(lineNum); - if (logBuffer != null) + //var logBuffer = GetBufferForLine(lineNum); + var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); + if (logBuffer != null && index != -1) { - var index = _bufferList.IndexOf(logBuffer); - if (index != -1) + for (var i = index; i < _bufferList.Count; ++i) { - for (var i = index; i < _bufferList.Count; ++i) + if (_bufferList[i].FileInfo != logBuffer.FileInfo) { - if (_bufferList[i].FileInfo != logBuffer.FileInfo) - { - result = _bufferList[i].StartLine; - break; - } + result = _bufferList[i].StartLine; + break; } } } @@ -664,19 +664,16 @@ public int GetPrevMultiFileLine (int lineNum) { var result = -1; AcquireBufferListReaderLock(); - var logBuffer = GetBufferForLine(lineNum); - if (logBuffer != null) + //var logBuffer = GetBufferForLine(lineNum); + var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); + if (logBuffer != null && index != -1) { - var index = _bufferList.IndexOf(logBuffer); - if (index != -1) + for (var i = index; i >= 0; --i) { - for (var i = index; i >= 0; --i) + if (_bufferList[i].FileInfo != logBuffer.FileInfo) { - if (_bufferList[i].FileInfo != logBuffer.FileInfo) - { - result = _bufferList[i].StartLine + _bufferList[i].LineCount; - break; - } + result = _bufferList[i].StartLine + _bufferList[i].LineCount; + break; } } } @@ -697,11 +694,12 @@ public int GetPrevMultiFileLine (int lineNum) public int GetRealLineNumForVirtualLineNum (int lineNum) { AcquireBufferListReaderLock(); - var logBuffer = GetBufferForLine(lineNum); + //var logBuffer = GetBufferForLine(lineNum); + var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); var result = -1; if (logBuffer != null) { - logBuffer = GetFirstBufferForFileByLogBuffer(logBuffer); + logBuffer = GetFirstBufferForFileByLogBuffer(logBuffer, index); if (logBuffer != null) { result = lineNum - logBuffer.StartLine; @@ -806,6 +804,8 @@ public void DeleteAllContent () _lruCacheDict.Clear(); _bufferList.Clear(); + _ = Interlocked.Exchange(ref _totalLineCount, 0); + ReleaseDisposeWriterLock(); ReleaseLRUCacheDictWriterLock(); ReleaseBufferListWriterLock(); @@ -958,13 +958,13 @@ private ILogFileInfo AddFile (string fileName) /// A task that represents the asynchronous operation. The task result contains the log line at the specified line /// number, or null if the file is deleted or the line does not exist. /// - private Task GetLogLineMemoryInternal (int lineNum) + private ValueTask GetLogLineMemoryInternal (int lineNum) { if (_isDeleted) { _logger.Debug(CultureInfo.InvariantCulture, "Returning null for line {0} because file is deleted.", lineNum); // fast fail if dead file was detected. Prevents repeated lags in GUI thread caused by callbacks from control (e.g. repaint) - return Task.FromResult(null); + return default; } AcquireBufferListReaderLock(); @@ -973,7 +973,7 @@ private Task GetLogLineMemoryInternal (int lineNum) { ReleaseBufferListReaderLock(); _logger.Error("Cannot find buffer for line {0}, file: {1}{2}", lineNum, _fileName, IsMultiFile ? " (MultiFile)" : ""); - return Task.FromResult(null); + return default; } // disposeLock prevents that the garbage collector is disposing just in the moment we use the buffer AcquireDisposeLockUpgradableReadLock(); @@ -993,7 +993,7 @@ private Task GetLogLineMemoryInternal (int lineNum) ReleaseDisposeUpgradeableReadLock(); ReleaseBufferListReaderLock(); - return Task.FromResult(line); + return new ValueTask(line); } /// @@ -1180,6 +1180,7 @@ private void RemoveFromBufferList (LogBuffer buffer) Util.AssertTrue(_bufferListLock.IsWriteLockHeld, "No _writer lock for buffer list"); _ = _lruCacheDict.Remove(buffer.StartLine); _ = _bufferList.Remove(buffer); + _ = Interlocked.Add(ref _totalLineCount, -buffer.LineCount); } /// @@ -1303,8 +1304,8 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start if (lineCount > _maxLinesPerBuffer && reader.IsBufferComplete) { //Rate Limited Progrress - var now = DateTime.Now; - bool shouldFireLoadFileEvent = (now - _lastProgressUpdate).TotalMilliseconds >= PROGRESS_UPDATE_INTERVAL_MS; + var now = Environment.TickCount64; + bool shouldFireLoadFileEvent = (now - _lastProgressUpdate) >= PROGRESS_UPDATE_INTERVAL_MS; if (shouldFireLoadFileEvent) { @@ -1361,7 +1362,16 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start Monitor.Exit(logBuffer); } - _isLineCountDirty = true; + var newTotal = 0; + AcquireBufferListReaderLock(); + + foreach (var buf in _bufferList) + { + newTotal += buf.LineCount; + } + + ReleaseBufferListReaderLock(); + _ = Interlocked.Exchange(ref _totalLineCount, newTotal); FileSize = reader.Position; // Reader may have detected another encoding @@ -1515,32 +1525,41 @@ private void GarbageCollectLruCache () _logger.Info(CultureInfo.InvariantCulture, "Removing {0} entries from LRU cache for {1}", diff, Util.GetNameFromPath(_fileName)); } #endif - SortedList useSorterList = []; - // sort by usage counter - foreach (var entry in _lruCacheDict.Values) + var count = _lruCacheDict.Count; + var sortArray = ArrayPool<(long Timestamp, int StartLine)>.Shared.Rent(count); + try { - if (!useSorterList.ContainsKey(entry.LastUseTimeStamp)) + var idx = 0; + foreach (var entry in _lruCacheDict.Values) { - useSorterList.Add(entry.LastUseTimeStamp, entry.LogBuffer.StartLine); + sortArray[idx++] = (entry.LastUseTimeStamp, entry.LogBuffer.StartLine); } - } - // remove first entries (least usage) - AcquireDisposeWriterLock(); - for (var i = 0; i < diff; ++i) - { - if (i >= useSorterList.Count) + Array.Sort(sortArray, 0, count); + + // remove first entries (least recently used) + AcquireDisposeWriterLock(); + for (var i = 0; i < diff; ++i) { - break; + if (i >= count) + { + break; + } + + var startLine = sortArray[i].StartLine; + if (_lruCacheDict.TryGetValue(startLine, out var entry)) + { + _lruCacheDict.Remove(startLine); + entry.LogBuffer.DisposeContent(); + } } - var startLine = useSorterList.Values[i]; - var entry = _lruCacheDict[startLine]; - _lruCacheDict.Remove(startLine); - entry.LogBuffer.DisposeContent(); + ReleaseDisposeWriterLock(); + } + finally + { + ArrayPool<(long Timestamp, int StartLine)>.Shared.Return(sortArray); } - - ReleaseDisposeWriterLock(); } ReleaseLRUCacheDictWriterLock(); @@ -1693,6 +1712,98 @@ private void ReReadBuffer (LogBuffer logBuffer) } } + private (LogBuffer? Buffer, int Index) GetBufferForLineWithIndex (int lineNum) + { + AcquireBufferListReaderLock(); + try + { + var arr = CollectionsMarshal.AsSpan(_bufferList); + var count = arr.Length; + + if (count == 0) + { + return (null, -1); + } + + // Layer 0: Last buffer cache + var lastIdx = _lastBufferIndex; + if (lastIdx >= 0 && lastIdx < count) + { + var buf = arr[lastIdx]; + if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) + { + return (buf, lastIdx); + } + + // Layer 1: Adjacent buffer prediction + if (lastIdx + 1 < count) + { + var next = arr[lastIdx + 1]; + if ((uint)(lineNum - next.StartLine) < (uint)next.LineCount) + { + _lastBufferIndex = lastIdx + 1; + UpdateLruCache(next); + return (next, lastIdx + 1); + } + } + + if (lastIdx - 1 >= 0) + { + var prev = arr[lastIdx - 1]; + if ((uint)(lineNum - prev.StartLine) < (uint)prev.LineCount) + { + _lastBufferIndex = lastIdx - 1; + UpdateLruCache(prev); + return (prev, lastIdx - 1); + } + } + } + + // Layer 2: Direct mapping guess + var guess = lineNum / _maxLinesPerBuffer; + if ((uint)guess < (uint)count) + { + var buf = arr[guess]; + if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) + { + _lastBufferIndex = guess; + UpdateLruCache(buf); + return (buf, guess); + } + } + + // Layer 3: Branchless binary search with power-of-two strides + var step = HighestPowerOfTwo(count); + var idx = (arr[step - 1].StartLine <= lineNum) ? count - step : 0; + + for (step >>= 1; step > 0; step >>= 1) + { + var probe = idx + step; + if (probe < count && arr[probe - 1].StartLine <= lineNum) + { + idx = probe; + } + } + + if (idx < count) + { + var buf = arr[idx]; + if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) + { + _lastBufferIndex = idx; + UpdateLruCache(buf); + return (buf, idx); + } + } + + return (null, -1); + } + finally + { + ReleaseBufferListReaderLock(); + } + } + /// /// Retrieves the log buffer that contains the specified line number. /// @@ -1728,7 +1839,7 @@ private LogBuffer GetBufferForLine (int lineNum) var buf = arr[lastIdx]; if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) { - UpdateLruCache(buf); + //dont UpdateLRUCache, the cache has not changed in layer 0 return buf; } @@ -1833,11 +1944,11 @@ private void GetLineMemoryFinishedCallback (ILogLineMemory line) /// The first LogBuffer in the buffer list that is associated with the same file as the specified buffer, searching /// in reverse order from the given buffer. Returns null if the specified buffer is not found in the buffer list. /// - private LogBuffer GetFirstBufferForFileByLogBuffer (LogBuffer logBuffer) + private LogBuffer GetFirstBufferForFileByLogBuffer (LogBuffer logBuffer, int index) { var info = logBuffer.FileInfo; AcquireBufferListReaderLock(); - var index = _bufferList.IndexOf(logBuffer); + if (index == -1) { ReleaseBufferListReaderLock(); diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.cs index 52be393d..132f4732 100644 --- a/src/LogExpert.UI/Dialogs/SettingsDialog.cs +++ b/src/LogExpert.UI/Dialogs/SettingsDialog.cs @@ -721,7 +721,7 @@ private void OnBtnChangeFontClick (object sender, EventArgs e) if (dlg.ShowDialog() == DialogResult.OK) { - Preferences.FontSize = dlg.Font.Size; + Preferences.FontSize = (int)dlg.Font.Size; Preferences.FontName = dlg.Font.FontFamily.Name; } From 132626ebd78e7181ef6f6b6d96e9480b665d03db Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Wed, 15 Apr 2026 09:09:51 +0200 Subject: [PATCH 05/24] revert to old lazy check since its better --- .../Classes/Log/LogfileReader.cs | 59 +++++++++++-------- src/LogExpert.Core/Config/Preferences.cs | 46 +++++++-------- 2 files changed, 56 insertions(+), 49 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index 69d6c4ce..3bae31d2 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -49,13 +49,15 @@ public partial class LogfileReader : IAutoLogLineMemoryColumnizerCallback, IDisp private Task _monitorTask; private bool _isDeleted; - private int _totalLineCount; + private IList _logFileInfoList = []; private Dictionary _lruCacheDict; private bool _shouldStop; private bool _disposed; private ILogFileInfo _watchedILogFileInfo; + private bool _isLineCountDirty = true; + private volatile bool _isFailModeCheckCallPending; private volatile bool _isFastFailOnGetLogLine; private volatile int _lastBufferIndex = -1; @@ -153,8 +155,36 @@ private LogfileReader (string[] fileNames, EncodingOptions encodingOptions, bool /// public int LineCount { - get => Volatile.Read(ref _totalLineCount); - private set => Interlocked.Exchange(ref _totalLineCount, value); + get + { + if (_isLineCountDirty) + { + field = 0; + if (_bufferListLock.IsReadLockHeld || _bufferListLock.IsWriteLockHeld) + { + foreach (var buffer in _bufferList) + { + field += buffer.LineCount; + } + } + else + { + AcquireBufferListReaderLock(); + foreach (var buffer in _bufferList) + { + field += buffer.LineCount; + } + + ReleaseBufferListReaderLock(); + } + + _isLineCountDirty = false; + } + + return field; + } + + private set; } /// @@ -291,6 +321,7 @@ public int ShiftBuffers () ClearBufferState(); var offset = 0; + _isLineCountDirty = true; lock (_monitor) { @@ -435,14 +466,6 @@ public int ShiftBuffers () _logger.Info(CultureInfo.InvariantCulture, "ShiftBuffers() end. offset={0}", offset); - var newTotal = 0; - foreach (var buf in _bufferList) - { - newTotal += buf.LineCount; - } - - _ = Interlocked.Exchange(ref _totalLineCount, newTotal); - ReleaseBufferListWriterLock(); return offset; @@ -804,8 +827,6 @@ public void DeleteAllContent () _lruCacheDict.Clear(); _bufferList.Clear(); - _ = Interlocked.Exchange(ref _totalLineCount, 0); - ReleaseDisposeWriterLock(); ReleaseLRUCacheDictWriterLock(); ReleaseBufferListWriterLock(); @@ -1180,7 +1201,6 @@ private void RemoveFromBufferList (LogBuffer buffer) Util.AssertTrue(_bufferListLock.IsWriteLockHeld, "No _writer lock for buffer list"); _ = _lruCacheDict.Remove(buffer.StartLine); _ = _bufferList.Remove(buffer); - _ = Interlocked.Add(ref _totalLineCount, -buffer.LineCount); } /// @@ -1362,16 +1382,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start Monitor.Exit(logBuffer); } - var newTotal = 0; - AcquireBufferListReaderLock(); - - foreach (var buf in _bufferList) - { - newTotal += buf.LineCount; - } - - ReleaseBufferListReaderLock(); - _ = Interlocked.Exchange(ref _totalLineCount, newTotal); + _isLineCountDirty = true; FileSize = reader.Position; // Reader may have detected another encoding diff --git a/src/LogExpert.Core/Config/Preferences.cs b/src/LogExpert.Core/Config/Preferences.cs index 1bc5abc0..a570b6cc 100644 --- a/src/LogExpert.Core/Config/Preferences.cs +++ b/src/LogExpert.Core/Config/Preferences.cs @@ -8,22 +8,21 @@ namespace LogExpert.Core.Config; [Serializable] public class Preferences { - /// /// List of highlight groups for syntax highlighting and text coloring. /// /// - /// Supports legacy property name "hilightGroupList" (with typo) for backward compatibility. - /// Old settings files using the incorrect spelling will be automatically imported. + /// Supports legacy property name "hilightGroupList" (with typo) for backward compatibility. Old settings files + /// using the incorrect spelling will be automatically imported. /// [Newtonsoft.Json.JsonProperty("HighlightGroupList")] [System.Text.Json.Serialization.JsonPropertyName("HighlightGroupList")] public List HighlightGroupList { get; set; } = []; /// - /// Legacy property for backward compatibility with old settings files that used the typo "hilightGroupList". - /// This setter redirects data to the correct property. - /// Will be removed in a future version once migration period is complete. + /// Legacy property for backward compatibility with old settings files that used the typo "hilightGroupList". This + /// setter redirects data to the correct property. Will be removed in a future + /// version once migration period is complete. /// [Obsolete("This property exists only for backward compatibility with old settings files. Use HighlightGroupList instead.")] [Newtonsoft.Json.JsonProperty("hilightGroupList", DefaultValueHandling = Newtonsoft.Json.DefaultValueHandling.Ignore, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] @@ -37,10 +36,11 @@ public List HilightGroupList public bool PortableMode { get; set; } /// - /// OBSOLETE: This setting is no longer used. It was originally intended to show an error dialog when "Allow Only One Instance" was enabled, - /// but this behavior was incorrect (showed dialog on success instead of failure). The feature now works silently on success and only shows - /// a warning on IPC failure. This property is kept for backward compatibility with old settings files but is no longer used or saved. - /// Will be removed in a future version. + /// OBSOLETE: This setting is no longer used. It was originally intended to show an error dialog when "Allow Only + /// One Instance" was enabled, but this behavior was incorrect (showed dialog on success instead of failure). The + /// feature now works silently on success and only shows a warning on IPC failure. This property is kept for + /// backward compatibility with old settings files but is no longer used or saved. Will be removed in a future + /// version. /// [Obsolete("This setting is no longer used and will be removed in a future version. The 'Allow Only One Instance' feature now works silently.")] [System.Text.Json.Serialization.JsonIgnore] @@ -48,27 +48,23 @@ public List HilightGroupList public bool ShowErrorMessageAllowOnlyOneInstances { get; set; } /// - /// Maximum length of lines that can be read from log files at the reader level. - /// Lines exceeding this length will be truncated during file reading operations. - /// This setting protects against memory issues and performance degradation from extremely long lines. + /// Maximum length of lines that can be read from log files at the reader level. Lines exceeding this length will be + /// truncated during file reading operations. This setting protects against memory issues and performance + /// degradation from extremely long lines. /// /// - /// - /// This property controls line truncation at the I/O reader level before lines are processed by columnizers. - /// It is implemented in - /// and . - /// - /// - /// Related property: controls display-level truncation in UI columns, - /// which must not exceed this value. Default is 20000 characters. - /// + /// This property controls line truncation at the I/O reader level before lines are processed by columnizers. + /// It is implemented in and + /// . Related property: + /// controls display-level truncation in UI columns, which must not exceed this + /// value. Default is 20000 characters. /// public int MaxLineLength { get; set; } = 20000; /// - /// Maximum length of text displayed in columns before truncation with "...". - /// This is separate from which controls reader-level line reading. - /// Must not exceed . Default is 20000 characters. + /// Maximum length of text displayed in columns before truncation with "...". This is separate from + /// which controls reader-level line reading. Must not exceed + /// . Default is 20000 characters. /// public int MaxDisplayLength { get; set; } = 20000; From 67656e0c16d352d1fe69c6b55895e2a4c9ce6601 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Wed, 15 Apr 2026 09:37:17 +0200 Subject: [PATCH 06/24] more optimizations --- src/ColumnizerLib/LogLine.cs | 2 +- src/LogExpert.Core/Classes/Log/LogBuffer.cs | 6 +- .../Classes/Log/LogBufferCacheEntry.cs | 14 +- .../Classes/Log/LogfileReader.cs | 183 ++++++++++-------- src/LogExpert.Tests/BufferShiftTest.cs | 8 +- 5 files changed, 114 insertions(+), 99 deletions(-) diff --git a/src/ColumnizerLib/LogLine.cs b/src/ColumnizerLib/LogLine.cs index 5f7a9fd4..262c3624 100644 --- a/src/ColumnizerLib/LogLine.cs +++ b/src/ColumnizerLib/LogLine.cs @@ -27,7 +27,7 @@ namespace ColumnizerLib; /// was replaced to better align with these performance and semantic requirements. /// /// -public class LogLine : ILogLineMemory +public readonly record struct LogLine : ILogLineMemory { public int LineNumber { get; } diff --git a/src/LogExpert.Core/Classes/Log/LogBuffer.cs b/src/LogExpert.Core/Classes/Log/LogBuffer.cs index 63a37203..d5541d05 100644 --- a/src/LogExpert.Core/Classes/Log/LogBuffer.cs +++ b/src/LogExpert.Core/Classes/Log/LogBuffer.cs @@ -14,7 +14,7 @@ public class LogBuffer private readonly List _filePositions; // file position for every line #endif - private readonly List _lineList; + private readonly List _lineList; private int MAX_LINES = 500; @@ -78,7 +78,7 @@ public long Size #region Public methods - public void AddLine (ILogLineMemory lineMemory, long filePos) + public void AddLine (LogLine lineMemory, long filePos) { _lineList.Add(lineMemory); #if DEBUG @@ -103,7 +103,7 @@ public void DisposeContent () #endif } - public ILogLineMemory GetLineMemoryOfBlock (int num) + public LogLine? GetLineMemoryOfBlock (int num) { return num < _lineList.Count && num >= 0 ? _lineList[num] diff --git a/src/LogExpert.Core/Classes/Log/LogBufferCacheEntry.cs b/src/LogExpert.Core/Classes/Log/LogBufferCacheEntry.cs index 0683012e..b983cfed 100644 --- a/src/LogExpert.Core/Classes/Log/LogBufferCacheEntry.cs +++ b/src/LogExpert.Core/Classes/Log/LogBufferCacheEntry.cs @@ -1,14 +1,12 @@ -namespace LogExpert.Core.Classes.Log; +namespace LogExpert.Core.Classes.Log; public class LogBufferCacheEntry { - #region Fields - - #endregion + private long _lastUseTimeStamp; #region cTor - public LogBufferCacheEntry() + public LogBufferCacheEntry () { Touch(); } @@ -19,15 +17,15 @@ public LogBufferCacheEntry() public LogBuffer LogBuffer { get; set; } - public long LastUseTimeStamp { get; private set; } + public long LastUseTimeStamp => Interlocked.Read(ref _lastUseTimeStamp); #endregion #region Public methods - public void Touch() + public void Touch () { - LastUseTimeStamp = Environment.TickCount & int.MaxValue; + _ = Interlocked.Exchange(ref _lastUseTimeStamp, Environment.TickCount64); } #endregion diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index 3bae31d2..e535e728 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -60,7 +60,7 @@ public partial class LogfileReader : IAutoLogLineMemoryColumnizerCallback, IDisp private volatile bool _isFailModeCheckCallPending; private volatile bool _isFastFailOnGetLogLine; - private volatile int _lastBufferIndex = -1; + private readonly ThreadLocal _lastBufferIndex = new(() => -1); #endregion @@ -574,7 +574,7 @@ public async Task GetLogLineMemoryWithWait (int lineNum) AcquireBufferListReaderLock(); try { - var logBuffer = GetBufferForLine(lineNum); + var logBuffer = GetBufferForLineCore(lineNum); canFastPath = logBuffer is { IsDisposed: false }; } finally @@ -637,7 +637,7 @@ public string GetLogFileNameForLine (int lineNum) public ILogFileInfo GetLogFileInfoForLine (int lineNum) { AcquireBufferListReaderLock(); - var logBuffer = GetBufferForLine(lineNum); + var logBuffer = GetBufferForLineCore(lineNum); var info = logBuffer?.FileInfo; ReleaseBufferListReaderLock(); return info; @@ -652,7 +652,6 @@ public int GetNextMultiFileLine (int lineNum) { var result = -1; AcquireBufferListReaderLock(); - //var logBuffer = GetBufferForLine(lineNum); var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); if (logBuffer != null && index != -1) { @@ -687,7 +686,6 @@ public int GetPrevMultiFileLine (int lineNum) { var result = -1; AcquireBufferListReaderLock(); - //var logBuffer = GetBufferForLine(lineNum); var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); if (logBuffer != null && index != -1) { @@ -717,7 +715,6 @@ public int GetPrevMultiFileLine (int lineNum) public int GetRealLineNumForVirtualLineNum (int lineNum) { AcquireBufferListReaderLock(); - //var logBuffer = GetBufferForLine(lineNum); var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); var result = -1; if (logBuffer != null) @@ -839,7 +836,7 @@ public void DeleteAllContent () /// private void ClearBufferState () { - _lastBufferIndex = -1; + _lastBufferIndex.Value = -1; } /// @@ -889,7 +886,7 @@ public IList GetBufferList () public void LogBufferInfoForLine (int lineNum) { AcquireBufferListReaderLock(); - var buffer = GetBufferForLine(lineNum); + var buffer = GetBufferForLineCore(lineNum); if (buffer == null) { ReleaseBufferListReaderLock(); @@ -989,7 +986,7 @@ private ValueTask GetLogLineMemoryInternal (int lineNum) } AcquireBufferListReaderLock(); - var logBuffer = GetBufferForLine(lineNum); + var logBuffer = GetBufferForLineCore(lineNum); if (logBuffer == null) { ReleaseBufferListReaderLock(); @@ -1014,7 +1011,9 @@ private ValueTask GetLogLineMemoryInternal (int lineNum) ReleaseDisposeUpgradeableReadLock(); ReleaseBufferListReaderLock(); - return new ValueTask(line); + return line.HasValue + ? new ValueTask(line.Value) + : default; } /// @@ -1367,7 +1366,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start } } - LogLine logLine = new(lineMemory, logBuffer.StartLine + logBuffer.LineCount); + var logLine = new LogLine(lineMemory, logBuffer.StartLine + logBuffer.LineCount); logBuffer.AddLine(logLine, filePos); filePos = reader.Position; lineNum++; @@ -1737,7 +1736,7 @@ private void ReReadBuffer (LogBuffer logBuffer) } // Layer 0: Last buffer cache - var lastIdx = _lastBufferIndex; + var lastIdx = _lastBufferIndex.Value; if (lastIdx >= 0 && lastIdx < count) { var buf = arr[lastIdx]; @@ -1752,7 +1751,7 @@ private void ReReadBuffer (LogBuffer logBuffer) var next = arr[lastIdx + 1]; if ((uint)(lineNum - next.StartLine) < (uint)next.LineCount) { - _lastBufferIndex = lastIdx + 1; + _lastBufferIndex.Value = lastIdx + 1; UpdateLruCache(next); return (next, lastIdx + 1); } @@ -1763,7 +1762,7 @@ private void ReReadBuffer (LogBuffer logBuffer) var prev = arr[lastIdx - 1]; if ((uint)(lineNum - prev.StartLine) < (uint)prev.LineCount) { - _lastBufferIndex = lastIdx - 1; + _lastBufferIndex.Value = lastIdx - 1; UpdateLruCache(prev); return (prev, lastIdx - 1); } @@ -1777,7 +1776,7 @@ private void ReReadBuffer (LogBuffer logBuffer) var buf = arr[guess]; if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) { - _lastBufferIndex = guess; + _lastBufferIndex.Value = guess; UpdateLruCache(buf); return (buf, guess); } @@ -1801,7 +1800,7 @@ private void ReReadBuffer (LogBuffer logBuffer) var buf = arr[idx]; if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) { - _lastBufferIndex = idx; + _lastBufferIndex.Value = idx; UpdateLruCache(buf); return (buf, idx); } @@ -1827,105 +1826,118 @@ private void ReReadBuffer (LogBuffer logBuffer) /// such buffer exists. /// private LogBuffer GetBufferForLine (int lineNum) + { + AcquireBufferListReaderLock(); + try + { + return GetBufferForLineCore(lineNum); + } + finally + { + ReleaseBufferListReaderLock(); + } + } + + /// + /// Core buffer lookup without acquiring _bufferListLock. + /// The caller MUST already hold a read or write lock on _bufferListLock. + /// + private LogBuffer GetBufferForLineCore (int lineNum) { #if DEBUG + Util.AssertTrue( + _bufferListLock.IsReadLockHeld || _bufferListLock.IsUpgradeableReadLockHeld || _bufferListLock.IsWriteLockHeld, + "No lock held for buffer list in GetBufferForLineCore"); long startTime = Environment.TickCount; #endif - AcquireBufferListReaderLock(); - try + var arr = CollectionsMarshal.AsSpan(_bufferList); + var count = arr.Length; + + if (count == 0) { - var arr = CollectionsMarshal.AsSpan(_bufferList); - var count = arr.Length; + return null; + } - if (count == 0) + // Layer 0: Last buffer cache — O(1) for sequential access + var lastIdx = _lastBufferIndex.Value; + if (lastIdx >= 0 && lastIdx < count) + { + var buf = arr[lastIdx]; + if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) { - return null; + //dont UpdateLRUCache, the cache has not changed in layer 0 + return buf; } - // Layer 0: Last buffer cache — O(1) for sequential access - var lastIdx = _lastBufferIndex; - if (lastIdx >= 0 && lastIdx < count) + // Layer 1: Adjacent buffer prediction — O(1) for buffer boundary crossings + if (lastIdx + 1 < count) { - var buf = arr[lastIdx]; - if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) - { - //dont UpdateLRUCache, the cache has not changed in layer 0 - return buf; - } - - // Layer 1: Adjacent buffer prediction — O(1) for buffer boundary crossings - if (lastIdx + 1 < count) + var next = arr[lastIdx + 1]; + if ((uint)(lineNum - next.StartLine) < (uint)next.LineCount) { - var next = arr[lastIdx + 1]; - if ((uint)(lineNum - next.StartLine) < (uint)next.LineCount) - { - _lastBufferIndex = lastIdx + 1; - UpdateLruCache(next); - return next; - } + _lastBufferIndex.Value = lastIdx + 1; + UpdateLruCache(next); + return next; } + } - if (lastIdx - 1 >= 0) + if (lastIdx - 1 >= 0) + { + var prev = arr[lastIdx - 1]; + if ((uint)(lineNum - prev.StartLine) < (uint)prev.LineCount) { - var prev = arr[lastIdx - 1]; - if ((uint)(lineNum - prev.StartLine) < (uint)prev.LineCount) - { - _lastBufferIndex = lastIdx - 1; - UpdateLruCache(prev); - return prev; - } + _lastBufferIndex.Value = lastIdx - 1; + UpdateLruCache(prev); + return prev; } } + } - // Layer 2: Direct mapping guess — O(1) speculative for uniform buffers - var guess = lineNum / _maxLinesPerBuffer; - if ((uint)guess < (uint)count) + // Layer 2: Direct mapping guess — O(1) speculative for uniform buffers + var guess = lineNum / _maxLinesPerBuffer; + if ((uint)guess < (uint)count) + { + var buf = arr[guess]; + if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) { - var buf = arr[guess]; - if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) - { - _lastBufferIndex = guess; - UpdateLruCache(buf); - return buf; - } + _lastBufferIndex.Value = guess; + UpdateLruCache(buf); + return buf; } + } - // Layer 3: Branchless binary search with power-of-two strides - var step = HighestPowerOfTwo(count); - var idx = (arr[step - 1].StartLine <= lineNum) ? count - step : 0; + // Layer 3: Branchless binary search with power-of-two strides + var step = HighestPowerOfTwo(count); + var idx = (arr[step - 1].StartLine <= lineNum) ? count - step : 0; - for (step >>= 1; step > 0; step >>= 1) + for (step >>= 1; step > 0; step >>= 1) + { + var probe = idx + step; + if (probe < count && arr[probe - 1].StartLine <= lineNum) { - var probe = idx + step; - if (probe < count && arr[probe - 1].StartLine <= lineNum) - { - idx = probe; - } + idx = probe; } + } - // idx is now the buffer index — verify bounds - if (idx < count) + // idx is now the buffer index — verify bounds + if (idx < count) + { + var buf = arr[idx]; + if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) { - var buf = arr[idx]; - if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) - { - _lastBufferIndex = idx; - UpdateLruCache(buf); - return buf; - } + _lastBufferIndex.Value = idx; + UpdateLruCache(buf); + return buf; } - - return null; } - finally - { + + return null; + #if DEBUG - long endTime = Environment.TickCount; - //_logger.logDebug("getBufferForLine(" + lineNum + ") duration: " + ((endTime - startTime)) + " ms."); + long endTime = Environment.TickCount; + _logger.Debug($"getBufferForLine({lineNum}) duration: {endTime - startTime} ms."); #endif - ReleaseBufferListReaderLock(); - } } private static int HighestPowerOfTwo (int n) => 1 << (31 - int.LeadingZeroCount(n)); @@ -2700,6 +2712,7 @@ protected virtual void Dispose (bool disposing) { DeleteAllContent(); _cts.Dispose(); + _lastBufferIndex.Dispose(); } _disposed = true; diff --git a/src/LogExpert.Tests/BufferShiftTest.cs b/src/LogExpert.Tests/BufferShiftTest.cs index 2baf9711..35939003 100644 --- a/src/LogExpert.Tests/BufferShiftTest.cs +++ b/src/LogExpert.Tests/BufferShiftTest.cs @@ -112,7 +112,9 @@ public void TestShiftBuffers1 (ReaderType readerType) { var logBuffer = logBuffers[i]; var line = logBuffer.GetLineMemoryOfBlock(0); - Assert.That(line.FullLine.Span.Contains(enumerator.Current.AsSpan(), StringComparison.Ordinal)); +#pragma warning disable CS8629 // Nullable value type may be null. + Assert.That(line.Value.FullLine.Span.Contains(enumerator.Current.AsSpan(), StringComparison.Ordinal)); +#pragma warning restore CS8629 // Nullable value type may be null. _ = enumerator.MoveNext(); } @@ -122,7 +124,9 @@ public void TestShiftBuffers1 (ReaderType readerType) { var logBuffer = logBuffers[i]; var line = logBuffer.GetLineMemoryOfBlock(0); - Assert.That(line.FullLine.Span.Contains(enumerator.Current.AsSpan(), StringComparison.Ordinal)); +#pragma warning disable CS8629 // Nullable value type may be null. + Assert.That(line.Value.FullLine.Span.Contains(enumerator.Current.AsSpan(), StringComparison.Ordinal)); +#pragma warning restore CS8629 // Nullable value type may be null. } oldCount = lil.Count; From deb44763a628da6026e3916fbb30437783fc76ab Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Wed, 15 Apr 2026 10:53:28 +0200 Subject: [PATCH 07/24] small optimisations --- src/ColumnizerLib/LogLine.cs | 33 +- .../Classes/Log/LogfileReader.cs | 349 +++--------------- 2 files changed, 55 insertions(+), 327 deletions(-) diff --git a/src/ColumnizerLib/LogLine.cs b/src/ColumnizerLib/LogLine.cs index 262c3624..c68b28e6 100644 --- a/src/ColumnizerLib/LogLine.cs +++ b/src/ColumnizerLib/LogLine.cs @@ -4,28 +4,17 @@ namespace ColumnizerLib; /// Represents a single log line, including its full text and line number. /// /// -/// -/// Purpose:
-/// The LogLine struct encapsulates the content and line number of a log entry. It is used throughout the -/// columnizer and log processing infrastructure to provide a strongly-typed, immutable representation of a log line. -///
-/// -/// Usage:
-/// This struct implements the interface, allowing it to be used wherever an ILogLineMemory -/// is expected. It provides value semantics and is intended to be lightweight and efficiently passed by value. -///
-/// -/// Relationship to ILogLineMemory:
-/// LogLine is a concrete, immutable implementation of the interface, providing -/// properties for the full line text and its line number. -///
-/// -/// Why struct instead of record:
-/// A struct is preferred over a record here to avoid heap allocations and to provide value-type -/// semantics, which are beneficial for performance when processing large numbers of log lines. The struct is -/// immutable (readonly), ensuring thread safety and predictability. The previous record implementation -/// was replaced to better align with these performance and semantic requirements. -///
+/// Purpose:
The LogLine struct encapsulates the content and line number of a log entry. It +/// is used throughout the columnizer and log processing infrastructure to provide a strongly-typed, immutable +/// representation of a log line.
+/// Usage:
This struct implements the +/// interface, allowing it to be used wherever an ILogLineMemory is expected. It +/// provides value semantics and is intended to be lightweight and efficiently passed by value.
+/// Relationship to ILogLineMemory:
LogLine is a concrete, immutable implementation of the +/// interface, providing properties for the full line text and its line number.
+/// This is a readonly record struct implementing +/// . Stored inline in List<LogLine> to avoid boxing and heap allocation. +/// Boxing occurs only when returned through the ILogLineMemory interface boundary. ///
public readonly record struct LogLine : ILogLineMemory { diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index e535e728..63254a52 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -1,4 +1,4 @@ -using System.Buffers; +using System.Collections.Concurrent; using System.Globalization; using System.Runtime.InteropServices; using System.Text; @@ -34,7 +34,6 @@ public partial class LogfileReader : IAutoLogLineMemoryColumnizerCallback, IDisp private readonly Lock _logBufferLock = new(); private readonly ReaderWriterLockSlim _bufferListLock = new(LockRecursionPolicy.SupportsRecursion); private readonly ReaderWriterLockSlim _disposeLock = new(LockRecursionPolicy.SupportsRecursion); - private readonly ReaderWriterLockSlim _lruCacheDictLock = new(LockRecursionPolicy.SupportsRecursion); private const int PROGRESS_UPDATE_INTERVAL_MS = 100; private const int WAIT_TIME = 1000; @@ -51,7 +50,7 @@ public partial class LogfileReader : IAutoLogLineMemoryColumnizerCallback, IDisp private IList _logFileInfoList = []; - private Dictionary _lruCacheDict; + private ConcurrentDictionary _lruCacheDict; private bool _shouldStop; private bool _disposed; private ILogFileInfo _watchedILogFileInfo; @@ -202,13 +201,11 @@ public int LineCount ///
public long FileSize { get; private set; } - //TODO: Change to private field. No need for a property. /// /// Gets or sets a value indicating whether XML mode is enabled. /// public bool IsXmlMode { get; set; } - //TODO: Change to private field. No need for a property. /// /// Gets or sets the XML log configuration used to control logging behavior and settings. /// @@ -257,9 +254,7 @@ public void ReadFiles () _lastProgressUpdate = 0; FileSize = 0; LineCount = 0; - //this.lastReturnedLine = ""; - //this.lastReturnedLineNum = -1; - //this.lastReturnedLineNumForBuffer = -1; + _isDeleted = false; ClearLru(); AcquireBufferListWriterLock(); @@ -269,7 +264,6 @@ public void ReadFiles () { foreach (var info in _logFileInfoList) { - //info.OpenFile(); ReadToBufferList(info, 0, LineCount); } @@ -391,10 +385,6 @@ public int ShiftBuffers () { _logger.Info(CultureInfo.InvariantCulture, "{0} does not exist", fileName); lostILogFileInfoList.Add(logFileInfo); -#if DEBUG // for better overview in logfile: - //ILogFileInfo newILogFileInfo = new ILogFileInfo(fileName); - //ReplaceBufferInfos(ILogFileInfo, newILogFileInfo); -#endif } } @@ -402,11 +392,8 @@ public int ShiftBuffers () { _logger.Info(CultureInfo.InvariantCulture, "Deleting buffers for lost files"); - AcquireLruCacheDictWriterLock(); - foreach (var logFileInfo in lostILogFileInfoList) { - //this.ILogFileInfoList.Remove(logFileInfo); var lastBuffer = DeleteBuffersForInfo(logFileInfo, false); if (lastBuffer != null) { @@ -420,7 +407,6 @@ public int ShiftBuffers () SetNewStartLineForBuffer(buffer, buffer.StartLine - offset); } - ReleaseLRUCacheDictWriterLock(); #if DEBUG if (_bufferList.Count > 0) { @@ -432,30 +418,23 @@ public int ShiftBuffers () // Read anew all buffers following a buffer info that couldn't be matched with the corresponding existing file _logger.Info(CultureInfo.InvariantCulture, "Deleting buffers for files that must be re-read"); - AcquireLruCacheDictWriterLock(); - foreach (var iLogFileInfo in readNewILogFileInfoList) { DeleteBuffersForInfo(iLogFileInfo, true); - //this.ILogFileInfoList.Remove(logFileInfo); } _logger.Info(CultureInfo.InvariantCulture, "Deleting buffers for the watched file"); DeleteBuffersForInfo(_watchedILogFileInfo, true); - ReleaseLRUCacheDictWriterLock(); _logger.Info(CultureInfo.InvariantCulture, "Re-Reading files"); foreach (var iLogFileInfo in readNewILogFileInfoList) { - //logFileInfo.OpenFile(); ReadToBufferList(iLogFileInfo, 0, LineCount); - //this.ILogFileInfoList.Add(logFileInfo); newFileInfoList.Add(iLogFileInfo); } - //this.watchedILogFileInfo = this.ILogFileInfoList[this.ILogFileInfoList.Count - 1]; _logFileInfoList = newFileInfoList; _watchedILogFileInfo = GetLogFileInfo(_watchedILogFileInfo.FullName); _logFileInfoList.Add(_watchedILogFileInfo); @@ -756,28 +735,23 @@ public void StopMonitoring () { _logger.Info(CultureInfo.InvariantCulture, "stopMonitoring()"); _shouldStop = true; + _cts.Cancel(); - Thread.Sleep(_watchedILogFileInfo.PollInterval); // leave time for the threads to stop by themselves - - if (_monitorTask != null) + try { - if (_monitorTask.Status == TaskStatus.Running) // if thread has not finished, abort it - { - _cts.Cancel(); - } + var timeout = TimeSpan.FromSeconds(5); + _ = _monitorTask?.Wait(timeout); + _ = _garbageCollectorTask?.Wait(timeout); } - - if (!_garbageCollectorTask.IsCanceled) + catch (AggregateException ex) when (ex.InnerExceptions.All(e => e is OperationCanceledException)) { - if (_garbageCollectorTask.Status == TaskStatus.Running) // if thread has not finished, abort it - { - _cts.Cancel(); - } + //Expected exceptions due to task cancellation, can be safely ignored. + } + catch (AggregateException ex) + { + _logger.Warn(ex, "Exception while waiting for monitor or GC task to complete"); } - //this.loadThread = null; - //_monitorThread = null; - //_garbageCollectorThread = null; // preventive call CloseFiles(); } @@ -788,12 +762,6 @@ public void StopMonitoring () public void StopMonitoringAsync () { var task = Task.Run(StopMonitoring); - - //Thread stopperThread = new(new ThreadStart(StopMonitoring)) - //{ - // IsBackground = true - //}; - //stopperThread.Start(); } /// @@ -810,7 +778,6 @@ public void DeleteAllContent () _logger.Info(CultureInfo.InvariantCulture, "Deleting all log buffers for {0}. Used mem: {1:N0}", Util.GetNameFromPath(_fileName), GC.GetTotalMemory(false)); AcquireBufferListWriterLock(); ClearBufferState(); - AcquireLruCacheDictWriterLock(); AcquireDisposeWriterLock(); foreach (var logBuffer in _bufferList) @@ -825,7 +792,6 @@ public void DeleteAllContent () _bufferList.Clear(); ReleaseDisposeWriterLock(); - ReleaseLRUCacheDictWriterLock(); ReleaseBufferListWriterLock(); _contentDeleted = true; _logger.Info(CultureInfo.InvariantCulture, "Deleting complete. Used mem: {0:N0}", GC.GetTotalMemory(false)); @@ -915,10 +881,8 @@ public void LogBufferInfoForLine (int lineNum) public void LogBufferDiagnostic () { _logger.Info(CultureInfo.InvariantCulture, "-------- Buffer diagnostics -------"); - AcquireLruCacheDictReaderLock(); var cacheCount = _lruCacheDict.Count; _logger.Info(CultureInfo.InvariantCulture, "LRU entries: {0}", cacheCount); - ReleaseLRUCacheDictReaderLock(); AcquireBufferListReaderLock(); _logger.Info(CultureInfo.InvariantCulture, "File: {0}\r\nBuffer count: {1}\r\nDisposed buffers: {2}", _fileName, _bufferList.Count, _bufferList.Count - cacheCount); @@ -1027,9 +991,7 @@ private void InitLruBuffers () { ClearBufferState(); _bufferList = []; - //_bufferLru = new List(_max_buffers + 1); - //this.lruDict = new Dictionary(this.MAX_BUFFERS + 1); // key=startline, value = index in bufferLru - _lruCacheDict = new Dictionary(_max_buffers + 1); + _lruCacheDict = new ConcurrentDictionary(concurrencyLevel: Environment.ProcessorCount, capacity: _max_buffers + 1); } /// @@ -1043,9 +1005,6 @@ private void InitLruBuffers () private void StartGCThread () { _garbageCollectorTask = Task.Run(GarbageCollectorThreadProc, _cts.Token); - //_garbageCollectorThread = new Thread(new ThreadStart(GarbageCollectorThreadProc)); - //_garbageCollectorThread.IsBackground = true; - //_garbageCollectorThread.Start(); } /// @@ -1059,9 +1018,6 @@ private void ResetBufferCache () { FileSize = 0; LineCount = 0; - //this.lastReturnedLine = ""; - //this.lastReturnedLineNum = -1; - //this.lastReturnedLineNumForBuffer = -1; } /// @@ -1069,15 +1025,8 @@ private void ResetBufferCache () /// private void CloseFiles () { - //foreach (ILogFileInfo info in this.ILogFileInfoList) - //{ - // info.CloseFile(); - //} FileSize = 0; LineCount = 0; - //this.lastReturnedLine = ""; - //this.lastReturnedLineNum = -1; - //this.lastReturnedLineNumForBuffer = -1; } /// @@ -1196,9 +1145,8 @@ private LogBuffer DeleteBuffersForInfo (ILogFileInfo iLogFileInfo, bool matchNam /// The log buffer to remove from the buffer list and cache. Must not be null. private void RemoveFromBufferList (LogBuffer buffer) { - Util.AssertTrue(_lruCacheDictLock.IsWriteLockHeld, "No _writer lock for lru cache"); Util.AssertTrue(_bufferListLock.IsWriteLockHeld, "No _writer lock for buffer list"); - _ = _lruCacheDict.Remove(buffer.StartLine); + _ = _lruCacheDict.TryRemove(buffer.StartLine, out _); _ = _bufferList.Remove(buffer); } @@ -1413,7 +1361,6 @@ private void AddBufferToList (LogBuffer logBuffer) _logger.Debug(CultureInfo.InvariantCulture, "AddBufferToList(): {0}/{1}/{2}", logBuffer.StartLine, logBuffer.LineCount, logBuffer.FileInfo.FullName); #endif _bufferList.Add(logBuffer); - //UpdateLru(logBuffer); UpdateLruCache(logBuffer); } @@ -1428,60 +1375,12 @@ private void AddBufferToList (LogBuffer logBuffer) /// The log buffer to add to or update in the LRU cache. Cannot be null. private void UpdateLruCache (LogBuffer logBuffer) { - AcquireLRUCacheDictUpgradeableReadLock(); - try - { - if (_lruCacheDict.TryGetValue(logBuffer.StartLine, out var cacheEntry)) - { - cacheEntry.Touch(); - } - else - { - UpgradeLRUCacheDicLockToWriterLock(); - try - { - if (!_lruCacheDict.TryGetValue(logBuffer.StartLine, out cacheEntry)) - { - cacheEntry = new LogBufferCacheEntry - { - LogBuffer = logBuffer - }; + var cacheEntry = _lruCacheDict.GetOrAdd( + logBuffer.StartLine, + static (_, buf) => new LogBufferCacheEntry { LogBuffer = buf }, + logBuffer); - try - { - _lruCacheDict.Add(logBuffer.StartLine, cacheEntry); - } - catch (ArgumentException e) - { - _logger.Error(e, "Error in LRU cache: " + e.Message); -#if DEBUG // there seems to be a bug with double added key - - _logger.Info(CultureInfo.InvariantCulture, "Added buffer:"); - DumpBufferInfos(logBuffer); - if (_lruCacheDict.TryGetValue(logBuffer.StartLine, out var existingEntry)) - { - _logger.Info(CultureInfo.InvariantCulture, "Existing buffer: "); - DumpBufferInfos(existingEntry.LogBuffer); - } - else - { - _logger.Warn(CultureInfo.InvariantCulture, "Ooops? Cannot find the already existing entry in LRU."); - } -#endif - throw; - } - } - } - finally - { - DowngradeLRUCacheLockFromWriterLock(); - } - } - } - finally - { - ReleaseLRUCacheDictUpgradeableReadLock(); - } + cacheEntry.Touch(); } /// @@ -1492,16 +1391,10 @@ private void UpdateLruCache (LogBuffer logBuffer) /// private void SetNewStartLineForBuffer (LogBuffer logBuffer, int newLineNum) { - Util.AssertTrue(_lruCacheDictLock.IsWriteLockHeld, "No _writer lock for lru cache"); - if (_lruCacheDict.ContainsKey(logBuffer.StartLine)) + if (_lruCacheDict.TryRemove(logBuffer.StartLine, out var cacheEntry)) { - _ = _lruCacheDict.Remove(logBuffer.StartLine); logBuffer.StartLine = newLineNum; - LogBufferCacheEntry cacheEntry = new() - { - LogBuffer = logBuffer - }; - _lruCacheDict.Add(logBuffer.StartLine, cacheEntry); + _ = _lruCacheDict.TryAdd(logBuffer.StartLine, cacheEntry); } else { @@ -1524,7 +1417,6 @@ private void GarbageCollectLruCache () #endif _logger.Debug(CultureInfo.InvariantCulture, "Starting garbage collection"); var threshold = 10; - AcquireLruCacheDictWriterLock(); var diff = 0; if (_lruCacheDict.Count - (_max_buffers + threshold) > 0) { @@ -1535,44 +1427,23 @@ private void GarbageCollectLruCache () _logger.Info(CultureInfo.InvariantCulture, "Removing {0} entries from LRU cache for {1}", diff, Util.GetNameFromPath(_fileName)); } #endif - var count = _lruCacheDict.Count; - var sortArray = ArrayPool<(long Timestamp, int StartLine)>.Shared.Rent(count); - try - { - var idx = 0; - foreach (var entry in _lruCacheDict.Values) - { - sortArray[idx++] = (entry.LastUseTimeStamp, entry.LogBuffer.StartLine); - } + // Snapshot values and sort by timestamp (ascending = least recently used first) + var entries = _lruCacheDict.ToArray(); + Array.Sort(entries, static (a, b) => a.Value.LastUseTimeStamp.CompareTo(b.Value.LastUseTimeStamp)); - Array.Sort(sortArray, 0, count); - - // remove first entries (least recently used) - AcquireDisposeWriterLock(); - for (var i = 0; i < diff; ++i) + AcquireDisposeWriterLock(); + for (var i = 0; i < diff && i < entries.Length; ++i) + { + var kvp = entries[i]; + if (_lruCacheDict.TryRemove(kvp.Key, out var removed)) { - if (i >= count) - { - break; - } - - var startLine = sortArray[i].StartLine; - if (_lruCacheDict.TryGetValue(startLine, out var entry)) - { - _lruCacheDict.Remove(startLine); - entry.LogBuffer.DisposeContent(); - } + removed.LogBuffer.DisposeContent(); } - - ReleaseDisposeWriterLock(); - } - finally - { - ArrayPool<(long Timestamp, int StartLine)>.Shared.Return(sortArray); } + + ReleaseDisposeWriterLock(); } - ReleaseLRUCacheDictWriterLock(); #if DEBUG if (diff > 0) { @@ -1591,17 +1462,17 @@ private void GarbageCollectLruCache () /// then invokes cache cleanup, continuing until a stop signal is received. Exceptions during the sleep interval are /// caught and ignored to ensure the thread remains active. /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Garbage collector Thread Process")] - private void GarbageCollectorThreadProc () + private async Task GarbageCollectorThreadProc () { while (!_shouldStop) { try { - Thread.Sleep(10000); + await Task.Delay(10000, _cts.Token).ConfigureAwait(false); } - catch (Exception) + catch (OperationCanceledException) { + break; } GarbageCollectLruCache(); @@ -1619,7 +1490,6 @@ private void GarbageCollectorThreadProc () private void ClearLru () { _logger.Info(CultureInfo.InvariantCulture, "Clearing LRU cache."); - AcquireLruCacheDictWriterLock(); AcquireDisposeWriterLock(); foreach (var entry in _lruCacheDict.Values) { @@ -1628,7 +1498,6 @@ private void ClearLru () _lruCacheDict.Clear(); ReleaseDisposeWriterLock(); - ReleaseLRUCacheDictWriterLock(); _logger.Info(CultureInfo.InvariantCulture, "Clearing done."); } @@ -1839,8 +1708,8 @@ private LogBuffer GetBufferForLine (int lineNum) } /// - /// Core buffer lookup without acquiring _bufferListLock. - /// The caller MUST already hold a read or write lock on _bufferListLock. + /// Core buffer lookup without acquiring _bufferListLock. The caller MUST already hold a read or write lock + /// on _bufferListLock. /// private LogBuffer GetBufferForLineCore (int lineNum) { @@ -2003,7 +1872,7 @@ private LogBuffer GetFirstBufferForFileByLogBuffer (LogBuffer logBuffer, int ind /// updated or deleted. The method runs until a stop signal is received. Exceptions encountered during monitoring /// are logged but do not terminate the monitoring loop. /// - private void MonitorThreadProc () + private async Task MonitorThreadProc () { Thread.CurrentThread.Name = "MonitorThread"; //IFileSystemPlugin fs = PluginRegistry.GetInstance().FindFileSystemForUri(this.watchedILogFileInfo.FullName); @@ -2030,15 +1899,9 @@ private void MonitorThreadProc () try { var pollInterval = _watchedILogFileInfo.PollInterval; - //#if DEBUG - // if (_logger.IsDebug) - // { - // _logger.logDebug("Poll interval for " + this.fileName + ": " + pollInterval); - // } - //#endif - Thread.Sleep(pollInterval); + await Task.Delay(pollInterval, _cts.Token).ConfigureAwait(false); } - catch (Exception e) + catch (OperationCanceledException e) { _logger.Error(e); } @@ -2166,7 +2029,6 @@ private void FireChangeEvent () } else { - // ReloadBufferList(); // removed because reloading is triggered by owning LogWindow // Trigger "new file" handling (reload) OnLoadFile(new LoadFileEventArgs(_fileName, 0, true, _fileLength, true)); @@ -2355,8 +2217,6 @@ private bool ReadLine (ILogStreamReader reader, int lineNum, int realLineNum, ou } return (true, lineMemory, false); - - //return (ReadLine(reader, lineNum, realLineNum, out var outLine), outLine.AsMemory()); } /// @@ -2395,42 +2255,6 @@ private void AcquireDisposeLockUpgradableReadLock () } } - /// - /// Acquires an upgradeable read lock on the LRU cache dictionary, waiting up to 10 seconds before blocking - /// indefinitely if the lock is not immediately available. - /// - /// - /// This method ensures that the calling thread holds an upgradeable read lock on the LRU cache dictionary, allowing - /// for safe read access and the potential to upgrade to a write lock if necessary. If the lock cannot be acquired - /// within 10 seconds, a warning is logged and the method blocks until the lock becomes available. This approach - /// helps prevent deadlocks and provides diagnostic information in case of lock contention. - /// - private void AcquireLRUCacheDictUpgradeableReadLock () - { - if (!_lruCacheDictLock.TryEnterUpgradeableReadLock(TimeSpan.FromSeconds(10))) - { - _logger.Warn("Upgradeable read lock timed out"); - _lruCacheDictLock.EnterUpgradeableReadLock(); - } - } - - /// - /// Acquires a read lock on the LRU cache dictionary to ensure thread-safe read access. - /// - /// - /// If the read lock cannot be acquired within 10 seconds, a warning is logged and the method will block until the - /// lock becomes available. Callers should ensure that this method is used in contexts where blocking is acceptable - /// to avoid potential deadlocks or performance issues. - /// - private void AcquireLruCacheDictReaderLock () - { - if (!_lruCacheDictLock.TryEnterReadLock(TimeSpan.FromSeconds(10))) - { - _logger.Warn("LRU cache dict reader lock timed out"); - _lruCacheDictLock.EnterReadLock(); - } - } - /// /// Acquires a read lock on the dispose lock, blocking the calling thread until the lock is obtained. /// @@ -2447,18 +2271,6 @@ private void AcquireDisposeReaderLock () } } - /// - /// Releases the writer lock held on the LRU cache dictionary, allowing other threads to acquire the lock. - /// - /// - /// Call this method after completing operations that require exclusive access to the LRU cache dictionary. Failing - /// to release the writer lock may result in deadlocks or reduced concurrency. - /// - private void ReleaseLRUCacheDictWriterLock () - { - _lruCacheDictLock.ExitWriteLock(); - } - /// /// Releases the writer lock held for disposing resources. /// @@ -2472,18 +2284,6 @@ private void ReleaseDisposeWriterLock () _disposeLock.ExitWriteLock(); } - /// - /// Releases the read lock on the LRU cache dictionary to allow write access by other threads. - /// - /// - /// Call this method after completing operations that require read access to the LRU cache dictionary. Failing to - /// release the lock may result in deadlocks or prevent other threads from acquiring write access. - /// - private void ReleaseLRUCacheDictReaderLock () - { - _lruCacheDictLock.ExitReadLock(); - } - /// /// Releases a reader lock held for disposing resources, allowing other threads to acquire the lock as needed. /// @@ -2496,19 +2296,6 @@ private void ReleaseDisposeReaderLock () _disposeLock.ExitReadLock(); } - /// - /// Releases the upgradeable read lock held on the LRU cache dictionary. - /// - /// - /// Call this method to release the upgradeable read lock previously acquired on the LRU cache dictionary. Failing - /// to release the lock may result in deadlocks or reduced concurrency. This method should be used in conjunction - /// with the corresponding lock acquisition method to ensure proper synchronization. - /// - private void ReleaseLRUCacheDictUpgradeableReadLock () - { - _lruCacheDictLock.ExitUpgradeableReadLock(); - } - /// /// Acquires the writer lock used to synchronize disposal operations, blocking the calling thread until the lock is /// obtained. @@ -2527,24 +2314,6 @@ private void AcquireDisposeWriterLock () } } - /// - /// Acquires an exclusive writer lock on the LRU cache dictionary, blocking if the lock is not immediately - /// available. - /// - /// - /// If the writer lock cannot be acquired within 10 seconds, a warning is logged and the method blocks until the - /// lock becomes available. This method should be called before performing write operations on the LRU cache - /// dictionary to ensure thread safety. - /// - private void AcquireLruCacheDictWriterLock () - { - if (!_lruCacheDictLock.TryEnterWriteLock(TimeSpan.FromSeconds(10))) - { - _logger.Warn("LRU cache dict writer lock timed out"); - _lruCacheDictLock.EnterWriteLock(); - } - } - /// /// Releases the upgradeable read lock on the buffer list, allowing other threads to acquire exclusive or read /// access. @@ -2593,24 +2362,6 @@ private void UpgradeDisposeLockToWriterLock () } } - /// - /// Upgrades the lock on the LRU cache dictionary from a reader lock to a writer lock, waiting up to 10 seconds - /// before forcing the upgrade. - /// - /// - /// If the writer lock cannot be acquired within 10 seconds, the method logs a warning and then blocks until the - /// writer lock is available. Call this method only when it is necessary to perform write operations on the LRU - /// cache dictionary after holding a reader lock. - /// - private void UpgradeLRUCacheDicLockToWriterLock () - { - if (!_lruCacheDictLock.TryEnterWriteLock(TimeSpan.FromSeconds(10))) - { - _logger.Warn("Writer lock upgrade timed out"); - _lruCacheDictLock.EnterWriteLock(); - } - } - /// /// Downgrades the buffer list lock from write mode to allow other threads to acquire read access. /// @@ -2623,18 +2374,6 @@ private void DowngradeBufferListLockFromWriterLock () _bufferListLock.ExitWriteLock(); } - /// - /// Downgrades the LRU cache lock from a writer lock, allowing other threads to acquire read access. - /// - /// - /// Call this method after completing operations that require exclusive write access to the LRU cache, to permit - /// concurrent read operations. The caller must hold the writer lock before invoking this method. - /// - private void DowngradeLRUCacheLockFromWriterLock () - { - _lruCacheDictLock.ExitWriteLock(); - } - /// /// Releases the writer lock on the dispose lock, downgrading from write access. /// @@ -2814,4 +2553,4 @@ protected virtual void OnRespawned () } #endregion Event Handlers -} +} \ No newline at end of file From 10e839c670f420962f6c61652956cecb32f25a5d Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Wed, 15 Apr 2026 11:40:54 +0200 Subject: [PATCH 08/24] more optimisations --- src/LogExpert.Core/Classes/Log/LogBuffer.cs | 17 ++ .../Classes/Log/LogfileReader.cs | 180 ++++++++++++------ src/LogExpert.Core/Config/Preferences.cs | 2 +- .../Dialogs/LogTabWindow/LogTabWindow.cs | 3 - src/LogExpert.UI/Dialogs/SettingsDialog.cs | 6 +- 5 files changed, 147 insertions(+), 61 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/LogBuffer.cs b/src/LogExpert.Core/Classes/Log/LogBuffer.cs index d5541d05..a83e9dc3 100644 --- a/src/LogExpert.Core/Classes/Log/LogBuffer.cs +++ b/src/LogExpert.Core/Classes/Log/LogBuffer.cs @@ -8,6 +8,7 @@ public class LogBuffer { #region Fields + private SpinLock _contentLock = new(enableThreadOwnerTracking: false); private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); #if DEBUG @@ -110,6 +111,22 @@ public void DisposeContent () : null; } + /// + /// Acquires the content lock. The caller MUST call in a finally block. + /// + public void AcquireContentLock (ref bool lockTaken) + { + _contentLock.Enter(ref lockTaken); + } + + /// + /// Releases the content lock previously acquired via . + /// + public void ReleaseContentLock () + { + _contentLock.Exit(useMemoryBarrier: false); + } + #endregion #if DEBUG diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index 63254a52..27fd2720 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -33,7 +33,6 @@ public partial class LogfileReader : IAutoLogLineMemoryColumnizerCallback, IDisp private readonly Lock _logBufferLock = new(); private readonly ReaderWriterLockSlim _bufferListLock = new(LockRecursionPolicy.SupportsRecursion); - private readonly ReaderWriterLockSlim _disposeLock = new(LockRecursionPolicy.SupportsRecursion); private const int PROGRESS_UPDATE_INTERVAL_MS = 100; private const int WAIT_TIME = 1000; @@ -500,7 +499,7 @@ private void ReleaseBufferListWriterLock () /// private void ReleaseDisposeUpgradeableReadLock () { - _disposeLock.ExitUpgradeableReadLock(); + //_disposeLock.ExitUpgradeableReadLock(); } /// @@ -778,7 +777,7 @@ public void DeleteAllContent () _logger.Info(CultureInfo.InvariantCulture, "Deleting all log buffers for {0}. Used mem: {1:N0}", Util.GetNameFromPath(_fileName), GC.GetTotalMemory(false)); AcquireBufferListWriterLock(); ClearBufferState(); - AcquireDisposeWriterLock(); + //AcquireDisposeWriterLock(); foreach (var logBuffer in _bufferList) { @@ -791,7 +790,7 @@ public void DeleteAllContent () _lruCacheDict.Clear(); _bufferList.Clear(); - ReleaseDisposeWriterLock(); + //ReleaseDisposeWriterLock(); ReleaseBufferListWriterLock(); _contentDeleted = true; _logger.Info(CultureInfo.InvariantCulture, "Deleting complete. Used mem: {0:N0}", GC.GetTotalMemory(false)); @@ -861,11 +860,11 @@ public void LogBufferInfoForLine (int lineNum) } _logger.Info(CultureInfo.InvariantCulture, "-----------------------------------"); - AcquireDisposeReaderLock(); + //AcquireDisposeReaderLock(); _logger.Info(CultureInfo.InvariantCulture, "Buffer info for line {0}", lineNum); DumpBufferInfos(buffer); _logger.Info(CultureInfo.InvariantCulture, "File pos for current line: {0}", buffer.GetFilePosForLineOfBlock(lineNum - buffer.StartLine)); - ReleaseDisposeReaderLock(); + //ReleaseDisposeReaderLock(); _logger.Info(CultureInfo.InvariantCulture, "-----------------------------------"); ReleaseBufferListReaderLock(); } @@ -893,7 +892,7 @@ public void LogBufferDiagnostic () for (var i = 0; i < _bufferList.Count; ++i) { var buffer = _bufferList[i]; - AcquireDisposeReaderLock(); + //AcquireDisposeReaderLock(); if (buffer.StartLine != lineNum) { _logger.Error("Start line of buffer is: {0}, expected: {1}", buffer.StartLine, lineNum); @@ -905,7 +904,7 @@ public void LogBufferDiagnostic () disposeSum += buffer.DisposeCount; maxDispose = Math.Max(maxDispose, buffer.DisposeCount); minDispose = Math.Min(minDispose, buffer.DisposeCount); - ReleaseDisposeReaderLock(); + //ReleaseDisposeReaderLock(); } ReleaseBufferListReaderLock(); @@ -957,27 +956,58 @@ private ValueTask GetLogLineMemoryInternal (int lineNum) _logger.Error("Cannot find buffer for line {0}, file: {1}{2}", lineNum, _fileName, IsMultiFile ? " (MultiFile)" : ""); return default; } - // disposeLock prevents that the garbage collector is disposing just in the moment we use the buffer - AcquireDisposeLockUpgradableReadLock(); - if (logBuffer.IsDisposed) + + var lockTaken = false; + try { - UpgradeDisposeLockToWriterLock(); + logBuffer.AcquireContentLock(ref lockTaken); - lock (logBuffer.FileInfo) + if (logBuffer.IsDisposed) { - ReReadBuffer(logBuffer); + UpgradeDisposeLockToWriterLock(); + + lock (logBuffer.FileInfo) + { + ReReadBuffer(logBuffer); + } } - DowngradeDisposeLockFromWriterLock(); + var line = logBuffer.GetLineMemoryOfBlock(lineNum - logBuffer.StartLine); + return line.HasValue + ? new ValueTask(line.Value) + : default; } + finally + { + if (lockTaken) + { + logBuffer.ReleaseContentLock(); + } - var line = logBuffer.GetLineMemoryOfBlock(lineNum - logBuffer.StartLine); - ReleaseDisposeUpgradeableReadLock(); - ReleaseBufferListReaderLock(); + ReleaseBufferListReaderLock(); + } - return line.HasValue - ? new ValueTask(line.Value) - : default; + //// disposeLock prevents that the garbage collector is disposing just in the moment we use the buffer + //AcquireDisposeLockUpgradableReadLock(); + //if (logBuffer.IsDisposed) + //{ + // UpgradeDisposeLockToWriterLock(); + + // lock (logBuffer.FileInfo) + // { + // ReReadBuffer(logBuffer); + // } + + // DowngradeDisposeLockFromWriterLock(); + //} + + //var line = logBuffer.GetLineMemoryOfBlock(lineNum - logBuffer.StartLine); + //ReleaseDisposeUpgradeableReadLock(); + //ReleaseBufferListReaderLock(); + + //return line.HasValue + // ? new ValueTask(line.Value) + // : default; } /// @@ -1226,15 +1256,33 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start } } - AcquireDisposeLockUpgradableReadLock(); - if (logBuffer.IsDisposed) + var lockTaken = false; + + try + { + logBuffer.AcquireContentLock(ref lockTaken); + if (logBuffer.IsDisposed) + { + ReReadBuffer(logBuffer); + } + } + finally { - UpgradeDisposeLockToWriterLock(); - ReReadBuffer(logBuffer); - DowngradeDisposeLockFromWriterLock(); + if (lockTaken) + { + logBuffer.ReleaseContentLock(); + } } - ReleaseDisposeUpgradeableReadLock(); + //AcquireDisposeLockUpgradableReadLock(); + //if (logBuffer.IsDisposed) + //{ + // UpgradeDisposeLockToWriterLock(); + // ReReadBuffer(logBuffer); + // DowngradeDisposeLockFromWriterLock(); + //} + + //ReleaseDisposeUpgradeableReadLock(); } } finally @@ -1431,17 +1479,29 @@ private void GarbageCollectLruCache () var entries = _lruCacheDict.ToArray(); Array.Sort(entries, static (a, b) => a.Value.LastUseTimeStamp.CompareTo(b.Value.LastUseTimeStamp)); - AcquireDisposeWriterLock(); + //AcquireDisposeWriterLock(); for (var i = 0; i < diff && i < entries.Length; ++i) { var kvp = entries[i]; if (_lruCacheDict.TryRemove(kvp.Key, out var removed)) { - removed.LogBuffer.DisposeContent(); + var lockTaken = false; + try + { + removed.LogBuffer.AcquireContentLock(ref lockTaken); + removed.LogBuffer.DisposeContent(); + } + finally + { + if (lockTaken) + { + removed.LogBuffer.ReleaseContentLock(); + } + } } } - ReleaseDisposeWriterLock(); + //ReleaseDisposeWriterLock(); } #if DEBUG @@ -1493,7 +1553,19 @@ private void ClearLru () AcquireDisposeWriterLock(); foreach (var entry in _lruCacheDict.Values) { - entry.LogBuffer.DisposeContent(); + var lockTaken = false; + try + { + entry.LogBuffer.AcquireContentLock(ref lockTaken); + entry.LogBuffer.DisposeContent(); + } + finally + { + if (lockTaken) + { + entry.LogBuffer.ReleaseContentLock(); + } + } } _lruCacheDict.Clear(); @@ -2248,11 +2320,11 @@ private void AcquireBufferListUpgradeableReadLock () /// private void AcquireDisposeLockUpgradableReadLock () { - if (!_disposeLock.TryEnterUpgradeableReadLock(TimeSpan.FromSeconds(10))) - { - _logger.Warn("Upgradeable read lock timed out"); - _disposeLock.EnterUpgradeableReadLock(); - } + //if (!_disposeLock.TryEnterUpgradeableReadLock(TimeSpan.FromSeconds(10))) + //{ + // _logger.Warn("Upgradeable read lock timed out"); + // _disposeLock.EnterUpgradeableReadLock(); + //} } /// @@ -2264,11 +2336,11 @@ private void AcquireDisposeLockUpgradableReadLock () /// private void AcquireDisposeReaderLock () { - if (!_disposeLock.TryEnterReadLock(TimeSpan.FromSeconds(10))) - { - _logger.Warn("Dispose reader lock timed out"); - _disposeLock.EnterReadLock(); - } + //if (!_disposeLock.TryEnterReadLock(TimeSpan.FromSeconds(10))) + //{ + // _logger.Warn("Dispose reader lock timed out"); + // _disposeLock.EnterReadLock(); + //} } /// @@ -2281,7 +2353,7 @@ private void AcquireDisposeReaderLock () /// private void ReleaseDisposeWriterLock () { - _disposeLock.ExitWriteLock(); + //_disposeLock.ExitWriteLock(); } /// @@ -2293,7 +2365,7 @@ private void ReleaseDisposeWriterLock () /// private void ReleaseDisposeReaderLock () { - _disposeLock.ExitReadLock(); + //_disposeLock.ExitReadLock(); } /// @@ -2307,11 +2379,11 @@ private void ReleaseDisposeReaderLock () /// private void AcquireDisposeWriterLock () { - if (!_disposeLock.TryEnterWriteLock(TimeSpan.FromSeconds(10))) - { - _logger.Warn("Dispose writer lock timed out"); - _disposeLock.EnterWriteLock(); - } + //if (!_disposeLock.TryEnterWriteLock(TimeSpan.FromSeconds(10))) + //{ + // _logger.Warn("Dispose writer lock timed out"); + // _disposeLock.EnterWriteLock(); + //} } /// @@ -2355,11 +2427,11 @@ private void UpgradeBufferlistLockToWriterLock () /// private void UpgradeDisposeLockToWriterLock () { - if (!_disposeLock.TryEnterWriteLock(TimeSpan.FromSeconds(10))) - { - _logger.Warn("Writer lock upgrade timed out"); - _disposeLock.EnterWriteLock(); - } + //if (!_disposeLock.TryEnterWriteLock(TimeSpan.FromSeconds(10))) + //{ + // _logger.Warn("Writer lock upgrade timed out"); + // _disposeLock.EnterWriteLock(); + //} } /// @@ -2384,7 +2456,7 @@ private void DowngradeBufferListLockFromWriterLock () /// private void DowngradeDisposeLockFromWriterLock () { - _disposeLock.ExitWriteLock(); + //_disposeLock.ExitWriteLock(); } #if DEBUG diff --git a/src/LogExpert.Core/Config/Preferences.cs b/src/LogExpert.Core/Config/Preferences.cs index a570b6cc..a63e190c 100644 --- a/src/LogExpert.Core/Config/Preferences.cs +++ b/src/LogExpert.Core/Config/Preferences.cs @@ -160,7 +160,7 @@ public List HilightGroupList public string FontName { get; set; } = "Courier New"; - public int FontSize { get; set; } = 9; + public float FontSize { get => field; set => field = MathF.Round(value, 1); } = 9.0f; public List HighlightMaskList { get; set; } = []; } \ No newline at end of file diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index 03c79b66..c3342509 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -1403,13 +1403,10 @@ private void NotifyWindowsForChangedPrefs (SettingsFlags flags) var fontName = ConfigManager.Settings.Preferences.FontName; var fontSize = ConfigManager.Settings.Preferences.FontSize; - //lock (_logWindowList) - //{ foreach (var logWindow in _tabController.GetAllWindows()) { logWindow.PreferencesChanged(fontName, fontSize, setLastColumnWidth, lastColumnWidth, false, flags); } - //} _toolWindowCoordinator.ApplyPreferences(fontName, fontSize, setLastColumnWidth, lastColumnWidth, flags); diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.cs index 132f4732..7aed69d7 100644 --- a/src/LogExpert.UI/Dialogs/SettingsDialog.cs +++ b/src/LogExpert.UI/Dialogs/SettingsDialog.cs @@ -124,9 +124,9 @@ private void FillDialog () Preferences.FontName = DEFAULT_FONT_NAME; } - if (Preferences.FontSize <= 0) + if (Math.Abs(Preferences.FontSize) <= 0.1) { - Preferences.FontSize = 9; + Preferences.FontSize = 9.0f; } FillPortableMode(); @@ -721,7 +721,7 @@ private void OnBtnChangeFontClick (object sender, EventArgs e) if (dlg.ShowDialog() == DialogResult.OK) { - Preferences.FontSize = (int)dlg.Font.Size; + Preferences.FontSize = dlg.Font.Size; Preferences.FontName = dlg.Font.FontFamily.Name; } From 286119f5e09aa485e63908652df7e45079ba7465 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Wed, 15 Apr 2026 15:14:21 +0200 Subject: [PATCH 09/24] more optimisations --- .../Classes/Log/LogfileReader.cs | 163 +----------------- .../Extensions/EnumerableTests.cs | 16 +- 2 files changed, 8 insertions(+), 171 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index 27fd2720..aab8d1e5 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -490,18 +490,6 @@ private void ReleaseBufferListWriterLock () _bufferListLock.ExitWriteLock(); } - /// - /// Releases an upgradeable read lock held by the current thread on the associated lock object. - /// - /// - /// Call this method to exit an upgradeable read lock previously acquired on the underlying lock. Failing to release - /// the lock may result in deadlocks or resource contention. - /// - private void ReleaseDisposeUpgradeableReadLock () - { - //_disposeLock.ExitUpgradeableReadLock(); - } - /// /// Acquires the writer lock for the buffer list, blocking the calling thread until the lock is obtained. /// @@ -860,11 +848,9 @@ public void LogBufferInfoForLine (int lineNum) } _logger.Info(CultureInfo.InvariantCulture, "-----------------------------------"); - //AcquireDisposeReaderLock(); _logger.Info(CultureInfo.InvariantCulture, "Buffer info for line {0}", lineNum); DumpBufferInfos(buffer); _logger.Info(CultureInfo.InvariantCulture, "File pos for current line: {0}", buffer.GetFilePosForLineOfBlock(lineNum - buffer.StartLine)); - //ReleaseDisposeReaderLock(); _logger.Info(CultureInfo.InvariantCulture, "-----------------------------------"); ReleaseBufferListReaderLock(); } @@ -889,10 +875,10 @@ public void LogBufferDiagnostic () long disposeSum = 0; long maxDispose = 0; long minDispose = int.MaxValue; + for (var i = 0; i < _bufferList.Count; ++i) { var buffer = _bufferList[i]; - //AcquireDisposeReaderLock(); if (buffer.StartLine != lineNum) { _logger.Error("Start line of buffer is: {0}, expected: {1}", buffer.StartLine, lineNum); @@ -904,7 +890,6 @@ public void LogBufferDiagnostic () disposeSum += buffer.DisposeCount; maxDispose = Math.Max(maxDispose, buffer.DisposeCount); minDispose = Math.Min(minDispose, buffer.DisposeCount); - //ReleaseDisposeReaderLock(); } ReleaseBufferListReaderLock(); @@ -964,8 +949,6 @@ private ValueTask GetLogLineMemoryInternal (int lineNum) if (logBuffer.IsDisposed) { - UpgradeDisposeLockToWriterLock(); - lock (logBuffer.FileInfo) { ReReadBuffer(logBuffer); @@ -986,28 +969,6 @@ private ValueTask GetLogLineMemoryInternal (int lineNum) ReleaseBufferListReaderLock(); } - - //// disposeLock prevents that the garbage collector is disposing just in the moment we use the buffer - //AcquireDisposeLockUpgradableReadLock(); - //if (logBuffer.IsDisposed) - //{ - // UpgradeDisposeLockToWriterLock(); - - // lock (logBuffer.FileInfo) - // { - // ReReadBuffer(logBuffer); - // } - - // DowngradeDisposeLockFromWriterLock(); - //} - - //var line = logBuffer.GetLineMemoryOfBlock(lineNum - logBuffer.StartLine); - //ReleaseDisposeUpgradeableReadLock(); - //ReleaseBufferListReaderLock(); - - //return line.HasValue - // ? new ValueTask(line.Value) - // : default; } /// @@ -1273,16 +1234,6 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start logBuffer.ReleaseContentLock(); } } - - //AcquireDisposeLockUpgradableReadLock(); - //if (logBuffer.IsDisposed) - //{ - // UpgradeDisposeLockToWriterLock(); - // ReReadBuffer(logBuffer); - // DowngradeDisposeLockFromWriterLock(); - //} - - //ReleaseDisposeUpgradeableReadLock(); } } finally @@ -1479,7 +1430,6 @@ private void GarbageCollectLruCache () var entries = _lruCacheDict.ToArray(); Array.Sort(entries, static (a, b) => a.Value.LastUseTimeStamp.CompareTo(b.Value.LastUseTimeStamp)); - //AcquireDisposeWriterLock(); for (var i = 0; i < diff && i < entries.Length; ++i) { var kvp = entries[i]; @@ -1500,8 +1450,6 @@ private void GarbageCollectLruCache () } } } - - //ReleaseDisposeWriterLock(); } #if DEBUG @@ -1550,7 +1498,6 @@ private async Task GarbageCollectorThreadProc () private void ClearLru () { _logger.Info(CultureInfo.InvariantCulture, "Clearing LRU cache."); - AcquireDisposeWriterLock(); foreach (var entry in _lruCacheDict.Values) { var lockTaken = false; @@ -1569,7 +1516,6 @@ private void ClearLru () } _lruCacheDict.Clear(); - ReleaseDisposeWriterLock(); _logger.Info(CultureInfo.InvariantCulture, "Clearing done."); } @@ -2309,83 +2255,6 @@ private void AcquireBufferListUpgradeableReadLock () } } - /// - /// Acquires an upgradeable read lock on the dispose lock, waiting up to 10 seconds before blocking indefinitely if - /// the lock is not immediately available. - /// - /// - /// This method ensures that the current thread holds an upgradeable read lock on the dispose lock, allowing for - /// potential escalation to a write lock if needed. If the lock cannot be acquired within 10 seconds, a warning is - /// logged and the method blocks until the lock becomes available. - /// - private void AcquireDisposeLockUpgradableReadLock () - { - //if (!_disposeLock.TryEnterUpgradeableReadLock(TimeSpan.FromSeconds(10))) - //{ - // _logger.Warn("Upgradeable read lock timed out"); - // _disposeLock.EnterUpgradeableReadLock(); - //} - } - - /// - /// Acquires a read lock on the dispose lock, blocking the calling thread until the lock is obtained. - /// - /// - /// If the read lock cannot be acquired within 10 seconds, a warning is logged and the method will block until the - /// lock becomes available. This method is intended to ensure thread-safe access during disposal operations. - /// - private void AcquireDisposeReaderLock () - { - //if (!_disposeLock.TryEnterReadLock(TimeSpan.FromSeconds(10))) - //{ - // _logger.Warn("Dispose reader lock timed out"); - // _disposeLock.EnterReadLock(); - //} - } - - /// - /// Releases the writer lock held for disposing resources. - /// - /// - /// Call this method to exit the write lock acquired for resource disposal. This should be used in conjunction with - /// the corresponding method that acquires the writer lock to ensure proper synchronization during disposal - /// operations. - /// - private void ReleaseDisposeWriterLock () - { - //_disposeLock.ExitWriteLock(); - } - - /// - /// Releases a reader lock held for disposing resources, allowing other threads to acquire the lock as needed. - /// - /// - /// Call this method to release the read lock previously acquired for resource disposal operations. Failing to - /// release the lock may result in deadlocks or prevent other threads from accessing the protected resource. - /// - private void ReleaseDisposeReaderLock () - { - //_disposeLock.ExitReadLock(); - } - - /// - /// Acquires the writer lock used to synchronize disposal operations, blocking the calling thread until the lock is - /// obtained. - /// - /// - /// If the writer lock cannot be acquired within 10 seconds, a warning is logged and the method waits indefinitely - /// until the lock becomes available. Callers should ensure that holding the lock for extended periods does not - /// cause deadlocks or performance issues. - /// - private void AcquireDisposeWriterLock () - { - //if (!_disposeLock.TryEnterWriteLock(TimeSpan.FromSeconds(10))) - //{ - // _logger.Warn("Dispose writer lock timed out"); - // _disposeLock.EnterWriteLock(); - //} - } - /// /// Releases the upgradeable read lock on the buffer list, allowing other threads to acquire exclusive or read /// access. @@ -2417,23 +2286,6 @@ private void UpgradeBufferlistLockToWriterLock () } } - /// - /// Upgrades the current dispose lock to a writer lock, blocking if necessary until the upgrade is successful. - /// - /// - /// This method attempts to upgrade the dispose lock to a writer lock with a timeout. If the upgrade cannot be - /// completed within the timeout period, it logs a warning and blocks until the writer lock is acquired. Call this - /// method when exclusive access is required for disposal or resource modification. - /// - private void UpgradeDisposeLockToWriterLock () - { - //if (!_disposeLock.TryEnterWriteLock(TimeSpan.FromSeconds(10))) - //{ - // _logger.Warn("Writer lock upgrade timed out"); - // _disposeLock.EnterWriteLock(); - //} - } - /// /// Downgrades the buffer list lock from write mode to allow other threads to acquire read access. /// @@ -2446,19 +2298,6 @@ private void DowngradeBufferListLockFromWriterLock () _bufferListLock.ExitWriteLock(); } - /// - /// Releases the writer lock on the dispose lock, downgrading from write access. - /// - /// - /// Call this method to release write access to the dispose lock when a downgrade is required, such as when - /// transitioning from exclusive to shared access. This method should only be called when the current thread holds - /// the writer lock. - /// - private void DowngradeDisposeLockFromWriterLock () - { - //_disposeLock.ExitWriteLock(); - } - #if DEBUG /// /// Outputs detailed information about the specified log buffer to the trace logger for debugging purposes. diff --git a/src/LogExpert.Tests/Extensions/EnumerableTests.cs b/src/LogExpert.Tests/Extensions/EnumerableTests.cs index cbc4fd7d..749f3f92 100644 --- a/src/LogExpert.Tests/Extensions/EnumerableTests.cs +++ b/src/LogExpert.Tests/Extensions/EnumerableTests.cs @@ -1,16 +1,14 @@ -using LogExpert.Core.Extensions; +using LogExpert.Core.Extensions; using NUnit.Framework; -using System.Collections.Generic; - namespace LogExpert.Tests.Extensions; [TestFixture] public class EnumerableTests { [Test] - public void Extensions_IsEmpty_NullArray() + public void Extensions_IsEmpty_NullArray () { object[] arrayObject = null; @@ -18,7 +16,7 @@ public void Extensions_IsEmpty_NullArray() } [Test] - public void Extensions_IsEmpty_EmptyArray() + public void Extensions_IsEmpty_EmptyArray () { object[] arrayObject = []; @@ -26,7 +24,7 @@ public void Extensions_IsEmpty_EmptyArray() } [Test] - public void Extensions_IsEmpty_FilledArray() + public void Extensions_IsEmpty_FilledArray () { object[] arrayObject = [new()]; @@ -34,7 +32,7 @@ public void Extensions_IsEmpty_FilledArray() } [Test] - public void Extensions_IsEmpty_NullIEnumerable() + public void Extensions_IsEmpty_NullIEnumerable () { IEnumerable arrayObject = null; @@ -42,7 +40,7 @@ public void Extensions_IsEmpty_NullIEnumerable() } [Test] - public void Extensions_IsEmpty_EmptyIEnumerable() + public void Extensions_IsEmpty_EmptyIEnumerable () { IEnumerable arrayObject = []; @@ -50,7 +48,7 @@ public void Extensions_IsEmpty_EmptyIEnumerable() } [Test] - public void Extensions_IsEmpty_FilledIEnumerable() + public void Extensions_IsEmpty_FilledIEnumerable () { IEnumerable arrayObject = new List([new object()]); From c5b7c03bba7f89f5e53460db9d7e6d645fb85236 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Wed, 15 Apr 2026 16:03:23 +0200 Subject: [PATCH 10/24] caching --- .../Classes/Log/LogfileReader.cs | 83 +++++++++++++++++++ .../Controls/LogWindow/ColumnCache.cs | 44 +++++++++- .../Controls/LogWindow/LogWindow.cs | 36 ++++---- 3 files changed, 144 insertions(+), 19 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index aab8d1e5..8c42e292 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -915,6 +915,89 @@ private ILogFileInfo AddFile (string fileName) return info; } + /// + /// Retrieves a contiguous range of log lines starting at the specified line number. + /// Acquires locks once for the entire batch, amortising synchronisation overhead. + /// + /// The zero-based line number of the first line to retrieve. + /// The number of lines to retrieve. May be clamped to available lines. + /// + /// An array of instances. The array length may be less than + /// if the end of file is reached. Entries may be null if a buffer + /// is unavailable. + /// + public ILogLineMemory[] GetLogLineMemories (int startLine, int count) + { + if (_isDeleted || count <= 0) + { + return []; + } + + var result = new ILogLineMemory[count]; + var filled = 0; + + AcquireBufferListReaderLock(); + try + { + var lineNum = startLine; + while (filled < count) + { + var logBuffer = GetBufferForLineCore(lineNum); + if (logBuffer == null) + { + break; + } + + // Protect against concurrent disposal + var lockTaken = false; + try + { + logBuffer.AcquireContentLock(ref lockTaken); + + if (logBuffer.IsDisposed) + { + lock (logBuffer.FileInfo) + { + ReReadBuffer(logBuffer); + } + } + + // Copy lines from this buffer + var bufferOffset = lineNum - logBuffer.StartLine; + var availableInBuffer = logBuffer.LineCount - bufferOffset; + var toCopy = Math.Min(count - filled, availableInBuffer); + + for (var i = 0; i < toCopy; i++) + { + result[filled + i] = logBuffer.GetLineMemoryOfBlock(bufferOffset + i); + } + + filled += toCopy; + lineNum += toCopy; + } + finally + { + if (lockTaken) + { + logBuffer.ReleaseContentLock(); + } + } + } + } + finally + { + ReleaseBufferListReaderLock(); + } + + // Trim if we got fewer lines than requested + if (filled < count) + { + Array.Resize(ref result, filled); + } + + return result; + } + /// /// Retrieves the log line at the specified line number, or returns null if the file has been deleted or the line /// cannot be found. diff --git a/src/LogExpert.UI/Controls/LogWindow/ColumnCache.cs b/src/LogExpert.UI/Controls/LogWindow/ColumnCache.cs index c79d0074..54887a63 100644 --- a/src/LogExpert.UI/Controls/LogWindow/ColumnCache.cs +++ b/src/LogExpert.UI/Controls/LogWindow/ColumnCache.cs @@ -13,17 +13,59 @@ internal class ColumnCache private ILogLineMemoryColumnizer _lastColumnizer; private int _lastLineNumber = -1; + // Prefetch state + private ILogLineMemory[] _prefetchedLines; + private int _prefetchStartLine = -1; + private int _prefetchCount; + #endregion #region Internals + /// + /// Prefetch a range of lines in a single batch call. + /// Call this before a paint cycle with the visible row range. + /// + internal void Prefetch (LogfileReader logFileReader, int startLine, int count) + { + if (startLine == _prefetchStartLine && count == _prefetchCount) + { + return; // already prefetched this exact range + } + + _prefetchedLines = logFileReader.GetLogLineMemories(startLine, count); + _prefetchStartLine = startLine; + _prefetchCount = _prefetchedLines.Length; + } + + /// + /// Invalidates the prefetch cache. Call on scroll, data change, or columnizer change. + /// + internal void InvalidatePrefetch () + { + _prefetchedLines = null; + _prefetchStartLine = -1; + _prefetchCount = 0; + _lastLineNumber = -1; + } + internal IColumnizedLogLineMemory GetColumnsForLine (LogfileReader logFileReader, int lineNumber, ILogLineMemoryColumnizer columnizer, ColumnizerCallback columnizerCallback) { if (_lastColumnizer != columnizer || (_lastLineNumber != lineNumber && _cachedColumns != null) || columnizerCallback.LineNum != lineNumber) { _lastColumnizer = columnizer; _lastLineNumber = lineNumber; - var line = logFileReader.GetLogLineMemoryWithWait(lineNumber).Result; + + ILogLineMemory line = null; + + if (_prefetchedLines != null + && lineNumber >= _prefetchStartLine + && lineNumber < _prefetchStartLine + _prefetchCount) + { + line = _prefetchedLines[lineNumber - _prefetchStartLine]; + } + + line ??= logFileReader.GetLogLineMemoryWithWait(lineNumber).Result; if (line != null) { diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs index 73c388be..ee10dde3 100644 --- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs +++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs @@ -939,17 +939,12 @@ private void OnLogFileReaderLoadFile (object sender, LoadFileEventArgs e) { if (e.NewFile) { - //_logger.Info("OnLogFileReaderLoadFile: New File created."); - // File was new created (e.g. rollover) _isDeadFile = false; UnRegisterLogFileReaderEvents(); dataGridView.CurrentCellChanged -= OnDataGridViewCurrentCellChanged; MethodInvoker invoker = ReloadNewFile; _ = BeginInvoke(invoker); - //Thread loadThread = new Thread(new ThreadStart(ReloadNewFile)); - //loadThread.Start(); - //_logger.Debug("OnLogFileReaderLoadFile: Reloading invoked."); } else if (_isLoading) { @@ -959,18 +954,6 @@ private void OnLogFileReaderLoadFile (object sender, LoadFileEventArgs e) private void OnFileSizeChanged (object sender, LogEventArgs e) { - //OnFileSizeChanged(e); // now done in UpdateGrid() - //_logger.Info($"Got FileSizeChanged event. prevLines:{e.PrevLineCount}, curr lines: {e.LineCount}"); - - // - now done in the thread that works on the event args list - //if (e.IsRollover) - //{ - // ShiftBookmarks(e.RolloverOffset); - // ShiftFilterPipes(e.RolloverOffset); - //} - - //UpdateGridCallback callback = new UpdateGridCallback(UpdateGrid); - //this.BeginInvoke(callback, new object[] { e }); lock (_logEventArgsList) { _logEventArgsList.Add(e); @@ -981,8 +964,9 @@ private void OnFileSizeChanged (object sender, LogEventArgs e) [SupportedOSPlatform("windows")] private void OnDataGridViewCellValueNeeded (object sender, DataGridViewCellValueEventArgs e) { - var startCount = CurrentColumnizer?.GetColumnCount() ?? 0; + PrefetchVisibleLines(); + var startCount = CurrentColumnizer?.GetColumnCount() ?? 0; e.Value = GetCellValue(e.RowIndex, e.ColumnIndex); // The new column could be find dynamically. @@ -998,6 +982,22 @@ private void OnDataGridViewCellValueNeeded (object sender, DataGridViewCellValue } } + private void PrefetchVisibleLines () + { + if (_logFileReader == null) + { + return; + } + + var firstVisible = dataGridView.FirstDisplayedScrollingRowIndex; + var visibleCount = dataGridView.DisplayedRowCount(includePartialRow: true); + + if (firstVisible >= 0 && visibleCount > 0) + { + _columnCache.Prefetch(_logFileReader, firstVisible, visibleCount); + } + } + [SupportedOSPlatform("windows")] private void OnDataGridViewCellValuePushed (object sender, DataGridViewCellValueEventArgs e) { From 94061f1731b385ae71b7ca4401926761f1f324a3 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Wed, 15 Apr 2026 16:46:57 +0200 Subject: [PATCH 11/24] MemoryMappedFileReader added (for future and non multifiles) --- .../Classes/Log/LogfileReader.cs | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index 8c42e292..6ab6cf57 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -34,6 +34,8 @@ public partial class LogfileReader : IAutoLogLineMemoryColumnizerCallback, IDisp private readonly Lock _logBufferLock = new(); private readonly ReaderWriterLockSlim _bufferListLock = new(LockRecursionPolicy.SupportsRecursion); + private MemoryMappedFileReader _mmfReader; + private const int PROGRESS_UPDATE_INTERVAL_MS = 100; private const int WAIT_TIME = 1000; @@ -47,7 +49,6 @@ public partial class LogfileReader : IAutoLogLineMemoryColumnizerCallback, IDisp private Task _monitorTask; private bool _isDeleted; - private IList _logFileInfoList = []; private ConcurrentDictionary _lruCacheDict; private bool _shouldStop; @@ -126,6 +127,18 @@ private LogfileReader (string[] fileNames, EncodingOptions encodingOptions, bool _watchedILogFileInfo = fileInfo; + if (!IsMultiFile && _watchedILogFileInfo.Uri?.Scheme is null or "file") + { + try + { + _mmfReader = new MemoryMappedFileReader(_watchedILogFileInfo.FullName, EncodingOptions.Encoding ?? Encoding.Default); + } + catch (IOException) + { + _mmfReader = null; // fallback to buffer path + } + } + StartGCThread(); } @@ -1016,6 +1029,12 @@ private ValueTask GetLogLineMemoryInternal (int lineNum) return default; } + if (_mmfReader != null && lineNum < _mmfReader.LineCount) + { + var line = _mmfReader.GetLine(lineNum); + return new ValueTask(line); + } + AcquireBufferListReaderLock(); var logBuffer = GetBufferForLineCore(lineNum); if (logBuffer == null) @@ -1902,12 +1921,12 @@ private LogBuffer GetBufferForLineCore (int lineNum) } } - return null; - #if DEBUG long endTime = Environment.TickCount; _logger.Debug($"getBufferForLine({lineNum}) duration: {endTime - startTime} ms."); #endif + + return null; } private static int HighestPowerOfTwo (int n) => 1 << (31 - int.LeadingZeroCount(n)); @@ -2086,6 +2105,8 @@ private void FileChanged () _logger.Info(CultureInfo.InvariantCulture, "file size changed. new size={0}, file: {1}", newSize, _fileName); FireChangeEvent(); } + + _mmfReader?.ExtendIndex(); } /// @@ -2446,6 +2467,7 @@ protected virtual void Dispose (bool disposing) DeleteAllContent(); _cts.Dispose(); _lastBufferIndex.Dispose(); + _mmfReader?.Dispose(); } _disposed = true; From 7c6080fe6769f01bebb6d07e2954988d9b0f6d89 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Thu, 16 Apr 2026 08:41:18 +0200 Subject: [PATCH 12/24] sortedlist for bufferlist optimisation --- .../Classes/Log/LogfileReader.cs | 72 ++++++++++--------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index 6ab6cf57..db82f223 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -1,6 +1,5 @@ using System.Collections.Concurrent; using System.Globalization; -using System.Runtime.InteropServices; using System.Text; using ColumnizerLib; @@ -34,12 +33,12 @@ public partial class LogfileReader : IAutoLogLineMemoryColumnizerCallback, IDisp private readonly Lock _logBufferLock = new(); private readonly ReaderWriterLockSlim _bufferListLock = new(LockRecursionPolicy.SupportsRecursion); - private MemoryMappedFileReader _mmfReader; + private readonly MemoryMappedFileReader _mmfReader; private const int PROGRESS_UPDATE_INTERVAL_MS = 100; private const int WAIT_TIME = 1000; - private List _bufferList; + private SortedList _bufferList; private bool _contentDeleted; private long _lastProgressUpdate; @@ -173,7 +172,7 @@ public int LineCount field = 0; if (_bufferListLock.IsReadLockHeld || _bufferListLock.IsWriteLockHeld) { - foreach (var buffer in _bufferList) + foreach (var buffer in _bufferList.Values) { field += buffer.LineCount; } @@ -181,7 +180,7 @@ public int LineCount else { AcquireBufferListReaderLock(); - foreach (var buffer in _bufferList) + foreach (var buffer in _bufferList.Values) { field += buffer.LineCount; } @@ -347,7 +346,9 @@ public int ShiftBuffers () var logFileInfo = enumerator.Current; var fileName = logFileInfo.FullName; _logger.Debug(CultureInfo.InvariantCulture, "Testing file {0}", fileName); + var node = fileNameList.Find(fileName); + if (node == null) { _logger.Warn(CultureInfo.InvariantCulture, "File {0} not found", fileName); @@ -414,15 +415,15 @@ public int ShiftBuffers () } _logger.Info(CultureInfo.InvariantCulture, "Adjusting StartLine values in {0} buffers by offset {1}", _bufferList.Count, offset); - foreach (var buffer in _bufferList) + foreach (var buffer in _bufferList.Values) { SetNewStartLineForBuffer(buffer, buffer.StartLine - offset); } #if DEBUG - if (_bufferList.Count > 0) + if (_bufferList.Values.Count > 0) { - _logger.Debug(CultureInfo.InvariantCulture, "First buffer now has StartLine {0}", _bufferList[0].StartLine); + _logger.Debug(CultureInfo.InvariantCulture, "First buffer now has StartLine {0}", _bufferList.Values[0].StartLine); } #endif } @@ -634,11 +635,11 @@ public int GetNextMultiFileLine (int lineNum) var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); if (logBuffer != null && index != -1) { - for (var i = index; i < _bufferList.Count; ++i) + for (var i = index; i < _bufferList.Values.Count; ++i) { - if (_bufferList[i].FileInfo != logBuffer.FileInfo) + if (_bufferList.Values[i].FileInfo != logBuffer.FileInfo) { - result = _bufferList[i].StartLine; + result = _bufferList.Values[i].StartLine; break; } } @@ -670,9 +671,9 @@ public int GetPrevMultiFileLine (int lineNum) { for (var i = index; i >= 0; --i) { - if (_bufferList[i].FileInfo != logBuffer.FileInfo) + if (_bufferList.Values[i].FileInfo != logBuffer.FileInfo) { - result = _bufferList[i].StartLine + _bufferList[i].LineCount; + result = _bufferList.Values[i].StartLine + _bufferList.Values[i].LineCount; break; } } @@ -780,7 +781,7 @@ public void DeleteAllContent () ClearBufferState(); //AcquireDisposeWriterLock(); - foreach (var logBuffer in _bufferList) + foreach (var logBuffer in _bufferList.Values) { if (!logBuffer.IsDisposed) { @@ -832,7 +833,7 @@ public IList GetLogFileInfoList () /// public IList GetBufferList () { - return _bufferList; + return _bufferList.Values; } #endregion @@ -889,9 +890,9 @@ public void LogBufferDiagnostic () long maxDispose = 0; long minDispose = int.MaxValue; - for (var i = 0; i < _bufferList.Count; ++i) + for (var i = 0; i < _bufferList.Values.Count; ++i) { - var buffer = _bufferList[i]; + var buffer = _bufferList.Values[i]; if (buffer.StartLine != lineNum) { _logger.Error("Start line of buffer is: {0}, expected: {1}", buffer.StartLine, lineNum); @@ -1155,7 +1156,7 @@ private ILogFileInfo GetLogFileInfo (string fileNameOrUri) //TODO: I changed to private void ReplaceBufferInfos (ILogFileInfo oldLogFileInfo, ILogFileInfo newLogFileInfo) { _logger.Debug(CultureInfo.InvariantCulture, "ReplaceBufferInfos() " + oldLogFileInfo.FullName + " -> " + newLogFileInfo.FullName); - foreach (var buffer in _bufferList) + foreach (var buffer in _bufferList.Values) { if (buffer.FileInfo == oldLogFileInfo) { @@ -1189,7 +1190,7 @@ private LogBuffer DeleteBuffersForInfo (ILogFileInfo iLogFileInfo, bool matchNam if (matchNamesOnly) { - foreach (var buffer in _bufferList) + foreach (var buffer in _bufferList.Values) { if (buffer.FileInfo.FullName.Equals(iLogFileInfo.FullName, StringComparison.Ordinal)) { @@ -1200,7 +1201,7 @@ private LogBuffer DeleteBuffersForInfo (ILogFileInfo iLogFileInfo, bool matchNam } else { - foreach (var buffer in _bufferList) + foreach (var buffer in _bufferList.Values) { if (buffer.FileInfo == iLogFileInfo) { @@ -1240,7 +1241,7 @@ private void RemoveFromBufferList (LogBuffer buffer) { Util.AssertTrue(_bufferListLock.IsWriteLockHeld, "No _writer lock for buffer list"); _ = _lruCacheDict.TryRemove(buffer.StartLine, out _); - _ = _bufferList.Remove(buffer); + _ = _bufferList.Remove(buffer.StartLine); } /// @@ -1297,7 +1298,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start } else { - logBuffer = _bufferList[^1]; + logBuffer = _bufferList.Values[_bufferList.Count - 1]; if (!logBuffer.FileInfo.FullName.Equals(logFileInfo.FullName, StringComparison.Ordinal)) { @@ -1461,7 +1462,7 @@ private void AddBufferToList (LogBuffer logBuffer) #if DEBUG _logger.Debug(CultureInfo.InvariantCulture, "AddBufferToList(): {0}/{1}/{2}", logBuffer.StartLine, logBuffer.LineCount, logBuffer.FileInfo.FullName); #endif - _bufferList.Add(logBuffer); + _bufferList[logBuffer.StartLine] = logBuffer; UpdateLruCache(logBuffer); } @@ -1492,15 +1493,16 @@ private void UpdateLruCache (LogBuffer logBuffer) /// private void SetNewStartLineForBuffer (LogBuffer logBuffer, int newLineNum) { - if (_lruCacheDict.TryRemove(logBuffer.StartLine, out var cacheEntry)) + var hadCache = _lruCacheDict.TryRemove(logBuffer.StartLine, out var cacheEntry); + + _ = _bufferList.Remove(logBuffer.StartLine); + logBuffer.StartLine = newLineNum; + _bufferList[newLineNum] = logBuffer; + + if (hadCache) { - logBuffer.StartLine = newLineNum; _ = _lruCacheDict.TryAdd(logBuffer.StartLine, cacheEntry); } - else - { - logBuffer.StartLine = newLineNum; - } } /// @@ -1716,8 +1718,8 @@ private void ReReadBuffer (LogBuffer logBuffer) AcquireBufferListReaderLock(); try { - var arr = CollectionsMarshal.AsSpan(_bufferList); - var count = arr.Length; + var arr = _bufferList.Values; + var count = arr.Count; if (count == 0) { @@ -1840,8 +1842,8 @@ private LogBuffer GetBufferForLineCore (int lineNum) long startTime = Environment.TickCount; #endif - var arr = CollectionsMarshal.AsSpan(_bufferList); - var count = arr.Length; + var arr = _bufferList.Values; + var count = arr.Count; if (count == 0) { @@ -1971,12 +1973,12 @@ private LogBuffer GetFirstBufferForFileByLogBuffer (LogBuffer logBuffer, int ind while (true) { index--; - if (index < 0 || _bufferList[index].FileInfo != info) + if (index < 0 || _bufferList.Values[index].FileInfo != info) { break; } - resultBuffer = _bufferList[index]; + resultBuffer = _bufferList.Values[index]; } ReleaseBufferListReaderLock(); From 37d7a4939943b6db47ac70d5a69dd6cc0fbbebfd Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Thu, 16 Apr 2026 12:47:27 +0200 Subject: [PATCH 13/24] acccess optimisations --- .../Classes/Log/LogfileReader.cs | 271 +++++++----------- 1 file changed, 99 insertions(+), 172 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index db82f223..774ffb48 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -554,7 +554,7 @@ public async Task GetLogLineMemoryWithWait (int lineNum) AcquireBufferListReaderLock(); try { - var logBuffer = GetBufferForLineCore(lineNum); + var (logBuffer, _) = GetBufferForLineWithIndex(lineNum); canFastPath = logBuffer is { IsDisposed: false }; } finally @@ -605,8 +605,7 @@ public async Task GetLogLineMemoryWithWait (int lineNum) public string GetLogFileNameForLine (int lineNum) { var logBuffer = GetBufferForLine(lineNum); - var fileName = logBuffer?.FileInfo.FullName; - return fileName; + return logBuffer?.FileInfo.FullName; } /// @@ -616,11 +615,8 @@ public string GetLogFileNameForLine (int lineNum) /// public ILogFileInfo GetLogFileInfoForLine (int lineNum) { - AcquireBufferListReaderLock(); - var logBuffer = GetBufferForLineCore(lineNum); - var info = logBuffer?.FileInfo; - ReleaseBufferListReaderLock(); - return info; + var logBuffer = GetBufferForLine(lineNum); + return logBuffer?.FileInfo; } /// @@ -632,20 +628,27 @@ public int GetNextMultiFileLine (int lineNum) { var result = -1; AcquireBufferListReaderLock(); - var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); - if (logBuffer != null && index != -1) + + try { - for (var i = index; i < _bufferList.Values.Count; ++i) + var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); + if (logBuffer != null && index != -1) { - if (_bufferList.Values[i].FileInfo != logBuffer.FileInfo) + for (var i = index; i < _bufferList.Values.Count; ++i) { - result = _bufferList.Values[i].StartLine; - break; + if (_bufferList.Values[i].FileInfo != logBuffer.FileInfo) + { + result = _bufferList.Values[i].StartLine; + break; + } } } } + finally + { + ReleaseBufferListReaderLock(); + } - ReleaseBufferListReaderLock(); return result; } @@ -666,20 +669,27 @@ public int GetPrevMultiFileLine (int lineNum) { var result = -1; AcquireBufferListReaderLock(); - var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); - if (logBuffer != null && index != -1) + + try { - for (var i = index; i >= 0; --i) + var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); + if (logBuffer != null && index != -1) { - if (_bufferList.Values[i].FileInfo != logBuffer.FileInfo) + for (var i = index; i >= 0; --i) { - result = _bufferList.Values[i].StartLine + _bufferList.Values[i].LineCount; - break; + if (_bufferList.Values[i].FileInfo != logBuffer.FileInfo) + { + result = _bufferList.Values[i].StartLine + _bufferList.Values[i].LineCount; + break; + } } } } + finally + { + ReleaseBufferListReaderLock(); + } - ReleaseBufferListReaderLock(); return result; } @@ -694,19 +704,25 @@ public int GetPrevMultiFileLine (int lineNum) /// public int GetRealLineNumForVirtualLineNum (int lineNum) { - AcquireBufferListReaderLock(); - var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); var result = -1; - if (logBuffer != null) + AcquireBufferListReaderLock(); + try { - logBuffer = GetFirstBufferForFileByLogBuffer(logBuffer, index); + var (logBuffer, index) = GetBufferForLineWithIndex(lineNum); if (logBuffer != null) { - result = lineNum - logBuffer.StartLine; + logBuffer = GetFirstBufferForFileByLogBuffer(logBuffer, index); + if (logBuffer != null) + { + result = lineNum - logBuffer.StartLine; + } } } + finally + { + ReleaseBufferListReaderLock(); + } - ReleaseBufferListReaderLock(); return result; } @@ -853,20 +869,26 @@ public IList GetBufferList () public void LogBufferInfoForLine (int lineNum) { AcquireBufferListReaderLock(); - var buffer = GetBufferForLineCore(lineNum); - if (buffer == null) + + try + { + var (buffer, _) = GetBufferForLineWithIndex(lineNum); + if (buffer == null) + { + _logger.Error("Cannot find buffer for line {0}, file: {1}{2}", lineNum, _fileName, IsMultiFile ? " (MultiFile)" : ""); + return; + } + + _logger.Info(CultureInfo.InvariantCulture, "-----------------------------------"); + _logger.Info(CultureInfo.InvariantCulture, "Buffer info for line {0}", lineNum); + DumpBufferInfos(buffer); + _logger.Info(CultureInfo.InvariantCulture, "File pos for current line: {0}", buffer.GetFilePosForLineOfBlock(lineNum - buffer.StartLine)); + _logger.Info(CultureInfo.InvariantCulture, "-----------------------------------"); + } + finally { ReleaseBufferListReaderLock(); - _logger.Error("Cannot find buffer for line {0}, file: {1}{2}", lineNum, _fileName, IsMultiFile ? " (MultiFile)" : ""); - return; } - - _logger.Info(CultureInfo.InvariantCulture, "-----------------------------------"); - _logger.Info(CultureInfo.InvariantCulture, "Buffer info for line {0}", lineNum); - DumpBufferInfos(buffer); - _logger.Info(CultureInfo.InvariantCulture, "File pos for current line: {0}", buffer.GetFilePosForLineOfBlock(lineNum - buffer.StartLine)); - _logger.Info(CultureInfo.InvariantCulture, "-----------------------------------"); - ReleaseBufferListReaderLock(); } /// @@ -930,15 +952,14 @@ private ILogFileInfo AddFile (string fileName) } /// - /// Retrieves a contiguous range of log lines starting at the specified line number. - /// Acquires locks once for the entire batch, amortising synchronisation overhead. + /// Retrieves a contiguous range of log lines starting at the specified line number. Acquires locks once for the + /// entire batch, amortising synchronisation overhead. /// /// The zero-based line number of the first line to retrieve. /// The number of lines to retrieve. May be clamped to available lines. /// - /// An array of instances. The array length may be less than - /// if the end of file is reached. Entries may be null if a buffer - /// is unavailable. + /// An array of instances. The array length may be less than + /// if the end of file is reached. Entries may be null if a buffer is unavailable. /// public ILogLineMemory[] GetLogLineMemories (int startLine, int count) { @@ -956,7 +977,7 @@ public ILogLineMemory[] GetLogLineMemories (int startLine, int count) var lineNum = startLine; while (filled < count) { - var logBuffer = GetBufferForLineCore(lineNum); + var (logBuffer, _) = GetBufferForLineWithIndex(lineNum); if (logBuffer == null) { break; @@ -1037,7 +1058,7 @@ private ValueTask GetLogLineMemoryInternal (int lineNum) } AcquireBufferListReaderLock(); - var logBuffer = GetBufferForLineCore(lineNum); + var (logBuffer, _) = GetBufferForLineWithIndex(lineNum); if (logBuffer == null) { ReleaseBufferListReaderLock(); @@ -1713,127 +1734,11 @@ private void ReReadBuffer (LogBuffer logBuffer) } } - private (LogBuffer? Buffer, int Index) GetBufferForLineWithIndex (int lineNum) - { - AcquireBufferListReaderLock(); - try - { - var arr = _bufferList.Values; - var count = arr.Count; - - if (count == 0) - { - return (null, -1); - } - - // Layer 0: Last buffer cache - var lastIdx = _lastBufferIndex.Value; - if (lastIdx >= 0 && lastIdx < count) - { - var buf = arr[lastIdx]; - if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) - { - return (buf, lastIdx); - } - - // Layer 1: Adjacent buffer prediction - if (lastIdx + 1 < count) - { - var next = arr[lastIdx + 1]; - if ((uint)(lineNum - next.StartLine) < (uint)next.LineCount) - { - _lastBufferIndex.Value = lastIdx + 1; - UpdateLruCache(next); - return (next, lastIdx + 1); - } - } - - if (lastIdx - 1 >= 0) - { - var prev = arr[lastIdx - 1]; - if ((uint)(lineNum - prev.StartLine) < (uint)prev.LineCount) - { - _lastBufferIndex.Value = lastIdx - 1; - UpdateLruCache(prev); - return (prev, lastIdx - 1); - } - } - } - - // Layer 2: Direct mapping guess - var guess = lineNum / _maxLinesPerBuffer; - if ((uint)guess < (uint)count) - { - var buf = arr[guess]; - if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) - { - _lastBufferIndex.Value = guess; - UpdateLruCache(buf); - return (buf, guess); - } - } - - // Layer 3: Branchless binary search with power-of-two strides - var step = HighestPowerOfTwo(count); - var idx = (arr[step - 1].StartLine <= lineNum) ? count - step : 0; - - for (step >>= 1; step > 0; step >>= 1) - { - var probe = idx + step; - if (probe < count && arr[probe - 1].StartLine <= lineNum) - { - idx = probe; - } - } - - if (idx < count) - { - var buf = arr[idx]; - if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) - { - _lastBufferIndex.Value = idx; - UpdateLruCache(buf); - return (buf, idx); - } - } - - return (null, -1); - } - finally - { - ReleaseBufferListReaderLock(); - } - } - - /// - /// Retrieves the log buffer that contains the specified line number. - /// - /// - /// The zero-based line number for which to retrieve the corresponding log buffer. Must be greater than or equal to - /// zero. - /// - /// - /// The instance that contains the specified line number, or if no - /// such buffer exists. - /// - private LogBuffer GetBufferForLine (int lineNum) - { - AcquireBufferListReaderLock(); - try - { - return GetBufferForLineCore(lineNum); - } - finally - { - ReleaseBufferListReaderLock(); - } - } - /// /// Core buffer lookup without acquiring _bufferListLock. The caller MUST already hold a read or write lock /// on _bufferListLock. /// - private LogBuffer GetBufferForLineCore (int lineNum) + private (LogBuffer? Buffer, int Index) GetBufferForLineWithIndex (int lineNum) { #if DEBUG Util.AssertTrue( @@ -1841,13 +1746,12 @@ private LogBuffer GetBufferForLineCore (int lineNum) "No lock held for buffer list in GetBufferForLineCore"); long startTime = Environment.TickCount; #endif - var arr = _bufferList.Values; var count = arr.Count; if (count == 0) { - return null; + return (null, -1); } // Layer 0: Last buffer cache — O(1) for sequential access @@ -1858,7 +1762,7 @@ private LogBuffer GetBufferForLineCore (int lineNum) if ((uint)(lineNum - buf.StartLine) < (uint)buf.LineCount) { //dont UpdateLRUCache, the cache has not changed in layer 0 - return buf; + return (buf, lastIdx); } // Layer 1: Adjacent buffer prediction — O(1) for buffer boundary crossings @@ -1869,7 +1773,7 @@ private LogBuffer GetBufferForLineCore (int lineNum) { _lastBufferIndex.Value = lastIdx + 1; UpdateLruCache(next); - return next; + return (next, lastIdx + 1); } } @@ -1880,7 +1784,7 @@ private LogBuffer GetBufferForLineCore (int lineNum) { _lastBufferIndex.Value = lastIdx - 1; UpdateLruCache(prev); - return prev; + return (prev, lastIdx - 1); } } } @@ -1894,7 +1798,7 @@ private LogBuffer GetBufferForLineCore (int lineNum) { _lastBufferIndex.Value = guess; UpdateLruCache(buf); - return buf; + return (buf, guess); } } @@ -1919,16 +1823,39 @@ private LogBuffer GetBufferForLineCore (int lineNum) { _lastBufferIndex.Value = idx; UpdateLruCache(buf); - return buf; + return (buf, idx); } } - #if DEBUG long endTime = Environment.TickCount; _logger.Debug($"getBufferForLine({lineNum}) duration: {endTime - startTime} ms."); #endif + return (null, -1); + } - return null; + /// + /// Retrieves the log buffer that contains the specified line number. + /// + /// + /// The zero-based line number for which to retrieve the corresponding log buffer. Must be greater than or equal to + /// zero. + /// + /// + /// The instance that contains the specified line number, or if no + /// such buffer exists. + /// + private LogBuffer GetBufferForLine (int lineNum) + { + AcquireBufferListReaderLock(); + try + { + var (buffer, _) = GetBufferForLineWithIndex(lineNum); + return buffer; + } + finally + { + ReleaseBufferListReaderLock(); + } } private static int HighestPowerOfTwo (int n) => 1 << (31 - int.LeadingZeroCount(n)); From 665015851ed1f18d9a29b9d55ed4fb2f3e253e74 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Thu, 16 Apr 2026 13:47:02 +0200 Subject: [PATCH 14/24] LogBufferPool --- src/LogExpert.Core/Classes/Log/LogBuffer.cs | 73 ++++++++++++++++--- .../Classes/Log/LogBufferPool.cs | 33 +++++++++ .../Classes/Log/LogfileReader.cs | 51 ++++++++----- 3 files changed, 131 insertions(+), 26 deletions(-) create mode 100644 src/LogExpert.Core/Classes/Log/LogBufferPool.cs diff --git a/src/LogExpert.Core/Classes/Log/LogBuffer.cs b/src/LogExpert.Core/Classes/Log/LogBuffer.cs index a83e9dc3..f95ae691 100644 --- a/src/LogExpert.Core/Classes/Log/LogBuffer.cs +++ b/src/LogExpert.Core/Classes/Log/LogBuffer.cs @@ -1,3 +1,5 @@ +using System.Buffers; + using ColumnizerLib; using NLog; @@ -15,7 +17,10 @@ public class LogBuffer private readonly List _filePositions; // file position for every line #endif - private readonly List _lineList; + private LogLine[] _lineArray; + private int _lineArrayLength; // capacity of the rented array + + //private readonly List _lineList; private int MAX_LINES = 500; @@ -31,7 +36,10 @@ public LogBuffer (ILogFileInfo fileInfo, int maxLines) { FileInfo = fileInfo; MAX_LINES = maxLines; - _lineList = new(MAX_LINES); + //_lineList = new(MAX_LINES); + + _lineArray = ArrayPool.Shared.Rent(maxLines); + _lineArrayLength = _lineArray.Length; #if DEBUG _filePositions = new(MAX_LINES); #endif @@ -81,23 +89,67 @@ public long Size public void AddLine (LogLine lineMemory, long filePos) { - _lineList.Add(lineMemory); + //_lineList.Add(lineMemory); + + if (LineCount < _lineArrayLength) + { + _lineArray[LineCount] = lineMemory; + LineCount++; + } +#if DEBUG + else + { + _logger.Error("AddLine overflow: LineCount={0} >= _lineArrayLength={1}", LineCount, _lineArrayLength); + } +#endif + #if DEBUG _filePositions.Add(filePos); #endif - LineCount++; IsDisposed = false; } public void ClearLines () { - _lineList.Clear(); + Array.Clear(_lineArray, 0, LineCount); + //_lineList.Clear(); + LineCount = 0; + } + + /// + /// Prepares the buffer for reuse from the pool. + /// + public void Reinitialise (ILogFileInfo fileInfo, int maxLines) + { + FileInfo = fileInfo; + MAX_LINES = maxLines; + StartLine = 0; + StartPos = 0; + Size = 0; LineCount = 0; + DroppedLinesCount = 0; + PrevBuffersDroppedLinesSum = 0; + IsDisposed = false; + _lineArray = ArrayPool.Shared.Rent(maxLines); + _lineArrayLength = _lineArray.Length; +#if DEBUG + _filePositions.Clear(); + DisposeCount = 0; +#endif } public void DisposeContent () { - _lineList.Clear(); + //_lineList.Clear(); + + if (_lineArray != null) + { + Array.Clear(_lineArray, 0, LineCount); + ArrayPool.Shared.Return(_lineArray); + _lineArray = null; + LineCount = 0; + } + IsDisposed = true; #if DEBUG DisposeCount++; @@ -106,9 +158,12 @@ public void DisposeContent () public LogLine? GetLineMemoryOfBlock (int num) { - return num < _lineList.Count && num >= 0 - ? _lineList[num] - : null; + return num < LineCount && num >= 0 + ? _lineArray[num] + : null; + //return num < _lineList.Count && num >= 0 + // ? _lineList[num] + // : null; } /// diff --git a/src/LogExpert.Core/Classes/Log/LogBufferPool.cs b/src/LogExpert.Core/Classes/Log/LogBufferPool.cs new file mode 100644 index 00000000..b5a8b73f --- /dev/null +++ b/src/LogExpert.Core/Classes/Log/LogBufferPool.cs @@ -0,0 +1,33 @@ +using System.Collections.Concurrent; + +using ColumnizerLib; + +namespace LogExpert.Core.Classes.Log; + +public sealed class LogBufferPool (int maxSize) +{ + private readonly ConcurrentBag _pool = []; + private readonly int _maxSize = maxSize; + + public LogBuffer Rent (ILogFileInfo fileInfo, int maxLines) + { + if (_pool.TryTake(out var buffer)) + { + buffer.Reinitialise(fileInfo, maxLines); + return buffer; + } + + return new LogBuffer(fileInfo, maxLines); + } + + public void Return (LogBuffer buffer) + { + ArgumentNullException.ThrowIfNull(buffer); + + buffer.DisposeContent(); + if (_pool.Count < _maxSize) + { + _pool.Add(buffer); + } + } +} diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index 774ffb48..15d78fd1 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -30,6 +30,7 @@ public partial class LogfileReader : IAutoLogLineMemoryColumnizerCallback, IDisp private readonly ReaderType _readerType; private readonly int _maximumLineLength; + private readonly LogBufferPool _bufferPool; private readonly Lock _logBufferLock = new(); private readonly ReaderWriterLockSlim _bufferListLock = new(LockRecursionPolicy.SupportsRecursion); @@ -101,6 +102,8 @@ private LogfileReader (string[] fileNames, EncodingOptions encodingOptions, bool _pluginRegistry = pluginRegistry; _disposed = false; + _bufferPool = new LogBufferPool(_max_buffers * 2); + InitLruBuffers(); ILogFileInfo fileInfo = null; @@ -1300,11 +1303,15 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start { if (_bufferList.Count == 0) { - logBuffer = new LogBuffer(logFileInfo, _maxLinesPerBuffer) - { - StartLine = startLine, - StartPos = filePos - }; + logBuffer = _bufferPool.Rent(logFileInfo, _maxLinesPerBuffer); + logBuffer.StartLine = startLine; + logBuffer.StartPos = filePos; + // logBuffer.PrevBuffersDroppedLinesSum = droppedLines; + // logBuffer = new LogBuffer(logFileInfo, _maxLinesPerBuffer) + // { + // StartLine = startLine, + // StartPos = filePos + // }; UpgradeBufferlistLockToWriterLock(); @@ -1323,11 +1330,15 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start if (!logBuffer.FileInfo.FullName.Equals(logFileInfo.FullName, StringComparison.Ordinal)) { - logBuffer = new LogBuffer(logFileInfo, _maxLinesPerBuffer) - { - StartLine = startLine, - StartPos = filePos - }; + logBuffer = _bufferPool.Rent(logFileInfo, _maxLinesPerBuffer); + logBuffer.StartLine = startLine; + logBuffer.StartPos = filePos; + // logBuffer.PrevBuffersDroppedLinesSum = droppedLines; + // logBuffer = new LogBuffer(logFileInfo, _maxLinesPerBuffer) + // { + // StartLine = startLine, + // StartPos = filePos + // }; UpgradeBufferlistLockToWriterLock(); @@ -1408,12 +1419,17 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start Monitor.Exit(logBuffer); try { - var newBuffer = new LogBuffer(logFileInfo, _maxLinesPerBuffer) - { - StartLine = lineNum, - StartPos = filePos, - PrevBuffersDroppedLinesSum = droppedLines - }; + var newBuffer = _bufferPool.Rent(logFileInfo, _maxLinesPerBuffer); + newBuffer.StartLine = lineNum; + newBuffer.StartPos = filePos; + newBuffer.PrevBuffersDroppedLinesSum = droppedLines; + + //var newBuffer = new LogBuffer(logFileInfo, _maxLinesPerBuffer) + //{ + // StartLine = lineNum, + // StartPos = filePos, + // PrevBuffersDroppedLinesSum = droppedLines + //}; AcquireBufferListWriterLock(); @@ -1564,7 +1580,8 @@ private void GarbageCollectLruCache () try { removed.LogBuffer.AcquireContentLock(ref lockTaken); - removed.LogBuffer.DisposeContent(); + //removed.LogBuffer.DisposeContent(); + _bufferPool.Return(removed.LogBuffer); } finally { From a1c8dc858efb757c055622c11d3fb8c85d26f525 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Thu, 16 Apr 2026 14:45:36 +0200 Subject: [PATCH 15/24] optimisations --- src/LogExpert.Core/Classes/Log/LogBuffer.cs | 14 -- .../Classes/Log/LogBufferPool.cs | 7 + .../Classes/Log/LogfileReader.cs | 134 +++++++++--------- 3 files changed, 72 insertions(+), 83 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/LogBuffer.cs b/src/LogExpert.Core/Classes/Log/LogBuffer.cs index f95ae691..4c4f5578 100644 --- a/src/LogExpert.Core/Classes/Log/LogBuffer.cs +++ b/src/LogExpert.Core/Classes/Log/LogBuffer.cs @@ -20,24 +20,18 @@ public class LogBuffer private LogLine[] _lineArray; private int _lineArrayLength; // capacity of the rented array - //private readonly List _lineList; - private int MAX_LINES = 500; #endregion #region cTor - //public LogBuffer() { } - // Don't use a primary constructor here: field initializers (like MAX_LINES) run before primary constructor parameters are assigned, // so MAX_LINES would always be set to its default value before the constructor body can assign it. Use a regular constructor instead. public LogBuffer (ILogFileInfo fileInfo, int maxLines) { FileInfo = fileInfo; MAX_LINES = maxLines; - //_lineList = new(MAX_LINES); - _lineArray = ArrayPool.Shared.Rent(maxLines); _lineArrayLength = _lineArray.Length; #if DEBUG @@ -89,8 +83,6 @@ public long Size public void AddLine (LogLine lineMemory, long filePos) { - //_lineList.Add(lineMemory); - if (LineCount < _lineArrayLength) { _lineArray[LineCount] = lineMemory; @@ -112,7 +104,6 @@ public void AddLine (LogLine lineMemory, long filePos) public void ClearLines () { Array.Clear(_lineArray, 0, LineCount); - //_lineList.Clear(); LineCount = 0; } @@ -140,8 +131,6 @@ public void Reinitialise (ILogFileInfo fileInfo, int maxLines) public void DisposeContent () { - //_lineList.Clear(); - if (_lineArray != null) { Array.Clear(_lineArray, 0, LineCount); @@ -161,9 +150,6 @@ public void DisposeContent () return num < LineCount && num >= 0 ? _lineArray[num] : null; - //return num < _lineList.Count && num >= 0 - // ? _lineList[num] - // : null; } /// diff --git a/src/LogExpert.Core/Classes/Log/LogBufferPool.cs b/src/LogExpert.Core/Classes/Log/LogBufferPool.cs index b5a8b73f..b7bbed59 100644 --- a/src/LogExpert.Core/Classes/Log/LogBufferPool.cs +++ b/src/LogExpert.Core/Classes/Log/LogBufferPool.cs @@ -20,6 +20,13 @@ public LogBuffer Rent (ILogFileInfo fileInfo, int maxLines) return new LogBuffer(fileInfo, maxLines); } + /// + /// Returns a to the pool for reuse. + /// + /// + /// Disposing the buffer's content is handled by this method, so callers should not dispose the buffer themselves. + /// + /// The buffer to return. public void Return (LogBuffer buffer) { ArgumentNullException.ThrowIfNull(buffer); diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index 15d78fd1..0b35ea33 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -55,7 +55,7 @@ public partial class LogfileReader : IAutoLogLineMemoryColumnizerCallback, IDisp private bool _disposed; private ILogFileInfo _watchedILogFileInfo; - private bool _isLineCountDirty = true; + private volatile bool _isLineCountDirty = true; private volatile bool _isFailModeCheckCallPending; private volatile bool _isFastFailOnGetLogLine; @@ -183,12 +183,17 @@ public int LineCount else { AcquireBufferListReaderLock(); - foreach (var buffer in _bufferList.Values) + try { - field += buffer.LineCount; + foreach (var buffer in _bufferList.Values) + { + field += buffer.LineCount; + } + } + finally + { + ReleaseBufferListReaderLock(); } - - ReleaseBufferListReaderLock(); } _isLineCountDirty = false; @@ -410,10 +415,10 @@ public int ShiftBuffers () foreach (var logFileInfo in lostILogFileInfoList) { - var lastBuffer = DeleteBuffersForInfo(logFileInfo, false); - if (lastBuffer != null) + var lastDeletedBufferInfo = DeleteBuffersForInfo(logFileInfo, false); + if (lastDeletedBufferInfo != null) { - offset += lastBuffer.StartLine + lastBuffer.LineCount; + offset += lastDeletedBufferInfo.Value.StartLine + lastDeletedBufferInfo.Value.LineCount; } } @@ -1061,39 +1066,43 @@ private ValueTask GetLogLineMemoryInternal (int lineNum) } AcquireBufferListReaderLock(); - var (logBuffer, _) = GetBufferForLineWithIndex(lineNum); - if (logBuffer == null) - { - ReleaseBufferListReaderLock(); - _logger.Error("Cannot find buffer for line {0}, file: {1}{2}", lineNum, _fileName, IsMultiFile ? " (MultiFile)" : ""); - return default; - } - - var lockTaken = false; try { - logBuffer.AcquireContentLock(ref lockTaken); + var (logBuffer, _) = GetBufferForLineWithIndex(lineNum); + if (logBuffer == null) + { + _logger.Error("Cannot find buffer for line {0}, file: {1}{2}", lineNum, _fileName, IsMultiFile ? " (MultiFile)" : ""); + return default; + } - if (logBuffer.IsDisposed) + var lockTaken = false; + try { - lock (logBuffer.FileInfo) + logBuffer.AcquireContentLock(ref lockTaken); + + if (logBuffer.IsDisposed) { - ReReadBuffer(logBuffer); + lock (logBuffer.FileInfo) + { + ReReadBuffer(logBuffer); + } } - } - var line = logBuffer.GetLineMemoryOfBlock(lineNum - logBuffer.StartLine); - return line.HasValue - ? new ValueTask(line.Value) - : default; + var line = logBuffer.GetLineMemoryOfBlock(lineNum - logBuffer.StartLine); + return line.HasValue + ? new ValueTask(line.Value) + : default; + } + finally + { + if (lockTaken) + { + logBuffer.ReleaseContentLock(); + } + } } finally { - if (lockTaken) - { - logBuffer.ReleaseContentLock(); - } - ReleaseBufferListReaderLock(); } } @@ -1204,12 +1213,12 @@ private void ReplaceBufferInfos (ILogFileInfo oldLogFileInfo, ILogFileInfo newLo /// /// true to match buffers by file name only; false to require an exact object match for the log file information. /// - /// The last LogBuffer instance that was removed; or null if no matching buffers were found. - private LogBuffer DeleteBuffersForInfo (ILogFileInfo iLogFileInfo, bool matchNamesOnly) + /// The StartLine and LineCount of the Logbuffer that was removed or null + private (int StartLine, int LineCount)? DeleteBuffersForInfo (ILogFileInfo iLogFileInfo, bool matchNamesOnly) { _logger.Info($"Deleting buffers for file {iLogFileInfo.FullName}"); ClearBufferState(); - LogBuffer lastRemovedBuffer = null; + (int StartLine, int LineCount)? lastRemovedInfo = null; IList deleteList = []; if (matchNamesOnly) @@ -1218,7 +1227,7 @@ private LogBuffer DeleteBuffersForInfo (ILogFileInfo iLogFileInfo, bool matchNam { if (buffer.FileInfo.FullName.Equals(iLogFileInfo.FullName, StringComparison.Ordinal)) { - lastRemovedBuffer = buffer; + lastRemovedInfo = (buffer.StartLine, buffer.LineCount); deleteList.Add(buffer); } } @@ -1229,7 +1238,7 @@ private LogBuffer DeleteBuffersForInfo (ILogFileInfo iLogFileInfo, bool matchNam { if (buffer.FileInfo == iLogFileInfo) { - lastRemovedBuffer = buffer; + lastRemovedInfo = (buffer.StartLine, buffer.LineCount); deleteList.Add(buffer); } } @@ -1238,18 +1247,32 @@ private LogBuffer DeleteBuffersForInfo (ILogFileInfo iLogFileInfo, bool matchNam foreach (var buffer in deleteList) { RemoveFromBufferList(buffer); + + var lockTaken = false; + try + { + buffer.AcquireContentLock(ref lockTaken); + _bufferPool.Return(buffer); + } + finally + { + if (lockTaken) + { + buffer.ReleaseContentLock(); + } + } } - if (lastRemovedBuffer == null) + if (lastRemovedInfo == null) { _logger.Info(CultureInfo.InvariantCulture, "lastRemovedBuffer is null"); } else { - _logger.Info(CultureInfo.InvariantCulture, "lastRemovedBuffer: startLine={0}", lastRemovedBuffer.StartLine); + _logger.Info(CultureInfo.InvariantCulture, $"lastRemovedBuffer: startLine={lastRemovedInfo.Value.StartLine}, lineCount={lastRemovedInfo.Value.LineCount}"); } - return lastRemovedBuffer; + return lastRemovedInfo; } /// @@ -1306,12 +1329,6 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start logBuffer = _bufferPool.Rent(logFileInfo, _maxLinesPerBuffer); logBuffer.StartLine = startLine; logBuffer.StartPos = filePos; - // logBuffer.PrevBuffersDroppedLinesSum = droppedLines; - // logBuffer = new LogBuffer(logFileInfo, _maxLinesPerBuffer) - // { - // StartLine = startLine, - // StartPos = filePos - // }; UpgradeBufferlistLockToWriterLock(); @@ -1333,12 +1350,6 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start logBuffer = _bufferPool.Rent(logFileInfo, _maxLinesPerBuffer); logBuffer.StartLine = startLine; logBuffer.StartPos = filePos; - // logBuffer.PrevBuffersDroppedLinesSum = droppedLines; - // logBuffer = new LogBuffer(logFileInfo, _maxLinesPerBuffer) - // { - // StartLine = startLine, - // StartPos = filePos - // }; UpgradeBufferlistLockToWriterLock(); @@ -1424,13 +1435,6 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start newBuffer.StartPos = filePos; newBuffer.PrevBuffersDroppedLinesSum = droppedLines; - //var newBuffer = new LogBuffer(logFileInfo, _maxLinesPerBuffer) - //{ - // StartLine = lineNum, - // StartPos = filePos, - // PrevBuffersDroppedLinesSum = droppedLines - //}; - AcquireBufferListWriterLock(); try @@ -1580,7 +1584,6 @@ private void GarbageCollectLruCache () try { removed.LogBuffer.AcquireContentLock(ref lockTaken); - //removed.LogBuffer.DisposeContent(); _bufferPool.Return(removed.LogBuffer); } finally @@ -1646,7 +1649,7 @@ private void ClearLru () try { entry.LogBuffer.AcquireContentLock(ref lockTaken); - entry.LogBuffer.DisposeContent(); + _bufferPool.Return(entry.LogBuffer); } finally { @@ -1760,7 +1763,7 @@ private void ReReadBuffer (LogBuffer logBuffer) #if DEBUG Util.AssertTrue( _bufferListLock.IsReadLockHeld || _bufferListLock.IsUpgradeableReadLockHeld || _bufferListLock.IsWriteLockHeld, - "No lock held for buffer list in GetBufferForLineCore"); + "No lock held for buffer list in GetBufferForLineWithIndex"); long startTime = Environment.TickCount; #endif var arr = _bufferList.Values; @@ -1845,7 +1848,7 @@ private void ReReadBuffer (LogBuffer logBuffer) } #if DEBUG long endTime = Environment.TickCount; - _logger.Debug($"getBufferForLine({lineNum}) duration: {endTime - startTime} ms."); + _logger.Debug($"GetBufferForLineWithIndex({lineNum}) duration: {endTime - startTime} ms."); #endif return (null, -1); } @@ -1905,11 +1908,8 @@ private void GetLineMemoryFinishedCallback (ILogLineMemory line) private LogBuffer GetFirstBufferForFileByLogBuffer (LogBuffer logBuffer, int index) { var info = logBuffer.FileInfo; - AcquireBufferListReaderLock(); - if (index == -1) { - ReleaseBufferListReaderLock(); return null; } @@ -1925,7 +1925,6 @@ private LogBuffer GetFirstBufferForFileByLogBuffer (LogBuffer logBuffer, int ind resultBuffer = _bufferList.Values[index]; } - ReleaseBufferListReaderLock(); return resultBuffer; } @@ -1944,14 +1943,12 @@ private async Task MonitorThreadProc () //IFileSystemPlugin fs = PluginRegistry.GetInstance().FindFileSystemForUri(this.watchedILogFileInfo.FullName); _logger.Info(CultureInfo.InvariantCulture, "MonitorThreadProc() for file {0}", _watchedILogFileInfo.FullName); - long oldSize; try { OnLoadingStarted(new LoadFileEventArgs(_fileName, 0, false, 0, false)); ReadFiles(); if (!_isDeleted) { - oldSize = _fileLength; OnLoadingFinished(); } } @@ -1988,7 +1985,6 @@ private async Task MonitorThreadProc () } else { - oldSize = _fileLength; FileChanged(); } } From 1f282bbac9a7d583249f4b705f05a6631ad1ddf1 Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Thu, 16 Apr 2026 16:38:06 +0200 Subject: [PATCH 16/24] logrotator test tool --- .../Classes/Log/LogfileReader.cs | 204 +++++++++--------- src/LogExpert.Tests/BufferShiftTest.cs | 4 +- .../RolloverHandlerTestBase.cs | 2 +- src/tools/LogRotator/LogRotator.cs | 111 ++++++++++ src/tools/LogRotator/LogRotator.csproj | 7 + 5 files changed, 227 insertions(+), 101 deletions(-) create mode 100644 src/tools/LogRotator/LogRotator.cs create mode 100644 src/tools/LogRotator/LogRotator.csproj diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index 0b35ea33..be8df3ff 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -331,144 +331,150 @@ public int ShiftBuffers () _logger.Info(CultureInfo.InvariantCulture, "ShiftBuffers() begin for {0}{1}", _fileName, IsMultiFile ? " (MultiFile)" : ""); AcquireBufferListWriterLock(); - ClearBufferState(); - - var offset = 0; - _isLineCountDirty = true; - lock (_monitor) + try { - RolloverFilenameHandler rolloverHandler = new(_watchedILogFileInfo, _multiFileOptions); - var fileNameList = rolloverHandler.GetNameList(_pluginRegistry); + ClearBufferState(); - ResetBufferCache(); + var offset = 0; + _isLineCountDirty = true; - IList lostILogFileInfoList = []; - IList readNewILogFileInfoList = []; - IList newFileInfoList = []; + lock (_monitor) + { + RolloverFilenameHandler rolloverHandler = new(_watchedILogFileInfo, _multiFileOptions); + var fileNameList = rolloverHandler.GetNameList(_pluginRegistry); - var enumerator = _logFileInfoList.GetEnumerator(); + ResetBufferCache(); - while (enumerator.MoveNext()) - { - var logFileInfo = enumerator.Current; - var fileName = logFileInfo.FullName; - _logger.Debug(CultureInfo.InvariantCulture, "Testing file {0}", fileName); + IList lostILogFileInfoList = []; + IList readNewILogFileInfoList = []; + IList newFileInfoList = []; - var node = fileNameList.Find(fileName); + var enumerator = _logFileInfoList.GetEnumerator(); - if (node == null) + while (enumerator.MoveNext()) { - _logger.Warn(CultureInfo.InvariantCulture, "File {0} not found", fileName); - continue; - } + var logFileInfo = enumerator.Current; + var fileName = logFileInfo.FullName; + _logger.Debug(CultureInfo.InvariantCulture, "Testing file {0}", fileName); - if (node.Previous != null) - { - fileName = node.Previous.Value; - var newILogFileInfo = GetLogFileInfo(fileName); - _logger.Debug(CultureInfo.InvariantCulture, "{0} exists\r\nOld size={1}, new size={2}", fileName, logFileInfo.OriginalLength, newILogFileInfo.Length); - // is the new file the same as the old buffer info? - if (newILogFileInfo.Length == logFileInfo.OriginalLength) + var node = fileNameList.Find(fileName); + + if (node == null) { - ReplaceBufferInfos(logFileInfo, newILogFileInfo); - newFileInfoList.Add(newILogFileInfo); + _logger.Warn(CultureInfo.InvariantCulture, "File {0} not found", fileName); + continue; } - else + + if (node.Previous != null) { - _logger.Debug(CultureInfo.InvariantCulture, "Buffer for {0} must be re-read.", fileName); - // not the same. so must read the rest of the list anew from the files - readNewILogFileInfoList.Add(newILogFileInfo); - while (enumerator.MoveNext()) + fileName = node.Previous.Value; + var newILogFileInfo = GetLogFileInfo(fileName); + _logger.Debug(CultureInfo.InvariantCulture, "{0} exists\r\nOld size={1}, new size={2}", fileName, logFileInfo.OriginalLength, newILogFileInfo.Length); + // is the new file the same as the old buffer info? + if (newILogFileInfo.Length == logFileInfo.OriginalLength) { - fileName = enumerator.Current.FullName; - node = fileNameList.Find(fileName); - if (node == null) - { - _logger.Warn(CultureInfo.InvariantCulture, "File {0} not found", fileName); - continue; - } - - if (node.Previous != null) - { - fileName = node.Previous.Value; - _logger.Debug(CultureInfo.InvariantCulture, "New name is {0}", fileName); - readNewILogFileInfoList.Add(GetLogFileInfo(fileName)); - } - else + ReplaceBufferInfos(logFileInfo, newILogFileInfo); + newFileInfoList.Add(newILogFileInfo); + } + else + { + _logger.Debug(CultureInfo.InvariantCulture, "Buffer for {0} must be re-read.", fileName); + // not the same. so must read the rest of the list anew from the files + readNewILogFileInfoList.Add(newILogFileInfo); + while (enumerator.MoveNext()) { - _logger.Warn(CultureInfo.InvariantCulture, "No previous file for {0} found", fileName); + fileName = enumerator.Current.FullName; + node = fileNameList.Find(fileName); + if (node == null) + { + _logger.Warn(CultureInfo.InvariantCulture, "File {0} not found", fileName); + continue; + } + + if (node.Previous != null) + { + fileName = node.Previous.Value; + _logger.Debug(CultureInfo.InvariantCulture, "New name is {0}", fileName); + readNewILogFileInfoList.Add(GetLogFileInfo(fileName)); + } + else + { + _logger.Warn(CultureInfo.InvariantCulture, "No previous file for {0} found", fileName); + } } } } + else + { + _logger.Info(CultureInfo.InvariantCulture, "{0} does not exist", fileName); + lostILogFileInfoList.Add(logFileInfo); + } } - else + + if (lostILogFileInfoList.Count > 0) { - _logger.Info(CultureInfo.InvariantCulture, "{0} does not exist", fileName); - lostILogFileInfoList.Add(logFileInfo); - } - } + _logger.Info(CultureInfo.InvariantCulture, "Deleting buffers for lost files"); - if (lostILogFileInfoList.Count > 0) - { - _logger.Info(CultureInfo.InvariantCulture, "Deleting buffers for lost files"); + foreach (var logFileInfo in lostILogFileInfoList) + { + var lastDeletedBufferInfo = DeleteBuffersForInfo(logFileInfo, false); + if (lastDeletedBufferInfo != null) + { + offset += lastDeletedBufferInfo.Value.StartLine + lastDeletedBufferInfo.Value.LineCount; + } + } - foreach (var logFileInfo in lostILogFileInfoList) - { - var lastDeletedBufferInfo = DeleteBuffersForInfo(logFileInfo, false); - if (lastDeletedBufferInfo != null) + _logger.Info(CultureInfo.InvariantCulture, "Adjusting StartLine values in {0} buffers by offset {1}", _bufferList.Count, offset); + foreach (var buffer in _bufferList.Values.ToList()) { - offset += lastDeletedBufferInfo.Value.StartLine + lastDeletedBufferInfo.Value.LineCount; + SetNewStartLineForBuffer(buffer, buffer.StartLine - offset); } - } - _logger.Info(CultureInfo.InvariantCulture, "Adjusting StartLine values in {0} buffers by offset {1}", _bufferList.Count, offset); - foreach (var buffer in _bufferList.Values) - { - SetNewStartLineForBuffer(buffer, buffer.StartLine - offset); +#if DEBUG + if (_bufferList.Values.Count > 0) + { + _logger.Debug(CultureInfo.InvariantCulture, "First buffer now has StartLine {0}", _bufferList.Values[0].StartLine); + } +#endif } -#if DEBUG - if (_bufferList.Values.Count > 0) + // Read anew all buffers following a buffer info that couldn't be matched with the corresponding existing file + _logger.Info(CultureInfo.InvariantCulture, "Deleting buffers for files that must be re-read"); + + foreach (var iLogFileInfo in readNewILogFileInfoList) { - _logger.Debug(CultureInfo.InvariantCulture, "First buffer now has StartLine {0}", _bufferList.Values[0].StartLine); + DeleteBuffersForInfo(iLogFileInfo, true); } -#endif - } - // Read anew all buffers following a buffer info that couldn't be matched with the corresponding existing file - _logger.Info(CultureInfo.InvariantCulture, "Deleting buffers for files that must be re-read"); + _logger.Info(CultureInfo.InvariantCulture, "Deleting buffers for the watched file"); - foreach (var iLogFileInfo in readNewILogFileInfoList) - { - DeleteBuffersForInfo(iLogFileInfo, true); - } + DeleteBuffersForInfo(_watchedILogFileInfo, true); - _logger.Info(CultureInfo.InvariantCulture, "Deleting buffers for the watched file"); + _logger.Info(CultureInfo.InvariantCulture, "Re-Reading files"); - DeleteBuffersForInfo(_watchedILogFileInfo, true); + foreach (var iLogFileInfo in readNewILogFileInfoList) + { + ReadToBufferList(iLogFileInfo, 0, LineCount); + newFileInfoList.Add(iLogFileInfo); + } - _logger.Info(CultureInfo.InvariantCulture, "Re-Reading files"); + _logFileInfoList = newFileInfoList; + _watchedILogFileInfo = GetLogFileInfo(_watchedILogFileInfo.FullName); + _logFileInfoList.Add(_watchedILogFileInfo); + _logger.Info(CultureInfo.InvariantCulture, "Reading watched file"); - foreach (var iLogFileInfo in readNewILogFileInfoList) - { - ReadToBufferList(iLogFileInfo, 0, LineCount); - newFileInfoList.Add(iLogFileInfo); + ReadToBufferList(_watchedILogFileInfo, 0, LineCount); } - _logFileInfoList = newFileInfoList; - _watchedILogFileInfo = GetLogFileInfo(_watchedILogFileInfo.FullName); - _logFileInfoList.Add(_watchedILogFileInfo); - _logger.Info(CultureInfo.InvariantCulture, "Reading watched file"); + _logger.Info(CultureInfo.InvariantCulture, "ShiftBuffers() end. offset={0}", offset); - ReadToBufferList(_watchedILogFileInfo, 0, LineCount); + return offset; + } + finally + { + ReleaseBufferListWriterLock(); } - - _logger.Info(CultureInfo.InvariantCulture, "ShiftBuffers() end. offset={0}", offset); - - ReleaseBufferListWriterLock(); - - return offset; } /// diff --git a/src/LogExpert.Tests/BufferShiftTest.cs b/src/LogExpert.Tests/BufferShiftTest.cs index 35939003..f5e6d452 100644 --- a/src/LogExpert.Tests/BufferShiftTest.cs +++ b/src/LogExpert.Tests/BufferShiftTest.cs @@ -27,6 +27,9 @@ public void Boot () [Test] [TestCase(ReaderType.System)] //[TestCase(ReaderType.Legacy)] Legacy Reader does not Support this + //TO Test real life scenario, use the LogRotator tool, in the src/Tools/LogRotator directory, + //to create files and perform rollovers while watching the files in LogExpert with MultiFile enabled + //(pattern: *$J(.)) public void TestShiftBuffers1 (ReaderType readerType) { var linesPerFile = 10; @@ -118,7 +121,6 @@ public void TestShiftBuffers1 (ReaderType readerType) _ = enumerator.MoveNext(); } - _ = enumerator.MoveNext(); // the last 2 files now contain the content of the previously watched file for (; i < logBuffers.Count; ++i) { diff --git a/src/LogExpert.Tests/RolloverHandlerTestBase.cs b/src/LogExpert.Tests/RolloverHandlerTestBase.cs index 44a23625..1aaffa7f 100644 --- a/src/LogExpert.Tests/RolloverHandlerTestBase.cs +++ b/src/LogExpert.Tests/RolloverHandlerTestBase.cs @@ -63,7 +63,7 @@ protected static LinkedList RolloverSimulation (LinkedList files _ = enumerator.MoveNext(); } - _ = CreateFile(null, nextEnumerator.Current); + _ = CreateFile(null, enumerator.Current); if (deleteLatestFile) { diff --git a/src/tools/LogRotator/LogRotator.cs b/src/tools/LogRotator/LogRotator.cs new file mode 100644 index 00000000..038dcd2b --- /dev/null +++ b/src/tools/LogRotator/LogRotator.cs @@ -0,0 +1,111 @@ +using System.Text; + +const string baseDir = "logs"; +const string baseName = "engine.log"; +const int maxBackups = 6; +const int linesPerFile = 50; + +Directory.CreateDirectory(baseDir); + +// Create initial set of files +Console.WriteLine($"Creating initial files in '{Path.GetFullPath(baseDir)}'..."); +WriteLogFile(Path.Combine(baseDir, baseName), 0); + +for (var i = 1; i <= maxBackups; i++) +{ + WriteLogFile(Path.Combine(baseDir, $"{baseName}.{i}"), i); +} + +PrintFiles(); +Console.WriteLine(); +Console.WriteLine("Open the 'engine.log' file in LogExpert with MultiFile enabled (pattern: *$J(.))"); +Console.WriteLine("Press ENTER to perform a rotation (with oldest file deletion), or Q to quit."); + +var rotationCount = 0; + +while (true) +{ + var key = Console.ReadKey(true); + + if (key.Key == ConsoleKey.Q) + { + break; + } + + if (key.Key != ConsoleKey.Enter) + { + continue; + } + + rotationCount++; + Console.WriteLine($"\n--- Rotation #{rotationCount} ---"); + + // Delete the oldest file (simulates maxBackups limit) + var oldest = Path.Combine(baseDir, $"{baseName}.{maxBackups}"); + + if (File.Exists(oldest)) + { + File.Delete(oldest); + Console.WriteLine($" Deleted: {baseName}.{maxBackups}"); + } + + // Shift all numbered files up by one + for (var i = maxBackups - 1; i >= 1; i--) + { + var src = Path.Combine(baseDir, $"{baseName}.{i}"); + var dst = Path.Combine(baseDir, $"{baseName}.{i + 1}"); + + if (File.Exists(src)) + { + File.Move(src, dst); + Console.WriteLine($" Renamed: {baseName}.{i} -> {baseName}.{i + 1}"); + } + } + + // Rename current log to .1 + var current = Path.Combine(baseDir, baseName); + var first = Path.Combine(baseDir, $"{baseName}.1"); + + if (File.Exists(current)) + { + File.Move(current, first); + Console.WriteLine($" Renamed: {baseName} -> {baseName}.1"); + } + + // Create empty file first (like real log frameworks do), so LogExpert detects + // newSize < oldSize and triggers ShiftBuffers() + File.Create(current).Dispose(); + Console.WriteLine($" Created: {baseName} (empty - triggers rollover detection)"); + + PrintFiles(); + + // Wait for LogExpert's poll interval to detect the smaller file, then write content + Console.WriteLine(" Waiting 2s for LogExpert to detect rollover..."); + Thread.Sleep(2000); + + WriteLogFile(current, maxBackups + rotationCount); + Console.WriteLine($" Wrote content to {baseName}"); + + PrintFiles(); + Console.WriteLine("\nPress ENTER for next rotation, Q to quit."); +} + +static void WriteLogFile(string path, int fileId) +{ + using var writer = new StreamWriter(path, false, Encoding.UTF8); + + for (var i = 1; i <= linesPerFile; i++) + { + writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [INFO] File#{fileId:D3} Line {i:D3} - {Path.GetFileName(path)} - Sample log message"); + } +} + +static void PrintFiles() +{ + Console.WriteLine("\nCurrent files on disk:"); + + foreach (var f in Directory.GetFiles(baseDir, $"{baseName}*").OrderBy(f => f)) + { + Console.WriteLine($" {Path.GetFileName(f)} ({new FileInfo(f).Length} bytes)"); + } +} diff --git a/src/tools/LogRotator/LogRotator.csproj b/src/tools/LogRotator/LogRotator.csproj new file mode 100644 index 00000000..da829488 --- /dev/null +++ b/src/tools/LogRotator/LogRotator.csproj @@ -0,0 +1,7 @@ + + + Exe + net10.0 + enable + + From f7758dd32b9aad84786bd2c4fedd421e0d5652fc Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Thu, 16 Apr 2026 16:45:21 +0200 Subject: [PATCH 17/24] tests --- src/LogExpert.Tests/BufferShiftTest.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/LogExpert.Tests/BufferShiftTest.cs b/src/LogExpert.Tests/BufferShiftTest.cs index f5e6d452..189aed7d 100644 --- a/src/LogExpert.Tests/BufferShiftTest.cs +++ b/src/LogExpert.Tests/BufferShiftTest.cs @@ -115,9 +115,8 @@ public void TestShiftBuffers1 (ReaderType readerType) { var logBuffer = logBuffers[i]; var line = logBuffer.GetLineMemoryOfBlock(0); -#pragma warning disable CS8629 // Nullable value type may be null. + Assert.That(line.HasValue, Is.True); Assert.That(line.Value.FullLine.Span.Contains(enumerator.Current.AsSpan(), StringComparison.Ordinal)); -#pragma warning restore CS8629 // Nullable value type may be null. _ = enumerator.MoveNext(); } @@ -126,9 +125,8 @@ public void TestShiftBuffers1 (ReaderType readerType) { var logBuffer = logBuffers[i]; var line = logBuffer.GetLineMemoryOfBlock(0); -#pragma warning disable CS8629 // Nullable value type may be null. + Assert.That(line.HasValue, Is.True); Assert.That(line.Value.FullLine.Span.Contains(enumerator.Current.AsSpan(), StringComparison.Ordinal)); -#pragma warning restore CS8629 // Nullable value type may be null. } oldCount = lil.Count; From 9575de252ecff3a76ca006141f54493d892ad35b Mon Sep 17 00:00:00 2001 From: BRUNER Patrick Date: Thu, 16 Apr 2026 21:31:41 +0200 Subject: [PATCH 18/24] Add memory-mapped log reader and line offset index Introduce LineOffsetIndex for efficient line start tracking and MemoryMappedFileReader for fast, scalable log file access using memory-mapped files. Supports tail mode and random line access, improving performance for large and growing log files. --- .../Classes/Log/LineOffsetIndex.cs | 53 +++++++ .../Classes/Log/MemoryMappedFileReader.cs | 146 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 src/LogExpert.Core/Classes/Log/LineOffsetIndex.cs create mode 100644 src/LogExpert.Core/Classes/Log/MemoryMappedFileReader.cs diff --git a/src/LogExpert.Core/Classes/Log/LineOffsetIndex.cs b/src/LogExpert.Core/Classes/Log/LineOffsetIndex.cs new file mode 100644 index 00000000..34c9024f --- /dev/null +++ b/src/LogExpert.Core/Classes/Log/LineOffsetIndex.cs @@ -0,0 +1,53 @@ +namespace LogExpert.Core.Classes.Log; + +/// +/// Stores byte offsets for each line start in a file. Supports incremental appending for tail mode. +/// +internal sealed class LineOffsetIndex (int initialCapacity = 4096) +{ + private long[] _offsets = new long[initialCapacity]; + + public int LineCount { get; private set; } + + /// + /// Appends a line-start offset. + /// + public void Add (long offset) + { + if (LineCount == _offsets.Length) + { + Array.Resize(ref _offsets, _offsets.Length * 2); + } + + _offsets[LineCount++] = offset; + } + + /// + /// Returns the byte offset of the start of the given line. + /// + public long GetOffset (int lineNum) + { + return (uint)lineNum < (uint)LineCount ? _offsets[lineNum] : -1; + } + + /// + /// Returns the byte length of the given line (from its start to the next line's start). + /// For the last line, returns -1 (unknown length, read to end or newline). + /// + public long GetLineLength (int lineNum) + { + return (uint)lineNum >= (uint)LineCount + ? -1 + : lineNum + 1 < LineCount + ? _offsets[lineNum + 1] - _offsets[lineNum] + : -1; + } + + /// + /// Removes all offsets, resetting the index. + /// + public void Clear () + { + LineCount = 0; + } +} diff --git a/src/LogExpert.Core/Classes/Log/MemoryMappedFileReader.cs b/src/LogExpert.Core/Classes/Log/MemoryMappedFileReader.cs new file mode 100644 index 00000000..73f78455 --- /dev/null +++ b/src/LogExpert.Core/Classes/Log/MemoryMappedFileReader.cs @@ -0,0 +1,146 @@ +using System.Buffers; +using System.IO.MemoryMappedFiles; +using System.Text; + +using ColumnizerLib; + +namespace LogExpert.Core.Classes.Log; + +/// +/// Reads log lines via memory-mapped file access. Builds a line-offset index on load. +/// Supports tail mode by re-mapping when the file grows. +/// +internal sealed class MemoryMappedFileReader (string filePath, Encoding encoding) : IDisposable +{ + private readonly Encoding _encoding = encoding; + private readonly LineOffsetIndex _lineIndex = new(); + private MemoryMappedFile _mmf; + private MemoryMappedViewAccessor _accessor; + private long _mappedLength; + private readonly string _filePath = filePath; + + public int LineCount => _lineIndex.LineCount; + + /// + /// Builds (or rebuilds) the line-offset index by scanning for newline characters. + /// In tail mode, call with startOffset = previously mapped length to index only new content. + /// + public void BuildIndex (long startOffset = 0) + { + using var fs = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + var fileLength = fs.Length; + + if (startOffset == 0) + { + _lineIndex.Clear(); + _lineIndex.Add(0); // first line starts at offset 0 + } + + fs.Position = startOffset; + var buffer = ArrayPool.Shared.Rent(81920); + try + { + int bytesRead; + var position = startOffset; + while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0) + { + for (var i = 0; i < bytesRead; i++) + { + if (buffer[i] == (byte)'\n') + { + _lineIndex.Add(position + i + 1); + } + } + + position += bytesRead; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + // Re-map the file + RemapFile(fileLength); + } + + /// + /// Extends the mapping to cover new file content (for tail mode). + /// + public void ExtendIndex () + { + BuildIndex(_mappedLength); + } + + /// + /// Reads a single line by its zero-based line number. + /// Returns the line as an ILogLineMemory. + /// + public ILogLineMemory GetLine (int lineNum) + { + var offset = _lineIndex.GetOffset(lineNum); + if (offset < 0 || _accessor == null) + { + return null; + } + + var length = _lineIndex.GetLineLength(lineNum); + if (length < 0) + { + // Last line — read to the end of file or a reasonable limit + length = Math.Min(_mappedLength - offset, 1024 * 1024); + } + + if (length <= 0) + { + return new LogLine(ReadOnlyMemory.Empty, lineNum); + } + + // Read bytes from the mapped view + var bytes = new byte[length]; + _ = _accessor.ReadArray(offset, bytes, 0, (int)length); + + // Trim trailing \r\n + var end = (int)length; + if (end > 0 && bytes[end - 1] == '\n') + { + end--; + } + + if (end > 0 && bytes[end - 1] == '\r') + { + end--; + } + + var text = _encoding.GetString(bytes, 0, end); + return new LogLine(text, lineNum); + } + + private void RemapFile (long fileLength) + { + _accessor?.Dispose(); + _mmf?.Dispose(); + + if (fileLength == 0) + { + _mappedLength = 0; + return; + } + + _mmf = MemoryMappedFile.CreateFromFile( + _filePath, + FileMode.Open, + mapName: null, + capacity: fileLength, + MemoryMappedFileAccess.Read); + + _accessor = _mmf.CreateViewAccessor(0, fileLength, MemoryMappedFileAccess.Read); + _mappedLength = fileLength; + } + + public void Dispose () + { + _accessor?.Dispose(); + _mmf?.Dispose(); + } +} From de82346f6fc1fd24985540b951b465783bd0a4ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Apr 2026 19:34:28 +0000 Subject: [PATCH 19/24] chore: update plugin hashes [skip ci] --- .../PluginHashGenerator.Generated.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index 3e1c6d40..31fac0f0 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2026-04-11 07:50:46 UTC + /// Generated: 2026-04-16 19:34:26 UTC /// Configuration: Release /// Plugin count: 22 /// @@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "D36D2E597CB0013725F96DBCB7BBF5D7507DE06EFBFDB7052CA8A578FC73A2D0", + ["AutoColumnizer.dll"] = "9A05C10957AF63E5BCFD7C2340224752B1CA7371990E99A709DCE5CC476BE5D3", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "E6453F86132B5FC4684FAC1D7D295C8A07B57E0F130DCBAE2F5D0B519AE629A6", - ["CsvColumnizer.dll (x86)"] = "E6453F86132B5FC4684FAC1D7D295C8A07B57E0F130DCBAE2F5D0B519AE629A6", - ["DefaultPlugins.dll"] = "8229DED288584A0A194707FCD681729ED1C1A2AF4FDD8FA9979DD30FFF179494", - ["FlashIconHighlighter.dll"] = "4318D8B3F749EAE3B06B1927EE55F5B89DDCB365998389AD72D1B06774900534", - ["GlassfishColumnizer.dll"] = "9044D36D3746CC3435255560D85441AEA234B3AB1BAC0888CBA0DE5CFF3ADC52", - ["JsonColumnizer.dll"] = "06AD09BC01B20F66D9C60E1C047AA0E78375EB952779C910C2206BD1F3E4C893", - ["JsonCompactColumnizer.dll"] = "B2A6CD40D3717DC181E5C9D8FC1ED26117B181475D801FC942DF7769F85EBA2C", - ["Log4jXmlColumnizer.dll"] = "36F5648EBC0A007DF82F68933DF392CFD9942C1F31F166EF4CB8C60507997487", - ["LogExpert.Core.dll"] = "ED98A22A79F05DD2C0B595FB13C90729D1B3660034C813BD493A037602679232", - ["LogExpert.Resources.dll"] = "9A3F67A6405D2560FFAB54483229C686E5F9A9DE60F686910CEA23E19AC4FDAF", + ["CsvColumnizer.dll"] = "D50D62211B6EF99CFE3CEC82D31E93182D17E6310607D20ADAEFDEE2A5A34CBD", + ["CsvColumnizer.dll (x86)"] = "D50D62211B6EF99CFE3CEC82D31E93182D17E6310607D20ADAEFDEE2A5A34CBD", + ["DefaultPlugins.dll"] = "B597E9ABC2207EE844C04F3A250AAC11BC7349E83825C5CF7F74AFA25C92EC9E", + ["FlashIconHighlighter.dll"] = "6A7A39CC125AB6BD23E82805613D681F13FA71D41FE48A944F8664FEFF611148", + ["GlassfishColumnizer.dll"] = "9324A8FE8FA164B678079B76ED4A62978782F1CDFD0B61AEF25B44EE2B836D96", + ["JsonColumnizer.dll"] = "85F71B424E6FCE49F787E83E6853B2D0CEE63019811147A45FF5A0BD3B38C941", + ["JsonCompactColumnizer.dll"] = "F5394F43A8D6F62F51F0753CC849A2BED9E2DDD30DC47388238DE8E7BD073A94", + ["Log4jXmlColumnizer.dll"] = "5F4196B0B8238F7529E944E7B71EE39CC6B25958CAD9BA4A4A922ACD2C6CC3BE", + ["LogExpert.Core.dll"] = "713DDCE6ACCAC6D3FD9A2A49EB03797105289DF94DDD7840E574F8D90B782D4B", + ["LogExpert.Resources.dll"] = "51F1EB4C15B3A35BEAE4B2F87E6DCFB92E661ED0BDA24C123E3A9D4235EE2D96", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", ["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", - ["RegexColumnizer.dll"] = "F13240DC73541B68E24A1EED63E19052AD9D7FAD612DF644ACC053D023E3156A", - ["SftpFileSystem.dll"] = "1F6D11FA4C748E014A3A15A20EFFF4358AD14C391821F45F4AECA6D9A3D47C60", - ["SftpFileSystem.dll (x86)"] = "6A8CC68ED4BBED8838FCA45E74AA31F73F0BFEAFD400C278FBC48ED2FF8FF180", - ["SftpFileSystem.Resources.dll"] = "64686BBECECB92AA5A7B13EF98C1CDCC1D2ECA4D9BBE1A1B367A2CA39BB5B6BD", - ["SftpFileSystem.Resources.dll (x86)"] = "64686BBECECB92AA5A7B13EF98C1CDCC1D2ECA4D9BBE1A1B367A2CA39BB5B6BD", + ["RegexColumnizer.dll"] = "C2B56F0DDD206AE356EFAD9E9645664B6E42C8D8B9B3CA03B77DBB7BAB7DB182", + ["SftpFileSystem.dll"] = "CF4637BFA26933C5FBC228E80F41D3B67C26B9CFFA8C974510E68782C7745FE3", + ["SftpFileSystem.dll (x86)"] = "95A90F42E777D1C365097D7C8C46C02B2FC7AB25D7FACF26B0303B40B50EF4A0", + ["SftpFileSystem.Resources.dll"] = "A6289FF8AC57F1B1A5352B717D13DD8D07A4D3FD579F729B1ED26C15B05FBC65", + ["SftpFileSystem.Resources.dll (x86)"] = "A6289FF8AC57F1B1A5352B717D13DD8D07A4D3FD579F729B1ED26C15B05FBC65", }; } From d5a19c3ef3302a5cf84eaaa9ac7297d8009d8e1c Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 16 Apr 2026 21:44:37 +0200 Subject: [PATCH 20/24] change to 10.0.100 because 10.0.202 needs ms build 18.4 which for some reason does not work --- build/_build.csproj | 18 +++++++------- global.json | 2 +- src/Directory.Build.props | 12 +++++----- src/Directory.Packages.props | 24 +++++++++---------- .../EventArguments/ColumnizerEventArgs.cs | 2 +- 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/build/_build.csproj b/build/_build.csproj index 905a8dec..9573e682 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -10,18 +10,20 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers - - + + + - + + diff --git a/global.json b/global.json index b66ed848..971b5004 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.200", + "version": "10.0.100", "rollForward": "latestPatch" } } \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7b83055f..2d04fe22 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,9 +1,9 @@ - 1.31.0.0 - 1.31.0.0 - 1.31.0.0 - 1.31.0.0 + 1.40.0.0 + 1.40.0.0 + 1.40.0.0 + 1.40.0.0 Hirogen, zarunbal, RandallFlagg, TheNicker LogExperts enable @@ -21,8 +21,8 @@ https://github.com/LogExperts/LogExpert LogExpert, Columnizer, Logging, Windows, Winforms git - https://github.com/LogExperts/LogExpert/releases/tag/v.1.31.0 - 1.31.0.0 + https://github.com/LogExperts/LogExpert/releases/tag/v.1.40.0 + 1.40.0.0 true LogExpert Copyright © LogExpert 2025 diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 8e427ef2..d2dc0368 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -10,24 +10,24 @@ - + - + - + - + - + - - - - - - - + + + + + + + \ No newline at end of file diff --git a/src/LogExpert.Core/EventArguments/ColumnizerEventArgs.cs b/src/LogExpert.Core/EventArguments/ColumnizerEventArgs.cs index 9b1da054..9f89c137 100644 --- a/src/LogExpert.Core/EventArguments/ColumnizerEventArgs.cs +++ b/src/LogExpert.Core/EventArguments/ColumnizerEventArgs.cs @@ -2,7 +2,7 @@ namespace LogExpert.Core.EventArguments; -public class ColumnizerEventArgs(ILogLineMemoryColumnizer columnizer) : System.EventArgs +public class ColumnizerEventArgs (ILogLineMemoryColumnizer columnizer) : EventArgs { #region Properties From 45caafb47cec6cfe4aad356bc6debae28f818527 Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 16 Apr 2026 21:50:43 +0200 Subject: [PATCH 21/24] review comments --- src/LogExpert.Tests/BufferShiftTest.cs | 14 +++++++++++-- src/tools/LogRotator/LogRotator.cs | 27 +++++++++++++------------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/LogExpert.Tests/BufferShiftTest.cs b/src/LogExpert.Tests/BufferShiftTest.cs index 189aed7d..7f82e7e6 100644 --- a/src/LogExpert.Tests/BufferShiftTest.cs +++ b/src/LogExpert.Tests/BufferShiftTest.cs @@ -26,6 +26,7 @@ public void Boot () [Test] [TestCase(ReaderType.System)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Unit Test")] //[TestCase(ReaderType.Legacy)] Legacy Reader does not Support this //TO Test real life scenario, use the LogRotator tool, in the src/Tools/LogRotator directory, //to create files and perform rollovers while watching the files in LogExpert with MultiFile enabled @@ -115,7 +116,11 @@ public void TestShiftBuffers1 (ReaderType readerType) { var logBuffer = logBuffers[i]; var line = logBuffer.GetLineMemoryOfBlock(0); - Assert.That(line.HasValue, Is.True); + if (!line.HasValue) + { + Assert.Fail("Expected first block line to be present."); + } + Assert.That(line.Value.FullLine.Span.Contains(enumerator.Current.AsSpan(), StringComparison.Ordinal)); _ = enumerator.MoveNext(); } @@ -125,7 +130,12 @@ public void TestShiftBuffers1 (ReaderType readerType) { var logBuffer = logBuffers[i]; var line = logBuffer.GetLineMemoryOfBlock(0); - Assert.That(line.HasValue, Is.True); + + if (!line.HasValue) + { + Assert.Fail("Expected first block line to be present."); + } + Assert.That(line.Value.FullLine.Span.Contains(enumerator.Current.AsSpan(), StringComparison.Ordinal)); } diff --git a/src/tools/LogRotator/LogRotator.cs b/src/tools/LogRotator/LogRotator.cs index 038dcd2b..611dc47a 100644 --- a/src/tools/LogRotator/LogRotator.cs +++ b/src/tools/LogRotator/LogRotator.cs @@ -2,6 +2,7 @@ const string baseDir = "logs"; const string baseName = "engine.log"; +var safeBaseName = Path.GetFileName(baseName); const int maxBackups = 6; const int linesPerFile = 50; @@ -9,11 +10,11 @@ // Create initial set of files Console.WriteLine($"Creating initial files in '{Path.GetFullPath(baseDir)}'..."); -WriteLogFile(Path.Combine(baseDir, baseName), 0); +WriteLogFile(Path.Join(baseDir, safeBaseName), 0); for (var i = 1; i <= maxBackups; i++) { - WriteLogFile(Path.Combine(baseDir, $"{baseName}.{i}"), i); + WriteLogFile(Path.Join(baseDir, $"{safeBaseName}.{i}"), i); } PrintFiles(); @@ -41,41 +42,41 @@ Console.WriteLine($"\n--- Rotation #{rotationCount} ---"); // Delete the oldest file (simulates maxBackups limit) - var oldest = Path.Combine(baseDir, $"{baseName}.{maxBackups}"); + var oldest = Path.Join(baseDir, $"{safeBaseName}.{maxBackups}"); if (File.Exists(oldest)) { File.Delete(oldest); - Console.WriteLine($" Deleted: {baseName}.{maxBackups}"); + Console.WriteLine($" Deleted: {safeBaseName}.{maxBackups}"); } // Shift all numbered files up by one for (var i = maxBackups - 1; i >= 1; i--) { - var src = Path.Combine(baseDir, $"{baseName}.{i}"); - var dst = Path.Combine(baseDir, $"{baseName}.{i + 1}"); + var src = Path.Join(baseDir, $"{safeBaseName}.{i}"); + var dst = Path.Join(baseDir, $"{safeBaseName}.{i + 1}"); if (File.Exists(src)) { File.Move(src, dst); - Console.WriteLine($" Renamed: {baseName}.{i} -> {baseName}.{i + 1}"); + Console.WriteLine($" Renamed: {safeBaseName}.{i} -> {safeBaseName}.{i + 1}"); } } // Rename current log to .1 - var current = Path.Combine(baseDir, baseName); - var first = Path.Combine(baseDir, $"{baseName}.1"); + var current = Path.Join(baseDir, safeBaseName); + var first = Path.Join(baseDir, $"{safeBaseName}.1"); if (File.Exists(current)) { File.Move(current, first); - Console.WriteLine($" Renamed: {baseName} -> {baseName}.1"); + Console.WriteLine($" Renamed: {safeBaseName} -> {safeBaseName}.1"); } // Create empty file first (like real log frameworks do), so LogExpert detects // newSize < oldSize and triggers ShiftBuffers() File.Create(current).Dispose(); - Console.WriteLine($" Created: {baseName} (empty - triggers rollover detection)"); + Console.WriteLine($" Created: {safeBaseName} (empty - triggers rollover detection)"); PrintFiles(); @@ -84,7 +85,7 @@ Thread.Sleep(2000); WriteLogFile(current, maxBackups + rotationCount); - Console.WriteLine($" Wrote content to {baseName}"); + Console.WriteLine($" Wrote content to {safeBaseName}"); PrintFiles(); Console.WriteLine("\nPress ENTER for next rotation, Q to quit."); @@ -104,7 +105,7 @@ static void PrintFiles() { Console.WriteLine("\nCurrent files on disk:"); - foreach (var f in Directory.GetFiles(baseDir, $"{baseName}*").OrderBy(f => f)) + foreach (var f in Directory.GetFiles(baseDir, $"{safeBaseName}*").OrderBy(f => f)) { Console.WriteLine($" {Path.GetFileName(f)} ({new FileInfo(f).Length} bytes)"); } From ddd8de1be79c955a83fd9936e5bfa7ce5c8edb6e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Apr 2026 19:54:00 +0000 Subject: [PATCH 22/24] chore: update plugin hashes [skip ci] --- .../PluginHashGenerator.Generated.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index 2cdfbff8..98722b67 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2026-04-14 23:54:32 UTC + /// Generated: 2026-04-16 19:53:58 UTC /// Configuration: Release /// Plugin count: 22 /// @@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "124A98DE9C460F57BE4692AF23090212BDAB5988765073DB0368678A4C865773", + ["AutoColumnizer.dll"] = "C4A1113D249C7D5C7B6B3F108D61926839DE8EBF85AE6CB6A107CCC03DF43BC3", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "A149CBD0B8CBB5E2CDA0D743DF8DF72A9CF200870A043BBA1CCB1D7382A519A1", - ["CsvColumnizer.dll (x86)"] = "A149CBD0B8CBB5E2CDA0D743DF8DF72A9CF200870A043BBA1CCB1D7382A519A1", - ["DefaultPlugins.dll"] = "EC6A52A622F016AB543A2A7B45BBEDEC33EF400BAD4CD9084B2257A923E3AEED", - ["FlashIconHighlighter.dll"] = "C29627F6CED75FFBA2A7AD9D73AD5F94CDC784E998F9C07617B1EE70E3DD26EF", - ["GlassfishColumnizer.dll"] = "697C4EE7DAEACCCFE2B95B424CE5E408DA32FB616F088CC5B89C37B2C7BBD2AD", - ["JsonColumnizer.dll"] = "9AE0842AE7EFF9501CB5FD458989E25FAFC521A2F2E3A081E63999873D400AE7", - ["JsonCompactColumnizer.dll"] = "B23A3CB9E777D52E6E16354E932F00C128AF64A4902FA0FD0C47F81F7532C5EC", - ["Log4jXmlColumnizer.dll"] = "5309B51357477763D742B1410F1D4CDAB3E07E446773153FBE271DB2E934D3D4", - ["LogExpert.Core.dll"] = "4A795F3C3084606EC84ABCFE151AE088EA0F52B5A6789318428D5FCC4FBE049A", - ["LogExpert.Resources.dll"] = "AC5AF7E66CAECC43586F88A1BD8F8A4498404831AD4F3136B6FA950FF1924257", + ["CsvColumnizer.dll"] = "C153444B8022FFB2C0C53BCD09E438BA727C1F6DB391650093DC277EF20EC0E1", + ["CsvColumnizer.dll (x86)"] = "C153444B8022FFB2C0C53BCD09E438BA727C1F6DB391650093DC277EF20EC0E1", + ["DefaultPlugins.dll"] = "5989312E8FB21850CFF757E858C0418A2F96E2B1BE43BB56A1CF22EFDC94B3B5", + ["FlashIconHighlighter.dll"] = "95B6CE5394068FEF4815E4277EBFF078D4C3471FA69588B24C157DE2B2BA33A3", + ["GlassfishColumnizer.dll"] = "CC3229EF363E9CA904E1E755DA98BC2C8B8785E338F34C6B0B0916C4E612D9D3", + ["JsonColumnizer.dll"] = "E688C5AAE06DB272461D1C0283035F061AD8DA79970C5513B3345CF67192FD8B", + ["JsonCompactColumnizer.dll"] = "7BFCFD4386E8F73BBF33840CFE2E54691E960A85DBB9E69214DCEB9D547EB283", + ["Log4jXmlColumnizer.dll"] = "43AE954C3ABC9EA133283F6B5334A5D9678E89D98642E41741A72366D28EDB83", + ["LogExpert.Core.dll"] = "36C4312EC02F656657951D4C052084171ACD7801804CD22A208376794D99DDDD", + ["LogExpert.Resources.dll"] = "D58BC133FE8A309B98ABABC004EFE4E9B6C12583321A8BFBD6BED7D8DDA1EEC5", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", ["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", - ["RegexColumnizer.dll"] = "FB84AA7E28E457E313D1FC3EAEEC566E1F5C07E652222A6A6DBBFB022F08AB0F", - ["SftpFileSystem.dll"] = "3250E5152CEB939C6F52C9237936FB840F451AD86B9A316E632D9671E4E88148", - ["SftpFileSystem.dll (x86)"] = "06ABE273490359EE50CA0B130B14B4BC57FA8CA8D8E1707CB6887F87F23CC9A4", - ["SftpFileSystem.Resources.dll"] = "4597034DB1E07AA1799E614AB1DE97DA2A043A27EBDBD67C95494C1C1D7455B0", - ["SftpFileSystem.Resources.dll (x86)"] = "4597034DB1E07AA1799E614AB1DE97DA2A043A27EBDBD67C95494C1C1D7455B0", + ["RegexColumnizer.dll"] = "9C4886E642A711AE3F590F2F2FF88802A609A8A358A7E0F11F1C74A82B09F874", + ["SftpFileSystem.dll"] = "1134EAB54CC4E3031C3DCA1B942FE3BF7E4376B9C1B5EB7BDA5CAD07CA711994", + ["SftpFileSystem.dll (x86)"] = "4113B346A7B592F3E8F7F8DC6736A9F15281CD3F3C5FE4E9EEAD721A54D519CC", + ["SftpFileSystem.Resources.dll"] = "E27E94D71625E68A10A02E577EA7D2ED7D4DF8CA24B2FC2070C8AC0E34B7825D", + ["SftpFileSystem.Resources.dll (x86)"] = "E27E94D71625E68A10A02E577EA7D2ED7D4DF8CA24B2FC2070C8AC0E34B7825D", }; } From f29523ba1ceedf11bde3d2d8af53e1bcfbfb30ff Mon Sep 17 00:00:00 2001 From: Hirogen Date: Thu, 16 Apr 2026 21:55:36 +0200 Subject: [PATCH 23/24] fix for warning --- src/LogExpert.Tests/BufferShiftTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/LogExpert.Tests/BufferShiftTest.cs b/src/LogExpert.Tests/BufferShiftTest.cs index 7f82e7e6..b98367ec 100644 --- a/src/LogExpert.Tests/BufferShiftTest.cs +++ b/src/LogExpert.Tests/BufferShiftTest.cs @@ -119,6 +119,7 @@ public void TestShiftBuffers1 (ReaderType readerType) if (!line.HasValue) { Assert.Fail("Expected first block line to be present."); + continue; } Assert.That(line.Value.FullLine.Span.Contains(enumerator.Current.AsSpan(), StringComparison.Ordinal)); @@ -134,6 +135,7 @@ public void TestShiftBuffers1 (ReaderType readerType) if (!line.HasValue) { Assert.Fail("Expected first block line to be present."); + continue; } Assert.That(line.Value.FullLine.Span.Contains(enumerator.Current.AsSpan(), StringComparison.Ordinal)); From fa1c5b8eaf2d384447aa956973c69aa8cca0dd28 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Apr 2026 19:58:25 +0000 Subject: [PATCH 24/24] chore: update plugin hashes [skip ci] --- .../PluginHashGenerator.Generated.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index 98722b67..72fac169 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2026-04-16 19:53:58 UTC + /// Generated: 2026-04-16 19:58:24 UTC /// Configuration: Release /// Plugin count: 22 /// @@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "C4A1113D249C7D5C7B6B3F108D61926839DE8EBF85AE6CB6A107CCC03DF43BC3", + ["AutoColumnizer.dll"] = "0B08628655B0073E6A6FB53DC19662526E5DE51753579E803C97B09FAAC76C4E", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "C153444B8022FFB2C0C53BCD09E438BA727C1F6DB391650093DC277EF20EC0E1", - ["CsvColumnizer.dll (x86)"] = "C153444B8022FFB2C0C53BCD09E438BA727C1F6DB391650093DC277EF20EC0E1", - ["DefaultPlugins.dll"] = "5989312E8FB21850CFF757E858C0418A2F96E2B1BE43BB56A1CF22EFDC94B3B5", - ["FlashIconHighlighter.dll"] = "95B6CE5394068FEF4815E4277EBFF078D4C3471FA69588B24C157DE2B2BA33A3", - ["GlassfishColumnizer.dll"] = "CC3229EF363E9CA904E1E755DA98BC2C8B8785E338F34C6B0B0916C4E612D9D3", - ["JsonColumnizer.dll"] = "E688C5AAE06DB272461D1C0283035F061AD8DA79970C5513B3345CF67192FD8B", - ["JsonCompactColumnizer.dll"] = "7BFCFD4386E8F73BBF33840CFE2E54691E960A85DBB9E69214DCEB9D547EB283", - ["Log4jXmlColumnizer.dll"] = "43AE954C3ABC9EA133283F6B5334A5D9678E89D98642E41741A72366D28EDB83", - ["LogExpert.Core.dll"] = "36C4312EC02F656657951D4C052084171ACD7801804CD22A208376794D99DDDD", - ["LogExpert.Resources.dll"] = "D58BC133FE8A309B98ABABC004EFE4E9B6C12583321A8BFBD6BED7D8DDA1EEC5", + ["CsvColumnizer.dll"] = "92FD615A78C97D08286427E40D32C6081DFEFEC029F07A7E8BDF25199892883F", + ["CsvColumnizer.dll (x86)"] = "92FD615A78C97D08286427E40D32C6081DFEFEC029F07A7E8BDF25199892883F", + ["DefaultPlugins.dll"] = "87379D1A506345225623095555C02648AF771222797F3C55FB781286E31A19A6", + ["FlashIconHighlighter.dll"] = "1B8E7C9D4E3857B02B5BEF5F2D364860C6588E1FB0A01E955430D5A801AF0590", + ["GlassfishColumnizer.dll"] = "C0809AE0789B1D5C62EA50566DA60AF2EE0E4F08E22812F012185E4882409E5E", + ["JsonColumnizer.dll"] = "53A9B10E9F1E1A65C3C6B486232558941BDC368C4400687F963F2C19FDADF704", + ["JsonCompactColumnizer.dll"] = "41D19FDFDD28D535F540C98FDB783F472059DD8CF8EBFBE69E7BF55C9039B5B4", + ["Log4jXmlColumnizer.dll"] = "F7EBF4DA582A3A50A11B2DB738EE36B47DBE81F4DFD666476D516BEDCD46238F", + ["LogExpert.Core.dll"] = "8A14498D18E7109D0C38982A029CF60E53722968365A9A7CF679CF9BE3BE33F7", + ["LogExpert.Resources.dll"] = "7C4B4BE07D7929808B73059C3290AE831AC8BEEAC2ACE8F741F38034B3AB7D80", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", ["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", - ["RegexColumnizer.dll"] = "9C4886E642A711AE3F590F2F2FF88802A609A8A358A7E0F11F1C74A82B09F874", - ["SftpFileSystem.dll"] = "1134EAB54CC4E3031C3DCA1B942FE3BF7E4376B9C1B5EB7BDA5CAD07CA711994", - ["SftpFileSystem.dll (x86)"] = "4113B346A7B592F3E8F7F8DC6736A9F15281CD3F3C5FE4E9EEAD721A54D519CC", - ["SftpFileSystem.Resources.dll"] = "E27E94D71625E68A10A02E577EA7D2ED7D4DF8CA24B2FC2070C8AC0E34B7825D", - ["SftpFileSystem.Resources.dll (x86)"] = "E27E94D71625E68A10A02E577EA7D2ED7D4DF8CA24B2FC2070C8AC0E34B7825D", + ["RegexColumnizer.dll"] = "AEFA75BB2DC6DA4737DE966C05B15129EC0B058DBD49FBD7CBAA5F7DDF555406", + ["SftpFileSystem.dll"] = "10D577087C1E64AAA038DCB45E9557142846D47F87422D308882C2C3D7A44375", + ["SftpFileSystem.dll (x86)"] = "E52F4EF64341540A259660401D36E5DC24A75F976D4EE9D466DC174D049824B5", + ["SftpFileSystem.Resources.dll"] = "F663FEF53804EE82D60CD115B172B525C50FE4D3811794AAA71AFC22E61387DB", + ["SftpFileSystem.Resources.dll (x86)"] = "F663FEF53804EE82D60CD115B172B525C50FE4D3811794AAA71AFC22E61387DB", }; }