diff --git a/src/Microsoft.Blazor/Microsoft.Blazor.csproj b/src/Microsoft.Blazor/Microsoft.Blazor.csproj index 9f5c4f4abb..37d1f5cd0e 100644 --- a/src/Microsoft.Blazor/Microsoft.Blazor.csproj +++ b/src/Microsoft.Blazor/Microsoft.Blazor.csproj @@ -2,6 +2,7 @@ netstandard2.0 + 7.2 diff --git a/src/Microsoft.Blazor/RenderTree/ArrayBuilder.cs b/src/Microsoft.Blazor/RenderTree/ArrayBuilder.cs new file mode 100644 index 0000000000..b07445d878 --- /dev/null +++ b/src/Microsoft.Blazor/RenderTree/ArrayBuilder.cs @@ -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 +{ + /// + /// Implements a list that uses an array of objects to store the elements. + /// + public class ArrayBuilder + { + private const int MinCapacity = 10; + private T[] _items; + private int _itemsInUse; + + /// + /// Constructs a new instance of . + /// + public ArrayBuilder() : this(MinCapacity) + { + } + + /// + /// Constructs a new instance of . + /// + public ArrayBuilder(int capacity) + { + _items = new T[capacity < MinCapacity ? MinCapacity : capacity]; + _itemsInUse = 0; + } + + /// + /// Gets the number of items. + /// + public int Count => _itemsInUse; + + /// + /// Gets the underlying buffer. + /// + public T[] Buffer => _items; + + /// + /// Appends a new item, automatically resizing the underlying array if necessary. + /// + /// The item to append. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(T item) + { + if (_itemsInUse == _items.Length) + { + SetCapacity(_itemsInUse * 2, preserveContents: true); + } + + _items[_itemsInUse++] = item; + } + + /// + /// Removes the last item. + /// + public void RemoveLast() + { + _itemsInUse--; + _items[_itemsInUse] = default(T); // Release to GC + } + + /// + /// Marks the array as empty, also shrinking the underlying storage if it was + /// not being used to near its full capacity. + /// + 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 + } + } + + /// + /// Produces an structure describing the current contents. + /// + /// The . + public ArrayRange ToRange() + => new ArrayRange(_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); + } + } + } +} diff --git a/src/Microsoft.Blazor/RenderTree/ArrayRange.cs b/src/Microsoft.Blazor/RenderTree/ArrayRange.cs new file mode 100644 index 0000000000..37e5e1351f --- /dev/null +++ b/src/Microsoft.Blazor/RenderTree/ArrayRange.cs @@ -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 +{ + /// + /// Represents a range of elements in an array that are in use. + /// + /// The array item type. + public readonly struct ArrayRange : IEnumerable, IEnumerable + { + /// + /// Gets the underlying array instance. + /// + public T[] Array { get; } + + /// + /// Gets the number of items in the array that are considered to be in use. + /// + public int Count { get; } + + /// + /// Constructs an instance of . + /// + /// The array. + /// The number of items in the array that are in use. + public ArrayRange(T[] array, int count) + { + Array = array; + Count = count; + } + + /// + IEnumerator IEnumerable.GetEnumerator() + => ((IEnumerable)new ArraySegment(Array, 0, Count)).GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() + => ((IEnumerable)new ArraySegment(Array, 0, Count)).GetEnumerator(); + } +} diff --git a/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs b/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs index 83acd927a5..22327e818d 100644 --- a/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs +++ b/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs @@ -13,10 +13,8 @@ namespace Microsoft.Blazor.RenderTree /// 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 _entries = new ArrayBuilder(10); private readonly Stack _openElementIndices = new Stack(); private RenderTreeNodeType? _lastNonAttributeNodeType; @@ -39,7 +37,7 @@ namespace Microsoft.Blazor.RenderTree /// A value representing the type of the element. 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); } /// @@ -149,35 +147,21 @@ namespace Microsoft.Blazor.RenderTree /// 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; } /// /// Returns the values that have been appended. - /// The return value's is always zero. /// - /// An array segment of values. - public ArraySegment GetNodes() => - new ArraySegment(_entries, 0, _entriesInUse); + /// An array range of values. + public ArrayRange 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) diff --git a/src/Microsoft.Blazor/RenderTree/RenderTreeDiff.cs b/src/Microsoft.Blazor/RenderTree/RenderTreeDiff.cs index 497ddc1231..094c7a58e0 100644 --- a/src/Microsoft.Blazor/RenderTree/RenderTreeDiff.cs +++ b/src/Microsoft.Blazor/RenderTree/RenderTreeDiff.cs @@ -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. /// - public struct RenderTreeDiff + public readonly struct RenderTreeDiff { /// /// Gets the changes to the render tree since a previous state. /// - public ArraySegment Edits { get; private set; } + public ArrayRange Edits { get; } /// /// Gets the latest render tree. That is, the result of applying the /// to the previous state. /// - public ArraySegment CurrentState { get; private set; } + public ArrayRange CurrentState { get; } internal RenderTreeDiff( - ArraySegment entries, - ArraySegment referenceTree) + ArrayRange entries, + ArrayRange referenceTree) { Edits = entries; CurrentState = referenceTree; diff --git a/src/Microsoft.Blazor/RenderTree/RenderTreeDiffComputer.cs b/src/Microsoft.Blazor/RenderTree/RenderTreeDiffComputer.cs index d66eadd7a4..0ea51a3f37 100644 --- a/src/Microsoft.Blazor/RenderTree/RenderTreeDiffComputer.cs +++ b/src/Microsoft.Blazor/RenderTree/RenderTreeDiffComputer.cs @@ -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 _entries = new ArrayBuilder(10); public RenderTreeDiff ComputeDifference( - ArraySegment oldTree, - ArraySegment newTree) + ArrayRange oldTree, + ArrayRange 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(_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); } } } diff --git a/src/Microsoft.Blazor/Rendering/ComponentState.cs b/src/Microsoft.Blazor/Rendering/ComponentState.cs index 0d5794fdac..e0cbdd4959 100644 --- a/src/Microsoft.Blazor/Rendering/ComponentState.cs +++ b/src/Microsoft.Blazor/Rendering/ComponentState.cs @@ -55,11 +55,11 @@ namespace Microsoft.Blazor.Rendering _renderer.UpdateDisplay(_componentId, diff); } - private void EnsureChildComponentsInstantiated(ArraySegment renderTree) + private void EnsureChildComponentsInstantiated(ArrayRange 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) diff --git a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs index 439d140ad3..81b6353dcf 100644 --- a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs @@ -336,7 +336,7 @@ namespace Microsoft.Blazor.Build.Test => node.NodeType != RenderTreeNodeType.Text || !string.IsNullOrWhiteSpace(node.TextContent); - private static ArraySegment GetRenderTree(IComponent component) + private static ArrayRange GetRenderTree(IComponent component) { var treeBuilder = new RenderTreeBuilder(new TestRenderer()); component.BuildRenderTree(treeBuilder); diff --git a/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs b/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs index aebf92cd26..e2fa28cea3 100644 --- a/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs +++ b/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs @@ -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] diff --git a/test/Microsoft.Blazor.Test/RendererTest.cs b/test/Microsoft.Blazor.Test/RendererTest.cs index c6b1860b88..caf12e43a1 100644 --- a/test/Microsoft.Blazor.Test/RendererTest.cs +++ b/test/Microsoft.Blazor.Test/RendererTest.cs @@ -313,8 +313,8 @@ namespace Microsoft.Blazor.Test private class TestRenderer : Renderer { - public IDictionary> RenderTreesByComponentId { get; } - = new Dictionary>(); + public IDictionary> RenderTreesByComponentId { get; } + = new Dictionary>(); public new int AssignComponentId(IComponent component) => base.AssignComponentId(component);