diff --git a/build/_build.csproj b/build/_build.csproj
index d560f00d..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/ColumnizerLib/LogLine.cs b/src/ColumnizerLib/LogLine.cs
index 5f7a9fd4..c68b28e6 100644
--- a/src/ColumnizerLib/LogLine.cs
+++ b/src/ColumnizerLib/LogLine.cs
@@ -4,30 +4,19 @@ 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 class LogLine : ILogLineMemory
+public readonly record struct LogLine : ILogLineMemory
{
public int LineNumber { get; }
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/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/LogBuffer.cs b/src/LogExpert.Core/Classes/Log/LogBuffer.cs
index ae34615d..4c4f5578 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;
@@ -8,29 +10,33 @@ public class LogBuffer
{
#region Fields
+ private SpinLock _contentLock = new(enableThreadOwnerTracking: false);
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 LogLine[] _lineArray;
+ private int _lineArrayLength; // capacity of the rented array
private int MAX_LINES = 500;
- private long _size;
#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;
+ _lineArray = ArrayPool.Shared.Rent(maxLines);
+ _lineArrayLength = _lineArray.Length;
+#if DEBUG
+ _filePositions = new(MAX_LINES);
+#endif
}
#endregion
@@ -43,18 +49,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;
@@ -75,36 +81,91 @@ public long Size
#region Public methods
- public void AddLine (ILogLineMemory lineMemory, long filePos)
+ public void AddLine (LogLine lineMemory, long filePos)
{
- _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);
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();
+ if (_lineArray != null)
+ {
+ Array.Clear(_lineArray, 0, LineCount);
+ ArrayPool.Shared.Return(_lineArray);
+ _lineArray = null;
+ LineCount = 0;
+ }
+
IsDisposed = true;
#if DEBUG
DisposeCount++;
#endif
}
- public ILogLineMemory GetLineMemoryOfBlock (int num)
+ public LogLine? GetLineMemoryOfBlock (int num)
+ {
+ return num < LineCount && num >= 0
+ ? _lineArray[num]
+ : 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 ()
{
- return num < _lineList.Count && num >= 0
- ? _lineList[num]
- : null;
+ _contentLock.Exit(useMemoryBarrier: false);
}
#endregion
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/LogBufferPool.cs b/src/LogExpert.Core/Classes/Log/LogBufferPool.cs
new file mode 100644
index 00000000..b7bbed59
--- /dev/null
+++ b/src/LogExpert.Core/Classes/Log/LogBufferPool.cs
@@ -0,0 +1,40 @@
+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);
+ }
+
+ ///
+ /// 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);
+
+ 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 ad2b43ce..be8df3ff 100644
--- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs
+++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs
@@ -1,5 +1,5 @@
+using System.Collections.Concurrent;
using System.Globalization;
-using System.Runtime.InteropServices;
using System.Text;
using ColumnizerLib;
@@ -30,32 +30,36 @@ 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);
- private readonly ReaderWriterLockSlim _disposeLock = new(LockRecursionPolicy.SupportsRecursion);
- private readonly ReaderWriterLockSlim _lruCacheDictLock = new(LockRecursionPolicy.SupportsRecursion);
+
+ 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 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 IList _logFileInfoList = [];
- private Dictionary _lruCacheDict;
+ private ConcurrentDictionary _lruCacheDict;
private bool _shouldStop;
private bool _disposed;
private ILogFileInfo _watchedILogFileInfo;
- private volatile int _lastBufferIndex = -1;
+ private volatile bool _isLineCountDirty = true;
+
+ private volatile bool _isFailModeCheckCallPending;
+ private volatile bool _isFastFailOnGetLogLine;
+ private readonly ThreadLocal _lastBufferIndex = new(() => -1);
#endregion
@@ -98,6 +102,8 @@ private LogfileReader (string[] fileNames, EncodingOptions encodingOptions, bool
_pluginRegistry = pluginRegistry;
_disposed = false;
+ _bufferPool = new LogBufferPool(_max_buffers * 2);
+
InitLruBuffers();
ILogFileInfo fileInfo = null;
@@ -123,6 +129,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();
}
@@ -157,7 +175,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;
}
@@ -165,12 +183,17 @@ public int LineCount
else
{
AcquireBufferListReaderLock();
- foreach (var buffer in _bufferList)
+ try
{
- field += buffer.LineCount;
+ foreach (var buffer in _bufferList.Values)
+ {
+ field += buffer.LineCount;
+ }
+ }
+ finally
+ {
+ ReleaseBufferListReaderLock();
}
-
- ReleaseBufferListReaderLock();
}
_isLineCountDirty = false;
@@ -197,13 +220,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.
///
@@ -249,12 +270,10 @@ private EncodingOptions EncodingOptions
//TODO: Make this private
public void ReadFiles ()
{
- _lastProgressUpdate = DateTime.MinValue;
+ _lastProgressUpdate = 0;
FileSize = 0;
LineCount = 0;
- //this.lastReturnedLine = "";
- //this.lastReturnedLineNum = -1;
- //this.lastReturnedLineNumForBuffer = -1;
+
_isDeleted = false;
ClearLru();
AcquireBufferListWriterLock();
@@ -264,7 +283,6 @@ public void ReadFiles ()
{
foreach (var info in _logFileInfoList)
{
- //info.OpenFile();
ReadToBufferList(info, 0, LineCount);
}
@@ -313,157 +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);
- var node = fileNameList.Find(fileName);
- if (node == null)
- {
- _logger.Warn(CultureInfo.InvariantCulture, "File {0} not found", fileName);
- continue;
- }
+ IList lostILogFileInfoList = [];
+ IList readNewILogFileInfoList = [];
+ IList newFileInfoList = [];
+
+ var enumerator = _logFileInfoList.GetEnumerator();
- if (node.Previous != null)
+ 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)
+ var logFileInfo = enumerator.Current;
+ var fileName = logFileInfo.FullName;
+ _logger.Debug(CultureInfo.InvariantCulture, "Testing file {0}", fileName);
+
+ 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);
-#if DEBUG // for better overview in logfile:
- //ILogFileInfo newILogFileInfo = new ILogFileInfo(fileName);
- //ReplaceBufferInfos(ILogFileInfo, newILogFileInfo);
-#endif
- }
- }
+ _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;
+ }
+ }
- AcquireLruCacheDictWriterLock();
+ _logger.Info(CultureInfo.InvariantCulture, "Adjusting StartLine values in {0} buffers by offset {1}", _bufferList.Count, offset);
+ foreach (var buffer in _bufferList.Values.ToList())
+ {
+ SetNewStartLineForBuffer(buffer, buffer.StartLine - offset);
+ }
- foreach (var logFileInfo in lostILogFileInfoList)
- {
- //this.ILogFileInfoList.Remove(logFileInfo);
- var lastBuffer = DeleteBuffersForInfo(logFileInfo, false);
- if (lastBuffer != null)
+#if DEBUG
+ if (_bufferList.Values.Count > 0)
{
- offset += lastBuffer.StartLine + lastBuffer.LineCount;
+ _logger.Debug(CultureInfo.InvariantCulture, "First buffer now has StartLine {0}", _bufferList.Values[0].StartLine);
}
+#endif
}
- _logger.Info(CultureInfo.InvariantCulture, "Adjusting StartLine values in {0} buffers by offset {1}", _bufferList.Count, offset);
- foreach (var buffer in _bufferList)
- {
- SetNewStartLineForBuffer(buffer, buffer.StartLine - offset);
- }
+ // 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");
- ReleaseLRUCacheDictWriterLock();
-#if DEBUG
- if (_bufferList.Count > 0)
+ foreach (var iLogFileInfo in readNewILogFileInfoList)
{
- _logger.Debug(CultureInfo.InvariantCulture, "First buffer now has StartLine {0}", _bufferList[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");
- AcquireLruCacheDictWriterLock();
+ _logger.Info(CultureInfo.InvariantCulture, "Deleting buffers for the watched file");
- foreach (var iLogFileInfo in readNewILogFileInfoList)
- {
- DeleteBuffersForInfo(iLogFileInfo, true);
- //this.ILogFileInfoList.Remove(logFileInfo);
- }
+ DeleteBuffersForInfo(_watchedILogFileInfo, true);
- _logger.Info(CultureInfo.InvariantCulture, "Deleting buffers for the watched file");
+ _logger.Info(CultureInfo.InvariantCulture, "Re-Reading files");
- DeleteBuffersForInfo(_watchedILogFileInfo, true);
- ReleaseLRUCacheDictWriterLock();
+ 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)
- {
- //logFileInfo.OpenFile();
- ReadToBufferList(iLogFileInfo, 0, LineCount);
- //this.ILogFileInfoList.Add(logFileInfo);
- newFileInfoList.Add(iLogFileInfo);
+ ReadToBufferList(_watchedILogFileInfo, 0, LineCount);
}
- //this.watchedILogFileInfo = this.ILogFileInfoList[this.ILogFileInfoList.Count - 1];
- _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;
}
///
@@ -507,18 +518,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.
///
@@ -564,16 +563,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, _) = GetBufferForLineWithIndex(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
@@ -598,8 +619,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;
}
///
@@ -609,11 +629,8 @@ public string GetLogFileNameForLine (int lineNum)
///
public ILogFileInfo GetLogFileInfoForLine (int lineNum)
{
- AcquireBufferListReaderLock();
var logBuffer = GetBufferForLine(lineNum);
- var info = logBuffer?.FileInfo;
- ReleaseBufferListReaderLock();
- return info;
+ return logBuffer?.FileInfo;
}
///
@@ -625,24 +642,27 @@ public int GetNextMultiFileLine (int lineNum)
{
var result = -1;
AcquireBufferListReaderLock();
- var logBuffer = GetBufferForLine(lineNum);
- if (logBuffer != null)
+
+ try
{
- var index = _bufferList.IndexOf(logBuffer);
- if (index != -1)
+ 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;
}
}
}
}
+ finally
+ {
+ ReleaseBufferListReaderLock();
+ }
- ReleaseBufferListReaderLock();
return result;
}
@@ -663,24 +683,27 @@ public int GetPrevMultiFileLine (int lineNum)
{
var result = -1;
AcquireBufferListReaderLock();
- var logBuffer = GetBufferForLine(lineNum);
- if (logBuffer != null)
+
+ try
{
- var index = _bufferList.IndexOf(logBuffer);
- if (index != -1)
+ var (logBuffer, index) = GetBufferForLineWithIndex(lineNum);
+ if (logBuffer != null && index != -1)
{
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;
}
}
}
}
+ finally
+ {
+ ReleaseBufferListReaderLock();
+ }
- ReleaseBufferListReaderLock();
return result;
}
@@ -695,19 +718,25 @@ public int GetPrevMultiFileLine (int lineNum)
///
public int GetRealLineNumForVirtualLineNum (int lineNum)
{
- AcquireBufferListReaderLock();
- var logBuffer = GetBufferForLine(lineNum);
var result = -1;
- if (logBuffer != null)
+ AcquireBufferListReaderLock();
+ try
{
- logBuffer = GetFirstBufferForFileByLogBuffer(logBuffer);
+ 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;
}
@@ -737,28 +766,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();
}
@@ -769,12 +793,6 @@ public void StopMonitoring ()
public void StopMonitoringAsync ()
{
var task = Task.Run(StopMonitoring);
-
- //Thread stopperThread = new(new ThreadStart(StopMonitoring))
- //{
- // IsBackground = true
- //};
- //stopperThread.Start();
}
///
@@ -788,13 +806,12 @@ 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();
- AcquireDisposeWriterLock();
+ //AcquireDisposeWriterLock();
- foreach (var logBuffer in _bufferList)
+ foreach (var logBuffer in _bufferList.Values)
{
if (!logBuffer.IsDisposed)
{
@@ -805,12 +822,10 @@ public void DeleteAllContent ()
_lruCacheDict.Clear();
_bufferList.Clear();
- ReleaseDisposeWriterLock();
- ReleaseLRUCacheDictWriterLock();
+ //ReleaseDisposeWriterLock();
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));
}
///
@@ -818,7 +833,7 @@ public void DeleteAllContent ()
///
private void ClearBufferState ()
{
- _lastBufferIndex = -1;
+ _lastBufferIndex.Value = -1;
}
///
@@ -848,7 +863,7 @@ public IList GetLogFileInfoList ()
///
public IList GetBufferList ()
{
- return _bufferList;
+ return _bufferList.Values;
}
#endregion
@@ -868,22 +883,26 @@ public IList GetBufferList ()
public void LogBufferInfoForLine (int lineNum)
{
AcquireBufferListReaderLock();
- var buffer = GetBufferForLine(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, "-----------------------------------");
- 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();
}
///
@@ -897,10 +916,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);
@@ -908,10 +925,10 @@ public void LogBufferDiagnostic ()
long disposeSum = 0;
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];
- AcquireDisposeReaderLock();
+ var buffer = _bufferList.Values[i];
if (buffer.StartLine != lineNum)
{
_logger.Error("Start line of buffer is: {0}, expected: {1}", buffer.StartLine, lineNum);
@@ -923,7 +940,6 @@ public void LogBufferDiagnostic ()
disposeSum += buffer.DisposeCount;
maxDispose = Math.Max(maxDispose, buffer.DisposeCount);
minDispose = Math.Min(minDispose, buffer.DisposeCount);
- ReleaseDisposeReaderLock();
}
ReleaseBufferListReaderLock();
@@ -949,6 +965,88 @@ 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, _) = GetBufferForLineWithIndex(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.
@@ -958,42 +1056,61 @@ 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();
- var logBuffer = GetBufferForLine(lineNum);
- if (logBuffer == null)
+ if (_mmfReader != null && lineNum < _mmfReader.LineCount)
{
- ReleaseBufferListReaderLock();
- _logger.Error("Cannot find buffer for line {0}, file: {1}{2}", lineNum, _fileName, IsMultiFile ? " (MultiFile)" : "");
- return Task.FromResult(null);
+ var line = _mmfReader.GetLine(lineNum);
+ return new ValueTask(line);
}
- // disposeLock prevents that the garbage collector is disposing just in the moment we use the buffer
- AcquireDisposeLockUpgradableReadLock();
- if (logBuffer.IsDisposed)
- {
- UpgradeDisposeLockToWriterLock();
- lock (logBuffer.FileInfo)
+ AcquireBufferListReaderLock();
+ try
+ {
+ var (logBuffer, _) = GetBufferForLineWithIndex(lineNum);
+ if (logBuffer == null)
{
- ReReadBuffer(logBuffer);
+ _logger.Error("Cannot find buffer for line {0}, file: {1}{2}", lineNum, _fileName, IsMultiFile ? " (MultiFile)" : "");
+ return default;
}
- DowngradeDisposeLockFromWriterLock();
- }
+ var lockTaken = false;
+ try
+ {
+ logBuffer.AcquireContentLock(ref lockTaken);
- var line = logBuffer.GetLineMemoryOfBlock(lineNum - logBuffer.StartLine);
- ReleaseDisposeUpgradeableReadLock();
- ReleaseBufferListReaderLock();
+ if (logBuffer.IsDisposed)
+ {
+ lock (logBuffer.FileInfo)
+ {
+ ReReadBuffer(logBuffer);
+ }
+ }
- return Task.FromResult(line);
+ var line = logBuffer.GetLineMemoryOfBlock(lineNum - logBuffer.StartLine);
+ return line.HasValue
+ ? new ValueTask(line.Value)
+ : default;
+ }
+ finally
+ {
+ if (lockTaken)
+ {
+ logBuffer.ReleaseContentLock();
+ }
+ }
+ }
+ finally
+ {
+ ReleaseBufferListReaderLock();
+ }
}
///
@@ -1007,9 +1124,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);
}
///
@@ -1023,9 +1138,6 @@ private void InitLruBuffers ()
private void StartGCThread ()
{
_garbageCollectorTask = Task.Run(GarbageCollectorThreadProc, _cts.Token);
- //_garbageCollectorThread = new Thread(new ThreadStart(GarbageCollectorThreadProc));
- //_garbageCollectorThread.IsBackground = true;
- //_garbageCollectorThread.Start();
}
///
@@ -1039,9 +1151,6 @@ private void ResetBufferCache ()
{
FileSize = 0;
LineCount = 0;
- //this.lastReturnedLine = "";
- //this.lastReturnedLineNum = -1;
- //this.lastReturnedLineNumForBuffer = -1;
}
///
@@ -1049,15 +1158,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;
}
///
@@ -1093,7 +1195,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)
{
@@ -1117,32 +1219,32 @@ 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)
{
- foreach (var buffer in _bufferList)
+ foreach (var buffer in _bufferList.Values)
{
if (buffer.FileInfo.FullName.Equals(iLogFileInfo.FullName, StringComparison.Ordinal))
{
- lastRemovedBuffer = buffer;
+ lastRemovedInfo = (buffer.StartLine, buffer.LineCount);
deleteList.Add(buffer);
}
}
}
else
{
- foreach (var buffer in _bufferList)
+ foreach (var buffer in _bufferList.Values)
{
if (buffer.FileInfo == iLogFileInfo)
{
- lastRemovedBuffer = buffer;
+ lastRemovedInfo = (buffer.StartLine, buffer.LineCount);
deleteList.Add(buffer);
}
}
@@ -1151,18 +1253,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;
}
///
@@ -1176,10 +1292,9 @@ 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);
- _ = _bufferList.Remove(buffer);
+ _ = _lruCacheDict.TryRemove(buffer.StartLine, out _);
+ _ = _bufferList.Remove(buffer.StartLine);
}
///
@@ -1203,7 +1318,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;
@@ -1217,11 +1332,9 @@ 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;
UpgradeBufferlistLockToWriterLock();
@@ -1236,15 +1349,13 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start
}
else
{
- logBuffer = _bufferList[_bufferList.Count - 1];
+ logBuffer = _bufferList.Values[_bufferList.Count - 1];
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;
UpgradeBufferlistLockToWriterLock();
@@ -1258,15 +1369,23 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start
}
}
- AcquireDisposeLockUpgradableReadLock();
- if (logBuffer.IsDisposed)
+ var lockTaken = false;
+
+ try
{
- UpgradeDisposeLockToWriterLock();
- ReReadBuffer(logBuffer);
- DowngradeDisposeLockFromWriterLock();
+ logBuffer.AcquireContentLock(ref lockTaken);
+ if (logBuffer.IsDisposed)
+ {
+ ReReadBuffer(logBuffer);
+ }
+ }
+ finally
+ {
+ if (lockTaken)
+ {
+ logBuffer.ReleaseContentLock();
+ }
}
-
- ReleaseDisposeUpgradeableReadLock();
}
}
finally
@@ -1281,7 +1400,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 +1413,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;
}
@@ -1303,8 +1422,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)
{
@@ -1317,12 +1436,10 @@ 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;
AcquireBufferListWriterLock();
@@ -1346,12 +1463,12 @@ 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++;
- (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;
@@ -1392,8 +1509,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);
- //UpdateLru(logBuffer);
+ _bufferList[logBuffer.StartLine] = logBuffer;
UpdateLruCache(logBuffer);
}
@@ -1408,60 +1524,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();
}
///
@@ -1472,20 +1540,15 @@ 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))
- {
- _ = _lruCacheDict.Remove(logBuffer.StartLine);
- logBuffer.StartLine = newLineNum;
- LogBufferCacheEntry cacheEntry = new()
- {
- LogBuffer = logBuffer
- };
- _lruCacheDict.Add(logBuffer.StartLine, cacheEntry);
- }
- else
+ 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);
}
}
@@ -1504,7 +1567,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)
{
@@ -1515,35 +1577,32 @@ 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)
- {
- if (!useSorterList.ContainsKey(entry.LastUseTimeStamp))
- {
- useSorterList.Add(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));
- // remove first entries (least usage)
- AcquireDisposeWriterLock();
- for (var i = 0; i < diff; ++i)
+ for (var i = 0; i < diff && i < entries.Length; ++i)
{
- if (i >= useSorterList.Count)
+ var kvp = entries[i];
+ if (_lruCacheDict.TryRemove(kvp.Key, out var removed))
{
- break;
+ var lockTaken = false;
+ try
+ {
+ removed.LogBuffer.AcquireContentLock(ref lockTaken);
+ _bufferPool.Return(removed.LogBuffer);
+ }
+ finally
+ {
+ if (lockTaken)
+ {
+ removed.LogBuffer.ReleaseContentLock();
+ }
+ }
}
-
- var startLine = useSorterList.Values[i];
- var entry = _lruCacheDict[startLine];
- _lruCacheDict.Remove(startLine);
- entry.LogBuffer.DisposeContent();
}
-
- ReleaseDisposeWriterLock();
}
- ReleaseLRUCacheDictWriterLock();
#if DEBUG
if (diff > 0)
{
@@ -1562,17 +1621,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();
@@ -1590,16 +1649,24 @@ private void GarbageCollectorThreadProc ()
private void ClearLru ()
{
_logger.Info(CultureInfo.InvariantCulture, "Clearing LRU cache.");
- AcquireLruCacheDictWriterLock();
- AcquireDisposeWriterLock();
foreach (var entry in _lruCacheDict.Values)
{
- entry.LogBuffer.DisposeContent();
+ var lockTaken = false;
+ try
+ {
+ entry.LogBuffer.AcquireContentLock(ref lockTaken);
+ _bufferPool.Return(entry.LogBuffer);
+ }
+ finally
+ {
+ if (lockTaken)
+ {
+ entry.LogBuffer.ReleaseContentLock();
+ }
+ }
}
_lruCacheDict.Clear();
- ReleaseDisposeWriterLock();
- ReleaseLRUCacheDictWriterLock();
_logger.Info(CultureInfo.InvariantCulture, "Clearing done.");
}
@@ -1619,9 +1686,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 +1701,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 +1711,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 +1723,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 +1733,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,159 +1758,128 @@ 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.
+ /// Core buffer lookup without acquiring _bufferListLock. The caller MUST already hold a read or write lock
+ /// on _bufferListLock.
///
- ///
- /// 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)
+ private (LogBuffer? Buffer, int Index) GetBufferForLineWithIndex (int lineNum)
{
#if DEBUG
+ Util.AssertTrue(
+ _bufferListLock.IsReadLockHeld || _bufferListLock.IsUpgradeableReadLockHeld || _bufferListLock.IsWriteLockHeld,
+ "No lock held for buffer list in GetBufferForLineWithIndex");
long startTime = Environment.TickCount;
#endif
+ var arr = _bufferList.Values;
+ var count = arr.Count;
- AcquireBufferListReaderLock();
- try
+ if (count == 0)
{
- var arr = CollectionsMarshal.AsSpan(_bufferList);
- var count = arr.Length;
+ return (null, -1);
+ }
- 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, lastIdx);
}
- // 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)
+ var next = arr[lastIdx + 1];
+ if ((uint)(lineNum - next.StartLine) < (uint)next.LineCount)
{
- UpdateLruCache(buf);
- 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)
- {
- _lastBufferIndex = lastIdx + 1;
- UpdateLruCache(next);
- return next;
- }
+ _lastBufferIndex.Value = lastIdx + 1;
+ UpdateLruCache(next);
+ return (next, lastIdx + 1);
}
+ }
- 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, lastIdx - 1);
}
}
+ }
- // 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, 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;
+ // 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, idx);
}
+ }
+#if DEBUG
+ long endTime = Environment.TickCount;
+ _logger.Debug($"GetBufferForLineWithIndex({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
{
-#if DEBUG
- long endTime = Environment.TickCount;
- //_logger.logDebug("getBufferForLine(" + lineNum + ") duration: " + ((endTime - startTime)) + " ms.");
-#endif
ReleaseBufferListReaderLock();
}
}
@@ -1875,14 +1911,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();
return null;
}
@@ -1890,15 +1923,14 @@ private LogBuffer GetFirstBufferForFileByLogBuffer (LogBuffer logBuffer)
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();
return resultBuffer;
}
@@ -1911,20 +1943,18 @@ private LogBuffer GetFirstBufferForFileByLogBuffer (LogBuffer logBuffer)
/// 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);
_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();
}
}
@@ -1938,15 +1968,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);
}
@@ -1967,7 +1991,6 @@ private void MonitorThreadProc ()
}
else
{
- oldSize = _fileLength;
FileChanged();
}
}
@@ -2030,6 +2053,8 @@ private void FileChanged ()
_logger.Info(CultureInfo.InvariantCulture, "file size changed. new size={0}, file: {1}", newSize, _fileName);
FireChangeEvent();
}
+
+ _mmfReader?.ExtendIndex();
}
///
@@ -2074,7 +2099,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));
@@ -2263,8 +2287,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());
}
///
@@ -2285,174 +2307,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 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.
- ///
- ///
- /// 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 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.
- ///
- ///
- /// 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 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.
- ///
- ///
- /// 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();
- }
-
- ///
- /// 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.
- ///
- ///
- /// 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();
- }
- }
-
- ///
- /// 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.
@@ -2484,41 +2338,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();
- }
- }
-
- ///
- /// 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.
///
@@ -2531,31 +2350,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.
- ///
- ///
- /// 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.
@@ -2620,6 +2414,8 @@ protected virtual void Dispose (bool disposing)
{
DeleteAllContent();
_cts.Dispose();
+ _lastBufferIndex.Dispose();
+ _mmfReader?.Dispose();
}
_disposed = true;
@@ -2721,4 +2517,4 @@ protected virtual void OnRespawned ()
}
#endregion Event Handlers
-}
+}
\ No newline at end of file
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();
+ }
+}
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/Config/Preferences.cs b/src/LogExpert.Core/Config/Preferences.cs
index a8aaa13d..a63e190c 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;
@@ -164,7 +160,7 @@ public List HilightGroupList
public string FontName { get; set; } = "Courier New";
- public float 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.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
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.Tests/BufferShiftTest.cs b/src/LogExpert.Tests/BufferShiftTest.cs
index 2baf9711..b98367ec 100644
--- a/src/LogExpert.Tests/BufferShiftTest.cs
+++ b/src/LogExpert.Tests/BufferShiftTest.cs
@@ -26,7 +26,11 @@ 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
+ //(pattern: *$J(.))
public void TestShiftBuffers1 (ReaderType readerType)
{
var linesPerFile = 10;
@@ -112,17 +116,29 @@ 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));
+ 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));
_ = enumerator.MoveNext();
}
- _ = enumerator.MoveNext();
// the last 2 files now contain the content of the previously watched file
for (; i < logBuffers.Count; ++i)
{
var logBuffer = logBuffers[i];
var line = logBuffer.GetLineMemoryOfBlock(0);
- Assert.That(line.FullLine.Span.Contains(enumerator.Current.AsSpan(), StringComparison.Ordinal));
+
+ 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));
}
oldCount = lil.Count;
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