Use ArrayPool in ArrayBuilder<T> (#11830)

* Use ArrayPool
This commit is contained in:
Pranav K 2019-07-16 16:23:02 -07:00 committed by GitHub
parent d668e77e9e
commit 6c80c42511
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 515 additions and 84 deletions

View File

@ -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",

View File

@ -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

View File

@ -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;
}
else if (!preserveContents)
private void ReturnBuffer()
{
if (!ReferenceEquals(_items, Empty))
{
Array.Clear(_items, 0, _items.Length);
// 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);
}
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
ReturnBuffer();
}
}
private static void ThrowIndexOutOfBoundsException()
{
throw new ArgumentOutOfRangeException("index");
}
}
}

View File

@ -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;

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -663,13 +663,15 @@ namespace Microsoft.AspNetCore.Components.Rendering
{
try
{
disposable.Dispose();
componentState.Dispose();
}
catch (Exception exception)
{
HandleException(exception);
}
}
_batchBuilder.Dispose();
}
}

View File

@ -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
{

View File

@ -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();

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}