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

This commit is contained in:
Adam Hathcock
2026-10-20 08:45:43 +01:00
18 changed files with 474 additions and 98 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;
@@ -259,7 +260,7 @@ internal sealed class Lzma2EncoderStream : Stream
_bufferPosition = 0;
// Try compressing the data
(byte[] Buffer, int Length) compressed;
PooledMemoryStream compressed;
try
{
compressed = CompressBlock(_buffer, uncompressedSize);
@@ -271,13 +272,14 @@ internal sealed class Lzma2EncoderStream : Stream
return;
}
using var compressedScope = compressed;
// Check if compressed output fits in a single chunk and is actually smaller
if (
compressed.Length <= MAX_COMPRESSED_CHUNK_SIZE
&& compressed.Length < uncompressedData.Length
)
{
WriteCompressedChunk(uncompressedData.Length, compressed.Buffer, compressed.Length);
WriteCompressedChunk(uncompressedData.Length, compressed, (int)compressed.Length);
}
else
{
@@ -296,7 +298,7 @@ internal sealed class Lzma2EncoderStream : Stream
var uncompressedData = _buffer.AsMemory(0, uncompressedSize);
_bufferPosition = 0;
(byte[] Buffer, int Length) compressed;
PooledMemoryStream compressed;
try
{
compressed = CompressBlock(_buffer, uncompressedSize);
@@ -308,6 +310,7 @@ internal sealed class Lzma2EncoderStream : Stream
return;
}
using var compressedScope = compressed;
if (
compressed.Length <= MAX_COMPRESSED_CHUNK_SIZE
&& compressed.Length < uncompressedData.Length
@@ -315,8 +318,8 @@ internal sealed class Lzma2EncoderStream : Stream
{
await WriteCompressedChunkAsync(
uncompressedData.Length,
compressed.Buffer,
compressed.Length,
compressed,
(int)compressed.Length,
cancellationToken
)
.ConfigureAwait(false);
@@ -328,23 +331,29 @@ internal sealed class Lzma2EncoderStream : Stream
}
}
private (byte[] Buffer, int Length) CompressBlock(byte[] data, int length)
private PooledMemoryStream CompressBlock(byte[] data, int length)
{
using var inputMs = new MemoryStream(data, 0, length, writable: false);
using var outputMs = new PooledMemoryStream();
_encoder.Code(inputMs, outputMs, length, -1, null);
var fullCompressed = outputMs.ToArray();
// The LZMA range encoder flush writes trailing bytes the decoder doesn't consume.
// Trial-decode to find the exact byte count the decoder needs, so the LZMA2
// chunk header reports a compressed size that matches what the decoder reads.
var consumed = FindConsumedBytes(fullCompressed, fullCompressed.Length, length);
return (fullCompressed, consumed);
var outputMs = new PooledMemoryStream();
try
{
_encoder.Code(inputMs, outputMs, length, -1, null);
// Trial-decode to exclude trailing range-encoder bytes from the chunk size.
outputMs.Position = 0;
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
{
outputMs.Dispose();
throw;
}
}
private int FindConsumedBytes(byte[] compressedData, 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];
@@ -357,8 +366,7 @@ internal sealed class Lzma2EncoderStream : Stream
using var decoder = new Decoder();
decoder.SetDecoderProperties(props);
using var input = new MemoryStream(compressedData, 0, compressedSize, writable: false);
decoder.Code(input, Stream.Null, compressedSize, uncompressedSize, null);
decoder.Code(input, Stream.Null, input.Length, uncompressedSize, null);
return (int)input.Position;
}
@@ -369,7 +377,7 @@ internal sealed class Lzma2EncoderStream : Stream
/// </summary>
private void WriteCompressedChunk(
int uncompressedSize,
byte[] compressedData,
PooledMemoryStream compressedData,
int compressedSize
)
{
@@ -393,12 +401,12 @@ internal sealed class Lzma2EncoderStream : Stream
// 0xE0 (>= 0xC0) requires properties byte
_output.WriteByte(_lzmaPropertiesByte);
_output.Write(compressedData, 0, compressedSize);
compressedData.WriteTo(_output);
}
private async ValueTask WriteCompressedChunkAsync(
int uncompressedSize,
byte[] compressedData,
PooledMemoryStream compressedData,
int compressedSize,
CancellationToken cancellationToken = default
)
@@ -423,8 +431,8 @@ internal sealed class Lzma2EncoderStream : Stream
{
ArrayPool<byte>.Shared.Return(header);
}
await _output
.WriteAsync(compressedData, 0, compressedSize, cancellationToken)
await compressedData
.CopyToAsync(_output, Constants.BufferSize, cancellationToken)
.ConfigureAwait(false);
}

View File

@@ -465,8 +465,9 @@ public partial class LzmaStream
cancellationToken.ThrowIfCancellationRequested();
if (_encoder != null)
{
using var input = new ReadOnlyMemoryStream(buffer);
_position = await _encoder
.CodeAsync(new MemoryStream(buffer.ToArray()), false, cancellationToken)
.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

@@ -0,0 +1,99 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.IO;
// 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 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 =>
!_disposed
? _memory.Length
: throw new ObjectDisposedException(nameof(ReadOnlyMemoryStream));
public override long Position
{
get =>
!_disposed
? _position
: throw new ObjectDisposedException(nameof(ReadOnlyMemoryStream));
set => throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int 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 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.IsCancellationRequested
? Task.FromCanceled<int>(cancellationToken)
: Task.FromResult(Read(buffer, offset, count));
#if !LEGACY_DOTNET
public override int Read(Span<byte> buffer) => ReadCore(buffer);
public override ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
) =>
cancellationToken.IsCancellationRequested
? ValueTask.FromCanceled<int>(cancellationToken)
: ValueTask.FromResult(ReadCore(buffer.Span));
#endif
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
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