Skip to content

Commit 494a388

Browse files
authored
Merge pull request #542 from qmfrederik/fixes/packfiles
GitPackMemoryCache: Don't have multiple callers reuse the same stream
2 parents d5b1d15 + 593786e commit 494a388

File tree

5 files changed

+148
-7
lines changed

5 files changed

+148
-7
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.IO;
2+
using Nerdbank.GitVersioning.ManagedGit;
3+
using Xunit;
4+
5+
namespace NerdBank.GitVersioning.Tests.ManagedGit
6+
{
7+
/// <summary>
8+
/// Tests the <see cref="GitPackMemoryCache"/> class.
9+
/// </summary>
10+
public class GitPackMemoryCacheTests
11+
{
12+
[Fact]
13+
public void StreamsAreIndependent()
14+
{
15+
using (MemoryStream stream = new MemoryStream(
16+
new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }))
17+
{
18+
var cache = new GitPackMemoryCache();
19+
20+
var stream1 = cache.Add(0, stream);
21+
Assert.True(cache.TryOpen(0, out Stream stream2));
22+
23+
using (stream1)
24+
using (stream2)
25+
{
26+
stream1.Seek(5, SeekOrigin.Begin);
27+
Assert.Equal(5, stream1.Position);
28+
Assert.Equal(0, stream2.Position);
29+
Assert.Equal(5, stream1.ReadByte());
30+
31+
Assert.Equal(6, stream1.Position);
32+
Assert.Equal(0, stream2.Position);
33+
34+
Assert.Equal(0, stream2.ReadByte());
35+
Assert.Equal(6, stream1.Position);
36+
Assert.Equal(1, stream2.Position);
37+
}
38+
}
39+
}
40+
}
41+
}

src/NerdBank.GitVersioning/ManagedGit/GitPackDeltafiedStream.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public override int Read(Span<byte> span)
9393
var source = instruction.InstructionType == DeltaInstructionType.Copy ? this.baseStream : this.deltaStream;
9494

9595
Debug.Assert(instruction.Size > this.offset);
96+
Debug.Assert(source.Position + instruction.Size - this.offset <= source.Length);
9697
canRead = Math.Min(span.Length - read, instruction.Size - this.offset);
9798
didRead = source.Read(span.Slice(read, canRead));
9899

src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCache.cs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,50 @@
77

88
namespace Nerdbank.GitVersioning.ManagedGit
99
{
10-
internal class GitPackMemoryCache : GitPackCache
10+
/// <summary>
11+
/// <para>
12+
/// The <see cref="GitPackMemoryCache"/> implements the <see cref="GitPackCache"/> abstract class.
13+
/// When a <see cref="Stream"/> is added to the <see cref="GitPackMemoryCache"/>, it is wrapped in a
14+
/// <see cref="GitPackMemoryCacheStream"/>. This stream allows for just-in-time, random, read-only
15+
/// access to the underlying data (which may deltafied and/or compressed).
16+
/// </para>
17+
/// <para>
18+
/// Whenever data is read from a <see cref="GitPackMemoryCacheStream"/>, the call is forwarded to the
19+
/// underlying <see cref="Stream"/> and cached in a <see cref="MemoryStream"/>. If the same data is read
20+
/// twice, it is read from the <see cref="MemoryStream"/>, rather than the underlying <see cref="Stream"/>.
21+
/// </para>
22+
/// <para>
23+
/// <see cref="Add(long, Stream)"/> and <see cref="TryOpen(long, out Stream?)"/> return <see cref="Stream"/>
24+
/// objects which may operate on the same underlying <see cref="Stream"/>, but independently maintain
25+
/// their state.
26+
/// </para>
27+
/// </summary>
28+
public class GitPackMemoryCache : GitPackCache
1129
{
12-
private readonly Dictionary<long, Stream> cache = new Dictionary<long, Stream>();
30+
private readonly Dictionary<long, GitPackMemoryCacheStream> cache = new Dictionary<long, GitPackMemoryCacheStream>();
1331

32+
/// <inheritdoc/>
1433
public override Stream Add(long offset, Stream stream)
1534
{
1635
var cacheStream = new GitPackMemoryCacheStream(stream);
1736
this.cache.Add(offset, cacheStream);
18-
return cacheStream;
37+
return new GitPackMemoryCacheViewStream(cacheStream);
1938
}
2039

40+
/// <inheritdoc/>
2141
public override bool TryOpen(long offset, [NotNullWhen(true)] out Stream? stream)
2242
{
23-
if (this.cache.TryGetValue(offset, out stream))
43+
if (this.cache.TryGetValue(offset, out GitPackMemoryCacheStream? cacheStream))
2444
{
25-
stream.Seek(0, SeekOrigin.Begin);
45+
stream = new GitPackMemoryCacheViewStream(cacheStream!);
2646
return true;
2747
}
2848

49+
stream = null;
2950
return false;
3051
}
3152

53+
/// <inheritdoc/>
3254
public override void GetCacheStatistics(StringBuilder builder)
3355
{
3456
builder.AppendLine($"{this.cache.Count} items in cache");

src/NerdBank.GitVersioning/ManagedGit/GitPackMemoryCacheStream.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ public override long Seek(long offset, SeekOrigin origin)
7575
{
7676
var toRead = (int)(offset - this.cacheStream.Length);
7777
byte[] buffer = ArrayPool<byte>.Shared.Rent(toRead);
78-
this.stream.Read(buffer, 0, toRead);
78+
int read = this.stream.Read(buffer, 0, toRead);
7979
this.cacheStream.Seek(0, SeekOrigin.End);
80-
this.cacheStream.Write(buffer, 0, toRead);
80+
this.cacheStream.Write(buffer, 0, read);
8181
ArrayPool<byte>.Shared.Return(buffer);
8282

8383
this.DisposeStreamIfRead();
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text;
5+
6+
namespace Nerdbank.GitVersioning.ManagedGit
7+
{
8+
internal class GitPackMemoryCacheViewStream : Stream
9+
{
10+
private readonly GitPackMemoryCacheStream baseStream;
11+
12+
public GitPackMemoryCacheViewStream(GitPackMemoryCacheStream baseStream)
13+
{
14+
this.baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream));
15+
}
16+
17+
public override bool CanRead => true;
18+
19+
public override bool CanSeek => true;
20+
21+
public override bool CanWrite => false;
22+
23+
public override long Length => this.baseStream.Length;
24+
25+
private long position;
26+
27+
public override long Position
28+
{
29+
get => this.position;
30+
set => throw new NotSupportedException();
31+
}
32+
33+
public override void Flush() => throw new NotImplementedException();
34+
35+
public override int Read(byte[] buffer, int offset, int count)
36+
{
37+
return this.Read(buffer.AsSpan(offset, count));
38+
}
39+
40+
#if NETSTANDARD
41+
public int Read(Span<byte> buffer)
42+
#else
43+
/// <inheritdoc/>
44+
public override int Read(Span<byte> buffer)
45+
#endif
46+
{
47+
int read = 0;
48+
49+
lock (this.baseStream)
50+
{
51+
if (this.baseStream.Position != this.position)
52+
{
53+
this.baseStream.Seek(this.position, SeekOrigin.Begin);
54+
}
55+
56+
read = this.baseStream.Read(buffer);
57+
}
58+
59+
this.position += read;
60+
return read;
61+
}
62+
63+
public override long Seek(long offset, SeekOrigin origin)
64+
{
65+
if (origin != SeekOrigin.Begin)
66+
{
67+
throw new NotSupportedException();
68+
}
69+
70+
this.position = Math.Min(offset, this.Length);
71+
return this.position;
72+
}
73+
74+
public override void SetLength(long value) => throw new NotSupportedException();
75+
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
76+
}
77+
}

0 commit comments

Comments
 (0)