mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-09-22 15:04:43 +00:00
Merge pull request #1388 from julianxhokaxhiu/feat/lzma-perf-improvements
Improve LZMA decoding for Solid Archives
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user