diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index 98f5491e..a0c1fab7 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -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); @@ -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 @@ -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 { @@ -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) { diff --git a/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderDirect.cs b/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderDirect.cs index 5ae5f779..a02989d3 100644 --- a/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderDirect.cs +++ b/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderDirect.cs @@ -1,7 +1,6 @@ using System.Buffers; using System.Text; -using LogExpert.Core.Classes.Log.Buffers; using LogExpert.Core.Entities; using LogExpert.Core.Interfaces; @@ -156,17 +155,11 @@ public void ReturnMemory (ReadOnlyMemory memory) // Bulk return via DetachBlocks()/Dispose(). Individual return not needed. } - /// - /// Gets the block allocator for compatibility with the DetachBlocks pattern. - /// This reader manages its own blocks directly rather than through CharBlockAllocator. - /// - public CharBlockAllocator? BlockAllocator => null; - /// /// Detaches completed blocks (fully scanned) for transfer to the LogBuffer. /// The current _readBlock (partially scanned) stays with the reader. /// - public List DetachBlocks () + public List DetachCharBlocks () { // Nothing to detach: no completed blocks and no lines were scanned from the current block. if (_completedBlocks.Count == 0 && _scanOffset == 0) diff --git a/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderLegacy.cs b/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderLegacy.cs index 207ae31e..2a2880be 100644 --- a/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderLegacy.cs +++ b/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderLegacy.cs @@ -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 @@ -47,7 +47,20 @@ public bool TryReadLine (out ReadOnlyMemory lineMemory) public void ReturnMemory (ReadOnlyMemory memory) { - // Bulk return via BlockAllocator.DetachBlocks() when the LogBuffer is evicted. + // Bulk return via DetachCharBlocks() when the LogBuffer is evicted, or Dispose(). + } + + public List DetachCharBlocks () => BlockAllocator.DetachBlocks(); + + protected override void Dispose (bool disposing) + { + if (disposing) + { + BlockAllocator?.Dispose(); + BlockAllocator = null; + } + + base.Dispose(disposing); } public override string ReadLine () diff --git a/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderSystem.cs b/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderSystem.cs index fce09075..55090406 100644 --- a/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderSystem.cs +++ b/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderSystem.cs @@ -36,13 +36,13 @@ public PositionAwareStreamReaderSystem (Stream stream, EncodingOptions encodingO #region Properties /// - /// 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 + /// ; ownership of the allocator does not cross the reader seam. /// - public CharBlockAllocator BlockAllocator + private CharBlockAllocator BlockAllocator { get => field ??= new CharBlockAllocator(); - private set; + set; } #endregion @@ -112,10 +112,12 @@ public bool TryReadLine (out ReadOnlyMemory lineMemory) /// public void ReturnMemory (ReadOnlyMemory 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 DetachCharBlocks () => BlockAllocator.DetachBlocks(); + #endregion #region Private Methods diff --git a/src/LogExpert.Core/Interfaces/ILogStreamReaderMemory.cs b/src/LogExpert.Core/Interfaces/ILogStreamReaderMemory.cs index 31a07649..31af8db6 100644 --- a/src/LogExpert.Core/Interfaces/ILogStreamReaderMemory.cs +++ b/src/LogExpert.Core/Interfaces/ILogStreamReaderMemory.cs @@ -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. /// void ReturnMemory (ReadOnlyMemory memory); + + /// + /// Detaches the completed character blocks that back the slices handed out by + /// , transferring their ownership to the caller (the LogBuffer that owns those + /// lines). + /// + /// + /// 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. + /// + /// + /// 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. + /// + List DetachCharBlocks (); } diff --git a/src/LogExpert.Tests/StreamReaderTests/LogStreamReaderTest.cs b/src/LogExpert.Tests/StreamReaderTests/LogStreamReaderTest.cs index b2788ee3..89b01592 100644 --- a/src/LogExpert.Tests/StreamReaderTests/LogStreamReaderTest.cs +++ b/src/LogExpert.Tests/StreamReaderTests/LogStreamReaderTest.cs @@ -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)); @@ -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)); @@ -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] diff --git a/src/LogExpert.Tests/StreamReaderTests/PositionAwareStreamReaderDirectTests.cs b/src/LogExpert.Tests/StreamReaderTests/PositionAwareStreamReaderDirectTests.cs index 0d44d3be..21d61b12 100644 --- a/src/LogExpert.Tests/StreamReaderTests/PositionAwareStreamReaderDirectTests.cs +++ b/src/LogExpert.Tests/StreamReaderTests/PositionAwareStreamReaderDirectTests.cs @@ -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(); @@ -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)); } @@ -506,7 +506,7 @@ public void TryReadLine_LineLongerThanBlockSize_WithMaxLineLengthTruncation () } [Test] - public void TryReadLine_LineLongerThanBlockSize_DetachBlocksAfterEachLine () + public void TryReadLine_LineLongerThanBlockSize_DetachCharBlocksAfterEachLine () { RunWithTimeout(() => { @@ -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); @@ -537,7 +537,7 @@ public void TryReadLine_LineLongerThanBlockSize_DetachBlocksAfterEachLine () } [Test] - public void TryReadLine_LineLongerThanBlockSize_DetachBlocksWithLargeTail () + public void TryReadLine_LineLongerThanBlockSize_DetachCharBlocksWithLargeTail () { RunWithTimeout(() => { @@ -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 diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index d5472889..c47db8ff 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2026-06-25 12:45:02 UTC + /// Generated: 2026-06-25 14:06:29 UTC /// Configuration: Release /// Plugin count: 21 /// @@ -18,27 +18,27 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(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", }; }