From 5b1d11bc1d8f6db3e423aea11709df5a85a8a970 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:31:32 +0000 Subject: [PATCH] Add WriteBenchmarks, BaselineComparisonBenchmarks, and comprehensive documentation Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../BaselineComparisonBenchmarks.cs | 47 +++++++ tests/SharpCompress.Performance/README.md | 131 ++++++++++++++++++ .../WriteBenchmarks.cs | 105 ++++++++++++++ 3 files changed, 283 insertions(+) create mode 100644 tests/SharpCompress.Performance/BaselineComparisonBenchmarks.cs create mode 100644 tests/SharpCompress.Performance/README.md create mode 100644 tests/SharpCompress.Performance/WriteBenchmarks.cs diff --git a/tests/SharpCompress.Performance/BaselineComparisonBenchmarks.cs b/tests/SharpCompress.Performance/BaselineComparisonBenchmarks.cs new file mode 100644 index 00000000..3907858f --- /dev/null +++ b/tests/SharpCompress.Performance/BaselineComparisonBenchmarks.cs @@ -0,0 +1,47 @@ +using System.IO; +using System.Linq; +using BenchmarkDotNet.Attributes; +using SharpCompress.Archives; + +namespace SharpCompress.Performance; + +/// +/// Benchmarks comparing current code against a baseline. +/// Use [Baseline] attribute to mark the reference benchmark. +/// +[MemoryDiagnoser] +[RankColumn] +public class BaselineComparisonBenchmarks : BenchmarkBase +{ + /// + /// Baseline benchmark for Zip archive reading. + /// This serves as the reference point for comparison. + /// + [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); + } + } + + /// + /// Current implementation benchmark for Zip archive reading. + /// BenchmarkDotNet will compare this against the baseline. + /// + [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); + } + } +} diff --git a/tests/SharpCompress.Performance/README.md b/tests/SharpCompress.Performance/README.md new file mode 100644 index 00000000..9dfc12dc --- /dev/null +++ b/tests/SharpCompress.Performance/README.md @@ -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) diff --git a/tests/SharpCompress.Performance/WriteBenchmarks.cs b/tests/SharpCompress.Performance/WriteBenchmarks.cs new file mode 100644 index 00000000..83d22cdd --- /dev/null +++ b/tests/SharpCompress.Performance/WriteBenchmarks.cs @@ -0,0 +1,105 @@ +using System.IO; +using System.Linq; +using BenchmarkDotNet.Attributes; +using SharpCompress.Common; +using SharpCompress.Writers; + +namespace SharpCompress.Performance; + +/// +/// Benchmarks for Writer operations. +/// Tests creating archives with different compression formats using forward-only Writer API. +/// +[MemoryDiagnoser] +public class WriteBenchmarks : BenchmarkBase +{ + private string _tempOutputPath = null!; + private readonly string[] _testFiles = null!; + + public WriteBenchmarks() + { + // 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(); + } + } + + [GlobalSetup] + public void Setup() + { + _tempOutputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(_tempOutputPath); + } + + [GlobalCleanup] + public void Cleanup() + { + if (Directory.Exists(_tempOutputPath)) + { + Directory.Delete(_tempOutputPath, true); + } + } + + [Benchmark] + public void ZipWriterWrite() + { + if (_testFiles == null || _testFiles.Length == 0) + return; + + 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() + { + if (_testFiles == null || _testFiles.Length == 0) + return; + + 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() + { + if (_testFiles == null || _testFiles.Length == 0) + return; + + 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); + } + } +} +