Skip to content

Commit 3f8dde4

Browse files
committed
benchmark md updates
ReadOnlyMemory Interface for testing
1 parent a039252 commit 3f8dde4

6 files changed

Lines changed: 383 additions & 75 deletions

File tree

src/LogExpert.Core/Classes/Log/LogStreamReaderBase.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System;
21
using System.Text;
32

43
using LogExpert.Core.Interface;
@@ -9,12 +8,12 @@ public abstract class LogStreamReaderBase : ILogStreamReader
98
{
109
#region cTor
1110

12-
protected LogStreamReaderBase()
11+
protected LogStreamReaderBase ()
1312
{
1413

1514
}
1615

17-
~LogStreamReaderBase()
16+
~LogStreamReaderBase ()
1817
{
1918
Dispose(false);
2019
}
@@ -44,7 +43,7 @@ protected LogStreamReaderBase()
4443
/// <summary>
4544
/// Destroy and release the current stream reader.
4645
/// </summary>
47-
public void Dispose()
46+
public void Dispose ()
4847
{
4948
Dispose(true);
5049
GC.SuppressFinalize(this);
@@ -53,11 +52,11 @@ public void Dispose()
5352
/// Destroy and release the current stream reader.
5453
/// </summary>
5554
/// <param name="disposing">Specifies whether or not the managed objects should be released.</param>
56-
protected abstract void Dispose(bool disposing);
55+
protected abstract void Dispose (bool disposing);
5756

58-
public abstract int ReadChar();
57+
public abstract int ReadChar ();
5958

60-
public abstract string ReadLine();
59+
public abstract string ReadLine ();
6160

6261
#endregion
6362
}

src/LogExpert.Core/Classes/Log/PositionAwareStreamReaderPipeline.cs

Lines changed: 83 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
using System.Text;
55

66
using LogExpert.Core.Entities;
7+
using LogExpert.Core.Interface;
78

89
namespace LogExpert.Core.Classes.Log;
910

10-
public class PositionAwareStreamReaderPipeline : LogStreamReaderBase
11+
public class PositionAwareStreamReaderPipeline : LogStreamReaderBase, ILogStreamReaderSpan
1112
{
1213
private const int DEFAULT_BYTE_BUFFER_SIZE = 64 * 1024; // 64 KB
1314
private const int MINIMUM_READ_AHEAD_SIZE = 4 * 1024; // 4 KB
@@ -30,6 +31,8 @@ public class PositionAwareStreamReaderPipeline : LogStreamReaderBase
3031
private readonly int _charBufferSize;
3132
private readonly long _preambleLength;
3233

34+
private LineSegment? _currentSegment;
35+
3336
private PipeReader _pipeReader;
3437
private CancellationTokenSource _cts;
3538
private Task _producerTask;
@@ -99,42 +102,49 @@ public override int ReadChar ()
99102

100103
public override string ReadLine ()
101104
{
102-
ObjectDisposedException.ThrowIf(IsDisposed, GetType());
103-
104-
// Check for producer exception
105-
var producerEx = Volatile.Read(ref _producerException);
106-
if (producerEx != null)
107-
{
108-
throw new InvalidOperationException("Producer task encountered an error.", producerEx);
109-
}
110-
111-
LineSegment segment;
112-
try
113-
{
114-
// BlockingCollection.Take() blocks until an item is available or collection is completed
115-
// This eliminates the race condition present in the semaphore + queue approach
116-
segment = _lineQueue.Take(_cts?.Token ?? CancellationToken.None);
117-
}
118-
catch (OperationCanceledException)
119-
{
120-
return null;
121-
}
122-
catch (InvalidOperationException) // Thrown when collection is marked as completed and empty
123-
{
124-
return null;
125-
}
126-
127-
using (segment)
128-
{
129-
if (segment.IsEof)
130-
{
131-
return null;
132-
}
133-
134-
var line = new string(segment.Buffer, 0, segment.Length);
135-
_ = Interlocked.Exchange(ref _position, segment.ByteOffset + segment.ByteLength);
136-
return line;
137-
}
105+
if (TryReadLine(out var lineMemory))
106+
{
107+
return new string(lineMemory.Span); // Only allocate when explicitly requested
108+
}
109+
110+
return null;
111+
112+
//ObjectDisposedException.ThrowIf(IsDisposed, GetType());
113+
114+
//// Check for producer exception
115+
//var producerEx = Volatile.Read(ref _producerException);
116+
//if (producerEx != null)
117+
//{
118+
// throw new InvalidOperationException("Producer task encountered an error.", producerEx);
119+
//}
120+
121+
//LineSegment segment;
122+
//try
123+
//{
124+
// // BlockingCollection.Take() blocks until an item is available or collection is completed
125+
// // This eliminates the race condition present in the semaphore + queue approach
126+
// segment = _lineQueue.Take(_cts?.Token ?? CancellationToken.None);
127+
//}
128+
//catch (OperationCanceledException)
129+
//{
130+
// return null;
131+
//}
132+
//catch (InvalidOperationException) // Thrown when collection is marked as completed and empty
133+
//{
134+
// return null;
135+
//}
136+
137+
//using (segment)
138+
//{
139+
// if (segment.IsEof)
140+
// {
141+
// return null;
142+
// }
143+
144+
// var line = new string(segment.Buffer, 0, segment.Length);
145+
// _ = Interlocked.Exchange(ref _position, segment.ByteOffset + segment.ByteLength);
146+
// return line;
147+
//}
138148
}
139149

140150
protected override void Dispose (bool disposing)
@@ -621,6 +631,42 @@ private static (int length, Encoding? detectedEncoding) DetectPreambleLength (St
621631
return (0, null);
622632
}
623633

634+
public bool TryReadLine (out ReadOnlyMemory<char> lineMemory)
635+
{
636+
ObjectDisposedException.ThrowIf(IsDisposed, GetType());
637+
638+
var producerEx = Volatile.Read(ref _producerException);
639+
if (producerEx != null)
640+
{
641+
throw new InvalidOperationException("Producer task encountered an error.", producerEx);
642+
}
643+
644+
if (!_lineQueue.TryTake(out var segment, 100, _cts?.Token ?? CancellationToken.None))
645+
{
646+
lineMemory = default;
647+
return false;
648+
}
649+
650+
// Store segment for lifetime management
651+
_currentSegment?.Dispose();
652+
_currentSegment = segment;
653+
654+
if (segment.IsEof)
655+
{
656+
lineMemory = default;
657+
return false;
658+
}
659+
660+
lineMemory = new ReadOnlyMemory<char>(segment.Buffer, 0, segment.Length);
661+
_ = Interlocked.Exchange(ref _position, segment.ByteOffset + segment.ByteLength);
662+
return true;
663+
}
664+
665+
public void ReturnMemory (ReadOnlyMemory<char> memory)
666+
{
667+
throw new NotImplementedException();
668+
}
669+
624670
/// <summary>
625671
/// Represents a line segment with its position and metadata.
626672
/// Uses ArrayPool for efficient char buffer management.

src/LogExpert.Core/Interface/ILogStreamReader.cs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,154 @@
22

33
namespace LogExpert.Core.Interface;
44

5+
/// <summary>
6+
/// Provides a position-aware stream reader interface for reading log files with support for character encoding
7+
/// and position tracking.
8+
/// </summary>
9+
/// <remarks>
10+
/// <para>
11+
/// This interface abstracts log file reading operations, providing a consistent API for different stream reading
12+
/// implementations. All implementations must maintain accurate byte position tracking to support seeking and
13+
/// re-reading specific portions of the log file.
14+
/// </para>
15+
/// <para>
16+
/// Implementations include:
17+
/// <list type="bullet">
18+
/// <item><description><c>PositionAwareStreamReaderLegacy</c> - Character-by-character reading for precise position control</description></item>
19+
/// <item><description><c>PositionAwareStreamReaderSystem</c> - Uses .NET's StreamReader.ReadLine() for improved performance</description></item>
20+
/// <item><description><c>PositionAwareStreamReaderPipeline</c> - Modern async pipeline-based implementation using System.IO.Pipelines</description></item>
21+
/// <item><description><c>XmlLogReader</c> - Decorator for reading structured XML log blocks (e.g., Log4j XML format)</description></item>
22+
/// </list>
23+
/// </para>
24+
/// </remarks>
525
public interface ILogStreamReader : IDisposable
626
{
727
#region Properties
828

29+
/// <summary>
30+
/// Gets or sets the current byte position in the stream.
31+
/// </summary>
32+
/// <value>
33+
/// The zero-based byte offset from the beginning of the stream, accounting for any byte order mark (BOM).
34+
/// </value>
35+
/// <remarks>
36+
/// <para>
37+
/// Setting the position causes the reader to seek to the specified byte offset in the underlying stream.
38+
/// This operation may be expensive as it requires resetting internal buffers and decoder state.
39+
/// </para>
40+
/// <para>
41+
/// The position should always represent a valid character boundary. Setting the position to the middle
42+
/// of a multi-byte character may result in decoding errors or incorrect output.
43+
/// </para>
44+
/// <para>
45+
/// After seeking, the next <see cref="ReadLine"/> or <see cref="ReadChar"/> operation will begin reading
46+
/// from the new position.
47+
/// </para>
48+
/// </remarks>
949
long Position { get; set; }
1050

51+
/// <summary>
52+
/// Gets a value indicating whether the internal buffer has been completely filled from the stream.
53+
/// </summary>
54+
/// <value>
55+
/// <see langword="true"/> if the buffer is complete and no additional data needs to be loaded;
56+
/// otherwise, <see langword="false"/>.
57+
/// </value>
58+
/// <remarks>
59+
/// This property is primarily used to determine if the reader is still waiting for data to become
60+
/// available in the stream. Most implementations return <see langword="true"/> as they read directly
61+
/// from the stream without pre-buffering.
62+
/// </remarks>
1163
bool IsBufferComplete { get; }
1264

65+
/// <summary>
66+
/// Gets the character encoding used by the stream reader.
67+
/// </summary>
68+
/// <value>
69+
/// The <see cref="System.Text.Encoding"/> object representing the character encoding of the stream.
70+
/// </value>
71+
/// <remarks>
72+
/// <para>
73+
/// The encoding is determined during initialization and may be detected from a byte order mark (BOM)
74+
/// at the beginning of the stream, explicitly specified via <c>EncodingOptions</c>, or defaulted to
75+
/// the system default encoding.
76+
/// </para>
77+
/// <para>
78+
/// Supported BOM detection includes:
79+
/// <list type="bullet">
80+
/// <item><description>UTF-8 (EF BB BF)</description></item>
81+
/// <item><description>UTF-16 Little Endian (FF FE)</description></item>
82+
/// <item><description>UTF-16 Big Endian (FE FF)</description></item>
83+
/// <item><description>UTF-32 Little Endian (FF FE 00 00)</description></item>
84+
/// <item><description>UTF-32 Big Endian (00 00 FE FF)</description></item>
85+
/// </list>
86+
/// </para>
87+
/// </remarks>
1388
Encoding Encoding { get; }
1489

1590
#endregion
1691

1792
#region Public methods
1893

94+
/// <summary>
95+
/// Reads the next character from the stream and advances the position by the number of bytes consumed.
96+
/// </summary>
97+
/// <returns>
98+
/// The next character as an <see cref="int"/>, or -1 if the end of the stream has been reached.
99+
/// </returns>
100+
/// <remarks>
101+
/// <para>
102+
/// The return value is an <see cref="int"/> rather than a <see cref="char"/> to allow returning -1
103+
/// for end-of-stream, following the convention established by <see cref="StreamReader.Read()"/>.
104+
/// </para>
105+
/// <para>
106+
/// After reading a character, the <see cref="Position"/> property is automatically advanced by the
107+
/// number of bytes consumed from the stream. For single-byte encodings this is always 1, for UTF-16
108+
/// this is always 2, but for variable-width encodings like UTF-8 this may be 1-4 bytes depending on
109+
/// the character.
110+
/// </para>
111+
/// <para>
112+
/// Some implementations (like <c>PositionAwareStreamReaderPipeline</c>) may not support this method
113+
/// and will throw <see cref="NotSupportedException"/> as they are optimized for line-based reading only.
114+
/// </para>
115+
/// </remarks>
116+
/// <exception cref="ObjectDisposedException">The reader has been disposed.</exception>
117+
/// <exception cref="NotSupportedException">The implementation does not support character-level reading.</exception>
118+
/// <exception cref="IOException">An I/O error occurred while reading from the stream.</exception>
19119
int ReadChar ();
20120

121+
/// <summary>
122+
/// Reads a line of characters from the stream and advances the position by the number of bytes consumed.
123+
/// </summary>
124+
/// <returns>
125+
/// A string containing the next line from the stream (excluding newline characters), or <see langword="null"/>
126+
/// if the end of the stream has been reached.
127+
/// </returns>
128+
/// <remarks>
129+
/// <para>
130+
/// A line is defined as a sequence of characters followed by a line feed (\n), a carriage return (\r),
131+
/// or a carriage return followed by a line feed (\r\n). The returned string does not include the
132+
/// terminating newline character(s).
133+
/// </para>
134+
/// <para>
135+
/// The <see cref="Position"/> property is automatically advanced by the total number of bytes consumed,
136+
/// including the newline character(s).
137+
/// </para>
138+
/// <para>
139+
/// Implementations may enforce a maximum line length constraint. Lines exceeding this limit will be
140+
/// truncated to the maximum length. The specific limit is implementation-dependent and typically
141+
/// specified during construction.
142+
/// </para>
143+
/// <para>
144+
/// If the stream ends without a trailing newline, the remaining characters are returned as the last line.
145+
/// Subsequent calls will return <see langword="null"/> to indicate end-of-stream.
146+
/// </para>
147+
/// </remarks>
148+
/// <exception cref="ObjectDisposedException">The reader has been disposed.</exception>
149+
/// <exception cref="IOException">An I/O error occurred while reading from the stream.</exception>
150+
/// <exception cref="InvalidOperationException">
151+
/// The internal producer task encountered an error (specific to async implementations).
152+
/// </exception>
21153
string ReadLine ();
22154

23155
#endregion
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace LogExpert.Core.Interface;
2+
3+
public interface ISpanLineReader
4+
{
5+
bool TryReadLine (out ReadOnlySpan<char> line);
6+
7+
long Position { get; }
8+
}

src/LogExpert.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogExpert.Persister.Tests",
9898
EndProject
9999
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogExpert.Benchmarks", "LogExpert.Benchmarks\LogExpert.Benchmarks.csproj", "{1046779B-500D-8260-33BA-BC778C4B836F}"
100100
EndProject
101+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "performance", "performance", "{C83F15B6-F6E0-4526-A5C5-47806772E49A}"
102+
ProjectSection(SolutionItems) = preProject
103+
docs\performance\BENCHMARK_SUMMARY.md = docs\performance\BENCHMARK_SUMMARY.md
104+
EndProjectSection
105+
EndProject
101106
Global
102107
GlobalSection(SolutionConfigurationPlatforms) = preSolution
103108
Debug|Any CPU = Debug|Any CPU
@@ -230,6 +235,7 @@ Global
230235
{39822C1B-E4C6-40F3-86C4-74C68BDEF3D0} = {DE6375A4-B4C4-4620-8FFB-B9D5A4E21144}
231236
{CAD17410-CE8C-4FE5-91DE-1B3DE2945135} = {848C24BA-BEBA-48EC-90E6-526ECAB6BB4A}
232237
{1046779B-500D-8260-33BA-BC778C4B836F} = {848C24BA-BEBA-48EC-90E6-526ECAB6BB4A}
238+
{C83F15B6-F6E0-4526-A5C5-47806772E49A} = {39822C1B-E4C6-40F3-86C4-74C68BDEF3D0}
233239
EndGlobalSection
234240
GlobalSection(ExtensibilityGlobals) = postSolution
235241
SolutionGuid = {15924D5F-B90B-4BC7-9E7D-BCCB62EBABAD}

0 commit comments

Comments
 (0)