Compare commits

...

13 Commits

Author SHA1 Message Date
Adam Hathcock
daacb93902 more pooling 2026-01-14 16:49:34 +00:00
Adam Hathcock
eb2f60fb53 rar byte[] better 2026-01-14 16:38:57 +00:00
Adam Hathcock
bd0439c424 change byte[] to memory using pool 2026-01-14 15:55:14 +00:00
Adam Hathcock
b935bcfaef reduce memory usage on headers 2026-01-14 15:33:08 +00:00
Adam Hathcock
56c22cee78 Merge branch 'copilot/add-performance-benchmarking' into adam/async-again 2026-01-14 14:53:17 +00:00
Adam Hathcock
5c4b83e501 Merge remote-tracking branch 'origin/master' into copilot/add-performance-benchmarking
# Conflicts:
#	tests/SharpCompress.Performance/packages.lock.json
2026-01-14 14:52:21 +00:00
copilot-swe-agent[bot]
80ac10a5fe Merge latest master branch and resolve conflicts
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-12 14:11:57 +00:00
copilot-swe-agent[bot]
a92ce90252 Fix path validation and add iteration cleanup to prevent file reuse
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-05 17:38:17 +00:00
copilot-swe-agent[bot]
e519f61f0f Address code review feedback: fix exception handling and initialization order
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-05 17:36:08 +00:00
copilot-swe-agent[bot]
49f2271253 Format code with CSharpier
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-05 17:33:06 +00:00
copilot-swe-agent[bot]
5b1d11bc1d Add WriteBenchmarks, BaselineComparisonBenchmarks, and comprehensive documentation
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-05 17:31:32 +00:00
copilot-swe-agent[bot]
aa3a40d968 Add BenchmarkDotNet integration with Archive and Reader benchmarks
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-05 17:27:50 +00:00
copilot-swe-agent[bot]
6125654b2e Initial plan 2026-01-05 17:21:29 +00:00
24 changed files with 783 additions and 183 deletions

4
.gitignore vendored
View File

@@ -20,3 +20,7 @@ artifacts/
.DS_Store
*.snupkg
# BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts/
**/BenchmarkDotNet.Artifacts/

View File

@@ -1,5 +1,6 @@
<Project>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="Bullseye" Version="6.1.0" />
<PackageVersion Include="AwesomeAssertions" Version="9.3.0" />
<PackageVersion Include="Glob" Version="1.1.9" />

View File

@@ -48,6 +48,20 @@ public partial class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>, I
}
}
public override async ValueTask DisposeAsync()
{
if (!_disposed)
{
if (UnpackV1.IsValueCreated && UnpackV1.Value is IDisposable unpackV1)
{
unpackV1.Dispose();
}
_disposed = true;
await base.DisposeAsync();
}
}
protected override IEnumerable<RarArchiveEntry> LoadEntries(IEnumerable<RarVolume> volumes) =>
RarArchiveEntryFactory.GetEntries(this, volumes, ReaderOptions);

View File

@@ -51,11 +51,14 @@ namespace SharpCompress.Common
return BinaryPrimitives.ReadUInt64LittleEndian(_buffer);
}
public async ValueTask<byte[]> ReadBytesAsync(int count, CancellationToken ct = default)
public async ValueTask ReadBytesAsync(byte[] bytes, int offset, int count, CancellationToken ct = default)
{
var result = new byte[count];
await _stream.ReadExactAsync(result, 0, count, ct).ConfigureAwait(false);
return result;
await _stream.ReadExactAsync(bytes, offset, count, ct).ConfigureAwait(false);
}
public async ValueTask SkipAsync(int count, CancellationToken ct = default)
{
await _stream.SkipAsync( count, ct).ConfigureAwait(false);
}
public void Dispose()

View File

@@ -29,7 +29,8 @@ internal class DirectoryEndHeader : ZipHeader
DirectorySize = await reader.ReadUInt32Async();
DirectoryStartOffsetRelativeToDisk = await reader.ReadUInt32Async();
CommentLength = await reader.ReadUInt16Async();
Comment = await reader.ReadBytesAsync(CommentLength);
Comment = new byte[CommentLength];
await reader.ReadBytesAsync(Comment, 0, CommentLength);
}
public ushort VolumeNumber { get; private set; }

View File

@@ -53,10 +53,12 @@ internal class DirectoryEntryHeader : ZipFileEntry
InternalFileAttributes = await reader.ReadUInt16Async();
ExternalFileAttributes = await reader.ReadUInt32Async();
RelativeOffsetOfEntryHeader = await reader.ReadUInt32Async();
var name = await reader.ReadBytesAsync(nameLength);
var extra = await reader.ReadBytesAsync(extraLength);
var comment = await reader.ReadBytesAsync(commentLength);
var name = new byte[nameLength];
var extra = new byte[extraLength];
var comment = new byte[commentLength];
await reader.ReadBytesAsync(name,0 ,nameLength);
await reader.ReadBytesAsync(extra, 0, extraLength);
await reader.ReadBytesAsync(comment, 0, commentLength);
ProcessReadData(name, extra, comment);
}

View File

@@ -37,8 +37,10 @@ internal class LocalEntryHeader(IArchiveEncoding archiveEncoding)
UncompressedSize = await reader.ReadUInt32Async();
var nameLength = await reader.ReadUInt16Async();
var extraLength = await reader.ReadUInt16Async();
var name = await reader.ReadBytesAsync(nameLength);
var extra = await reader.ReadBytesAsync(extraLength);
var name = new byte[nameLength];
var extra = new byte[extraLength];
await reader.ReadBytesAsync(name,0 ,nameLength);
await reader.ReadBytesAsync(extra, 0, extraLength);
ProcessReadData(name, extra);
}

View File

@@ -38,12 +38,12 @@ internal class Zip64DirectoryEndHeader : ZipHeader
TotalNumberOfEntries = (long)await reader.ReadUInt64Async();
DirectorySize = (long)await reader.ReadUInt64Async();
DirectoryStartOffsetRelativeToDisk = (long)await reader.ReadUInt64Async();
DataSector = await reader.ReadBytesAsync(
(int)(
SizeOfDirectoryEndRecord
- SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS
)
var size = (int)(
SizeOfDirectoryEndRecord
- SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS
);
DataSector = new byte[size];
await reader.ReadBytesAsync(DataSector, 0, size);
}
private const int SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS = 44;

View File

@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
@@ -16,6 +17,8 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory
private const int MAX_SEARCH_LENGTH_FOR_EOCD = 65557;
private bool _zip64;
private static readonly byte[] needle = { 0x06, 0x05, 0x4b, 0x50 };
internal SeekableZipHeaderFactory(string? password, IArchiveEncoding archiveEncoding)
: base(StreamingMode.Seekable, password, archiveEncoding) { }
@@ -153,74 +156,8 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory
}
}
internal async IAsyncEnumerable<ZipHeader> ReadSeekableHeaderAsync(Stream stream, bool useSync)
{
var reader = new AsyncBinaryReader(stream);
await SeekBackToHeaderAsync(stream, reader);
var eocd_location = stream.Position;
var entry = new DirectoryEndHeader();
await entry.Read(reader);
if (entry.IsZip64)
{
_zip64 = true;
// ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR should be before the EOCD
stream.Seek(eocd_location - ZIP64_EOCD_LENGTH - 4, SeekOrigin.Begin);
var zip64_locator = await reader.ReadUInt32Async();
if (zip64_locator != ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR)
{
throw new ArchiveException("Failed to locate the Zip64 Directory Locator");
}
var zip64Locator = new Zip64DirectoryEndLocatorHeader();
await zip64Locator.Read(reader);
stream.Seek(zip64Locator.RelativeOffsetOfTheEndOfDirectoryRecord, SeekOrigin.Begin);
var zip64Signature = await reader.ReadUInt32Async();
if (zip64Signature != ZIP64_END_OF_CENTRAL_DIRECTORY)
{
throw new ArchiveException("Failed to locate the Zip64 Header");
}
var zip64Entry = new Zip64DirectoryEndHeader();
await zip64Entry.Read(reader);
stream.Seek(zip64Entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin);
}
else
{
stream.Seek(entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin);
}
var position = stream.Position;
while (true)
{
stream.Position = position;
var signature = await reader.ReadUInt32Async();
var nextHeader = await ReadHeader(signature, reader, _zip64);
position = stream.Position;
if (nextHeader is null)
{
yield break;
}
if (nextHeader is DirectoryEntryHeader entryHeader)
{
//entry could be zero bytes so we need to know that.
entryHeader.HasData = entryHeader.CompressedSize != 0;
yield return entryHeader;
}
else if (nextHeader is DirectoryEndHeader endHeader)
{
yield return endHeader;
}
}
}
private static bool IsMatch(byte[] haystack, int position, byte[] needle)
private static bool IsMatch(Span<byte> haystack, int position, byte[] needle)
{
for (var i = 0; i < needle.Length; i++)
{
@@ -247,29 +184,35 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory
stream.Length < MAX_SEARCH_LENGTH_FOR_EOCD
? (int)stream.Length
: MAX_SEARCH_LENGTH_FOR_EOCD;
// We search for marker in reverse to find the first occurance
byte[] needle = { 0x06, 0x05, 0x4b, 0x50 };
stream.Seek(-len, SeekOrigin.End);
var seek = ArrayPool<byte>.Shared.Rent(len);
var seek = await reader.ReadBytesAsync(len);
// Search in reverse
Array.Reverse(seek);
// don't exclude the minimum eocd region, otherwise you fail to locate the header in empty zip files
var max_search_area = len; // - MINIMUM_EOCD_LENGTH;
for (var pos_from_end = 0; pos_from_end < max_search_area; ++pos_from_end)
try
{
if (IsMatch(seek, pos_from_end, needle))
{
stream.Seek(-pos_from_end, SeekOrigin.End);
return;
}
}
await reader.ReadBytesAsync(seek, 0, len, default);
var memory = new Memory<byte>(seek, 0, len);
var span = memory.Span;
span.Reverse();
throw new ArchiveException("Failed to locate the Zip Header");
// don't exclude the minimum eocd region, otherwise you fail to locate the header in empty zip files
var max_search_area = len; // - MINIMUM_EOCD_LENGTH;
for (var pos_from_end = 0; pos_from_end < max_search_area; ++pos_from_end)
{
if (IsMatch(span, pos_from_end, needle))
{
stream.Seek(-pos_from_end, SeekOrigin.End);
return;
}
}
throw new ArchiveException("Failed to locate the Zip Header");
}
finally
{
ArrayPool<byte>.Shared.Return(seek);
}
}
private static void SeekBackToHeader(Stream stream, BinaryReader reader)
@@ -286,9 +229,6 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory
stream.Length < MAX_SEARCH_LENGTH_FOR_EOCD
? (int)stream.Length
: MAX_SEARCH_LENGTH_FOR_EOCD;
// We search for marker in reverse to find the first occurance
byte[] needle = { 0x06, 0x05, 0x4b, 0x50 };
stream.Seek(-len, SeekOrigin.End);
var seek = reader.ReadBytes(len);

View File

@@ -79,7 +79,7 @@ internal class ZipHeaderFactory
}
else
{
await reader.ReadBytesAsync(zip64 ? 20 : 12);
await reader.SkipAsync(zip64 ? 20 : 12);
}
return null;
}

View File

@@ -64,6 +64,7 @@
// -----------------------------------------------------------------------
using System;
using System.Buffers;
using SharpCompress.Algorithms;
namespace SharpCompress.Compressors.Deflate;
@@ -116,14 +117,14 @@ internal sealed class InflateBlocks
internal int readAt; // window read pointer
internal int table; // table lengths (14 bits)
internal int[] tb = new int[1]; // bit length decoding tree
internal byte[] window; // sliding window
internal IMemoryOwner<byte> window; // sliding window
internal int writeAt; // window write pointer
internal InflateBlocks(ZlibCodec codec, object checkfn, int w)
{
_codec = codec;
hufts = new int[MANY * 3];
window = new byte[w];
window = MemoryPool<byte>.Shared.Rent(w);
end = w;
this.checkfn = checkfn;
mode = InflateBlockMode.TYPE;
@@ -340,7 +341,7 @@ internal sealed class InflateBlocks
{
t = m;
}
Array.Copy(_codec.InputBuffer, p, window, q, t);
_codec.InputBuffer.AsSpan(p, t).CopyTo(window.Memory.Span.Slice(q));
p += t;
n -= t;
q += t;
@@ -715,13 +716,14 @@ internal sealed class InflateBlocks
internal void Free()
{
Reset();
window?.Dispose();
window = null;
hufts = null;
}
internal void SetDictionary(byte[] d, int start, int n)
{
Array.Copy(d, start, window, 0, n);
d.AsSpan(start, n).CopyTo(window.Memory.Span.Slice(0, n));
readAt = writeAt = n;
}
@@ -774,11 +776,11 @@ internal sealed class InflateBlocks
// update check information
if (checkfn != null)
{
_codec._adler32 = check = Adler32.Calculate(check, window.AsSpan(readAt, nBytes));
_codec._adler32 = check = Adler32.Calculate(check, window.Memory.Span.Slice(readAt, nBytes));
}
// copy as far as end of window
Array.Copy(window, readAt, _codec.OutputBuffer, _codec.NextOut, nBytes);
window.Memory.Span.Slice(readAt, nBytes).CopyTo(_codec.OutputBuffer.AsSpan(_codec.NextOut));
_codec.NextOut += nBytes;
readAt += nBytes;
@@ -1213,7 +1215,7 @@ internal sealed class InflateCodes
}
}
blocks.window[q++] = blocks.window[f++];
blocks.window.Memory.Span[q++] = blocks.window.Memory.Span[f++];
m--;
if (f == blocks.end)
@@ -1259,7 +1261,7 @@ internal sealed class InflateCodes
}
r = ZlibConstants.Z_OK;
blocks.window[q++] = (byte)lit;
blocks.window.Memory.Span[q++] = (byte)lit;
m--;
mode = START;
@@ -1396,7 +1398,7 @@ internal sealed class InflateCodes
b >>= (tp[tp_index_t_3 + 1]);
k -= (tp[tp_index_t_3 + 1]);
s.window[q++] = (byte)tp[tp_index_t_3 + 2];
s.window.Memory.Span[q++] = (byte)tp[tp_index_t_3 + 2];
m--;
continue;
}
@@ -1461,13 +1463,13 @@ internal sealed class InflateCodes
r = q - d;
if (q - r > 0 && 2 > (q - r))
{
s.window[q++] = s.window[r++]; // minimum count is three,
s.window[q++] = s.window[r++]; // so unroll loop a little
s.window.Memory.Span[q++] = s.window.Memory.Span[r++]; // minimum count is three,
s.window.Memory.Span[q++] = s.window.Memory.Span[r++]; // so unroll loop a little
c -= 2;
}
else
{
Array.Copy(s.window, r, s.window, q, 2);
s.window.Memory.Span.Slice(r, 2).CopyTo(s.window.Memory.Span.Slice(q));
q += 2;
r += 2;
c -= 2;
@@ -1490,12 +1492,12 @@ internal sealed class InflateCodes
{
do
{
s.window[q++] = s.window[r++];
s.window.Memory.Span[q++] = s.window.Memory.Span[r++];
} while (--e != 0);
}
else
{
Array.Copy(s.window, r, s.window, q, e);
s.window.Memory.Span.Slice(r, e).CopyTo(s.window.Memory.Span.Slice(q));
q += e;
r += e;
e = 0;
@@ -1509,12 +1511,12 @@ internal sealed class InflateCodes
{
do
{
s.window[q++] = s.window[r++];
s.window.Memory.Span[q++] = s.window.Memory.Span[r++];
} while (--c != 0);
}
else
{
Array.Copy(s.window, r, s.window, q, c);
s.window.Memory.Span.Slice(r, c).CopyTo(s.window.Memory.Span.Slice(q));
q += c;
r += c;
c = 0;
@@ -1560,7 +1562,7 @@ internal sealed class InflateCodes
{
b >>= (tp[tp_index_t_3 + 1]);
k -= (tp[tp_index_t_3 + 1]);
s.window[q++] = (byte)tp[tp_index_t_3 + 2];
s.window.Memory.Span[q++] = (byte)tp[tp_index_t_3 + 2];
m--;
break;
}

View File

@@ -87,10 +87,10 @@ internal class RarStream : Stream, IStreamStack
#endif
ArrayPool<byte>.Shared.Return(this.tmpBuffer);
this.tmpBuffer = null;
readStream.Dispose();
}
isDisposed = true;
base.Dispose(disposing);
readStream.Dispose();
}
}

View File

@@ -126,17 +126,13 @@ internal sealed partial class Unpack : BitInput, IRarUnpack
private FileHeader fileHeader;
private void Init(byte[] window)
private void Init()
{
if (this.window is null && window is null)
if (this.window is null)
{
this.window = ArrayPool<byte>.Shared.Rent(PackDef.MAXWINSIZE);
}
else if (window is not null)
{
this.window = window;
externalWindow = true;
}
inAddr = 0;
UnpInitData(false);
}
@@ -149,7 +145,7 @@ internal sealed partial class Unpack : BitInput, IRarUnpack
this.writeStream = writeStream;
if (!fileHeader.IsSolid)
{
Init(null);
Init();
}
suspended = false;
DoUnpack();
@@ -168,7 +164,7 @@ internal sealed partial class Unpack : BitInput, IRarUnpack
this.writeStream = writeStream;
if (!fileHeader.IsSolid)
{
Init(null);
Init();
}
suspended = false;
await DoUnpackAsync(cancellationToken).ConfigureAwait(false);

View File

@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -28,13 +29,16 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
#if DEBUG_STREAMS
this.DebugDispose(typeof(BufferedSubStream));
#endif
if (disposing) { }
if (disposing)
{
ArrayPool<byte>.Shared.Return(_cache);
}
base.Dispose(disposing);
}
private int _cacheOffset;
private int _cacheLength;
private readonly byte[] _cache = new byte[32 << 10];
private readonly byte[] _cache = ArrayPool<byte>.Shared.Rent(32 << 10);
private long origin;
private long BytesLeftToRead { get; set; }

View File

@@ -1,8 +1,6 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

View File

@@ -0,0 +1,86 @@
using System.IO;
using System.Linq;
using BenchmarkDotNet.Attributes;
using SharpCompress.Archives;
namespace SharpCompress.Performance;
/// <summary>
/// Benchmarks for Archive API operations across different formats.
/// Archive API is used for random access to entries with seekable streams.
/// </summary>
[MemoryDiagnoser]
public class ArchiveReadBenchmarks : BenchmarkBase
{
[Benchmark]
public void ZipArchiveRead()
{
var path = GetTestArchivePath("Zip.deflate.zip");
using var archive = ArchiveFactory.Open(path);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var stream = entry.OpenEntryStream();
stream.CopyTo(Stream.Null);
}
}
[Benchmark]
public void TarArchiveRead()
{
var path = GetTestArchivePath("Tar.tar");
using var archive = ArchiveFactory.Open(path);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var stream = entry.OpenEntryStream();
stream.CopyTo(Stream.Null);
}
}
[Benchmark]
public void TarGzArchiveRead()
{
var path = GetTestArchivePath("Tar.tar.gz");
using var archive = ArchiveFactory.Open(path);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var stream = entry.OpenEntryStream();
stream.CopyTo(Stream.Null);
}
}
[Benchmark]
public void TarBz2ArchiveRead()
{
var path = GetTestArchivePath("Tar.tar.bz2");
using var archive = ArchiveFactory.Open(path);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var stream = entry.OpenEntryStream();
stream.CopyTo(Stream.Null);
}
}
[Benchmark]
public void SevenZipArchiveRead()
{
var path = GetTestArchivePath("7Zip.LZMA2.7z");
using var archive = ArchiveFactory.Open(path);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var stream = entry.OpenEntryStream();
stream.CopyTo(Stream.Null);
}
}
[Benchmark]
public void RarArchiveRead()
{
var path = GetTestArchivePath("Rar.rar");
using var archive = ArchiveFactory.Open(path);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var stream = entry.OpenEntryStream();
stream.CopyTo(Stream.Null);
}
}
}

View File

@@ -0,0 +1,47 @@
using System.IO;
using System.Linq;
using BenchmarkDotNet.Attributes;
using SharpCompress.Archives;
namespace SharpCompress.Performance;
/// <summary>
/// Benchmarks comparing current code against a baseline.
/// Use [Baseline] attribute to mark the reference benchmark.
/// </summary>
[MemoryDiagnoser]
[RankColumn]
public class BaselineComparisonBenchmarks : BenchmarkBase
{
/// <summary>
/// Baseline benchmark for Zip archive reading.
/// This serves as the reference point for comparison.
/// </summary>
[Benchmark(Baseline = true)]
public void ZipArchiveRead_Baseline()
{
var path = GetTestArchivePath("Zip.deflate.zip");
using var archive = ArchiveFactory.Open(path);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var stream = entry.OpenEntryStream();
stream.CopyTo(Stream.Null);
}
}
/// <summary>
/// Current implementation benchmark for Zip archive reading.
/// BenchmarkDotNet will compare this against the baseline.
/// </summary>
[Benchmark]
public void ZipArchiveRead_Current()
{
var path = GetTestArchivePath("Zip.deflate.zip");
using var archive = ArchiveFactory.Open(path);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var stream = entry.OpenEntryStream();
stream.CopyTo(Stream.Null);
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.IO;
namespace SharpCompress.Performance;
/// <summary>
/// Base class for all benchmarks providing common setup for test archives path
/// </summary>
public class BenchmarkBase
{
protected readonly string TEST_ARCHIVES_PATH;
public BenchmarkBase()
{
var index = AppDomain.CurrentDomain.BaseDirectory.IndexOf(
"SharpCompress.Performance",
StringComparison.OrdinalIgnoreCase
);
if (index == -1)
{
throw new InvalidOperationException(
"Could not locate SharpCompress.Performance in the base directory path"
);
}
var path = AppDomain.CurrentDomain.BaseDirectory.Substring(0, index);
var solutionBasePath =
Path.GetDirectoryName(path)
?? throw new InvalidOperationException("Could not determine solution base path");
TEST_ARCHIVES_PATH = Path.Combine(solutionBasePath, "TestArchives", "Archives");
}
protected string GetTestArchivePath(string filename) =>
Path.Combine(TEST_ARCHIVES_PATH, filename);
}

View File

@@ -1,54 +1,16 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Performance;
using SharpCompress.Readers;
using SharpCompress.Test;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
var index = AppDomain.CurrentDomain.BaseDirectory.IndexOf(
"SharpCompress.Performance",
StringComparison.OrdinalIgnoreCase
);
var path = AppDomain.CurrentDomain.BaseDirectory.Substring(0, index);
var SOLUTION_BASE_PATH = Path.GetDirectoryName(path) ?? throw new ArgumentNullException();
namespace SharpCompress.Performance;
var TEST_ARCHIVES_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives", "Archives");
//using var _ = JetbrainsProfiler.Memory($"/Users/adam/temp/");
using (var __ = JetbrainsProfiler.Cpu($"/Users/adam/temp/"))
internal class Program
{
var testArchives = new[]
static void Main(string[] args)
{
"Rar.Audio_program.rar",
// Run all benchmarks in the assembly
var config = DefaultConfig.Instance;
//"64bitstream.zip.7z",
//"TarWithSymlink.tar.gz"
};
var arcs = testArchives.Select(a => Path.Combine(TEST_ARCHIVES_PATH, a)).ToArray();
for (int i = 0; i < 50; i++)
{
using var found = ArchiveFactory.Open(arcs[0]);
foreach (var entry in found.Entries.Where(entry => !entry.IsDirectory))
{
Console.WriteLine($"Extracting {entry.Key}");
using var entryStream = entry.OpenEntryStream();
entryStream.CopyTo(Stream.Null);
}
/*using var found = ReaderFactory.Open(arcs[0]);
while (found.MoveToNextEntry())
{
var entry = found.Entry;
if (entry.IsDirectory)
continue;
Console.WriteLine($"Extracting {entry.Key}");
found.WriteEntryTo(Stream.Null);
}*/
// BenchmarkRunner will find all classes with [Benchmark] attributes
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
}
Console.WriteLine("Still running...");
}
await Task.Delay(500);

View File

@@ -0,0 +1,131 @@
# SharpCompress Performance Benchmarks
This project uses [BenchmarkDotNet](https://benchmarkdotnet.org/) to measure and track performance of SharpCompress archive operations.
## Running Benchmarks
### Run All Benchmarks
```bash
cd tests/SharpCompress.Performance
dotnet run -c Release
```
### Run Specific Benchmark Classes
```bash
# Run only Archive API benchmarks
dotnet run -c Release -- --filter "*ArchiveReadBenchmarks*"
# Run only Reader API benchmarks
dotnet run -c Release -- --filter "*ReaderBenchmarks*"
```
### Run Specific Benchmark Methods
```bash
# Run only Zip benchmarks
dotnet run -c Release -- --filter "*Zip*"
# Run a specific method
dotnet run -c Release -- --filter "ArchiveReadBenchmarks.ZipArchiveRead"
```
### Quick Dry Run (for testing)
```bash
dotnet run -c Release -- --job dry
```
## Benchmark Categories
### ArchiveReadBenchmarks
Tests the **Archive API** which provides random access to entries with seekable streams. Covers:
- Zip (deflate compression)
- Tar (uncompressed)
- Tar.gz (gzip compression)
- Tar.bz2 (bzip2 compression)
- 7Zip (LZMA2 compression)
- Rar
### ReaderBenchmarks
Tests the **Reader API** which provides forward-only streaming for non-seekable streams. Covers:
- Zip
- Tar
- Tar.gz
- Tar.bz2
- Rar
### WriteBenchmarks
Tests the **Writer API** for creating archives using forward-only writing. Covers:
- Zip (deflate compression)
- Tar (uncompressed)
- Tar.gz (gzip compression)
### BaselineComparisonBenchmarks
Example benchmark showing how to compare implementations using the `[Baseline]` attribute. The baseline benchmark serves as a reference point, and BenchmarkDotNet calculates the ratio of performance between baseline and other methods.
## Comparing Against Previous Versions
### Using Baseline Attribute
Mark one benchmark with `[Baseline = true]` and BenchmarkDotNet will show relative performance:
```csharp
[Benchmark(Baseline = true)]
public void MethodA() { /* ... */ }
[Benchmark]
public void MethodB() { /* ... */ }
```
Results will show ratios like "1.5x slower" or "0.8x faster" compared to the baseline.
### Using BenchmarkDotNet.Artifacts for Historical Comparison
BenchmarkDotNet saves results to `BenchmarkDotNet.Artifacts/results/`. You can:
1. Run benchmarks and save the results
2. Keep a snapshot of the results file
3. Compare new runs against saved results
### Using Different NuGet Versions (Advanced)
To compare against a published NuGet package:
1. Create a separate benchmark project referencing the NuGet package
2. Use BenchmarkDotNet's `[SimpleJob]` attribute with different runtimes
3. Reference both the local project and NuGet package in different jobs
## Interpreting Results
BenchmarkDotNet provides:
- **Mean**: Average execution time
- **Error**: Half of 99.9% confidence interval
- **StdDev**: Standard deviation of measurements
- **Allocated**: Memory allocated per operation
- **Rank**: Relative ranking (when using `[RankColumn]`)
- **Ratio**: Relative performance vs baseline (when using `[Baseline]`)
## Output Artifacts
Results are saved to `BenchmarkDotNet.Artifacts/results/`:
- `*.csv`: Raw data for further analysis
- `*-report.html`: HTML report with charts
- `*-report-github.md`: Markdown report for GitHub
- `*.log`: Detailed execution log
## Best Practices
1. **Always run in Release mode**: Debug builds have significant overhead
2. **Close other applications**: Minimize system noise during benchmarks
3. **Run multiple times**: Look for consistency across runs
4. **Use appropriate workload**: Ensure benchmarks run for at least 100ms
5. **Track trends**: Compare results over time to detect regressions
6. **Archive results**: Keep snapshots of benchmark results for historical comparison
## CI/CD Integration
Consider adding benchmarks to CI/CD to:
- Detect performance regressions automatically
- Track performance trends over time
- Compare PR performance against main branch
## Additional Resources
- [BenchmarkDotNet Documentation](https://benchmarkdotnet.org/articles/overview.html)
- [BenchmarkDotNet Configuration](https://benchmarkdotnet.org/articles/configs/configs.html)
- [BenchmarkDotNet Baseline](https://benchmarkdotnet.org/articles/features/baselines.html)

View File

@@ -0,0 +1,88 @@
using System.IO;
using BenchmarkDotNet.Attributes;
using SharpCompress.Readers;
namespace SharpCompress.Performance;
/// <summary>
/// Benchmarks for Reader API operations across different formats.
/// Reader API is used for forward-only streaming with non-seekable streams.
/// </summary>
[MemoryDiagnoser]
public class ReaderBenchmarks : BenchmarkBase
{
[Benchmark]
public void ZipReaderRead()
{
var path = GetTestArchivePath("Zip.deflate.zip");
using var stream = File.OpenRead(path);
using var reader = ReaderFactory.Open(stream);
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
reader.WriteEntryTo(Stream.Null);
}
}
}
[Benchmark]
public void TarReaderRead()
{
var path = GetTestArchivePath("Tar.tar");
using var stream = File.OpenRead(path);
using var reader = ReaderFactory.Open(stream);
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
reader.WriteEntryTo(Stream.Null);
}
}
}
[Benchmark]
public void TarGzReaderRead()
{
var path = GetTestArchivePath("Tar.tar.gz");
using var stream = File.OpenRead(path);
using var reader = ReaderFactory.Open(stream);
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
reader.WriteEntryTo(Stream.Null);
}
}
}
[Benchmark]
public void TarBz2ReaderRead()
{
var path = GetTestArchivePath("Tar.tar.bz2");
using var stream = File.OpenRead(path);
using var reader = ReaderFactory.Open(stream);
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
reader.WriteEntryTo(Stream.Null);
}
}
}
[Benchmark]
public void RarReaderRead()
{
var path = GetTestArchivePath("Rar.rar");
using var stream = File.OpenRead(path);
using var reader = ReaderFactory.Open(stream);
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
reader.WriteEntryTo(Stream.Null);
}
}
}
}

View File

@@ -4,6 +4,7 @@
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="JetBrains.Profiler.SelfApi" />
<ProjectReference Include="..\..\src\SharpCompress\SharpCompress.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,106 @@
using System.IO;
using System.Linq;
using BenchmarkDotNet.Attributes;
using SharpCompress.Common;
using SharpCompress.Writers;
namespace SharpCompress.Performance;
/// <summary>
/// Benchmarks for Writer operations.
/// Tests creating archives with different compression formats using forward-only Writer API.
/// </summary>
[MemoryDiagnoser]
public class WriteBenchmarks : BenchmarkBase
{
private string _tempOutputPath = null!;
private string[] _testFiles = null!;
[GlobalSetup]
public void Setup()
{
_tempOutputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(_tempOutputPath);
// Get some test files to compress
var originalPath = Path.Combine(Path.GetDirectoryName(TEST_ARCHIVES_PATH)!, "Original");
if (Directory.Exists(originalPath))
{
_testFiles = Directory.GetFiles(originalPath).Take(5).ToArray();
}
else
{
_testFiles = [];
}
}
[IterationCleanup]
public void IterationCleanup()
{
// Clean up created archives after each iteration to avoid file reuse affecting measurements
if (Directory.Exists(_tempOutputPath))
{
foreach (var file in Directory.GetFiles(_tempOutputPath))
{
File.Delete(file);
}
}
}
[GlobalCleanup]
public void Cleanup()
{
if (Directory.Exists(_tempOutputPath))
{
Directory.Delete(_tempOutputPath, true);
}
}
[Benchmark]
public void ZipWriterWrite()
{
var outputFile = Path.Combine(_tempOutputPath, "test.zip");
using var stream = File.Create(outputFile);
using var writer = WriterFactory.Open(
stream,
ArchiveType.Zip,
new WriterOptions(CompressionType.Deflate)
);
foreach (var file in _testFiles)
{
writer.Write(Path.GetFileName(file), file);
}
}
[Benchmark]
public void TarWriterWrite()
{
var outputFile = Path.Combine(_tempOutputPath, "test.tar");
using var stream = File.Create(outputFile);
using var writer = WriterFactory.Open(
stream,
ArchiveType.Tar,
new WriterOptions(CompressionType.None)
);
foreach (var file in _testFiles)
{
writer.Write(Path.GetFileName(file), file);
}
}
[Benchmark]
public void TarGzWriterWrite()
{
var outputFile = Path.Combine(_tempOutputPath, "test.tar.gz");
using var stream = File.Create(outputFile);
using var writer = WriterFactory.Open(
stream,
ArchiveType.Tar,
new WriterOptions(CompressionType.GZip)
);
foreach (var file in _testFiles)
{
writer.Write(Path.GetFileName(file), file);
}
}
}

View File

@@ -2,6 +2,24 @@
"version": 2,
"dependencies": {
"net10.0": {
"BenchmarkDotNet": {
"type": "Direct",
"requested": "[0.14.0, )",
"resolved": "0.14.0",
"contentHash": "eIPSDKi3oni734M1rt/XJAwGQQOIf9gLjRRKKJ0HuVy3vYd7gnmAIX1bTjzI9ZbAY/nPddgqqgM/TeBYitMCIg==",
"dependencies": {
"BenchmarkDotNet.Annotations": "0.14.0",
"CommandLineParser": "2.9.1",
"Gee.External.Capstone": "2.3.0",
"Iced": "1.17.0",
"Microsoft.CodeAnalysis.CSharp": "4.1.0",
"Microsoft.Diagnostics.Runtime": "2.2.332302",
"Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.8",
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
"Perfolizer": "[0.3.17]",
"System.Management": "5.0.0"
}
},
"JetBrains.Profiler.SelfApi": {
"type": "Direct",
"requested": "[2.5.15, )",
@@ -37,6 +55,26 @@
"resolved": "17.14.15",
"contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw=="
},
"BenchmarkDotNet.Annotations": {
"type": "Transitive",
"resolved": "0.14.0",
"contentHash": "CUDCg6bgHrDzhjnA+IOBl5gAo8Y5hZ2YSs7MBXrYMlMKpBZqrD5ez0537uDveOkcf+YWAoK+S4sMcuWPbIz8bw=="
},
"CommandLineParser": {
"type": "Transitive",
"resolved": "2.9.1",
"contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA=="
},
"Gee.External.Capstone": {
"type": "Transitive",
"resolved": "2.3.0",
"contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw=="
},
"Iced": {
"type": "Transitive",
"resolved": "1.17.0",
"contentHash": "8x+HCVTl/HHTGpscH3vMBhV8sknN/muZFw9s3TsI8SA6+c43cOTCi2+jE4KsU8pNLbJ++iF2ZFcpcXHXtDglnw=="
},
"JetBrains.FormatRipper": {
"type": "Transitive",
"resolved": "2.4.0",
@@ -63,6 +101,118 @@
"resolved": "8.0.0",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
},
"Microsoft.CodeAnalysis.Analyzers": {
"type": "Transitive",
"resolved": "3.3.3",
"contentHash": "j/rOZtLMVJjrfLRlAMckJLPW/1rze9MT1yfWqSIbUPGRu1m1P0fuo9PmqapwsmePfGB5PJrudQLvmUOAMF0DqQ=="
},
"Microsoft.CodeAnalysis.Common": {
"type": "Transitive",
"resolved": "4.1.0",
"contentHash": "bNzTyxP3iD5FPFHfVDl15Y6/wSoI7e3MeV0lOaj9igbIKTjgrmuw6LoVJ06jUNFA7+KaDC/OIsStWl/FQJz6sQ==",
"dependencies": {
"Microsoft.CodeAnalysis.Analyzers": "3.3.3"
}
},
"Microsoft.CodeAnalysis.CSharp": {
"type": "Transitive",
"resolved": "4.1.0",
"contentHash": "sbu6kDGzo9bfQxuqWpeEE7I9P30bSuZEnpDz9/qz20OU6pm79Z63+/BsAzO2e/R/Q97kBrpj647wokZnEVr97w==",
"dependencies": {
"Microsoft.CodeAnalysis.Common": "[4.1.0]"
}
},
"Microsoft.Diagnostics.NETCore.Client": {
"type": "Transitive",
"resolved": "0.2.251802",
"contentHash": "bqnYl6AdSeboeN4v25hSukK6Odm6/54E3Y2B8rBvgqvAW0mF8fo7XNRVE2DMOG7Rk0fiuA079QIH28+V+W1Zdg==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "1.1.0",
"Microsoft.Extensions.Logging": "2.1.1"
}
},
"Microsoft.Diagnostics.Runtime": {
"type": "Transitive",
"resolved": "2.2.332302",
"contentHash": "Hp84ivxSKIMTBzYSATxmUsm3YSXHWivcwiRRbsydGmqujMUK8BAueLN0ssAVEOkOBmh0vjUBhrq7YcroT7VCug==",
"dependencies": {
"Microsoft.Diagnostics.NETCore.Client": "0.2.251802"
}
},
"Microsoft.Diagnostics.Tracing.TraceEvent": {
"type": "Transitive",
"resolved": "3.1.8",
"contentHash": "kl3UMrZKSeSEYZ8rt/GjLUQToREjgQABqfg6PzQBmSlYHTZOKE9ePEOS2xptROQ9SVvngg3QGX51TIT11iZ0wA=="
},
"Microsoft.DotNet.PlatformAbstractions": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "LjVKO6P2y52c5ZhTLX/w8zc5H4Y3J/LJsgqTBj49TtFq/hAtVNue/WA0F6/7GMY90xhD7K0MDZ4qpOeWXbLvzg==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "2.1.1"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "VfuZJNa0WUshZ/+8BFZAhwFKiKuu/qOUCFntfdLpHj7vcRnsGHqd3G2Hse78DM+pgozczGM63lGPRLmy+uhUOA==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.1.1"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "fcLCTS03poWE4v9tSNBr3pWn0QwGgAn1vzqHXlXgvqZeOc7LvQNzaWcKRQZTdEc3+YhQKwMsOtm3VKSA2aWQ8w==",
"dependencies": {
"Microsoft.Extensions.Configuration": "2.1.1"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "MgYpU5cwZohUMKKg3sbPhvGG+eAZ/59E9UwPwlrUkyXU+PGzqwZg9yyQNjhxuAWmoNoFReoemeCku50prYSGzA=="
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "hh+mkOAQDTp6XH80xJt3+wwYVzkbwYQl9XZRCz4Um0JjP/o7N9vHM3rZ6wwwtr+BBe/L6iBO2sz0px6OWBzqZQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Binder": "2.1.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.1",
"Microsoft.Extensions.Logging.Abstractions": "2.1.1",
"Microsoft.Extensions.Options": "2.1.1"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "XRzK7ZF+O6FzdfWrlFTi1Rgj2080ZDsd46vzOjadHUB0Cz5kOvDG8vI7caa5YFrsHQpcfn0DxtjS4E46N4FZsA=="
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "V7lXCU78lAbzaulCGFKojcCyG8RTJicEbiBkPJjFqiqXwndEBBIehdXRMWEVU3UtzQ1yDvphiWUL9th6/4gJ7w==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.1",
"Microsoft.Extensions.Primitives": "2.1.1"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "scJ1GZNIxMmjpENh0UZ8XCQ6vzr/LzeF9WvEA51Ix2OQGAs9WPgPu8ABVUdvpKPLuor/t05gm6menJK3PwqOXg=="
},
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ=="
},
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
"type": "Transitive",
"resolved": "1.0.3",
@@ -73,8 +223,33 @@
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
},
"Perfolizer": {
"type": "Transitive",
"resolved": "0.3.17",
"contentHash": "FQgtCoF2HFwvzKWulAwBS5BGLlh8pgbrJtOp47jyBwh2CW16juVtacN1azOA2BqdrJXkXTNLNRMo7ZlHHiuAnA=="
},
"System.CodeDom": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "JPJArwA1kdj8qDAkY2XGjSWoYnqiM7q/3yRNkt6n28Mnn95MuEGkZXUbPBf7qc3IjwrGY5ttQon7yqHZyQJmOQ=="
},
"System.Management": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "MF1CHaRcC+MLFdnDthv4/bKWBZnlnSpkGqa87pKukQefgEdwtb9zFW6zs0GjPp73qtpYYg4q6PEKbzJbxCpKfw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"System.CodeDom": "5.0.0"
}
},
"sharpcompress": {
"type": "Project"
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "CentralTransitive",
"requested": "[10.0.0, )",
"resolved": "1.1.0",
"contentHash": "1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg=="
}
}
}