Merge pull request #1388 from julianxhokaxhiu/feat/lzma-perf-improvements

Improve LZMA decoding for Solid Archives
This commit is contained in:
Adam Hathcock
2026-07-29 10:52:52 +01:00
committed by GitHub
6 changed files with 229 additions and 17 deletions

View File

@@ -78,6 +78,12 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
}
}
public override void Dispose()
{
_database?.DisposeCachedFolderStream();
base.Dispose();
}
private void LoadFactory(Stream stream)
{
if (_database is null)

View File

@@ -1,3 +1,4 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -35,4 +36,71 @@ internal sealed partial class ArchiveDatabase
)
.ConfigureAwait(false);
}
/// <summary>
/// Async counterpart of the caching <c>GetFolderStream</c> overload. Reuses a cached decoder
/// stream for the folder when possible instead of recreating and re-decoding from the start.
/// </summary>
internal async ValueTask<Stream> GetFolderStreamAsync(
Stream stream,
CFolder folder,
IPasswordProvider pw,
long skipSize,
long entrySize,
CancellationToken cancellationToken
)
{
if (_cachedFolder == folder && _cachedFolderStream != null)
{
if (skipSize >= _cachedFolderStreamPosition)
{
var delta = skipSize - _cachedFolderStreamPosition;
if (delta > 0)
{
await _cachedFolderStream
.SkipAsync(delta, cancellationToken)
.ConfigureAwait(false);
}
// Assume the caller will fully consume the returned entry stream, advancing the
// shared stream by entrySize bytes.
_cachedFolderStreamPosition = skipSize + entrySize;
return _cachedFolderStream;
}
// Non-sequential (backward) access within the same folder requires restarting.
await DisposeCachedFolderStreamAsync().ConfigureAwait(false);
}
else if (_cachedFolderStream != null)
{
await DisposeCachedFolderStreamAsync().ConfigureAwait(false);
}
var newStream = await GetFolderStreamAsync(stream, folder, pw, cancellationToken)
.ConfigureAwait(false);
if (skipSize > 0)
{
await newStream.SkipAsync(skipSize, cancellationToken).ConfigureAwait(false);
}
_cachedFolder = folder;
_cachedFolderStream = newStream;
_cachedFolderStreamPosition = skipSize + entrySize;
return newStream;
}
private async ValueTask DisposeCachedFolderStreamAsync()
{
if (_cachedFolderStream is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
}
else
{
#pragma warning disable VSTHRD103 // Fallback for streams that do not support async disposal.
_cachedFolderStream?.Dispose();
#pragma warning restore VSTHRD103
}
_cachedFolderStream = null;
_cachedFolder = null;
}
}

View File

@@ -163,4 +163,71 @@ internal partial class ArchiveDatabase
pw
);
}
// Cache used to avoid re-decoding a solid folder from scratch for every file it contains.
// Without this, extracting N files from the same solid folder decodes O(N^2) bytes since each
// file's stream was previously created fresh from the folder start and skipped forward.
private CFolder? _cachedFolder;
private Stream? _cachedFolderStream;
private long _cachedFolderStreamPosition;
/// <summary>
/// Returns a stream positioned at <paramref name="skipSize"/> bytes into the decompressed
/// contents of <paramref name="folder"/>. Reuses a cached decoder stream for the folder when
/// possible instead of recreating and re-decoding from the start.
/// </summary>
internal Stream GetFolderStream(
Stream stream,
CFolder folder,
IPasswordProvider pw,
long skipSize,
long entrySize
)
{
if (_cachedFolder == folder && _cachedFolderStream != null)
{
if (skipSize >= _cachedFolderStreamPosition)
{
var delta = skipSize - _cachedFolderStreamPosition;
if (delta > 0)
{
_cachedFolderStream.Skip(delta);
}
// Assume the caller will fully consume the returned entry stream, advancing the
// shared stream by entrySize bytes.
_cachedFolderStreamPosition = skipSize + entrySize;
return _cachedFolderStream;
}
// Non-sequential (backward) access within the same folder requires restarting.
_cachedFolderStream.Dispose();
_cachedFolderStream = null;
_cachedFolder = null;
}
else if (_cachedFolderStream != null)
{
_cachedFolderStream.Dispose();
_cachedFolderStream = null;
_cachedFolder = null;
}
var newStream = GetFolderStream(stream, folder, pw);
if (skipSize > 0)
{
newStream.Skip(skipSize);
}
_cachedFolder = folder;
_cachedFolderStream = newStream;
_cachedFolderStreamPosition = skipSize + entrySize;
return newStream;
}
internal void DisposeCachedFolderStream()
{
_cachedFolderStream?.Dispose();
_cachedFolderStream = null;
_cachedFolder = null;
_cachedFolderStreamPosition = 0;
}
}

View File

@@ -44,7 +44,6 @@ internal class SevenZipFilePart : FilePart
{
return Stream.Null;
}
var folderStream = _database.GetFolderStream(_stream, Folder!, _database.PasswordProvider);
var firstFileIndex = _database._folderStartFileIndex[_database._folders.IndexOf(Folder!)];
var skipCount = Index - firstFileIndex;
@@ -53,11 +52,15 @@ internal class SevenZipFilePart : FilePart
{
skipSize += _database._files[firstFileIndex + i].Size;
}
if (skipSize > 0)
{
folderStream.Skip(skipSize);
}
return new ReadOnlySubStream(folderStream, Header.Size, leaveOpen: false);
var folderStream = _database.GetFolderStream(
_stream,
Folder!,
_database.PasswordProvider,
skipSize,
Header.Size
);
return new ReadOnlySubStream(folderStream, Header.Size, leaveOpen: true);
}
internal override async ValueTask<Stream?> GetCompressedStreamAsync(
@@ -68,9 +71,6 @@ internal class SevenZipFilePart : FilePart
{
return Stream.Null;
}
var folderStream = await _database
.GetFolderStreamAsync(_stream, Folder!, _database.PasswordProvider, cancellationToken)
.ConfigureAwait(false);
var firstFileIndex = _database._folderStartFileIndex[_database._folders.IndexOf(Folder!)];
var skipCount = Index - firstFileIndex;
@@ -79,11 +79,18 @@ internal class SevenZipFilePart : FilePart
{
skipSize += _database._files[firstFileIndex + i].Size;
}
if (skipSize > 0)
{
await folderStream.SkipAsync(skipSize, cancellationToken).ConfigureAwait(false);
}
return new ReadOnlySubStream(folderStream, Header.Size, leaveOpen: false);
var folderStream = await _database
.GetFolderStreamAsync(
_stream,
Folder!,
_database.PasswordProvider,
skipSize,
Header.Size,
cancellationToken
)
.ConfigureAwait(false);
return new ReadOnlySubStream(folderStream, Header.Size, leaveOpen: true);
}
public CompressionType CompressionType

View File

@@ -36,8 +36,10 @@ internal partial class BufferedSubStream
BytesLeftToRead -= _cacheLength;
}
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public override async Task<int> ReadAsync(
// Fast, synchronous-completion path used when the requested bytes are already sitting in the
// in-memory cache. Avoids the async state-machine / Task allocation overhead incurred by every
// single-byte read the LZMA range decoder issues, without changing buffering/caching semantics.
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
@@ -49,6 +51,24 @@ internal partial class BufferedSubStream
count = (int)Length;
}
if (count > 0 && _cacheOffset < _cacheLength)
{
count = Math.Min(count, _cacheLength - _cacheOffset);
Buffer.BlockCopy(_cache!, _cacheOffset, buffer, offset, count);
_cacheOffset += count;
return Task.FromResult(count);
}
return ReadSlowAsync(buffer, offset, count, cancellationToken);
}
private async Task<int> ReadSlowAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (count > 0)
{
if (_cacheOffset == _cacheLength)
@@ -65,7 +85,7 @@ internal partial class BufferedSubStream
}
#if !LEGACY_DOTNET
public override async ValueTask<int> ReadAsync(
public override ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
@@ -76,6 +96,28 @@ internal partial class BufferedSubStream
count = (int)Length;
}
if (count > 0 && _cacheOffset < _cacheLength)
{
count = Math.Min(count, _cacheLength - _cacheOffset);
_cache!.AsSpan(_cacheOffset, count).CopyTo(buffer.Span);
_cacheOffset += count;
return new ValueTask<int>(count);
}
return ReadSlowAsync(buffer, cancellationToken);
}
private async ValueTask<int> ReadSlowAsync(
Memory<byte> buffer,
CancellationToken cancellationToken
)
{
var count = buffer.Length;
if (count > Length)
{
count = (int)Length;
}
if (count > 0)
{
if (_cacheOffset == _cacheLength)

View File

@@ -73,6 +73,28 @@ internal partial class BufferedSubStream : Stream, IStreamStack
return _cache![_cacheOffset++];
}
public override int Read(byte[] buffer, int offset, int count)
{
if (count > Length)
{
count = (int)Length;
}
if (count > 0)
{
if (_cacheOffset == _cacheLength)
{
RefillCache();
}
count = Math.Min(count, _cacheLength - _cacheOffset);
Buffer.BlockCopy(_cache!, _cacheOffset, buffer, offset, count);
_cacheOffset += count;
}
return count;
}
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();