Merge remote-tracking branch 'origin/master' into perf/7zip-profile-pooling

# Conflicts:
#	src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs
#	src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs
#	src/SharpCompress/IO/ReadOnlyMemoryStream.cs
This commit is contained in:
Adam Hathcock
2026-10-20 08:31:32 +01:00
18 changed files with 411 additions and 102 deletions

View File

@@ -3,6 +3,7 @@ using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
namespace SharpCompress.Compressors.LZMA;
@@ -339,11 +340,12 @@ internal sealed class Lzma2EncoderStream : Stream
try
{
_encoder.Code(inputMs, outputMs, length, -1, null);
// Trial-decode to exclude trailing range-encoder bytes from the chunk size.
outputMs.Position = 0;
// Trial decoding excludes range-encoder flush bytes from the chunk size.
var consumed = FindConsumedBytes(outputMs, (int)outputMs.Length, length);
var consumed = FindConsumedBytes(outputMs, length);
outputMs.SetLength(consumed);
outputMs.Position = 0;
// The caller owns the pooled output until the chunk write completes.
return outputMs;
}
catch
@@ -356,7 +358,7 @@ internal sealed class Lzma2EncoderStream : Stream
}
}
private int FindConsumedBytes(Stream input, int compressedSize, int uncompressedSize)
private int FindConsumedBytes(PooledMemoryStream input, int uncompressedSize)
{
// Build 5-byte LZMA property header: [pb/lp/lc byte] [dictSize as LE int32]
Span<byte> props = stackalloc byte[5];
@@ -369,7 +371,7 @@ internal sealed class Lzma2EncoderStream : Stream
var decoder = _trialDecoder ??= new Decoder();
decoder.SetDecoderProperties(props);
decoder.Code(input, Stream.Null, compressedSize, uncompressedSize, null);
decoder.Code(input, Stream.Null, input.Length, uncompressedSize, null);
return (int)input.Position;
}
@@ -434,7 +436,14 @@ internal sealed class Lzma2EncoderStream : Stream
{
ArrayPool<byte>.Shared.Return(header);
}
await compressedData.CopyToAsync(_output, 65536, cancellationToken).ConfigureAwait(false);
// Compressed chunks fit 64 KiB; avoid an 80 KiB request rounding to 128 KiB.
await compressedData
.CopyToAsync(
_output,
Math.Min(Constants.BufferSize, MAX_COMPRESSED_CHUNK_SIZE),
cancellationToken
)
.ConfigureAwait(false);
}
/// <summary>

View File

@@ -465,7 +465,7 @@ public partial class LzmaStream
cancellationToken.ThrowIfCancellationRequested();
if (_encoder != null)
{
using var input = new SharpCompress.IO.ReadOnlyMemoryStream(buffer);
using var input = new ReadOnlyMemoryStream(buffer);
_position = await _encoder
.CodeAsync(input, false, cancellationToken)
.ConfigureAwait(false);

View File

@@ -412,13 +412,9 @@ public class PpmdStream : Stream, IAsyncDisposable
cancellationToken.ThrowIfCancellationRequested();
if (_compress)
{
using var input = new ReadOnlyMemoryStream(buffer);
await _model
.EncodeBlockAsync(
_stream,
new MemoryStream(buffer.ToArray()),
false,
cancellationToken
)
.EncodeBlockAsync(_stream, input, false, cancellationToken)
.ConfigureAwait(false);
}
}

View File

@@ -5,55 +5,80 @@ using System.Threading.Tasks;
namespace SharpCompress.IO;
// Borrows caller memory only until the awaited encoder operation completes.
// Borrows memory for the lifetime of an encoder call; never copies or owns the backing storage.
internal sealed class ReadOnlyMemoryStream(ReadOnlyMemory<byte> memory) : Stream
{
private int position;
public override bool CanRead => true;
private ReadOnlyMemory<byte> _memory = memory;
private int _position;
private bool _disposed;
public override bool CanRead => !_disposed;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => memory.Length;
public override long Length =>
!_disposed
? _memory.Length
: throw new ObjectDisposedException(nameof(ReadOnlyMemoryStream));
public override long Position
{
get => position;
get =>
!_disposed
? _position
: throw new ObjectDisposedException(nameof(ReadOnlyMemoryStream));
set => throw new NotSupportedException();
}
private int ReadMemory(Span<byte> buffer)
public override int Read(byte[] buffer, int offset, int count)
{
var count = Math.Min(buffer.Length, memory.Length - position);
memory.Span.Slice(position, count).CopyTo(buffer);
position += count;
ThrowHelper.ThrowIfNull(buffer, nameof(buffer));
if (offset < 0 || count < 0 || offset > buffer.Length - count)
{
throw new ArgumentOutOfRangeException(nameof(count), "Invalid buffer range.");
}
return ReadCore(buffer.AsSpan(offset, count));
}
private int ReadCore(Span<byte> buffer)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ReadOnlyMemoryStream));
}
var count = Math.Min(buffer.Length, _memory.Length - _position);
_memory.Span.Slice(_position, count).CopyTo(buffer);
_position += count;
return count;
}
public override int Read(byte[] buffer, int offset, int count) =>
ReadMemory(buffer.AsSpan(offset, count));
public override int ReadByte() => position < memory.Length ? memory.Span[position++] : -1;
public override int ReadByte()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ReadOnlyMemoryStream));
}
return _position < _memory.Length ? _memory.Span[_position++] : -1;
}
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(Read(buffer, offset, count));
}
) =>
cancellationToken.IsCancellationRequested
? Task.FromCanceled<int>(cancellationToken)
: Task.FromResult(Read(buffer, offset, count));
#if !LEGACY_DOTNET
public override int Read(Span<byte> buffer) => ReadMemory(buffer);
public override int Read(Span<byte> buffer) => ReadCore(buffer);
public override ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new ValueTask<int>(ReadMemory(buffer.Span));
}
) =>
cancellationToken.IsCancellationRequested
? ValueTask.FromCanceled<int>(cancellationToken)
: ValueTask.FromResult(ReadCore(buffer.Span));
#endif
public override void Flush() { }
@@ -64,4 +89,11 @@ internal sealed class ReadOnlyMemoryStream(ReadOnlyMemory<byte> memory) : Stream
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
protected override void Dispose(bool disposing)
{
_disposed = true;
_memory = default;
base.Dispose(disposing);
}
}

View File

@@ -103,21 +103,18 @@ public partial class SevenZipWriter
// Write the encoded header to the output
var headerStartPos = output.Position;
encodedHeaderStream.Position = 0;
await encodedHeaderStream.CopyToAsync(output).ConfigureAwait(false);
// Compute CRC of the encoded header
var headerCrc = Crc32Stream.Compute(
Crc32Stream.DEFAULT_POLYNOMIAL,
Crc32Stream.DEFAULT_SEED,
encodedHeaderStream.GetBuffer().AsSpan(0, (int)encodedHeaderStream.Length)
);
// Compute the CRC during the copy without materializing the pooled header.
using var crcStream = new Crc32Stream(output);
await encodedHeaderStream
.CopyToAsync(crcStream, Constants.BufferSize, CancellationToken.None)
.ConfigureAwait(false);
// Back-patch signature header
var nextHeaderOffset = (ulong)(headerStartPos - SevenZipSignatureHeaderWriter.HeaderSize);
var nextHeaderSize = (ulong)encodedHeaderStream.Length;
await SevenZipSignatureHeaderWriter
.WriteFinalAsync(output, nextHeaderOffset, nextHeaderSize, headerCrc)
.WriteFinalAsync(output, nextHeaderOffset, nextHeaderSize, crcStream.Crc)
.ConfigureAwait(false);
// Seek to end
@@ -134,21 +131,18 @@ public partial class SevenZipWriter
// Write raw header directly
var headerStartPos = output.Position;
rawHeaderStream.Position = 0;
await rawHeaderStream.CopyToAsync(output).ConfigureAwait(false);
// Compute CRC of the raw header
var headerCrc = Crc32Stream.Compute(
Crc32Stream.DEFAULT_POLYNOMIAL,
Crc32Stream.DEFAULT_SEED,
rawHeaderStream.GetBuffer().AsSpan(0, (int)rawHeaderStream.Length)
);
// Compute the CRC during the copy without materializing the pooled header.
using var crcStream = new Crc32Stream(output);
await rawHeaderStream
.CopyToAsync(crcStream, Constants.BufferSize, CancellationToken.None)
.ConfigureAwait(false);
// Back-patch signature header
var nextHeaderOffset = (ulong)(headerStartPos - SevenZipSignatureHeaderWriter.HeaderSize);
var nextHeaderSize = (ulong)rawHeaderStream.Length;
await SevenZipSignatureHeaderWriter
.WriteFinalAsync(output, nextHeaderOffset, nextHeaderSize, headerCrc)
.WriteFinalAsync(output, nextHeaderOffset, nextHeaderSize, crcStream.Crc)
.ConfigureAwait(false);
// Seek to end