Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 9 additions & 25 deletions src/LogExpert.Core/Classes/Log/LogfileReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,11 @@ public ILogLineMemory[] GetLogLineMemories (int startLine, int count)
var availableInBuffer = logBufferEntry.Buffer.LineCount - bufferOffset;
var toCopy = Math.Min(count - filled, availableInBuffer);

if (bufferOffset < 0 || toCopy <= 0)
{
break;
}

for (var i = 0; i < toCopy; i++)
{
result[filled + i] = logBufferEntry.Buffer.GetLineMemoryOfBlock(bufferOffset + i);
Expand Down Expand Up @@ -1256,16 +1261,9 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start

logBuffer.Size = filePos - logBuffer.StartPos;

// Detach char blocks from the reader's allocator and attach to the completed buffer.
// Detach char blocks from the reader and attach to the completed buffer.
// Must happen before Monitor.Exit so the buffer is still exclusively owned.
if (reader is PositionAwareStreamReaderSystem systemDetachBlockReader)
{
logBuffer.AttachCharBlocks(systemDetachBlockReader.BlockAllocator.DetachBlocks());
}
else if (reader is PositionAwareStreamReaderDirect directReader)
{
logBuffer.AttachCharBlocks(directReader.DetachBlocks());
}
logBuffer.AttachCharBlocks(reader.DetachCharBlocks());

Monitor.Exit(logBuffer);
try
Expand Down Expand Up @@ -1297,14 +1295,7 @@ private void ReadToBufferList (ILogFileInfo logFileInfo, long filePos, int start
logBuffer.Size = filePos - logBuffer.StartPos;

// Attach remaining blocks to the final buffer
if (reader is PositionAwareStreamReaderSystem systemDetachBlockReader2)
{
logBuffer.AttachCharBlocks(systemDetachBlockReader2.BlockAllocator.DetachBlocks());
}
else if (reader is PositionAwareStreamReaderDirect directReader)
{
logBuffer.AttachCharBlocks(directReader.DetachBlocks());
}
logBuffer.AttachCharBlocks(reader.DetachCharBlocks());
}
finally
{
Expand Down Expand Up @@ -1427,14 +1418,7 @@ private void ReReadBuffer (LogBuffer logBuffer)
}

// Attach char blocks from the reader to the re-read buffer
if (reader is PositionAwareStreamReaderSystem systemReader)
{
logBuffer.AttachCharBlocks(systemReader.BlockAllocator.DetachBlocks());
}
else if (reader is PositionAwareStreamReaderDirect directReader)
{
logBuffer.AttachCharBlocks(directReader.DetachBlocks());
}
logBuffer.AttachCharBlocks(reader.DetachCharBlocks());

if (maxLinesCount != logBuffer.LineCount)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Buffers;
using System.Text;

using LogExpert.Core.Classes.Log.Buffers;
using LogExpert.Core.Entities;
using LogExpert.Core.Interfaces;

Expand Down Expand Up @@ -156,17 +155,11 @@ public void ReturnMemory (ReadOnlyMemory<char> memory)
// Bulk return via DetachBlocks()/Dispose(). Individual return not needed.
}

/// <summary>
/// Gets the block allocator for compatibility with the DetachBlocks pattern.
/// This reader manages its own blocks directly rather than through CharBlockAllocator.
/// </summary>
public CharBlockAllocator? BlockAllocator => null;

/// <summary>
/// Detaches completed blocks (fully scanned) for transfer to the LogBuffer.
/// The current _readBlock (partially scanned) stays with the reader.
/// </summary>
public List<char[]> DetachBlocks ()
public List<char[]> DetachCharBlocks ()
{
// Nothing to detach: no completed blocks and no lines were scanned from the current block.
if (_completedBlocks.Count == 0 && _scanOffset == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ public class PositionAwareStreamReaderLegacy (Stream stream, EncodingOptions enc

#region Properties

public CharBlockAllocator BlockAllocator
private CharBlockAllocator BlockAllocator
{
get => field ??= new CharBlockAllocator();
private set;
set;
}

#endregion
Expand All @@ -47,7 +47,20 @@ public bool TryReadLine (out ReadOnlyMemory<char> lineMemory)

public void ReturnMemory (ReadOnlyMemory<char> memory)
{
// Bulk return via BlockAllocator.DetachBlocks() when the LogBuffer is evicted.
// Bulk return via DetachCharBlocks() when the LogBuffer is evicted, or Dispose().
}

public List<char[]> DetachCharBlocks () => BlockAllocator.DetachBlocks();

protected override void Dispose (bool disposing)
{
if (disposing)
{
BlockAllocator?.Dispose();
BlockAllocator = null;
}

base.Dispose(disposing);
}

public override string ReadLine ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ public PositionAwareStreamReaderSystem (Stream stream, EncodingOptions encodingO
#region Properties

/// <summary>
/// Gets or creates the block allocator used by this reader instance.
/// The caller can detach the blocks after reading a buffer's worth of lines.
/// The block allocator used by this reader instance. Blocks are handed to the owning buffer via
/// <see cref="DetachCharBlocks"/>; ownership of the allocator does not cross the reader seam.
/// </summary>
public CharBlockAllocator BlockAllocator
private CharBlockAllocator BlockAllocator
{
get => field ??= new CharBlockAllocator();
private set;
set;
}

#endregion
Expand Down Expand Up @@ -112,10 +112,12 @@ public bool TryReadLine (out ReadOnlyMemory<char> lineMemory)
/// </summary>
public void ReturnMemory (ReadOnlyMemory<char> memory)
{
// Bulk return via BlockAllocator.DetachBlocks() or Dispose().
// Bulk return via DetachCharBlocks() or Dispose().
// Individual per-line return is not needed with block-based allocation.
}

public List<char[]> DetachCharBlocks () => BlockAllocator.DetachBlocks();

#endregion

#region Private Methods
Expand Down
15 changes: 15 additions & 0 deletions src/LogExpert.Core/Interfaces/ILogStreamReaderMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,19 @@ public interface ILogStreamReaderMemory : ILogStreamReader
/// call this method multiple times for the same memory, but only the first call will have an effect.
/// </remarks>
void ReturnMemory (ReadOnlyMemory<char> memory);

/// <summary>
/// Detaches the completed character blocks that back the <see cref="ReadOnlyMemory{Char}"/> slices handed out by
/// <see cref="TryReadLine"/>, transferring their ownership to the caller (the <c>LogBuffer</c> that owns those
/// lines).
/// </summary>
/// <returns>
/// The list of blocks the reader has filled. The caller owns them until every slice into them is released, at
/// which point they are returned to the shared pool. Readers that have nothing to hand over return an empty list.
/// </returns>
/// <remarks>
/// Call this after a buffer's worth of lines has been read and before the reader continues filling the next
/// buffer. The reader retains any partially-scanned block so reading can continue seamlessly.
/// </remarks>
List<char[]> DetachCharBlocks ();
}
13 changes: 5 additions & 8 deletions src/LogExpert.Tests/StreamReaderTests/LogStreamReaderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public void TryReadLine_ReturnsBlockBackedMemory_NotStringBacked ()
}

[Test]
public void TryReadLine_BlockAllocatorHasBlocks_AfterReading ()
public void TryReadLine_DetachCharBlocks_ReturnsFilledBlocks_AfterReading ()
{
var text = "Line 1\nLine 2\nLine 3\n";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(text));
Expand All @@ -189,12 +189,12 @@ public void TryReadLine_BlockAllocatorHasBlocks_AfterReading ()
// Intentionally empty: consume all lines to advance reader state.
}

// The allocator should have at least 1 block
Assert.That(reader.BlockAllocator.BlockCount, Is.GreaterThanOrEqualTo(1));
// Reading rented at least one block, exposed only through the seam.
Assert.That(reader.DetachCharBlocks(), Has.Count.GreaterThanOrEqualTo(1));
}

[Test]
public void TryReadLine_DetachBlocks_TransfersOwnership ()
public void TryReadLine_DetachCharBlocks_TransfersOwnership ()
{
var text = "Line 1\nLine 2\nLine 3\n";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(text));
Expand All @@ -205,11 +205,8 @@ public void TryReadLine_DetachBlocks_TransfersOwnership ()
// Intentionally empty: consume all lines to advance reader state.
}

var blocks = reader.BlockAllocator.DetachBlocks();
var blocks = reader.DetachCharBlocks();
Assert.That(blocks, Has.Count.GreaterThanOrEqualTo(1));

// After detach, allocator should have a fresh block
Assert.That(reader.BlockAllocator.BlockCount, Is.EqualTo(1));
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,10 @@ public void ContentParity_LargeFile ()

#endregion

#region DetachBlocks
#region DetachCharBlocks

[Test]
public void DetachBlocks_ReturnsCompletedBlocks ()
public void DetachCharBlocks_ReturnsCompletedBlocks ()
{
// Create enough content to fill multiple blocks
var sb = new StringBuilder();
Expand All @@ -279,11 +279,11 @@ public void DetachBlocks_ReturnsCompletedBlocks ()
// Intentionally empty: consume all lines to advance reader state.
}

var blocks = reader.DetachBlocks();
var blocks = reader.DetachCharBlocks();
Assert.That(blocks.Count, Is.GreaterThan(0), "Should have completed blocks to detach");

// Second detach should be empty
var blocks2 = reader.DetachBlocks();
var blocks2 = reader.DetachCharBlocks();
Assert.That(blocks2.Count, Is.EqualTo(0));
}

Expand Down Expand Up @@ -506,7 +506,7 @@ public void TryReadLine_LineLongerThanBlockSize_WithMaxLineLengthTruncation ()
}

[Test]
public void TryReadLine_LineLongerThanBlockSize_DetachBlocksAfterEachLine ()
public void TryReadLine_LineLongerThanBlockSize_DetachCharBlocksAfterEachLine ()
{
RunWithTimeout(() =>
{
Expand All @@ -521,12 +521,12 @@ public void TryReadLine_LineLongerThanBlockSize_DetachBlocksAfterEachLine ()

Assert.That(reader.TryReadLine(out var line1), Is.True);
Assert.That(line1.Span.ToString(), Is.EqualTo("line1"));
var blocks1 = reader.DetachBlocks();
var blocks1 = reader.DetachCharBlocks();
Assert.That(blocks1.Count, Is.GreaterThan(0));

Assert.That(reader.TryReadLine(out var line2), Is.True);
Assert.That(line2.Length, Is.EqualTo(lineLength));
var blocks2 = reader.DetachBlocks();
var blocks2 = reader.DetachCharBlocks();
Assert.That(blocks2.Count, Is.GreaterThan(0));

Assert.That(reader.TryReadLine(out var line3), Is.True);
Expand All @@ -537,7 +537,7 @@ public void TryReadLine_LineLongerThanBlockSize_DetachBlocksAfterEachLine ()
}

[Test]
public void TryReadLine_LineLongerThanBlockSize_DetachBlocksWithLargeTail ()
public void TryReadLine_LineLongerThanBlockSize_DetachCharBlocksWithLargeTail ()
{
RunWithTimeout(() =>
{
Expand Down Expand Up @@ -573,14 +573,14 @@ public void TryReadLine_LineLongerThanBlockSize_DetachBlocksWithLargeTail ()
// Read and detach the prefix (resets reader to fresh BLOCK_SIZE buffer)
Assert.That(reader.TryReadLine(out var line1), Is.True);
Assert.That(line1.Span.ToString(), Is.EqualTo("prefix"));
_ = reader.DetachBlocks();
_ = reader.DetachCharBlocks();

// Read the long line — this grows the buffer to 131072
Assert.That(reader.TryReadLine(out var line2), Is.True);
Assert.That(line2.Length, Is.EqualTo(longLineLength));

// THIS IS THE CRITICAL CALL: DetachBlocks must handle tail > BLOCK_SIZE
var blocks = reader.DetachBlocks();
var blocks = reader.DetachCharBlocks();
Assert.That(blocks.Count, Is.GreaterThan(0));

// Verify we can still read subsequent lines correctly
Expand Down
32 changes: 16 additions & 16 deletions src/PluginRegistry/PluginHashGenerator.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,35 @@ public static partial class PluginValidator
{
/// <summary>
/// Gets pre-calculated SHA256 hashes for built-in plugins.
/// Generated: 2026-06-25 12:45:02 UTC
/// Generated: 2026-06-25 14:06:29 UTC
/// Configuration: Release
/// Plugin count: 21
/// </summary>
public static Dictionary<string, string> GetBuiltInPluginHashes()
{
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["AutoColumnizer.dll"] = "97CB14C8E6F7A714A74E92152CD947B2DF26742AC4C6F1550366B5C2586FB7F8",
["AutoColumnizer.dll"] = "E6BBCB994A140BC595E67D19A7E6E9975469718628C5BA8E423DCF13E076FA81",
["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
["CsvColumnizer.dll"] = "9C9F3914A4ECD1E9A45A0B87845E942162DB8203BCEF05FCA8E8E4A6C10D6AC2",
["CsvColumnizer.dll (x86)"] = "9C9F3914A4ECD1E9A45A0B87845E942162DB8203BCEF05FCA8E8E4A6C10D6AC2",
["DefaultPlugins.dll"] = "8EA6195A67840581DA4987B3B2893C68D8DC6DCA919B180896258386B9DB2713",
["FlashIconHighlighter.dll"] = "5095E95DD6AEED1747BB6CF259D550B2B3D530B6F925C47A7458ECEDBF2EBC60",
["GlassfishColumnizer.dll"] = "66730D4646D90568BFDCE500363CF2215B52F538B1E9172A78ECC6CAE0DD1C27",
["JsonColumnizer.dll"] = "7C064A5DE3A970C79518280D43E8C041322B4FBC27D0D8A77BC387DFD738936F",
["JsonCompactColumnizer.dll"] = "5EEAFD0882DD1DBE5F0FFF92A65580737B775E43CA7F3A0DC4EFDE6A28B0DE5E",
["Log4jXmlColumnizer.dll"] = "C8458E855FE59F799E2394500BF4991A7E5E7F8AA6B030E2B7A733322E2AFAEF",
["LogExpert.Resources.dll"] = "F765E294A0EEEC525FA8992D22B1A666880CE2D0F720EFAF154D01EBB7A8AEED",
["CsvColumnizer.dll"] = "024575D2CBD09E58AE7CFF058B92C3785006AD2280301B0DE0724409934F4BCA",
["CsvColumnizer.dll (x86)"] = "024575D2CBD09E58AE7CFF058B92C3785006AD2280301B0DE0724409934F4BCA",
["DefaultPlugins.dll"] = "19B0A154B48F99A616A8468E8AC45ACCF86B4402F9F4E7DC860C6FE1D7D92B13",
["FlashIconHighlighter.dll"] = "4A488895316B01053DD3287604DC4344CCEA918EB09EB664009BC419F29FC021",
["GlassfishColumnizer.dll"] = "93DF4EEDC1A695F06E7E09AB483782FA0FDC867BED61FB63BD25458389FA8E0E",
["JsonColumnizer.dll"] = "C9538CA60C8E1ADFEAC74C75CAE7F1627311FF500804C6FD8B2EFEF068C6FFFE",
["JsonCompactColumnizer.dll"] = "1F30ABDC119D4459A2272D96080F4BF0683D7420A624CC256C2F07EEE02B192A",
["Log4jXmlColumnizer.dll"] = "D99A998A0E0C3969EB8B49CB6E8BD9CCC934641EEE18852B202244C837ADC7F5",
["LogExpert.Resources.dll"] = "43FB2C2C6D53D1E2CDB0D8FD663AEE5CF3E4FC51A05D69DBFBBF2123E8F67935",
["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93",
["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93",
["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D",
["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D",
["RegexColumnizer.dll"] = "009C8C7E56BB89C9448091A294894FE69C661DF35F9082CD2A092EEAF85E23A7",
["SftpFileSystem.dll"] = "74624F9B6659F83B53B7FEC45267345B84BE3DB13C811E04926884E65C32D56E",
["SftpFileSystem.dll (x86)"] = "03A1F8E1A3DF8915404A2653FE55B39B196EFD19C6DE42586D0B9822EFFBC617",
["SftpFileSystem.Resources.dll"] = "09F468603D3EAF56B13058BCE104CF9EA3F7B757352E9199D54486C8734AC47B",
["SftpFileSystem.Resources.dll (x86)"] = "09F468603D3EAF56B13058BCE104CF9EA3F7B757352E9199D54486C8734AC47B",
["RegexColumnizer.dll"] = "926229B7C704E367BA768E44981D133E6B26356CCEC4BCF0108EC77B2521C33A",
["SftpFileSystem.dll"] = "066BDA9245EDCECCD49E3C36B6ACC05E52C8C34D25A1A9406E403987E603FC57",
["SftpFileSystem.dll (x86)"] = "A30EB5C2D2E8A403C0ADF179AB626278689198E64935DA8BA108B33FB164AECA",
["SftpFileSystem.Resources.dll"] = "E68455B141D714A19779E717558DEEA6C9A9829ECF5A3C5EDCD65F18C06E6D33",
["SftpFileSystem.Resources.dll (x86)"] = "E68455B141D714A19779E717558DEEA6C9A9829ECF5A3C5EDCD65F18C06E6D33",

};
}
Expand Down