diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
index 6c773e9d42..dffcf43a97 100644
--- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
+++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
@@ -11,6 +11,7 @@
+
diff --git a/src/Components/Components/src/RenderTree/ArrayBuilderExtensions.cs b/src/Components/Components/src/RenderTree/ArrayBuilderExtensions.cs
new file mode 100644
index 0000000000..3fb698d3ee
--- /dev/null
+++ b/src/Components/Components/src/RenderTree/ArrayBuilderExtensions.cs
@@ -0,0 +1,27 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNetCore.Components.RenderTree
+{
+ internal static class ArrayBuilderExtensions
+ {
+ ///
+ /// Produces an structure describing the current contents.
+ ///
+ /// The .
+ public static ArrayRange ToRange(this ArrayBuilder builder)
+ => new ArrayRange(builder.Buffer, builder.Count);
+
+ ///
+ /// Produces an structure describing the selected contents.
+ ///
+ /// The
+ /// The index of the first item in the segment.
+ /// One plus the index of the last item in the segment.
+ /// The .
+ public static ArrayBuilderSegment ToSegment(this ArrayBuilder builder, int fromIndexInclusive, int toIndexExclusive)
+ => new ArrayBuilderSegment(builder, fromIndexInclusive, toIndexExclusive - fromIndexInclusive);
+ }
+}
diff --git a/src/Components/Server/src/BlazorPack/BlazorPackHubProtocol.cs b/src/Components/Server/src/BlazorPack/BlazorPackHubProtocol.cs
index ea97e65cdc..efebec9920 100644
--- a/src/Components/Server/src/BlazorPack/BlazorPackHubProtocol.cs
+++ b/src/Components/Server/src/BlazorPack/BlazorPackHubProtocol.cs
@@ -424,8 +424,8 @@ namespace Microsoft.AspNetCore.Components.Server.BlazorPack
writer.Write(floatValue);
break;
- case byte[] byteArray:
- writer.Write(byteArray);
+ case ArraySegment bytes:
+ writer.Write(bytes);
break;
case Exception exception:
diff --git a/src/Components/Server/src/Circuits/ArrayBuilderMemoryStream.cs b/src/Components/Server/src/Circuits/ArrayBuilderMemoryStream.cs
new file mode 100644
index 0000000000..ff1ff63114
--- /dev/null
+++ b/src/Components/Server/src/Circuits/ArrayBuilderMemoryStream.cs
@@ -0,0 +1,125 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Components.Web.Rendering
+{
+ ///
+ /// Writeable memory stream backed by a an .
+ ///
+ internal sealed class ArrayBuilderMemoryStream : Stream
+ {
+ public ArrayBuilderMemoryStream(ArrayBuilder arrayBuilder)
+ {
+ ArrayBuilder = arrayBuilder;
+ }
+
+ ///
+ public override bool CanRead => false;
+
+ ///
+ public override bool CanSeek => false;
+
+ ///
+ public override bool CanWrite => true;
+
+ ///
+ public override long Length => ArrayBuilder.Count;
+
+ ///
+ public override long Position
+ {
+ get => ArrayBuilder.Count;
+ set => throw new NotSupportedException();
+ }
+
+ public ArrayBuilder ArrayBuilder { get; }
+
+ ///
+ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
+
+ ///
+ public override int Read(byte[] buffer, int offset, int count)
+ => throw new NotSupportedException();
+
+ ///
+ public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ => throw new NotSupportedException();
+
+ ///
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ ValidateArguments(buffer, offset, count);
+
+ ArrayBuilder.Append(buffer, offset, count);
+ }
+
+ ///
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ ValidateArguments(buffer, offset, count);
+
+ ArrayBuilder.Append(buffer, offset, count);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public override void Flush()
+ {
+ // Do nothing.
+ }
+
+ ///
+ public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ ///
+ public override void SetLength(long value) => throw new NotSupportedException();
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ // Do nothing.
+ }
+
+ ///
+ public override ValueTask DisposeAsync() => default;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void ValidateArguments(byte[] buffer, int offset, int count)
+ {
+ if (buffer == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(nameof(buffer));
+ }
+
+ if (offset < 0)
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(nameof(offset));
+ }
+
+ if (count < 0)
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(nameof(count));
+ }
+
+ if (buffer.Length - offset < count)
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(nameof(offset));
+ }
+ }
+
+ private static class ThrowHelper
+ {
+ public static void ThrowArgumentNullException(string name)
+ => throw new ArgumentNullException(name);
+
+ public static void ThrowArgumentOutOfRangeException(string name)
+ => throw new ArgumentOutOfRangeException(name);
+ }
+ }
+}
diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs
index 8065ca4afd..dd12dce2aa 100644
--- a/src/Components/Server/src/Circuits/RemoteRenderer.cs
+++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Concurrent;
-using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading;
@@ -102,12 +101,13 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
protected override void Dispose(bool disposing)
{
_disposing = true;
- base.Dispose(true);
+ _rendererRegistry.TryRemove(Id);
while (PendingRenderBatches.TryDequeue(out var entry))
{
entry.CompletionSource.TrySetCanceled();
+ entry.Data.Dispose();
}
- _rendererRegistry.TryRemove(Id);
+ base.Dispose(true);
}
///
@@ -123,30 +123,34 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
// SignalR's SendAsync can wait an arbitrary duration before serializing the params.
// The RenderBatch buffer will get reused by subsequent renders, so we need to
// snapshot its contents now.
- // TODO: Consider using some kind of array pool instead of allocating a new
- // buffer on every render.
- byte[] batchBytes;
- using (var memoryStream = new MemoryStream())
+ var arrayBuilder = new ArrayBuilder(2048);
+ using var memoryStream = new ArrayBuilderMemoryStream(arrayBuilder);
+ PendingRender pendingRender;
+ try
{
using (var renderBatchWriter = new RenderBatchWriter(memoryStream, false))
{
renderBatchWriter.Write(in batch);
}
- batchBytes = memoryStream.ToArray();
+ var renderId = Interlocked.Increment(ref _nextRenderId);
+
+ pendingRender = new PendingRender(
+ renderId,
+ arrayBuilder,
+ new TaskCompletionSource