aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc Brooks <IDisposable@gmail.com>2026-05-29 15:54:58 -0500
committerMarc Brooks <IDisposable@gmail.com>2026-05-29 15:54:58 -0500
commit5c7ee6a6356917252063078fdb3ff331f897bf69 (patch)
treefa8761dac4bd552552662d0efc48f4972bafc8a5
parent645ae6bb99671ec8bd87c6cb78e6fa3d77063c55 (diff)
Improved resilience for fast-paths
Use fast paths only if we can TryGetBuffer on MemoryStream using segment's Array. Reduce swap overhead for fast path B. Avoid multiple virtcalls by memoizing the CanSeeks. Overlap slow path stream async reads.
-rw-r--r--src/Jellyfin.Extensions/StreamExtensions.cs38
1 files changed, 23 insertions, 15 deletions
diff --git a/src/Jellyfin.Extensions/StreamExtensions.cs b/src/Jellyfin.Extensions/StreamExtensions.cs
index 15b44d8f40..36361c58e8 100644
--- a/src/Jellyfin.Extensions/StreamExtensions.cs
+++ b/src/Jellyfin.Extensions/StreamExtensions.cs
@@ -133,36 +133,40 @@ namespace Jellyfin.Extensions
return true;
}
- if (a.CanSeek)
+ if (a.CanSeek is var aCanSeek && aCanSeek)
{
a.Position = 0;
}
- if (b.CanSeek)
+ if (b.CanSeek is var bCanSeek && bCanSeek)
{
b.Position = 0;
}
- if (a.CanSeek && b.CanSeek && b.Length != a.Length)
+ if (aCanSeek && bCanSeek && b.Length != a.Length)
{
return false;
}
- // If b is MemoryStream but a is not, swap them to enable fast path B
- if (b is MemoryStream && a is not MemoryStream)
+ // MemoryStreams only unlock a fast path if their underlying buffer is exposed via TryGetBuffer.
+ var segmentA = a is MemoryStream streamA && streamA.TryGetBuffer(out var bufA) ? bufA : default;
+ var segmentB = b is MemoryStream streamB && streamB.TryGetBuffer(out var bufB) ? bufB : default;
+
+ // Fast path A: both streams expose buffers, compare segments directly
+ if (segmentA.Array is not null && segmentB.Array is not null)
{
- (a, b) = (b, a);
+ return segmentA.AsSpan().SequenceEqual(segmentB.AsSpan());
}
- if (a is MemoryStream streamA && streamA.TryGetBuffer(out var segmentA))
+ if (segmentB.Array is not null) // && segmentA.Array is null guaranteed by previous check
{
- // Fast path A: if both streams are MemoryStreams, compare directly against each other
- if (b is MemoryStream streamB && streamB.TryGetBuffer(out var segmentB))
- {
- return segmentA.AsSpan().SequenceEqual(segmentB.AsSpan());
- }
+ // swap so that segmentA is the non-null one, compared to b we need only one fast path B
+ (segmentA, b) = (segmentB, a);
+ }
- // Fast path B: only first stream is a MemoryStream, compare against second stream chunk-by-chunk
+ if (segmentA.Array is not null) // either a was non-null, or b was non-null and was swapped there
+ {
+ // Fast path B: only one stream exposed a buffer, compare against the other chunk-by-chunk
var bufferB = ArrayPool<byte>.Shared.Rent(StreamComparisonBufferSize);
try
{
@@ -198,8 +202,12 @@ namespace Jellyfin.Extensions
{
cancellationToken.ThrowIfCancellationRequested();
- var bytesReadA = await a.ReadAtLeastAsync(memoryA, memoryA.Length, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false);
- var bytesReadB = await b.ReadAtLeastAsync(memoryB, memoryB.Length, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false);
+ var taskA = a.ReadAtLeastAsync(memoryA, memoryA.Length, throwOnEndOfStream: false, cancellationToken).AsTask();
+ var taskB = b.ReadAtLeastAsync(memoryB, memoryB.Length, throwOnEndOfStream: false, cancellationToken).AsTask();
+ await Task.WhenAll(taskA, taskB).ConfigureAwait(false);
+
+ var bytesReadA = await taskA.ConfigureAwait(false);
+ var bytesReadB = await taskB.ConfigureAwait(false);
if (bytesReadA != bytesReadB)
{