diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/ICSharpCode.SharpZipLib.Benchmark.csproj b/benchmark/ICSharpCode.SharpZipLib.Benchmark/ICSharpCode.SharpZipLib.Benchmark.csproj index 7688d0ff2..7fa26f80f 100644 --- a/benchmark/ICSharpCode.SharpZipLib.Benchmark/ICSharpCode.SharpZipLib.Benchmark.csproj +++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/ICSharpCode.SharpZipLib.Benchmark.csproj @@ -1,18 +1,16 @@  - - Exe - net6.0;netcoreapp3.1;net462 - + + Exe + net462;net6.0 + - - 0.12.1 - + - + diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs index 697e2923a..3a7beebbb 100644 --- a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs +++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs @@ -9,9 +9,8 @@ public class MultipleRuntimes : ManualConfig { public MultipleRuntimes() { - AddJob(Job.Default.WithToolchain(CsProjClassicNetToolchain.Net461).AsBaseline()); // NET 4.6.1 - AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp21)); // .NET Core 2.1 - AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp31)); // .NET Core 3.1 + AddJob(Job.Default.WithToolchain(CsProjClassicNetToolchain.Net462).AsBaseline()); // NET 4.6.2 + AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp60)); // .NET 6.0 } } diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipFile.cs b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipFile.cs new file mode 100644 index 000000000..0a84e0b88 --- /dev/null +++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipFile.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using ICSharpCode.SharpZipLib.Zip; + +namespace ICSharpCode.SharpZipLib.Benchmark.Zip +{ + [MemoryDiagnoser] + [Config(typeof(MultipleRuntimes))] + public class ZipFile + { + private readonly byte[] readBuffer = new byte[4096]; + private string zipFileWithLargeAmountOfEntriesPath; + + [GlobalSetup] + public async Task GlobalSetup() + { + SharpZipLibOptions.InflaterPoolSize = 4; + + // large real-world test file from test262 repository + string commitSha = "2e4e0e6b8ebe3348a207144204cb6d7a5571c863"; + zipFileWithLargeAmountOfEntriesPath = Path.Combine(Path.GetTempPath(), $"{commitSha}.zip"); + if (!File.Exists(zipFileWithLargeAmountOfEntriesPath)) + { + var uri = $"https://github.com/tc39/test262/archive/{commitSha}.zip"; + + Console.WriteLine("Loading test262 repository archive from {0}", uri); + + using (var client = new HttpClient()) + { + using (var downloadStream = await client.GetStreamAsync(uri)) + { + using (var writeStream = File.OpenWrite(zipFileWithLargeAmountOfEntriesPath)) + { + await downloadStream.CopyToAsync(writeStream); + Console.WriteLine("File downloaded and saved to {0}", zipFileWithLargeAmountOfEntriesPath); + } + } + } + } + + } + + [Benchmark] + public void ReadLargeZipFile() + { + using (var file = new SharpZipLib.Zip.ZipFile(zipFileWithLargeAmountOfEntriesPath)) + { + foreach (ZipEntry entry in file) + { + using (var stream = file.GetInputStream(entry)) + { + while (stream.Read(readBuffer, 0, readBuffer.Length) > 0) + { + } + } + } + } + } + } +} diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs index eb099ebfd..2e0c057d8 100644 --- a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs +++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using BenchmarkDotNet.Attributes; namespace ICSharpCode.SharpZipLib.Benchmark.Zip @@ -15,7 +14,8 @@ public class ZipInputStream byte[] zippedData; byte[] readBuffer = new byte[4096]; - public ZipInputStream() + [GlobalSetup] + public void GlobalSetup() { using (var memoryStream = new MemoryStream()) { diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs index ed125c1c7..c4e8620e3 100644 --- a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs +++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -16,7 +15,8 @@ public class ZipOutputStream byte[] outputBuffer; byte[] inputBuffer; - public ZipOutputStream() + [GlobalSetup] + public void GlobalSetup() { inputBuffer = new byte[ChunkSize]; outputBuffer = new byte[N]; diff --git a/src/ICSharpCode.SharpZipLib/Core/InflaterPool.cs b/src/ICSharpCode.SharpZipLib/Core/InflaterPool.cs new file mode 100644 index 000000000..39db32e8c --- /dev/null +++ b/src/ICSharpCode.SharpZipLib/Core/InflaterPool.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Concurrent; +using ICSharpCode.SharpZipLib.Zip.Compression; + +namespace ICSharpCode.SharpZipLib.Core +{ + /// + /// Pool for instances as they can be costly due to byte array allocations. + /// + internal sealed class InflaterPool + { + private readonly ConcurrentQueue noHeaderPool = new ConcurrentQueue(); + private readonly ConcurrentQueue headerPool = new ConcurrentQueue(); + + internal static InflaterPool Instance { get; } = new InflaterPool(); + + private InflaterPool() + { + } + + internal Inflater Rent(bool noHeader = false) + { + if (SharpZipLibOptions.InflaterPoolSize <= 0) + { + return new Inflater(noHeader); + } + + var pool = GetPool(noHeader); + + PooledInflater inf; + if (pool.TryDequeue(out var inflater)) + { + inf = inflater; + inf.Reset(); + } + else + { + inf = new PooledInflater(noHeader); + } + + return inf; + } + + internal void Return(Inflater inflater) + { + if (SharpZipLibOptions.InflaterPoolSize <= 0) + { + return; + } + + if (!(inflater is PooledInflater pooledInflater)) + { + throw new ArgumentException("Returned inflater was not a pooled one"); + } + + var pool = GetPool(inflater.noHeader); + if (pool.Count < SharpZipLibOptions.InflaterPoolSize) + { + pooledInflater.Reset(); + pool.Enqueue(pooledInflater); + } + } + + private ConcurrentQueue GetPool(bool noHeader) => noHeader ? noHeaderPool : headerPool; + } +} diff --git a/src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs b/src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs index db5aef2da..feca66b3d 100644 --- a/src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs +++ b/src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Text; +using ICSharpCode.SharpZipLib.Core; namespace ICSharpCode.SharpZipLib.GZip { @@ -82,7 +83,7 @@ public GZipInputStream(Stream baseInputStream) /// Size of the buffer to use /// public GZipInputStream(Stream baseInputStream, int size) - : base(baseInputStream, new Inflater(true), size) + : base(baseInputStream, InflaterPool.Instance.Rent(true), size) { } diff --git a/src/ICSharpCode.SharpZipLib/SharpZipLibOptions.cs b/src/ICSharpCode.SharpZipLib/SharpZipLibOptions.cs new file mode 100644 index 000000000..a6694e71e --- /dev/null +++ b/src/ICSharpCode.SharpZipLib/SharpZipLibOptions.cs @@ -0,0 +1,15 @@ +using ICSharpCode.SharpZipLib.Zip.Compression; + +namespace ICSharpCode.SharpZipLib +{ + /// + /// Global options to alter behavior. + /// + public static class SharpZipLibOptions + { + /// + /// The max pool size allowed for reusing instances, defaults to 0 (disabled). + /// + public static int InflaterPoolSize { get; set; } = 0; + } +} diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs index 439b4c601..5bf2a985e 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs @@ -137,7 +137,7 @@ public class Inflater /// True means, that the inflated stream doesn't contain a Zlib header or /// footer. /// - private bool noHeader; + internal bool noHeader; private readonly StreamManipulator input; private OutputWindow outputWindow; diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/PooledInflater.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/PooledInflater.cs new file mode 100644 index 000000000..0828de3ef --- /dev/null +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/PooledInflater.cs @@ -0,0 +1,14 @@ +using ICSharpCode.SharpZipLib.Core; + +namespace ICSharpCode.SharpZipLib.Zip.Compression +{ + /// + /// A marker type for pooled version of an inflator that we can return back to . + /// + internal sealed class PooledInflater : Inflater + { + public PooledInflater(bool noHeader) : base(noHeader) + { + } + } +} diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs index 7790474d2..980ffc701 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Security.Cryptography; +using ICSharpCode.SharpZipLib.Core; namespace ICSharpCode.SharpZipLib.Zip.Compression.Streams { @@ -339,7 +340,7 @@ public class InflaterInputStream : Stream /// The InputStream to read bytes from /// public InflaterInputStream(Stream baseInputStream) - : this(baseInputStream, new Inflater(), 4096) + : this(baseInputStream, InflaterPool.Instance.Rent(), 4096) { } @@ -630,6 +631,12 @@ protected override void Dispose(bool disposing) baseInputStream.Dispose(); } } + + if (inf is PooledInflater inflater) + { + InflaterPool.Instance.Return(inflater); + } + inf = null; } /// diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs index 951cc6e20..7fc1c5592 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs @@ -314,7 +314,7 @@ public enum FileUpdateMode /// } /// /// - public class ZipFile : IEnumerable, IDisposable + public class ZipFile : IEnumerable, IDisposable { #region KeyHandling @@ -810,7 +810,31 @@ public StringCodec StringCodec /// /// The Zip file has been closed. /// - public IEnumerator GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Gets an enumerator for the Zip entries in this Zip file. + /// + /// Returns an for this archive. + /// + /// The Zip file has been closed. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Gets an enumerator for the Zip entries in this Zip file. + /// + /// Returns an for this archive. + /// + /// The Zip file has been closed. + /// + public ZipEntryEnumerator GetEnumerator() { if (isDisposed_) { @@ -954,7 +978,7 @@ public Stream GetInputStream(long entryIndex) case CompressionMethod.Deflated: // No need to worry about ownership and closing as underlying stream close does nothing. - result = new InflaterInputStream(result, new Inflater(true)); + result = new InflaterInputStream(result, InflaterPool.Instance.Rent(true)); break; case CompressionMethod.BZip2: @@ -4001,20 +4025,26 @@ public static implicit operator string(ZipString zipString) /// /// An enumerator for Zip entries /// - private class ZipEntryEnumerator : IEnumerator + public struct ZipEntryEnumerator : IEnumerator { #region Constructors + /// + /// Constructs a new instance of . + /// + /// Entries to iterate. public ZipEntryEnumerator(ZipEntry[] entries) { array = entries; + index = -1; } #endregion Constructors #region IEnumerator Members - public object Current + /// + public ZipEntry Current { get { @@ -4022,22 +4052,32 @@ public object Current } } + /// + object IEnumerator.Current => Current; + + /// public void Reset() { index = -1; } + /// public bool MoveNext() { return (++index < array.Length); } + /// + public void Dispose() + { + } + #endregion IEnumerator Members #region Instance Fields private ZipEntry[] array; - private int index = -1; + private int index; #endregion Instance Fields } diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs index 4d258afc8..37e9e8ba8 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs @@ -1,10 +1,11 @@ using ICSharpCode.SharpZipLib.Checksum; using ICSharpCode.SharpZipLib.Encryption; -using ICSharpCode.SharpZipLib.Zip.Compression; using ICSharpCode.SharpZipLib.Zip.Compression.Streams; using System; using System.Diagnostics; using System.IO; +using ICSharpCode.SharpZipLib.Core; +using ICSharpCode.SharpZipLib.Zip.Compression; namespace ICSharpCode.SharpZipLib.Zip { @@ -88,7 +89,7 @@ public class ZipInputStream : InflaterInputStream /// /// The underlying providing data. public ZipInputStream(Stream baseInputStream) - : base(baseInputStream, new Inflater(true)) + : base(baseInputStream, InflaterPool.Instance.Rent(true)) { internalReader = new ReadDataHandler(ReadingNotAvailable); } @@ -99,7 +100,7 @@ public ZipInputStream(Stream baseInputStream) /// The underlying providing data. /// Size of the buffer. public ZipInputStream(Stream baseInputStream, int bufferSize) - : base(baseInputStream, new Inflater(true), bufferSize) + : base(baseInputStream, InflaterPool.Instance.Rent(true), bufferSize) { internalReader = new ReadDataHandler(ReadingNotAvailable); }