diff --git a/.gitignore b/.gitignore index 6c6a863d..a657c03f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ artifacts/ .DS_Store *.snupkg + +# BenchmarkDotNet artifacts +BenchmarkDotNet.Artifacts/ +**/BenchmarkDotNet.Artifacts/ diff --git a/Directory.Packages.props b/Directory.Packages.props index 703b2803..74e6a08e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,5 +1,6 @@ + diff --git a/tests/SharpCompress.Performance/ArchiveReadBenchmarks.cs b/tests/SharpCompress.Performance/ArchiveReadBenchmarks.cs new file mode 100644 index 00000000..1f9d6cfd --- /dev/null +++ b/tests/SharpCompress.Performance/ArchiveReadBenchmarks.cs @@ -0,0 +1,86 @@ +using System.IO; +using System.Linq; +using BenchmarkDotNet.Attributes; +using SharpCompress.Archives; + +namespace SharpCompress.Performance; + +/// +/// Benchmarks for Archive API operations across different formats. +/// Archive API is used for random access to entries with seekable streams. +/// +[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); + } + } +} diff --git a/tests/SharpCompress.Performance/BenchmarkBase.cs b/tests/SharpCompress.Performance/BenchmarkBase.cs new file mode 100644 index 00000000..ae4a5b36 --- /dev/null +++ b/tests/SharpCompress.Performance/BenchmarkBase.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; + +namespace SharpCompress.Performance; + +/// +/// Base class for all benchmarks providing common setup for test archives path +/// +public class BenchmarkBase +{ + protected readonly string TEST_ARCHIVES_PATH; + + public BenchmarkBase() + { + 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(); + + TEST_ARCHIVES_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives", "Archives"); + } + + protected string GetTestArchivePath(string filename) => + Path.Combine(TEST_ARCHIVES_PATH, filename); +} diff --git a/tests/SharpCompress.Performance/Program.cs b/tests/SharpCompress.Performance/Program.cs index d445208a..dac63d95 100644 --- a/tests/SharpCompress.Performance/Program.cs +++ b/tests/SharpCompress.Performance/Program.cs @@ -1,54 +1,17 @@ -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); + diff --git a/tests/SharpCompress.Performance/ReaderBenchmarks.cs b/tests/SharpCompress.Performance/ReaderBenchmarks.cs new file mode 100644 index 00000000..639f785b --- /dev/null +++ b/tests/SharpCompress.Performance/ReaderBenchmarks.cs @@ -0,0 +1,88 @@ +using System.IO; +using BenchmarkDotNet.Attributes; +using SharpCompress.Readers; + +namespace SharpCompress.Performance; + +/// +/// Benchmarks for Reader API operations across different formats. +/// Reader API is used for forward-only streaming with non-seekable streams. +/// +[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); + } + } + } +} diff --git a/tests/SharpCompress.Performance/SharpCompress.Performance.csproj b/tests/SharpCompress.Performance/SharpCompress.Performance.csproj index cab757e4..d4ad46d2 100644 --- a/tests/SharpCompress.Performance/SharpCompress.Performance.csproj +++ b/tests/SharpCompress.Performance/SharpCompress.Performance.csproj @@ -4,6 +4,7 @@ net10.0 + diff --git a/tests/SharpCompress.Performance/packages.lock.json b/tests/SharpCompress.Performance/packages.lock.json index 255570ad..07abfe93 100644 --- a/tests/SharpCompress.Performance/packages.lock.json +++ b/tests/SharpCompress.Performance/packages.lock.json @@ -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, )", @@ -12,6 +30,26 @@ "JetBrains.Profiler.Api": "1.4.10" } }, + "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", @@ -33,8 +71,145 @@ "JetBrains.HabitatDetector": "1.4.5" } }, + "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==" + }, + "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==" } } }