diff --git a/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs b/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs index 87b1349844be8d658533b802a3cdcb119d3b7592..bf96e57a93926c8ad6785178475a533b075defe4 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 d4cafd7478fe55678bd930ce885dc6c54e875089..6097208bee14031bddb5330c8da76d5620bdfc31 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 6be35ded27ace306833770cf2f04acd63d716aba..9d3e71993aa80a2094d0f78cc2fc5dfe8b4cd5c9 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 a868b654cceca997de3ca3f01d87ed4cf5fa63cc..18317ece489a0bb0dc10bcdb2b7fb3148f6badab 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 85c1ffa15a7e3fcb94d550f97f87e448b087606e..b0e06876175fa97a35847897231f3e2f6d2a5aac 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 bc9b7e3277f1428881822c4a1a3e05d2ac472bfc..1d34ffff8408a78a290dca827569418c5870033d 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 bb5daacec41bfbf258dcc5057c11bcbce36ff512..6d2e2959d257c6b3a00e1a15a1e734343b4ebbda 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 f45e109a1452f61fb907b2026612093e82f01002..472c094b2937ce18dddd8407a9601dfe3c960c44 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 5f26e5dc7ebde81795083b2b2bc2a62fd20131e0..e4a5bfcfc520c171ca5042ae92ef4e4f9b61d335 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 00d21eb97cdb4bbd083016403cbd0423021bc9a3..6a9d9e9b15002365a971664641b4251f648ed9aa 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 35596f4b0e31a3035018af682c88275a8da61ce0..3e6a22fb6fed2c7ff57a2b39264596a63ef8679f 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 0000000000000000000000000000000000000000..6213b6fdd32223c2f17cfbdf5c3d36854de84b81 --- /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 0000000000000000000000000000000000000000..c5335a6b64a80c160ee467bb5c0402d636269d74 --- /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); + } + } +}