Move common resizable-array logic into shared ArrayBuilder. Replace ArraySegment with ArrayRange since Offset is never used.

This commit is contained in:
Steve Sanderson 2018-01-23 23:30:48 -08:00
parent cc2d097c99
commit 6e4d0dbca4
10 changed files with 195 additions and 65 deletions

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,119 @@
// 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.Runtime.CompilerServices;
namespace Microsoft.Blazor.RenderTree
{
/// <summary>
/// Implements a list that uses an array of objects to store the elements.
/// </summary>
public class ArrayBuilder<T>
{
private const int MinCapacity = 10;
private T[] _items;
private int _itemsInUse;
/// <summary>
/// Constructs a new instance of <see cref="ArrayBuilder{T}"/>.
/// </summary>
public ArrayBuilder() : this(MinCapacity)
{
}
/// <summary>
/// Constructs a new instance of <see cref="ArrayBuilder{T}"/>.
/// </summary>
public ArrayBuilder(int capacity)
{
_items = new T[capacity < MinCapacity ? MinCapacity : capacity];
_itemsInUse = 0;
}
/// <summary>
/// Gets the number of items.
/// </summary>
public int Count => _itemsInUse;
/// <summary>
/// Gets the underlying buffer.
/// </summary>
public T[] Buffer => _items;
/// <summary>
/// Appends a new item, automatically resizing the underlying array if necessary.
/// </summary>
/// <param name="item">The item to append.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(T item)
{
if (_itemsInUse == _items.Length)
{
SetCapacity(_itemsInUse * 2, preserveContents: true);
}
_items[_itemsInUse++] = item;
}
/// <summary>
/// Removes the last item.
/// </summary>
public void RemoveLast()
{
_itemsInUse--;
_items[_itemsInUse] = default(T); // Release to GC
}
/// <summary>
/// Marks the array as empty, also shrinking the underlying storage if it was
/// not being used to near its full capacity.
/// </summary>
public void Clear()
{
var previousItemsInUse = _itemsInUse;
_itemsInUse = 0;
if (_items.Length > previousItemsInUse * 1.5)
{
SetCapacity((previousItemsInUse + _items.Length) / 2, preserveContents: false);
}
else
{
Array.Clear(_items, 0, _itemsInUse); // Release to GC
}
}
/// <summary>
/// Produces an <see cref="ArrayRange{T}"/> structure describing the current contents.
/// </summary>
/// <returns>The <see cref="ArrayRange{T}"/>.</returns>
public ArrayRange<T> ToRange()
=> new ArrayRange<T>(_items, _itemsInUse);
private void SetCapacity(int desiredCapacity, bool preserveContents)
{
if (desiredCapacity < _itemsInUse)
{
throw new ArgumentOutOfRangeException(nameof(desiredCapacity), $"The value cannot be less than {nameof(Count)}");
}
var newCapacity = desiredCapacity < MinCapacity ? MinCapacity : desiredCapacity;
if (newCapacity != _items.Length)
{
var newItems = new T[newCapacity];
if (preserveContents)
{
Array.Copy(_items, newItems, _itemsInUse);
}
_items = newItems;
}
else if (!preserveContents)
{
Array.Clear(_items, 0, _items.Length);
}
}
}
}

View File

@ -0,0 +1,45 @@
// 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;
using System.Collections.Generic;
namespace Microsoft.Blazor.RenderTree
{
/// <summary>
/// Represents a range of elements in an array that are in use.
/// </summary>
/// <typeparam name="T">The array item type.</typeparam>
public readonly struct ArrayRange<T> : IEnumerable, IEnumerable<T>
{
/// <summary>
/// Gets the underlying array instance.
/// </summary>
public T[] Array { get; }
/// <summary>
/// Gets the number of items in the array that are considered to be in use.
/// </summary>
public int Count { get; }
/// <summary>
/// Constructs an instance of <see cref="ArrayRange{T}"/>.
/// </summary>
/// <param name="array">The array.</param>
/// <param name="count">The number of items in the array that are in use.</param>
public ArrayRange(T[] array, int count)
{
Array = array;
Count = count;
}
/// <inheritdoc />
IEnumerator<T> IEnumerable<T>.GetEnumerator()
=> ((IEnumerable<T>)new ArraySegment<T>(Array, 0, Count)).GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
=> ((IEnumerable)new ArraySegment<T>(Array, 0, Count)).GetEnumerator();
}
}

View File

@ -13,10 +13,8 @@ namespace Microsoft.Blazor.RenderTree
/// </summary>
public class RenderTreeBuilder
{
private const int MinBufferLength = 10;
private readonly Renderer _renderer;
private RenderTreeNode[] _entries = new RenderTreeNode[100];
private int _entriesInUse = 0;
private readonly ArrayBuilder<RenderTreeNode> _entries = new ArrayBuilder<RenderTreeNode>(10);
private readonly Stack<int> _openElementIndices = new Stack<int>();
private RenderTreeNodeType? _lastNonAttributeNodeType;
@ -39,7 +37,7 @@ namespace Microsoft.Blazor.RenderTree
/// <param name="elementName">A value representing the type of the element.</param>
public void OpenElement(int sequence, string elementName)
{
_openElementIndices.Push(_entriesInUse);
_openElementIndices.Push(_entries.Count);
Append(RenderTreeNode.Element(sequence, elementName));
}
@ -50,7 +48,7 @@ namespace Microsoft.Blazor.RenderTree
public void CloseElement()
{
var indexOfEntryBeingClosed = _openElementIndices.Pop();
_entries[indexOfEntryBeingClosed].CloseElement(_entriesInUse - 1);
_entries.Buffer[indexOfEntryBeingClosed].CloseElement(_entries.Count - 1);
}
/// <summary>
@ -149,35 +147,21 @@ namespace Microsoft.Blazor.RenderTree
/// </summary>
public void Clear()
{
// If the previous usage of the buffer showed that we have allocated
// much more space than needed, free up the excess memory
var shrinkToLength = Math.Max(MinBufferLength, _entries.Length / 2);
if (_entriesInUse < shrinkToLength)
{
Array.Resize(ref _entries, shrinkToLength);
}
_entriesInUse = 0;
_entries.Clear();
_openElementIndices.Clear();
_lastNonAttributeNodeType = null;
}
/// <summary>
/// Returns the <see cref="RenderTreeNode"/> values that have been appended.
/// The return value's <see cref="ArraySegment{T}.Offset"/> is always zero.
/// </summary>
/// <returns>An array segment of <see cref="RenderTreeNode"/> values.</returns>
public ArraySegment<RenderTreeNode> GetNodes() =>
new ArraySegment<RenderTreeNode>(_entries, 0, _entriesInUse);
/// <returns>An array range of <see cref="RenderTreeNode"/> values.</returns>
public ArrayRange<RenderTreeNode> GetNodes() =>
_entries.ToRange();
private void Append(RenderTreeNode node)
{
if (_entriesInUse == _entries.Length)
{
Array.Resize(ref _entries, _entries.Length * 2);
}
_entries[_entriesInUse++] = node;
_entries.Append(node);
var nodeType = node.NodeType;
if (nodeType != RenderTreeNodeType.Attribute)

View File

@ -9,22 +9,22 @@ namespace Microsoft.Blazor.RenderTree
/// Describes changes to a component's render tree between successive renders,
/// as well as the resulting state.
/// </summary>
public struct RenderTreeDiff
public readonly struct RenderTreeDiff
{
/// <summary>
/// Gets the changes to the render tree since a previous state.
/// </summary>
public ArraySegment<RenderTreeEdit> Edits { get; private set; }
public ArrayRange<RenderTreeEdit> Edits { get; }
/// <summary>
/// Gets the latest render tree. That is, the result of applying the <see cref="Edits"/>
/// to the previous state.
/// </summary>
public ArraySegment<RenderTreeNode> CurrentState { get; private set; }
public ArrayRange<RenderTreeNode> CurrentState { get; }
internal RenderTreeDiff(
ArraySegment<RenderTreeEdit> entries,
ArraySegment<RenderTreeNode> referenceTree)
ArrayRange<RenderTreeEdit> entries,
ArrayRange<RenderTreeNode> referenceTree)
{
Edits = entries;
CurrentState = referenceTree;

View File

@ -7,29 +7,17 @@ namespace Microsoft.Blazor.RenderTree
{
internal class RenderTreeDiffComputer
{
private const int MinBufferLength = 10;
private RenderTreeEdit[] _entries = new RenderTreeEdit[10];
private int _entriesInUse;
private readonly ArrayBuilder<RenderTreeEdit> _entries = new ArrayBuilder<RenderTreeEdit>(10);
public RenderTreeDiff ComputeDifference(
ArraySegment<RenderTreeNode> oldTree,
ArraySegment<RenderTreeNode> newTree)
ArrayRange<RenderTreeNode> oldTree,
ArrayRange<RenderTreeNode> newTree)
{
_entriesInUse = 0;
_entries.Clear();
var siblingIndex = 0;
AppendDiffEntriesForRange(oldTree.Array, 0, oldTree.Count, newTree.Array, 0, newTree.Count, ref siblingIndex);
// If the previous usage of the buffer showed that we have allocated
// much more space than needed, free up the excess memory
var shrinkToLength = Math.Max(MinBufferLength, _entries.Length / 2);
if (_entriesInUse < shrinkToLength)
{
Array.Resize(ref _entries, shrinkToLength);
}
return new RenderTreeDiff(
new ArraySegment<RenderTreeEdit>(_entries, 0, _entriesInUse),
newTree);
return new RenderTreeDiff(_entries.ToRange(), newTree);
}
private void AppendDiffEntriesForRange(
@ -303,19 +291,15 @@ namespace Microsoft.Blazor.RenderTree
if (entry.Type == RenderTreeEditType.StepOut)
{
// If the preceding node is a StepIn, then the StepOut cancels it out
if (_entriesInUse > 0 && _entries[_entriesInUse - 1].Type == RenderTreeEditType.StepIn)
var previousIndex = _entries.Count - 1;
if (previousIndex >= 0 && _entries.Buffer[previousIndex].Type == RenderTreeEditType.StepIn)
{
_entriesInUse--;
_entries.RemoveLast();
return;
}
}
if (_entriesInUse == _entries.Length)
{
Array.Resize(ref _entries, _entries.Length * 2);
}
_entries[_entriesInUse++] = entry;
_entries.Append(entry);
}
}
}

View File

@ -55,11 +55,11 @@ namespace Microsoft.Blazor.Rendering
_renderer.UpdateDisplay(_componentId, diff);
}
private void EnsureChildComponentsInstantiated(ArraySegment<RenderTreeNode> renderTree)
private void EnsureChildComponentsInstantiated(ArrayRange<RenderTreeNode> renderTree)
{
var array = renderTree.Array;
var offsetPlusCount = renderTree.Offset + renderTree.Count;
for (var i = renderTree.Offset; i < offsetPlusCount; i++)
var count = renderTree.Count;
for (var i = 0; i < count; i++)
{
if (array[i].NodeType == RenderTreeNodeType.Component
&& array[i].Component == null)

View File

@ -336,7 +336,7 @@ namespace Microsoft.Blazor.Build.Test
=> node.NodeType != RenderTreeNodeType.Text
|| !string.IsNullOrWhiteSpace(node.TextContent);
private static ArraySegment<RenderTreeNode> GetRenderTree(IComponent component)
private static ArrayRange<RenderTreeNode> GetRenderTree(IComponent component)
{
var treeBuilder = new RenderTreeBuilder(new TestRenderer());
component.BuildRenderTree(treeBuilder);

View File

@ -31,7 +31,6 @@ namespace Microsoft.Blazor.Test
// Assert
var nodes = builder.GetNodes();
Assert.NotNull(nodes.Array);
Assert.Equal(0, nodes.Offset);
Assert.Empty(nodes);
}
@ -49,7 +48,6 @@ namespace Microsoft.Blazor.Test
// Assert
var nodes = builder.GetNodes();
Assert.Equal(0, nodes.Offset);
Assert.Collection(nodes,
node => AssertNode.Text(node, "First item"),
node => AssertNode.Text(node, string.Empty),
@ -69,7 +67,6 @@ namespace Microsoft.Blazor.Test
// Assert
var nodes = builder.GetNodes();
Assert.Equal(0, nodes.Offset);
Assert.Collection(nodes,
node => AssertNode.Text(node, "1234"),
node => AssertNode.Text(node, string.Empty));
@ -103,7 +100,7 @@ namespace Microsoft.Blazor.Test
// Assert
var nodes = builder.GetNodes();
Assert.Equal(2, nodes.Count);
AssertNode.Element(nodes[1], "my element", 1);
AssertNode.Element(nodes.Array[1], "my element", 1);
}
[Fact]
@ -122,7 +119,7 @@ namespace Microsoft.Blazor.Test
// Assert
var nodes = builder.GetNodes();
Assert.Equal(4, nodes.Count);
AssertNode.Element(nodes[0], "my element", 2);
AssertNode.Element(nodes.Array[0], "my element", 2);
}
[Fact]

View File

@ -313,8 +313,8 @@ namespace Microsoft.Blazor.Test
private class TestRenderer : Renderer
{
public IDictionary<int, ArraySegment<RenderTreeNode>> RenderTreesByComponentId { get; }
= new Dictionary<int, ArraySegment<RenderTreeNode>>();
public IDictionary<int, ArrayRange<RenderTreeNode>> RenderTreesByComponentId { get; }
= new Dictionary<int, ArrayRange<RenderTreeNode>>();
public new int AssignComponentId(IComponent component)
=> base.AssignComponentId(component);