Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,11 @@ tests/
### Factory Pattern
Factory implementations can implement one or more interfaces (`IArchiveFactory`, `IReaderFactory`, `IWriterFactory`) depending on format capabilities:
- `ArchiveFactory.OpenArchive()` - Opens archive API objects from seekable streams/files
- `ArchiveFactory.OpenAsyncArchive()` - Opens async archive API objects for async archive use cases
- `ReaderFactory.OpenReader()` - Auto-detects and opens forward-only readers
- `ReaderFactory.OpenAsyncReader()` - Auto-detects and opens forward-only async readers
- `WriterFactory.OpenWriter()` - Creates a writer for a specified `ArchiveType`
- `WriterFactory.OpenAsyncWriter()` - Creates an async writer for async write scenarios
- Factories located in: `src/SharpCompress/Factories/`

## Nullable Reference Types
Expand Down Expand Up @@ -132,6 +135,9 @@ SharpCompress supports multiple archive and compression formats:
### Async/Await Patterns
- All I/O operations support async/await with `CancellationToken`
- Async methods follow the naming convention: `MethodNameAsync`
- For async archive scenarios, prefer `ArchiveFactory.OpenAsyncArchive(...)` over sync `OpenArchive(...)`.
- For async forward-only read scenarios, prefer `ReaderFactory.OpenAsyncReader(...)` over sync `OpenReader(...)`.
- For async write scenarios, prefer `WriterFactory.OpenAsyncWriter(...)` over sync `OpenWriter(...)`.
- Key async methods:
- `WriteEntryToAsync` - Extract entry asynchronously
- `WriteAllToDirectoryAsync` - Extract all entries asynchronously
Expand Down Expand Up @@ -199,7 +205,8 @@ SharpCompress supports multiple archive and compression formats:
## Common Pitfalls

1. **Don't mix Archive and Reader APIs** - Archive needs seekable stream, Reader doesn't
2. **Solid archives (Rar, 7Zip)** - Use `ExtractAllEntries()` for best performance, not individual entry extraction
3. **Stream disposal** - Always set `LeaveStreamOpen` explicitly when needed (default is to close)
4. **Tar + non-seekable stream** - Must provide file size or it will throw
5. **Format detection** - Use `ReaderFactory.OpenReader()` for auto-detection, test with actual archive files
2. **Don't mix sync and async open paths** - For async workflows use `OpenAsyncArchive`/`OpenAsyncReader`/`OpenAsyncWriter`, not `OpenArchive`/`OpenReader`/`OpenWriter`
3. **Solid archives (Rar, 7Zip)** - Use `ExtractAllEntries()` for best performance, not individual entry extraction
4. **Stream disposal** - Always set `LeaveStreamOpen` explicitly when needed (default is to close)
5. **Tar + non-seekable stream** - Must provide file size or it will throw
6. **Format detection** - Use `ReaderFactory.OpenReader()` / `ReaderFactory.OpenAsyncReader()` for auto-detection, test with actual archive files
17 changes: 17 additions & 0 deletions tests/SharpCompress.Performance/Benchmarks/GZipBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using SharpCompress.Compressors;
using SharpCompress.Compressors.Deflate;
Expand Down Expand Up @@ -36,11 +37,27 @@ public void GZipCompress()
gzipStream.Write(_sourceData, 0, _sourceData.Length);
}

[Benchmark(Description = "GZip: Compress 100KB (Async)")]
public async Task GZipCompressAsync()
{
using var outputStream = new MemoryStream();
using var gzipStream = new GZipStream(outputStream, CompressionMode.Compress);
await gzipStream.WriteAsync(_sourceData, 0, _sourceData.Length).ConfigureAwait(false);
}

[Benchmark(Description = "GZip: Decompress 100KB")]
public void GZipDecompress()
{
using var inputStream = new MemoryStream(_compressedData);
using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress);
gzipStream.CopyTo(Stream.Null);
}

[Benchmark(Description = "GZip: Decompress 100KB (Async)")]
public async Task GZipDecompressAsync()
{
using var inputStream = new MemoryStream(_compressedData);
using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress);
await gzipStream.CopyToAsync(Stream.Null).ConfigureAwait(false);
}
}
27 changes: 27 additions & 0 deletions tests/SharpCompress.Performance/Benchmarks/RarBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using SharpCompress.Archives.Rar;
using SharpCompress.Readers;
Expand Down Expand Up @@ -30,6 +31,18 @@ public void RarExtractArchiveApi()
}
}

[Benchmark(Description = "Rar: Extract all entries (Archive API, Async)")]
public async Task RarExtractArchiveApiAsync()
{
using var stream = new MemoryStream(_rarBytes);
await using var archive = RarArchive.OpenAsyncArchive(stream);
await foreach (var entry in archive.EntriesAsync.Where(e => !e.IsDirectory))
{
await using var entryStream = await entry.OpenEntryStreamAsync().ConfigureAwait(false);
await entryStream.CopyToAsync(Stream.Null).ConfigureAwait(false);
}
}

[Benchmark(Description = "Rar: Extract all entries (Reader API)")]
public void RarExtractReaderApi()
{
Expand All @@ -43,4 +56,18 @@ public void RarExtractReaderApi()
}
}
}

[Benchmark(Description = "Rar: Extract all entries (Reader API, Async)")]
public async Task RarExtractReaderApiAsync()
{
using var stream = new MemoryStream(_rarBytes);
await using var reader = await ReaderFactory.OpenAsyncReader(stream).ConfigureAwait(false);
while (await reader.MoveToNextEntryAsync().ConfigureAwait(false))
{
if (!reader.Entry.IsDirectory)
{
await reader.WriteEntryToAsync(Stream.Null).ConfigureAwait(false);
}
}
}
}
51 changes: 51 additions & 0 deletions tests/SharpCompress.Performance/Benchmarks/SevenZipBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using SharpCompress.Archives.SevenZip;

Expand Down Expand Up @@ -31,6 +32,18 @@ public void SevenZipLzmaExtract()
}
}

[Benchmark(Description = "7Zip LZMA: Extract all entries (Async)")]
public async Task SevenZipLzmaExtractAsync()
{
using var stream = new MemoryStream(_lzmaBytes);
await using var archive = SevenZipArchive.OpenAsyncArchive(stream);
await foreach (var entry in archive.EntriesAsync.Where(e => !e.IsDirectory))
{
await using var entryStream = await entry.OpenEntryStreamAsync().ConfigureAwait(false);
await entryStream.CopyToAsync(Stream.Null).ConfigureAwait(false);
}
}

[Benchmark(Description = "7Zip LZMA2: Extract all entries")]
public void SevenZipLzma2Extract()
{
Expand All @@ -42,4 +55,42 @@ public void SevenZipLzma2Extract()
entryStream.CopyTo(Stream.Null);
}
}

[Benchmark(Description = "7Zip LZMA2: Extract all entries (Async)")]
public async Task SevenZipLzma2ExtractAsync()
{
using var stream = new MemoryStream(_lzma2Bytes);
await using var archive = SevenZipArchive.OpenAsyncArchive(stream);
await foreach (var entry in archive.EntriesAsync.Where(e => !e.IsDirectory))
{
await using var entryStream = await entry.OpenEntryStreamAsync().ConfigureAwait(false);
await entryStream.CopyToAsync(Stream.Null).ConfigureAwait(false);
}
}

[Benchmark(Description = "7Zip LZMA2 Reader: Extract all entries")]
public void SevenZipLzma2Extract_Reader()
{
using var stream = new MemoryStream(_lzma2Bytes);
using var archive = SevenZipArchive.OpenArchive(stream);
using var reader = archive.ExtractAllEntries();
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var entryStream = entry.OpenEntryStream();
entryStream.CopyTo(Stream.Null);
}
}

[Benchmark(Description = "7Zip LZMA2 Reader: Extract all entries (Async)")]
public async Task SevenZipLzma2ExtractAsync_Reader()
{
using var stream = new MemoryStream(_lzma2Bytes);
await using var archive = SevenZipArchive.OpenAsyncArchive(stream);
await using var reader = await archive.ExtractAllEntriesAsync();
while (await reader.MoveToNextEntryAsync().ConfigureAwait(false))
{
await using var entryStream = await reader.OpenEntryStreamAsync().ConfigureAwait(false);
await entryStream.CopyToAsync(Stream.Null).ConfigureAwait(false);
}
}
}
57 changes: 57 additions & 0 deletions tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
Expand Down Expand Up @@ -34,6 +35,18 @@ public void TarExtractArchiveApi()
}
}

[Benchmark(Description = "Tar: Extract all entries (Archive API, Async)")]
public async Task TarExtractArchiveApiAsync()
{
using var stream = new MemoryStream(_tarBytes);
await using var archive = TarArchive.OpenAsyncArchive(stream);
await foreach (var entry in archive.EntriesAsync.Where(e => !e.IsDirectory))
{
await using var entryStream = await entry.OpenEntryStreamAsync().ConfigureAwait(false);
await entryStream.CopyToAsync(Stream.Null).ConfigureAwait(false);
}
}

[Benchmark(Description = "Tar: Extract all entries (Reader API)")]
public void TarExtractReaderApi()
{
Expand All @@ -48,6 +61,20 @@ public void TarExtractReaderApi()
}
}

[Benchmark(Description = "Tar: Extract all entries (Reader API, Async)")]
public async Task TarExtractReaderApiAsync()
{
using var stream = new MemoryStream(_tarBytes);
await using var reader = await ReaderFactory.OpenAsyncReader(stream).ConfigureAwait(false);
while (await reader.MoveToNextEntryAsync().ConfigureAwait(false))
{
if (!reader.Entry.IsDirectory)
{
await reader.WriteEntryToAsync(Stream.Null).ConfigureAwait(false);
}
}
}

[Benchmark(Description = "Tar.GZip: Extract all entries")]
public void TarGzipExtract()
{
Expand All @@ -60,6 +87,18 @@ public void TarGzipExtract()
}
}

[Benchmark(Description = "Tar.GZip: Extract all entries (Async)")]
public async Task TarGzipExtractAsync()
{
using var stream = new MemoryStream(_tarGzBytes);
await using var archive = TarArchive.OpenAsyncArchive(stream);
await foreach (var entry in archive.EntriesAsync.Where(e => !e.IsDirectory))
{
await using var entryStream = await entry.OpenEntryStreamAsync().ConfigureAwait(false);
await entryStream.CopyToAsync(Stream.Null).ConfigureAwait(false);
}
}

[Benchmark(Description = "Tar: Create archive with small files")]
public void TarCreateSmallFiles()
{
Expand All @@ -78,4 +117,22 @@ public void TarCreateSmallFiles()
writer.Write($"file{i}.txt", entryStream);
}
}

[Benchmark(Description = "Tar: Create archive with small files (Async)")]
public async Task TarCreateSmallFilesAsync()
{
using var outputStream = new MemoryStream();
await using var writer = WriterFactory.OpenAsyncWriter(
outputStream,
ArchiveType.Tar,
new WriterOptions(CompressionType.None) { LeaveStreamOpen = true }
);

for (int i = 0; i < 10; i++)
{
var data = new byte[1024];
using var entryStream = new MemoryStream(data);
await writer.WriteAsync($"file{i}.txt", entryStream).ConfigureAwait(false);
}
}
}
45 changes: 45 additions & 0 deletions tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
Expand Down Expand Up @@ -34,6 +35,18 @@ public void ZipExtractArchiveApi()
}
}

[Benchmark(Description = "Zip: Extract all entries (Archive API, Async)")]
public async Task ZipExtractArchiveApiAsync()
{
using var stream = new MemoryStream(_archiveBytes);
await using var archive = ZipArchive.OpenAsyncArchive(stream);
await foreach (var entry in archive.EntriesAsync.Where(e => !e.IsDirectory))
{
await using var entryStream = await entry.OpenEntryStreamAsync().ConfigureAwait(false);
await entryStream.CopyToAsync(Stream.Null).ConfigureAwait(false);
}
}

[Benchmark(Description = "Zip: Extract all entries (Reader API)")]
public void ZipExtractReaderApi()
{
Expand All @@ -48,6 +61,20 @@ public void ZipExtractReaderApi()
}
}

[Benchmark(Description = "Zip: Extract all entries (Reader API, Async)")]
public async Task ZipExtractReaderApiAsync()
{
using var stream = new MemoryStream(_archiveBytes);
await using var reader = await ReaderFactory.OpenAsyncReader(stream).ConfigureAwait(false);
while (await reader.MoveToNextEntryAsync().ConfigureAwait(false))
{
if (!reader.Entry.IsDirectory)
{
await reader.WriteEntryToAsync(Stream.Null).ConfigureAwait(false);
}
}
}

[Benchmark(Description = "Zip: Create archive with small files")]
public void ZipCreateSmallFiles()
{
Expand All @@ -66,4 +93,22 @@ public void ZipCreateSmallFiles()
writer.Write($"file{i}.txt", entryStream);
}
}

[Benchmark(Description = "Zip: Create archive with small files (Async)")]
public async Task ZipCreateSmallFilesAsync()
{
using var outputStream = new MemoryStream();
await using var writer = WriterFactory.OpenAsyncWriter(
outputStream,
ArchiveType.Zip,
new WriterOptions(CompressionType.Deflate) { LeaveStreamOpen = true }
);

for (int i = 0; i < 10; i++)
{
var data = new byte[1024];
using var entryStream = new MemoryStream(data);
await writer.WriteAsync($"file{i}.txt", entryStream).ConfigureAwait(false);
}
}
}
8 changes: 4 additions & 4 deletions tests/SharpCompress.Performance/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ public static void Main(string[] args)
// Default: Run BenchmarkDotNet
var config = DefaultConfig.Instance.AddJob(
Job.Default.WithToolchain(InProcessEmitToolchain.Instance)
.WithWarmupCount(3) // Minimal warmup iterations for CI
.WithIterationCount(10) // Minimal measurement iterations for CI
.WithInvocationCount(10)
.WithUnrollFactor(1)
.WithWarmupCount(5) // Minimal warmup iterations for CI
.WithIterationCount(30) // Minimal measurement iterations for CI
.WithInvocationCount(30)
.WithUnrollFactor(2)
);

BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
Expand Down
10 changes: 5 additions & 5 deletions tests/SharpCompress.Performance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ This project contains performance benchmarks for SharpCompress using [BenchmarkD
## Overview

The benchmarks test all major archive formats supported by SharpCompress:
- **Zip**: Read (Archive & Reader API) and Write operations
- **Tar**: Read (Archive & Reader API) and Write operations, including Tar.GZip
- **Rar**: Read operations (Archive & Reader API)
- **7Zip**: Read operations for LZMA and LZMA2 compression
- **GZip**: Compression and decompression
- **Zip**: Read (Archive & Reader API) and Write operations, each with sync and async variants
- **Tar**: Read (Archive & Reader API) and Write operations, including Tar.GZip, each with sync and async variants
- **Rar**: Read operations (Archive & Reader API), each with sync and async variants
- **7Zip**: Read operations for LZMA and LZMA2 compression, each with sync and async variants
- **GZip**: Compression and decompression, each with sync and async variants

## Running Benchmarks

Expand Down
Loading