Skip to content

Commit ff2d7c3

Browse files
authored
feat(zip): add optional inflater pooling (icsharpcode#843)
1 parent a73e64d commit ff2d7c3

13 files changed

+233
-29
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<PropertyGroup>
4-
<OutputType>Exe</OutputType>
5-
<TargetFrameworks>net6.0;netcoreapp3.1;net462</TargetFrameworks>
6-
</PropertyGroup>
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFrameworks>net462;net6.0</TargetFrameworks>
6+
</PropertyGroup>
77

88
<ItemGroup>
9-
<PackageReference Include="BenchmarkDotNet">
10-
<Version>0.12.1</Version>
11-
</PackageReference>
9+
<PackageReference Include="BenchmarkDotNet" Version="0.13.7" />
1210
</ItemGroup>
1311

1412
<ItemGroup>
15-
<ProjectReference Include="..\..\src\ICSharpCode.SharpZipLib\ICSharpCode.SharpZipLib.csproj" />
13+
<ProjectReference Include="..\..\src\ICSharpCode.SharpZipLib\ICSharpCode.SharpZipLib.csproj" />
1614
</ItemGroup>
1715

1816
</Project>

benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ public class MultipleRuntimes : ManualConfig
99
{
1010
public MultipleRuntimes()
1111
{
12-
AddJob(Job.Default.WithToolchain(CsProjClassicNetToolchain.Net461).AsBaseline()); // NET 4.6.1
13-
AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp21)); // .NET Core 2.1
14-
AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp31)); // .NET Core 3.1
12+
AddJob(Job.Default.WithToolchain(CsProjClassicNetToolchain.Net462).AsBaseline()); // NET 4.6.2
13+
AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp60)); // .NET 6.0
1514
}
1615
}
1716

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System;
2+
using System.IO;
3+
using System.Net.Http;
4+
using System.Threading.Tasks;
5+
using BenchmarkDotNet.Attributes;
6+
using ICSharpCode.SharpZipLib.Zip;
7+
8+
namespace ICSharpCode.SharpZipLib.Benchmark.Zip
9+
{
10+
[MemoryDiagnoser]
11+
[Config(typeof(MultipleRuntimes))]
12+
public class ZipFile
13+
{
14+
private readonly byte[] readBuffer = new byte[4096];
15+
private string zipFileWithLargeAmountOfEntriesPath;
16+
17+
[GlobalSetup]
18+
public async Task GlobalSetup()
19+
{
20+
SharpZipLibOptions.InflaterPoolSize = 4;
21+
22+
// large real-world test file from test262 repository
23+
string commitSha = "2e4e0e6b8ebe3348a207144204cb6d7a5571c863";
24+
zipFileWithLargeAmountOfEntriesPath = Path.Combine(Path.GetTempPath(), $"{commitSha}.zip");
25+
if (!File.Exists(zipFileWithLargeAmountOfEntriesPath))
26+
{
27+
var uri = $"https://github.com/tc39/test262/archive/{commitSha}.zip";
28+
29+
Console.WriteLine("Loading test262 repository archive from {0}", uri);
30+
31+
using (var client = new HttpClient())
32+
{
33+
using (var downloadStream = await client.GetStreamAsync(uri))
34+
{
35+
using (var writeStream = File.OpenWrite(zipFileWithLargeAmountOfEntriesPath))
36+
{
37+
await downloadStream.CopyToAsync(writeStream);
38+
Console.WriteLine("File downloaded and saved to {0}", zipFileWithLargeAmountOfEntriesPath);
39+
}
40+
}
41+
}
42+
}
43+
44+
}
45+
46+
[Benchmark]
47+
public void ReadLargeZipFile()
48+
{
49+
using (var file = new SharpZipLib.Zip.ZipFile(zipFileWithLargeAmountOfEntriesPath))
50+
{
51+
foreach (ZipEntry entry in file)
52+
{
53+
using (var stream = file.GetInputStream(entry))
54+
{
55+
while (stream.Read(readBuffer, 0, readBuffer.Length) > 0)
56+
{
57+
}
58+
}
59+
}
60+
}
61+
}
62+
}
63+
}

benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System;
2-
using System.IO;
1+
using System.IO;
32
using BenchmarkDotNet.Attributes;
43

54
namespace ICSharpCode.SharpZipLib.Benchmark.Zip
@@ -15,7 +14,8 @@ public class ZipInputStream
1514
byte[] zippedData;
1615
byte[] readBuffer = new byte[4096];
1716

18-
public ZipInputStream()
17+
[GlobalSetup]
18+
public void GlobalSetup()
1919
{
2020
using (var memoryStream = new MemoryStream())
2121
{

benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System;
2-
using System.IO;
1+
using System.IO;
32
using System.Threading.Tasks;
43
using BenchmarkDotNet.Attributes;
54

@@ -16,7 +15,8 @@ public class ZipOutputStream
1615
byte[] outputBuffer;
1716
byte[] inputBuffer;
1817

19-
public ZipOutputStream()
18+
[GlobalSetup]
19+
public void GlobalSetup()
2020
{
2121
inputBuffer = new byte[ChunkSize];
2222
outputBuffer = new byte[N];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using ICSharpCode.SharpZipLib.Zip.Compression;
4+
5+
namespace ICSharpCode.SharpZipLib.Core
6+
{
7+
/// <summary>
8+
/// Pool for <see cref="Inflater"/> instances as they can be costly due to byte array allocations.
9+
/// </summary>
10+
internal sealed class InflaterPool
11+
{
12+
private readonly ConcurrentQueue<PooledInflater> noHeaderPool = new ConcurrentQueue<PooledInflater>();
13+
private readonly ConcurrentQueue<PooledInflater> headerPool = new ConcurrentQueue<PooledInflater>();
14+
15+
internal static InflaterPool Instance { get; } = new InflaterPool();
16+
17+
private InflaterPool()
18+
{
19+
}
20+
21+
internal Inflater Rent(bool noHeader = false)
22+
{
23+
if (SharpZipLibOptions.InflaterPoolSize <= 0)
24+
{
25+
return new Inflater(noHeader);
26+
}
27+
28+
var pool = GetPool(noHeader);
29+
30+
PooledInflater inf;
31+
if (pool.TryDequeue(out var inflater))
32+
{
33+
inf = inflater;
34+
inf.Reset();
35+
}
36+
else
37+
{
38+
inf = new PooledInflater(noHeader);
39+
}
40+
41+
return inf;
42+
}
43+
44+
internal void Return(Inflater inflater)
45+
{
46+
if (SharpZipLibOptions.InflaterPoolSize <= 0)
47+
{
48+
return;
49+
}
50+
51+
if (!(inflater is PooledInflater pooledInflater))
52+
{
53+
throw new ArgumentException("Returned inflater was not a pooled one");
54+
}
55+
56+
var pool = GetPool(inflater.noHeader);
57+
if (pool.Count < SharpZipLibOptions.InflaterPoolSize)
58+
{
59+
pooledInflater.Reset();
60+
pool.Enqueue(pooledInflater);
61+
}
62+
}
63+
64+
private ConcurrentQueue<PooledInflater> GetPool(bool noHeader) => noHeader ? noHeaderPool : headerPool;
65+
}
66+
}

src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.IO;
66
using System.Text;
7+
using ICSharpCode.SharpZipLib.Core;
78

89
namespace ICSharpCode.SharpZipLib.GZip
910
{
@@ -82,7 +83,7 @@ public GZipInputStream(Stream baseInputStream)
8283
/// Size of the buffer to use
8384
/// </param>
8485
public GZipInputStream(Stream baseInputStream, int size)
85-
: base(baseInputStream, new Inflater(true), size)
86+
: base(baseInputStream, InflaterPool.Instance.Rent(true), size)
8687
{
8788
}
8889

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using ICSharpCode.SharpZipLib.Zip.Compression;
2+
3+
namespace ICSharpCode.SharpZipLib
4+
{
5+
/// <summary>
6+
/// Global options to alter behavior.
7+
/// </summary>
8+
public static class SharpZipLibOptions
9+
{
10+
/// <summary>
11+
/// The max pool size allowed for reusing <see cref="Inflater"/> instances, defaults to 0 (disabled).
12+
/// </summary>
13+
public static int InflaterPoolSize { get; set; } = 0;
14+
}
15+
}

src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public class Inflater
137137
/// True means, that the inflated stream doesn't contain a Zlib header or
138138
/// footer.
139139
/// </summary>
140-
private bool noHeader;
140+
internal bool noHeader;
141141

142142
private readonly StreamManipulator input;
143143
private OutputWindow outputWindow;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using ICSharpCode.SharpZipLib.Core;
2+
3+
namespace ICSharpCode.SharpZipLib.Zip.Compression
4+
{
5+
/// <summary>
6+
/// A marker type for pooled version of an inflator that we can return back to <see cref="InflaterPool"/>.
7+
/// </summary>
8+
internal sealed class PooledInflater : Inflater
9+
{
10+
public PooledInflater(bool noHeader) : base(noHeader)
11+
{
12+
}
13+
}
14+
}

src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.IO;
33
using System.Security.Cryptography;
4+
using ICSharpCode.SharpZipLib.Core;
45

56
namespace ICSharpCode.SharpZipLib.Zip.Compression.Streams
67
{
@@ -339,7 +340,7 @@ public class InflaterInputStream : Stream
339340
/// The InputStream to read bytes from
340341
/// </param>
341342
public InflaterInputStream(Stream baseInputStream)
342-
: this(baseInputStream, new Inflater(), 4096)
343+
: this(baseInputStream, InflaterPool.Instance.Rent(), 4096)
343344
{
344345
}
345346

@@ -630,6 +631,12 @@ protected override void Dispose(bool disposing)
630631
baseInputStream.Dispose();
631632
}
632633
}
634+
635+
if (inf is PooledInflater inflater)
636+
{
637+
InflaterPool.Instance.Return(inflater);
638+
}
639+
inf = null;
633640
}
634641

635642
/// <summary>

0 commit comments

Comments
 (0)