From 6c80c425114e81b1976f7680f272228c5ebe3adc Mon Sep 17 00:00:00 2001
From: Pranav K <prkrishn@hotmail.com>
Date: Tue, 16 Jul 2019 16:23:02 -0700
Subject: [PATCH] Use ArrayPool in ArrayBuilder<T> (#11830)

* Use ArrayPool
---
 .../test/RuntimeDependenciesResolverTest.cs   |   1 +
 ...ft.AspNetCore.Components.netstandard2.0.cs |   3 +-
 .../Components/src/RenderTree/ArrayBuilder.cs | 117 ++++---
 .../src/RenderTree/ArrayBuilderSegment.cs     |   1 +
 .../src/RenderTree/RenderTreeBuilder.cs       |   9 +-
 .../src/Rendering/ComponentState.cs           |  21 +-
 .../src/Rendering/RenderBatchBuilder.cs       |  16 +-
 .../Components/src/Rendering/Renderer.cs      |   4 +-
 .../Components/src/Routing/Router.cs          |   1 -
 .../test/RenderTreeDiffBuilderTest.cs         |  80 +++--
 .../test/Rendering/ArrayBuilderSegmentTest.cs |   4 +-
 .../test/Rendering/ArrayBuilderTest.cs        | 317 ++++++++++++++++++
 .../test/Rendering/TestArrayPool.cs           |  23 ++
 13 files changed, 514 insertions(+), 83 deletions(-)
 create mode 100644 src/Components/Components/test/Rendering/ArrayBuilderTest.cs
 create mode 100644 src/Components/Components/test/Rendering/TestArrayPool.cs

diff --git a/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs b/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs
index 87b1349844b..bf96e57a939 100644
--- a/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs
+++ b/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs
@@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
                 "StandaloneApp.dll",
                 "StandaloneApp.pdb",
                 "System.dll",
+                "System.Buffers.dll",
                 "System.Collections.Concurrent.dll",
                 "System.Collections.dll",
                 "System.ComponentModel.Composition.dll",
diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs
index d4cafd7478f..6097208bee1 100644
--- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs
+++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs
@@ -763,7 +763,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
         public ArrayRange(T[] array, int count) { throw null; }
         public Microsoft.AspNetCore.Components.RenderTree.ArrayRange<T> Clone() { throw null; }
     }
-    public partial class RenderTreeBuilder
+    public partial class RenderTreeBuilder : System.IDisposable
     {
         public const string ChildContent = "ChildContent";
         public RenderTreeBuilder(Microsoft.AspNetCore.Components.Rendering.Renderer renderer) { }
@@ -794,6 +794,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
         public void OpenElement(int sequence, string elementName) { }
         public void SetKey(object value) { }
         public void SetUpdatesAttributeName(string updatesAttributeName) { }
+        void System.IDisposable.Dispose() { }
     }
     [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
     public readonly partial struct RenderTreeDiff
diff --git a/src/Components/Components/src/RenderTree/ArrayBuilder.cs b/src/Components/Components/src/RenderTree/ArrayBuilder.cs
index 6be35ded27a..9d3e71993aa 100644
--- a/src/Components/Components/src/RenderTree/ArrayBuilder.cs
+++ b/src/Components/Components/src/RenderTree/ArrayBuilder.cs
@@ -2,6 +2,8 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using System.Buffers;
+using System.Diagnostics;
 using System.Runtime.CompilerServices;
 
 namespace Microsoft.AspNetCore.Components.RenderTree
@@ -15,26 +17,25 @@ namespace Microsoft.AspNetCore.Components.RenderTree
     /// components can be long-lived and re-render frequently, with the rendered size
     /// varying dramatically depending on the user's navigation in the app.
     /// </summary>
-    internal class ArrayBuilder<T>
+    internal class ArrayBuilder<T> : IDisposable
     {
-        private const int MinCapacity = 10;
+        // The following fields are memory mapped to the WASM client. Do not re-order or use auto-properties.
         private T[] _items;
         private int _itemsInUse;
 
-        /// <summary>
-        /// Constructs a new instance of <see cref="ArrayBuilder{T}"/>.
-        /// </summary>
-        public ArrayBuilder() : this(MinCapacity)
-        {
-        }
+        private static readonly T[] Empty = Array.Empty<T>();
+        private readonly ArrayPool<T> _arrayPool;
+        private readonly int _minCapacity;
+        private bool _disposed;
 
         /// <summary>
         /// Constructs a new instance of <see cref="ArrayBuilder{T}"/>.
         /// </summary>
-        public ArrayBuilder(int capacity)
+        public ArrayBuilder(int minCapacity = 32, ArrayPool<T> arrayPool = null)
         {
-            _items = new T[capacity < MinCapacity ? MinCapacity : capacity];
-            _itemsInUse = 0;
+            _arrayPool = arrayPool ?? ArrayPool<T>.Shared;
+            _minCapacity = minCapacity;
+            _items = Empty;
         }
 
         /// <summary>
@@ -57,7 +58,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
         {
             if (_itemsInUse == _items.Length)
             {
-                SetCapacity(_items.Length * 2, preserveContents: true);
+                GrowBuffer(_items.Length * 2);
             }
 
             var indexOfAppendedItem = _itemsInUse++;
@@ -72,13 +73,13 @@ namespace Microsoft.AspNetCore.Components.RenderTree
             var requiredCapacity = _itemsInUse + length;
             if (_items.Length < requiredCapacity)
             {
-                var candidateCapacity = _items.Length * 2;
+                var candidateCapacity = Math.Max(_items.Length * 2, _minCapacity);
                 while (candidateCapacity < requiredCapacity)
                 {
                     candidateCapacity *= 2;
                 }
 
-                SetCapacity(candidateCapacity, preserveContents: true);
+                GrowBuffer(candidateCapacity);
             }
 
             Array.Copy(source, startIndex, _items, _itemsInUse, length);
@@ -95,34 +96,51 @@ namespace Microsoft.AspNetCore.Components.RenderTree
         /// <param name="value">The value.</param>
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public void Overwrite(int index, in T value)
-            => _items[index] = value;
+        {
+            if (index > _itemsInUse)
+            {
+                ThrowIndexOutOfBoundsException();
+            }
+
+            _items[index] = value;
+        }
 
         /// <summary>
         /// Removes the last item.
         /// </summary>
         public void RemoveLast()
         {
+            if (_itemsInUse == 0)
+            {
+                ThrowIndexOutOfBoundsException();
+            }
+
             _itemsInUse--;
-            _items[_itemsInUse] = default(T); // Release to GC
+            _items[_itemsInUse] = default; // Release to GC
         }
 
         /// <summary>
         /// Inserts the item at the specified index, moving the contents of the subsequent entries along by one.
         /// </summary>
-        /// <param name="insertAtIndex">The index at which the value is to be inserted.</param>
+        /// <param name="index">The index at which the value is to be inserted.</param>
         /// <param name="value">The value to insert.</param>
-        public void InsertExpensive(int insertAtIndex, T value)
+        public void InsertExpensive(int index, T value)
         {
+            if (index > _itemsInUse)
+            {
+                ThrowIndexOutOfBoundsException();
+            }
+
             // Same expansion logic as elsewhere
             if (_itemsInUse == _items.Length)
             {
-                SetCapacity(_items.Length * 2, preserveContents: true);
+                GrowBuffer(_items.Length * 2);
             }
 
-            Array.Copy(_items, insertAtIndex, _items, insertAtIndex + 1, _itemsInUse - insertAtIndex);
+            Array.Copy(_items, index, _items, index + 1, _itemsInUse - index);
             _itemsInUse++;
 
-            _items[insertAtIndex] = value;
+            _items[index] = value;
         }
 
         /// <summary>
@@ -131,17 +149,9 @@ namespace Microsoft.AspNetCore.Components.RenderTree
         /// </summary>
         public void Clear()
         {
-            var previousItemsInUse = _itemsInUse;
+            ReturnBuffer();
+            _items = Empty;
             _itemsInUse = 0;
-
-            if (_items.Length > previousItemsInUse * 1.5)
-            {
-                SetCapacity((previousItemsInUse + _items.Length) / 2, preserveContents: false);
-            }
-            else if (previousItemsInUse > 0)
-            {
-                Array.Clear(_items, 0, previousItemsInUse); // Release to GC
-            }
         }
 
         /// <summary>
@@ -160,29 +170,42 @@ namespace Microsoft.AspNetCore.Components.RenderTree
         public ArrayBuilderSegment<T> ToSegment(int fromIndexInclusive, int toIndexExclusive)
             => new ArrayBuilderSegment<T>(this, fromIndexInclusive, toIndexExclusive - fromIndexInclusive);
 
-        private void SetCapacity(int desiredCapacity, bool preserveContents)
+        private void GrowBuffer(int desiredCapacity)
         {
-            if (desiredCapacity < _itemsInUse)
-            {
-                throw new ArgumentOutOfRangeException(nameof(desiredCapacity), $"The value cannot be less than {nameof(Count)}");
-            }
+            var newCapacity = Math.Max(desiredCapacity, _minCapacity);
+            Debug.Assert(newCapacity > _items.Length);
 
-            var newCapacity = desiredCapacity < MinCapacity ? MinCapacity : desiredCapacity;
-            if (newCapacity != _items.Length)
-            {
-                var newItems = new T[newCapacity];
+            var newItems = _arrayPool.Rent(newCapacity);
+            Array.Copy(_items, newItems, _itemsInUse);
 
-                if (preserveContents)
-                {
-                    Array.Copy(_items, newItems, _itemsInUse);
-                }
+            // Return the old buffer and start using the new buffer
+            ReturnBuffer();
+            _items = newItems;
+        }
 
-                _items = newItems;
+        private void ReturnBuffer()
+        {
+            if (!ReferenceEquals(_items, Empty))
+            {
+                // ArrayPool<>.Return with clearArray: true calls Array.Clear on the entire buffer.
+                // In the most common case, _itemsInUse would be much smaller than _items.Length so we'll specifically clear that subset.
+                Array.Clear(_items, 0, _itemsInUse);
+                _arrayPool.Return(_items);
             }
-            else if (!preserveContents)
+        }
+
+        public void Dispose()
+        {
+            if (!_disposed)
             {
-                Array.Clear(_items, 0, _items.Length);
+                _disposed = true;
+                ReturnBuffer();
             }
         }
+
+        private static void ThrowIndexOutOfBoundsException()
+        {
+            throw new ArgumentOutOfRangeException("index");
+        }
     }
 }
diff --git a/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs b/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs
index a868b654cce..18317ece489 100644
--- a/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs
+++ b/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs
@@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
     /// <typeparam name="T">The type of the elements in the array</typeparam>
     public readonly struct ArrayBuilderSegment<T> : IEnumerable<T>
     {
+        // The following fields are memory mapped to the WASM client. Do not re-order or use auto-properties.
         private readonly ArrayBuilder<T> _builder;
         private readonly int _offset;
         private readonly int _count;
diff --git a/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs
index 85c1ffa15a7..b0e06876175 100644
--- a/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs
+++ b/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs
@@ -17,14 +17,14 @@ namespace Microsoft.AspNetCore.Components.RenderTree
     /// <summary>
     /// Provides methods for building a collection of <see cref="RenderTreeFrame"/> entries.
     /// </summary>
-    public class RenderTreeBuilder
+    public class RenderTreeBuilder : IDisposable
     {
         private readonly static object BoxedTrue = true;
         private readonly static object BoxedFalse = false;
         private readonly static string ComponentReferenceCaptureInvalidParentMessage = $"Component reference captures may only be added as children of frames of type {RenderTreeFrameType.Component}";
 
         private readonly Renderer _renderer;
-        private readonly ArrayBuilder<RenderTreeFrame> _entries = new ArrayBuilder<RenderTreeFrame>(10);
+        private readonly ArrayBuilder<RenderTreeFrame> _entries = new ArrayBuilder<RenderTreeFrame>();
         private readonly Stack<int> _openElementIndices = new Stack<int>();
         private RenderTreeFrameType? _lastNonAttributeFrameType;
         private bool _hasSeenAddMultipleAttributes;
@@ -796,5 +796,10 @@ namespace Microsoft.AspNetCore.Components.RenderTree
             var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase));
             seenAttributeNames[name] = _entries.Count; // See comment in ProcessAttributes for why this is OK.
         }
+
+        void IDisposable.Dispose()
+        {
+            _entries.Dispose();
+        }
     }
 }
diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs
index bc9b7e3277f..1d34ffff840 100644
--- a/src/Components/Components/src/Rendering/ComponentState.cs
+++ b/src/Components/Components/src/Rendering/ComponentState.cs
@@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
     /// within the context of a <see cref="Renderer"/>. This is an internal implementation
     /// detail of <see cref="Renderer"/>.
     /// </summary>
-    internal class ComponentState
+    internal class ComponentState : IDisposable
     {
         private readonly Renderer _renderer;
         private readonly IReadOnlyList<CascadingParameterState> _cascadingParameters;
@@ -91,6 +91,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
             {
                 RemoveCascadingParameterSubscriptions();
             }
+
+            DisposeBuffers();
         }
 
         public Task NotifyRenderCompletedAsync()
@@ -172,5 +174,22 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 }
             }
         }
+
+        public void Dispose()
+        {
+            DisposeBuffers();
+
+            if (Component is IDisposable disposable)
+            {
+                disposable.Dispose();
+            }
+        }
+
+        private void DisposeBuffers()
+        {
+            ((IDisposable)_renderTreeBuilderPrevious).Dispose();
+            ((IDisposable)CurrrentRenderTree).Dispose();
+            _latestDirectParametersSnapshot?.Dispose();
+        }
     }
 }
diff --git a/src/Components/Components/src/Rendering/RenderBatchBuilder.cs b/src/Components/Components/src/Rendering/RenderBatchBuilder.cs
index bb5daacec41..6d2e2959d25 100644
--- a/src/Components/Components/src/Rendering/RenderBatchBuilder.cs
+++ b/src/Components/Components/src/Rendering/RenderBatchBuilder.cs
@@ -1,6 +1,7 @@
 // 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.Collections.Generic;
 using Microsoft.AspNetCore.Components.RenderTree;
 
@@ -12,7 +13,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
     /// and the intermediate states (such as the queue of components still to
     /// be rendered).
     /// </summary>
-    internal class RenderBatchBuilder
+    internal class RenderBatchBuilder : IDisposable
     {
         // Primary result data
         public ArrayBuilder<RenderTreeDiff> UpdatedComponentDiffs { get; } = new ArrayBuilder<RenderTreeDiff>();
@@ -20,8 +21,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public ArrayBuilder<int> DisposedEventHandlerIds { get; } = new ArrayBuilder<int>();
 
         // Buffers referenced by UpdatedComponentDiffs
-        public ArrayBuilder<RenderTreeEdit> EditsBuffer { get; } = new ArrayBuilder<RenderTreeEdit>();
-        public ArrayBuilder<RenderTreeFrame> ReferenceFramesBuffer { get; } = new ArrayBuilder<RenderTreeFrame>();
+        public ArrayBuilder<RenderTreeEdit> EditsBuffer { get; } = new ArrayBuilder<RenderTreeEdit>(64);
+        public ArrayBuilder<RenderTreeFrame> ReferenceFramesBuffer { get; } = new ArrayBuilder<RenderTreeFrame>(64);
 
         // State of render pipeline
         public Queue<RenderQueueEntry> ComponentRenderQueue { get; } = new Queue<RenderQueueEntry>();
@@ -56,5 +57,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 ReferenceFramesBuffer.ToRange(),
                 DisposedComponentIds.ToRange(),
                 DisposedEventHandlerIds.ToRange());
+
+        public void Dispose()
+        {
+            EditsBuffer.Dispose();
+            ReferenceFramesBuffer.Dispose();
+            UpdatedComponentDiffs.Dispose();
+            DisposedComponentIds.Dispose();
+            DisposedEventHandlerIds.Dispose();
+        }
     }
 }
diff --git a/src/Components/Components/src/Rendering/Renderer.cs b/src/Components/Components/src/Rendering/Renderer.cs
index f45e109a145..472c094b293 100644
--- a/src/Components/Components/src/Rendering/Renderer.cs
+++ b/src/Components/Components/src/Rendering/Renderer.cs
@@ -663,13 +663,15 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 {
                     try
                     {
-                        disposable.Dispose();
+                        componentState.Dispose();
                     }
                     catch (Exception exception)
                     {
                         HandleException(exception);
                     }
                 }
+
+                _batchBuilder.Dispose();
             }
         }
 
diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs
index 5f26e5dc7eb..e4a5bfcfc52 100644
--- a/src/Components/Components/src/Routing/Router.cs
+++ b/src/Components/Components/src/Routing/Router.cs
@@ -7,7 +7,6 @@ using System.Reflection;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components.RenderTree;
 using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
 
 namespace Microsoft.AspNetCore.Components.Routing
 {
diff --git a/src/Components/Components/test/RenderTreeDiffBuilderTest.cs b/src/Components/Components/test/RenderTreeDiffBuilderTest.cs
index 00d21eb97cd..6a9d9e9b150 100644
--- a/src/Components/Components/test/RenderTreeDiffBuilderTest.cs
+++ b/src/Components/Components/test/RenderTreeDiffBuilderTest.cs
@@ -13,11 +13,12 @@ using Xunit;
 
 namespace Microsoft.AspNetCore.Components.Test
 {
-    public class RenderTreeDiffBuilderTest
+    public class RenderTreeDiffBuilderTest : IDisposable
     {
         private readonly Renderer renderer;
         private readonly RenderTreeBuilder oldTree;
         private readonly RenderTreeBuilder newTree;
+        private RenderBatchBuilder batchBuilder;
 
         public RenderTreeDiffBuilderTest()
         {
@@ -26,6 +27,14 @@ namespace Microsoft.AspNetCore.Components.Test
             newTree = new RenderTreeBuilder(renderer);
         }
 
+        void IDisposable.Dispose()
+        {
+            renderer.Dispose();
+            ((IDisposable)oldTree).Dispose();
+            ((IDisposable)newTree).Dispose();
+            batchBuilder?.Dispose();
+        }
+
         [Theory]
         [MemberData(nameof(RecognizesEquivalentFramesAsSameCases))]
         public void RecognizesEquivalentFramesAsSame(RenderFragment appendFragment)
@@ -208,7 +217,8 @@ namespace Microsoft.AspNetCore.Components.Test
             oldTree.SetKey("retained key");
             oldTree.AddAttribute(1, "ParamName", "Param old value");
             oldTree.CloseComponent();
-            GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree, false); // Assign initial IDs
+            using var initial = new RenderTreeBuilder(renderer);
+            GetRenderedBatch(initial, oldTree, false); // Assign initial IDs
             var oldComponent = GetComponents<CaptureSetParametersComponent>(oldTree).Single();
 
             newTree.OpenComponent<CaptureSetParametersComponent>(0);
@@ -227,12 +237,12 @@ namespace Microsoft.AspNetCore.Components.Test
             // param on the second component.
 
             // Act
-            var batch = GetRenderedBatch(initializeFromFrames: false);
+            var batchBuilder = GetRenderedBatch(initializeFromFrames: false);
             var newComponents = GetComponents<CaptureSetParametersComponent>(newTree);
 
             // Assert: Inserts new component at position 0
-            Assert.Equal(1, batch.UpdatedComponents.Count);
-            Assert.Collection(batch.UpdatedComponents.Array[0].Edits,
+            Assert.Equal(1, batchBuilder.UpdatedComponents.Count);
+            Assert.Collection(batchBuilder.UpdatedComponents.Array[0].Edits,
                 entry => AssertEdit(entry, RenderTreeEditType.PrependFrame, 0));
 
             // Assert: Retains old component instance in position 1, and updates its params
@@ -255,7 +265,8 @@ namespace Microsoft.AspNetCore.Components.Test
             oldTree.CloseComponent();
 
             // Instantiate initial components
-            GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree, false);
+            using var initial = new RenderTreeBuilder(renderer);
+            GetRenderedBatch(initial, oldTree, false);
             var oldComponents = GetComponents(oldTree);
 
             newTree.OpenComponent<FakeComponent>(0);
@@ -286,7 +297,8 @@ namespace Microsoft.AspNetCore.Components.Test
             oldTree.CloseComponent();
 
             // Instantiate initial component
-            GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree, false);
+            using var renderTreeBuilder = new RenderTreeBuilder(renderer);
+            GetRenderedBatch(renderTreeBuilder, oldTree, false);
             var oldComponent = GetComponents(oldTree).Single();
             Assert.NotNull(oldComponent);
 
@@ -724,10 +736,11 @@ namespace Microsoft.AspNetCore.Components.Test
             // Arrange
             oldTree.OpenComponent<FakeComponent>(123);
             oldTree.CloseComponent();
-            GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree, false); // Assign initial IDs
+            using var initial = new RenderTreeBuilder(renderer);
+            GetRenderedBatch(initial, oldTree, false); // Assign initial IDs
             newTree.OpenComponent<FakeComponent2>(123);
             newTree.CloseComponent();
-            var batchBuilder = new RenderBatchBuilder();
+            using var batchBuilder = new RenderBatchBuilder();
 
             // Act
             var diff = RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, oldTree.GetFrames(), newTree.GetFrames());
@@ -835,7 +848,7 @@ namespace Microsoft.AspNetCore.Components.Test
             newTree.CloseElement();
 
             // Act
-            var (result, referenceFrames, batch) = GetSingleUpdatedComponentWithBatch(initializeFromFrames: true);
+            var (result, referenceFrames, batchBuilder) = GetSingleUpdatedComponentWithBatch(initializeFromFrames: true);
             var removedEventHandlerFrame = oldTree.GetFrames().Array[2];
 
             // Assert
@@ -849,7 +862,7 @@ namespace Microsoft.AspNetCore.Components.Test
             Assert.NotEqual(0, removedEventHandlerFrame.AttributeEventHandlerId);
             Assert.Equal(
                 new[] { removedEventHandlerFrame.AttributeEventHandlerId },
-                batch.DisposedEventHandlerIDs.AsEnumerable());
+                batchBuilder.DisposedEventHandlerIDs.AsEnumerable());
         }
 
         [Fact]
@@ -1539,7 +1552,9 @@ namespace Microsoft.AspNetCore.Components.Test
             newTree.CloseComponent();                           //       </FakeComponent2>
             newTree.CloseElement();                             //     </container>
 
-            RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
+            using var batchBuilder = new RenderBatchBuilder();
+            using var renderTreeBuilder = new RenderTreeBuilder(renderer);
+            RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, renderTreeBuilder.GetFrames(), oldTree.GetFrames());
             var originalFakeComponentInstance = oldTree.GetFrames().Array[2].Component;
             var originalFakeComponent2Instance = oldTree.GetFrames().Array[3].Component;
 
@@ -1569,7 +1584,7 @@ namespace Microsoft.AspNetCore.Components.Test
             newTree.CloseElement();
 
             // Act
-            var (result, referenceFrames, batch) = GetSingleUpdatedComponentWithBatch(initializeFromFrames: true);
+            var (result, referenceFrames, batchBuilder) = GetSingleUpdatedComponentWithBatch(initializeFromFrames: true);
             var oldAttributeFrame = oldTree.GetFrames().Array[1];
             var newAttributeFrame = newTree.GetFrames().Array[1];
 
@@ -1579,7 +1594,7 @@ namespace Microsoft.AspNetCore.Components.Test
             AssertFrame.Attribute(newAttributeFrame, "ontest", retainedHandler);
             Assert.NotEqual(0, oldAttributeFrame.AttributeEventHandlerId);
             Assert.Equal(oldAttributeFrame.AttributeEventHandlerId, newAttributeFrame.AttributeEventHandlerId);
-            Assert.Empty(batch.DisposedEventHandlerIDs.AsEnumerable());
+            Assert.Empty(batchBuilder.DisposedEventHandlerIDs.AsEnumerable());
         }
 
         [Fact]
@@ -1596,7 +1611,7 @@ namespace Microsoft.AspNetCore.Components.Test
             newTree.CloseElement();
 
             // Act
-            var (result, referenceFrames, batch) = GetSingleUpdatedComponentWithBatch(initializeFromFrames: true);
+            var (result, referenceFrames, batchBuilder) = GetSingleUpdatedComponentWithBatch(initializeFromFrames: true);
             var oldAttributeFrame = oldTree.GetFrames().Array[1];
             var newAttributeFrame = newTree.GetFrames().Array[2];
 
@@ -1606,7 +1621,7 @@ namespace Microsoft.AspNetCore.Components.Test
             AssertFrame.Attribute(newAttributeFrame, "ontest", retainedHandler);
             Assert.NotEqual(0, oldAttributeFrame.AttributeEventHandlerId);
             Assert.Equal(oldAttributeFrame.AttributeEventHandlerId, newAttributeFrame.AttributeEventHandlerId);
-            Assert.Empty(batch.DisposedEventHandlerIDs.AsEnumerable());
+            Assert.Empty(batchBuilder.DisposedEventHandlerIDs.AsEnumerable());
         }
 
         [Fact]
@@ -1623,7 +1638,9 @@ namespace Microsoft.AspNetCore.Components.Test
             newTree.AddAttribute(14, nameof(FakeComponent.ObjectProperty), objectWillNotChange);
             newTree.CloseComponent();
 
-            RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
+            using var batchBuilder = new RenderBatchBuilder();
+            using var renderTree = new RenderTreeBuilder(renderer);
+            RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, renderTree.GetFrames(), oldTree.GetFrames());
             var originalComponentInstance = (FakeComponent)oldTree.GetFrames().Array[0].Component;
 
             // Act
@@ -1661,7 +1678,9 @@ namespace Microsoft.AspNetCore.Components.Test
                 tree.CloseComponent();
             }
 
-            RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
+            using var batchBuilder = new RenderBatchBuilder();
+            using var renderTreeBuilder = new RenderTreeBuilder(renderer);
+            RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, renderTreeBuilder.GetFrames(), oldTree.GetFrames());
             var originalComponentInstance = (CaptureSetParametersComponent)oldTree.GetFrames().Array[0].Component;
             Assert.Equal(1, originalComponentInstance.SetParametersCallCount);
 
@@ -1689,7 +1708,9 @@ namespace Microsoft.AspNetCore.Components.Test
                 tree.CloseComponent();
             }
 
-            RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
+            using var batchBuilder = new RenderBatchBuilder();
+            using var renderTreeBuilder = new RenderTreeBuilder(renderer);
+            RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, renderTreeBuilder.GetFrames(), oldTree.GetFrames());
             var componentInstance = (CaptureSetParametersComponent)oldTree.GetFrames().Array[0].Component;
             Assert.Equal(1, componentInstance.SetParametersCallCount);
 
@@ -1713,8 +1734,9 @@ namespace Microsoft.AspNetCore.Components.Test
             newTree.OpenComponent<DisposableComponent>(30);       // <DisposableComponent>
             newTree.CloseComponent();                             // </DisposableComponent>
 
-            var batchBuilder = new RenderBatchBuilder();
-            RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
+            using var batchBuilder = new RenderBatchBuilder();
+            using var renderTree = new RenderTreeBuilder(renderer);
+            RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, renderTree.GetFrames(), oldTree.GetFrames());
 
             // Act/Assert
             // Note that we track NonDisposableComponent was disposed even though it's not IDisposable,
@@ -1923,7 +1945,8 @@ namespace Microsoft.AspNetCore.Components.Test
             oldTree.AddAttribute(1, nameof(FakeComponent.StringProperty), "Second param");
             oldTree.CloseComponent();
 
-            GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree, false); // Assign initial IDs
+            using var renderTreeBuilder = new RenderTreeBuilder(renderer);
+            GetRenderedBatch(renderTreeBuilder, oldTree, false); // Assign initial IDs
             var oldComponents = GetComponents<CaptureSetParametersComponent>(oldTree);
 
             newTree.OpenComponent<CaptureSetParametersComponent>(0);
@@ -2124,12 +2147,19 @@ namespace Microsoft.AspNetCore.Components.Test
         {
             if (initializeFromFrames)
             {
-                var emptyFrames = new RenderTreeBuilder(renderer).GetFrames();
+                using var renderTreeBuilder = new RenderTreeBuilder(renderer);
+                using var initializeBatchBuilder = new RenderBatchBuilder();
+
+                var emptyFrames = renderTreeBuilder.GetFrames();
                 var oldFrames = from.GetFrames();
-                RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, emptyFrames, oldFrames);
+
+                RenderTreeDiffBuilder.ComputeDiff(renderer, initializeBatchBuilder, 0, emptyFrames, oldFrames);
             }
 
-            var batchBuilder = new RenderBatchBuilder();
+            batchBuilder?.Dispose();
+            // This gets disposed as part of the test type's Dispose
+            batchBuilder = new RenderBatchBuilder();
+
             var diff = RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, from.GetFrames(), to.GetFrames());
             batchBuilder.UpdatedComponentDiffs.Append(diff);
             return batchBuilder.ToBatch();
diff --git a/src/Components/Components/test/Rendering/ArrayBuilderSegmentTest.cs b/src/Components/Components/test/Rendering/ArrayBuilderSegmentTest.cs
index 35596f4b0e3..3e6a22fb6fe 100644
--- a/src/Components/Components/test/Rendering/ArrayBuilderSegmentTest.cs
+++ b/src/Components/Components/test/Rendering/ArrayBuilderSegmentTest.cs
@@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void BasicPropertiesWork()
         {
             // Arrange: builder containing 1..5
-            var builder = new ArrayBuilder<int>();
+            using var builder = new ArrayBuilder<int>();
             builder.Append(new[] { 1, 2, 3, 4, 5 }, 0, 5);
 
             // Act: take segment containing 2..3
@@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void StillWorksAfterUnderlyingCapacityChange()
         {
             // Arrange: builder containing 1..8
-            var builder = new ArrayBuilder<int>(capacity: 10);
+            using var builder = new ArrayBuilder<int>(minCapacity: 10, new TestArrayPool<int>());
             builder.Append(new[] { 1, 2, 3, 4, 5, 6, 7, 8 }, 0, 8);
             var originalBuffer = builder.Buffer;
 
diff --git a/src/Components/Components/test/Rendering/ArrayBuilderTest.cs b/src/Components/Components/test/Rendering/ArrayBuilderTest.cs
new file mode 100644
index 00000000000..6213b6fdd32
--- /dev/null
+++ b/src/Components/Components/test/Rendering/ArrayBuilderTest.cs
@@ -0,0 +1,317 @@
+// 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.Buffers;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Components.RenderTree
+{
+    public class ArrayBuilderTest
+    {
+        private readonly TestArrayPool<int> ArrayPool = new TestArrayPool<int>();
+
+        [Fact]
+        public void Append_SingleItem()
+        {
+            // Arrange
+            var value = 7;
+            using var builder = CreateArrayBuilder();
+
+            // Act
+            builder.Append(value);
+
+            // Assert
+            Assert.Equal(1, builder.Count);
+            Assert.Equal(value, builder.Buffer[0]);
+        }
+
+        [Fact]
+        public void Append_ThreeItem()
+        {
+            // Arrange
+            var value1 = 7;
+            var value2 = 22;
+            var value3 = 3;
+            using var builder = CreateArrayBuilder();
+
+            // Act
+            builder.Append(value1);
+            builder.Append(value2);
+            builder.Append(value3);
+
+            // Assert
+            Assert.Equal(3, builder.Count);
+            Assert.Equal(new[] { value1, value2, value3 }, builder.Buffer.Take(3));
+        }
+
+        [Fact]
+        public void Append_FillBuffer()
+        {
+            // Arrange
+            var capacity = 8;
+            using var builder = new ArrayBuilder<int>(minCapacity: capacity);
+
+            // Act
+            for (var i = 0; i < capacity; i++)
+            {
+                builder.Append(5);
+            }
+
+            // Assert
+            Assert.Equal(capacity, builder.Count);
+            Assert.Equal(Enumerable.Repeat(5, capacity), builder.Buffer.Take(capacity));
+        }
+
+        [Fact]
+        public void AppendArray_CopySubset()
+        {
+            // Arrange
+            var array = Enumerable.Repeat(8, 5).ToArray();
+            using var builder = CreateArrayBuilder();
+
+            // Act
+            builder.Append(array, 0, 2);
+
+            // Assert
+            Assert.Equal(2, builder.Count);
+            Assert.Equal(new[] { 8, 8 }, builder.Buffer.Take(2));
+        }
+
+        [Fact]
+        public void AppendArray_CopyArray()
+        {
+            // Arrange
+            var array = Enumerable.Repeat(8, 5).ToArray();
+            using var builder = CreateArrayBuilder();
+
+            // Act
+            builder.Append(array, 0, array.Length);
+
+            // Assert
+            Assert.Equal(array.Length, builder.Count);
+            Assert.Equal(array, builder.Buffer.Take(array.Length));
+        }
+
+        [Fact]
+        public void AppendArray_AfterPriorInsertion()
+        {
+            // Arrange
+            var array = Enumerable.Repeat(8, 5).ToArray();
+            using var builder = CreateArrayBuilder();
+
+            // Act
+            builder.Append(118);
+            builder.Append(array, 0, 2);
+
+            // Assert
+            Assert.Equal(3, builder.Count);
+            Assert.Equal(new[] { 118, 8, 8 }, builder.Buffer.Take(3));
+        }
+
+        [Theory]
+        // These are at boundaries of our capacity increments.
+        [InlineData(1023)]
+        [InlineData(1024)]
+        [InlineData(1025)]
+        public void AppendArray_LargerThanBuffer(int size)
+        {
+            // Arrange
+            var array = Enumerable.Repeat(17, size).ToArray();
+            using var builder = CreateArrayBuilder();
+
+            // Act
+            builder.Append(array, 0, array.Length);
+
+            // Assert
+            Assert.Equal(array.Length, builder.Count);
+            Assert.Equal(array, builder.Buffer.Take(array.Length));
+        }
+
+        [Fact]
+        public void Overwrite_Works()
+        {
+            // Arrange
+            using var builder = CreateArrayBuilder();
+            builder.Append(7);
+            builder.Append(3);
+            builder.Append(9);
+
+            // Act
+            builder.Overwrite(1, 2);
+
+            // Assert
+            Assert.Equal(3, builder.Count);
+            Assert.Equal(new[]{ 7, 2, 9}, builder.Buffer.Take(3));
+        }
+
+        [Fact]
+        public void Insert_Works()
+        {
+            // Arrange
+            using var builder = CreateArrayBuilder();
+            builder.Append(7);
+            builder.Append(3);
+            builder.Append(9);
+
+            // Act
+            builder.InsertExpensive(1, 2);
+
+            // Assert
+            Assert.Equal(4, builder.Count);
+            Assert.Equal(new[] { 7, 2, 3, 9 }, builder.Buffer.Take(4));
+        }
+
+        [Fact]
+        public void Insert_WhenBufferIsAtCapacity()
+        {
+            // Arrange
+            using var builder = CreateArrayBuilder(2);
+            builder.Append(new[] { 1, 3 }, 0, 2);
+
+            // Act
+            builder.InsertExpensive(1, 2);
+
+            // Assert
+            Assert.Equal(3, builder.Count);
+            Assert.Equal(new[] { 1, 2, 3 }, builder.Buffer.Take(3));
+        }
+
+        [Fact]
+        public void RemoveLast_Works()
+        {
+            // Arrange
+            using var builder = CreateArrayBuilder();
+            builder.Append(1);
+            builder.Append(2);
+            builder.Append(3);
+
+            // Act
+            builder.RemoveLast();
+
+            // Assert
+            Assert.Equal(2, builder.Count);
+            Assert.Equal(new[] { 1, 2, }, builder.Buffer.Take(2));
+        }
+
+        [Fact]
+        public void RemoveLast_LastEntry()
+        {
+            // Arrange
+            int[] buffer;
+            using (var builder = CreateArrayBuilder())
+            {
+                builder.Append(1);
+                buffer = builder.Buffer;
+
+                // Act
+                builder.RemoveLast();
+
+                // Assert
+                Assert.Equal(0, builder.Count);
+            }
+
+            // Also verify that the buffer is indeed returned in this case.
+            var returnedBuffer = Assert.Single(ArrayPool.ReturnedBuffers);
+            Assert.Same(buffer, returnedBuffer);
+        }
+
+        [Fact]
+        public void Clear_ReturnsBuffer()
+        {
+            // Arrange
+            using var builder = CreateArrayBuilder();
+            builder.Append(1);
+            var buffer = builder.Buffer;
+
+            // Act
+            builder.Clear();
+
+            // Assert
+            Assert.Equal(0, builder.Count);
+            var returnedBuffer = Assert.Single(ArrayPool.ReturnedBuffers);
+            Assert.Same(buffer, returnedBuffer);
+        }
+
+        [Fact]
+        public void Dispose_WithEmptyBuffer_DoesNotReturnIt()
+        {
+            // Arrange
+            var builder = CreateArrayBuilder();
+
+            // Act
+            builder.Dispose();
+
+            // Assert
+            Assert.Empty(ArrayPool.ReturnedBuffers);
+        }
+
+        [Fact]
+        public void Dispose_NonEmptyBufferIsReturned()
+        {
+            // Arrange
+            var builder = CreateArrayBuilder();
+            builder.Append(1);
+            var buffer = builder.Buffer;
+
+            // Act
+            builder.Dispose();
+
+            // Assert
+            Assert.Single(ArrayPool.ReturnedBuffers);
+            var returnedBuffer = Assert.Single(ArrayPool.ReturnedBuffers);
+            Assert.Same(buffer, returnedBuffer);
+        }
+
+        [Fact]
+        public void DoubleDispose_DoesNotReturnBufferTwice()
+        {
+            // Arrange
+            var builder = CreateArrayBuilder();
+            builder.Append(1);
+            var buffer = builder.Buffer;
+
+            // Act
+            builder.Dispose();
+            builder.Dispose();
+
+            // Assert
+            Assert.Single(ArrayPool.ReturnedBuffers);
+            var returnedBuffer = Assert.Single(ArrayPool.ReturnedBuffers);
+            Assert.Same(buffer, returnedBuffer);
+        }
+
+        [Fact]
+        public void UnusedBufferIsReturned_OnResize()
+        {
+            // Arrange
+            var builder = CreateArrayBuilder(2);
+
+            // Act
+            for (var i = 0; i < 10; i++)
+            {
+                builder.Append(i);
+            }
+
+            // Assert
+            Assert.Collection(
+                ArrayPool.ReturnedBuffers,
+                buffer => Assert.Equal(2, buffer.Length),
+                buffer => Assert.Equal(4, buffer.Length),
+                buffer => Assert.Equal(8, buffer.Length));
+
+            // Clear this because this is no longer interesting.
+            ArrayPool.ReturnedBuffers.Clear();
+
+            var buffer = builder.Buffer;
+            builder.Dispose();
+
+            Assert.Same(buffer, Assert.Single(ArrayPool.ReturnedBuffers));
+        }
+
+        private ArrayBuilder<int> CreateArrayBuilder(int capacity = 32)
+        {
+            return new ArrayBuilder<int>(capacity, ArrayPool);
+        }
+    }
+}
diff --git a/src/Components/Components/test/Rendering/TestArrayPool.cs b/src/Components/Components/test/Rendering/TestArrayPool.cs
new file mode 100644
index 00000000000..c5335a6b64a
--- /dev/null
+++ b/src/Components/Components/test/Rendering/TestArrayPool.cs
@@ -0,0 +1,23 @@
+// 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.Buffers;
+using System.Collections.Generic;
+
+namespace Microsoft.AspNetCore.Components.RenderTree
+{
+    internal class TestArrayPool<T> : ArrayPool<T>
+    {
+        public override T[] Rent(int minimumLength)
+        {
+            return new T[minimumLength];
+        }
+
+        public List<T[]> ReturnedBuffers = new List<T[]>();
+
+        public override void Return(T[] array, bool clearArray = false)
+        {
+            ReturnedBuffers.Add(array);
+        }
+    }
+}
-- 
GitLab