Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 12 additions & 23 deletions src/ColumnizerLib/LogLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,19 @@ namespace ColumnizerLib;
/// Represents a single log line, including its full text and line number.
/// </summary>
/// <remarks>
/// <para>
/// <b>Purpose:</b> <br/>
/// The <c>LogLine</c> 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.
/// </para>
/// <para>
/// <b>Usage:</b> <br/>
/// This struct implements the <see cref="ILogLineMemory"/> interface, allowing it to be used wherever an <c>ILogLineMemory</c>
/// is expected. It provides value semantics and is intended to be lightweight and efficiently passed by value.
/// </para>
/// <para>
/// <b>Relationship to ILogLineMemory:</b> <br/>
/// <c>LogLine</c> is a concrete, immutable implementation of the <see cref="ILogLineMemory"/> interface, providing
/// properties for the full line text and its line number.
/// </para>
/// <para>
/// <b>Why struct instead of record:</b> <br/>
/// A <c>struct</c> is preferred over a <c>record</c> 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 <c>record</c> implementation
/// was replaced to better align with these performance and semantic requirements.
/// </para>
/// <para> <b> Purpose:</b> <br/> The <c> LogLine</c> 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. </para>
/// <para> <b> Usage:</b> <br/> This struct implements the
/// <see cref="ILogLineMemory"/> interface, allowing it to be used wherever an <c> ILogLineMemory</c> is expected. It
/// provides value semantics and is intended to be lightweight and efficiently passed by value. </para> <para> <b>
/// Relationship to ILogLineMemory:</b> <br/> <c> LogLine</c> is a concrete, immutable implementation of the
/// <see cref="ILogLineMemory"/> interface, providing properties for the full line text and its line number. </para>
/// This is a readonly record struct implementing
/// <see cref="ILogLineMemory"/>. Stored inline in <c> List&lt;LogLine&gt;</c> to avoid boxing and heap allocation.
/// Boxing occurs only when returned through the <c> ILogLineMemory</c> interface boundary.
/// </remarks>
public class LogLine : ILogLineMemory
public readonly record struct LogLine : ILogLineMemory
{
public int LineNumber { get; }

Expand Down
95 changes: 78 additions & 17 deletions src/LogExpert.Core/Classes/Log/LogBuffer.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Buffers;

using ColumnizerLib;

using NLog;
Expand All @@ -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<long> _filePositions = []; // file position for every line
private readonly List<long> _filePositions; // file position for every line
#endif

private readonly List<ILogLineMemory> _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<LogLine>.Shared.Rent(maxLines);
_lineArrayLength = _lineArray.Length;
#if DEBUG
_filePositions = new(MAX_LINES);
#endif
}

#endregion
Expand All @@ -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;
Expand All @@ -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;
}

/// <summary>
/// Prepares the buffer for reuse from the pool.
/// </summary>
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<LogLine>.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<LogLine>.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;
}

/// <summary>
/// Acquires the content lock. The caller MUST call <see cref="ReleaseContentLock"/> in a finally block.
/// </summary>
public void AcquireContentLock (ref bool lockTaken)
{
_contentLock.Enter(ref lockTaken);
}

/// <summary>
/// Releases the content lock previously acquired via <see cref="AcquireContentLock"/>.
/// </summary>
public void ReleaseContentLock ()
{
return num < _lineList.Count && num >= 0
? _lineList[num]
: null;
_contentLock.Exit(useMemoryBarrier: false);
}

#endregion
Expand Down
14 changes: 6 additions & 8 deletions src/LogExpert.Core/Classes/Log/LogBufferCacheEntry.cs
Original file line number Diff line number Diff line change
@@ -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();
}
Expand All @@ -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
Expand Down
40 changes: 40 additions & 0 deletions src/LogExpert.Core/Classes/Log/LogBufferPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Collections.Concurrent;

using ColumnizerLib;

namespace LogExpert.Core.Classes.Log;

public sealed class LogBufferPool (int maxSize)
{
private readonly ConcurrentBag<LogBuffer> _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);
}

/// <summary>
/// Returns a <see cref="LogBuffer"/> to the pool for reuse.
/// </summary>
/// <remarks>
/// Disposing the buffer's content is handled by this method, so callers should not dispose the buffer themselves.
/// </remarks>
/// <param name="buffer">The buffer to return.</param>
public void Return (LogBuffer buffer)
{
ArgumentNullException.ThrowIfNull(buffer);

buffer.DisposeContent();
if (_pool.Count < _maxSize)
{
_pool.Add(buffer);
}
}
}
Loading
Loading