diff --git a/src/Microsoft.Blazor/RenderTree/RenderTreeDiff.cs b/src/Microsoft.Blazor/RenderTree/RenderTreeDiff.cs index 8d6e6dbe01..209a24044d 100644 --- a/src/Microsoft.Blazor/RenderTree/RenderTreeDiff.cs +++ b/src/Microsoft.Blazor/RenderTree/RenderTreeDiff.cs @@ -9,7 +9,7 @@ namespace Microsoft.Blazor.RenderTree { private const int MinBufferLength = 10; private RenderTreeDiffEntry[] _entries = new RenderTreeDiffEntry[10]; - private int _entriesInUse = 0; + private int _entriesInUse; public ArraySegment ComputeDifference( ArraySegment oldTree, @@ -17,6 +17,7 @@ namespace Microsoft.Blazor.RenderTree { _entriesInUse = 0; AppendDiffEntriesForRange(oldTree.Array, 0, oldTree.Count, newTree.Array, 0, newTree.Count); + TrimTrailingContinueNodes(); // If the previous usage of the buffer showed that we have allocated // much more space than needed, free up the excess memory @@ -292,6 +293,19 @@ namespace Microsoft.Blazor.RenderTree private void Append(RenderTreeDiffEntry entry) { + if (entry.Type == RenderTreeDiffEntryType.StepOut) + { + TrimTrailingContinueNodes(); + + // If the preceding node is now a StepIn, then we can coalesce the StepIn+StepOut + // down to a single Continue + if (_entriesInUse > 0 && _entries[_entriesInUse - 1].Type == RenderTreeDiffEntryType.StepIn) + { + _entriesInUse--; + entry = RenderTreeDiffEntry.Continue(); + } + } + if (_entriesInUse == _entries.Length) { Array.Resize(ref _entries, _entries.Length * 2); @@ -299,5 +313,13 @@ namespace Microsoft.Blazor.RenderTree _entries[_entriesInUse++] = entry; } + + private void TrimTrailingContinueNodes() + { + while (_entriesInUse > 0 && _entries[_entriesInUse - 1].Type == RenderTreeDiffEntryType.Continue) + { + _entriesInUse--; + } + } } } diff --git a/test/Microsoft.Blazor.Test/RenderTreeDiffTest.cs b/test/Microsoft.Blazor.Test/RenderTreeDiffTest.cs index f54048f1f4..b40a9b2290 100644 --- a/test/Microsoft.Blazor.Test/RenderTreeDiffTest.cs +++ b/test/Microsoft.Blazor.Test/RenderTreeDiffTest.cs @@ -28,8 +28,7 @@ namespace Microsoft.Blazor.Test var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert - Assert.Collection(result, - entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); + Assert.Empty(result); } public static IEnumerable RecognizesEquivalentNodesAsSameCases() @@ -69,8 +68,7 @@ namespace Microsoft.Blazor.Test { Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type); Assert.Equal(1, entry.NewTreeIndex); - }, - entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); + }); } [Fact] @@ -92,8 +90,7 @@ namespace Microsoft.Blazor.Test // Assert Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), - entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type), - entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); + entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type)); } [Fact] @@ -117,8 +114,7 @@ namespace Microsoft.Blazor.Test Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type), - entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type), - entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); + entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type)); } [Fact] @@ -150,8 +146,7 @@ namespace Microsoft.Blazor.Test { Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type); Assert.Equal(2, entry.NewTreeIndex); - }, - entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); + }); } [Fact] @@ -241,8 +236,7 @@ namespace Microsoft.Blazor.Test { Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type); Assert.Equal(2, entry.NewTreeIndex); - }, - entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); + }); } [Fact] @@ -266,8 +260,7 @@ namespace Microsoft.Blazor.Test Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type), - entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type), - entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); + entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type)); } [Fact] @@ -387,8 +380,7 @@ namespace Microsoft.Blazor.Test { Assert.Equal(RenderTreeDiffEntryType.SetAttribute, entry.Type); Assert.Equal(2, entry.NewTreeIndex); - }, - entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); + }); } [Fact] @@ -415,8 +407,7 @@ namespace Microsoft.Blazor.Test { Assert.Equal(RenderTreeDiffEntryType.RemoveAttribute, entry.Type); Assert.Equal("will be removed", entry.RemovedAttributeName); - }, - entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); + }); } [Fact] @@ -444,8 +435,7 @@ namespace Microsoft.Blazor.Test { Assert.Equal(RenderTreeDiffEntryType.SetAttribute, entry.Type); Assert.Equal(2, entry.NewTreeIndex); - }, - entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); + }); } [Fact] @@ -476,8 +466,7 @@ namespace Microsoft.Blazor.Test { Assert.Equal(RenderTreeDiffEntryType.SetAttribute, entry.Type); Assert.Equal(2, entry.NewTreeIndex); - }, - entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); + }); } [Fact] @@ -508,8 +497,7 @@ namespace Microsoft.Blazor.Test { Assert.Equal(RenderTreeDiffEntryType.RemoveAttribute, entry.Type); Assert.Equal("oldname", entry.RemovedAttributeName); - }, - entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); + }); } [Fact] @@ -553,6 +541,74 @@ namespace Microsoft.Blazor.Test entry => Assert.Equal(RenderTreeDiffEntryType.StepOut, entry.Type)); } + [Fact] + public void SkipsUnmodifiedSubtrees() + { + // Arrange + var oldTree = new RenderTreeBuilder(new FakeRenderer()); + var newTree = new RenderTreeBuilder(new FakeRenderer()); + var diff = new RenderTreeDiff(); + oldTree.OpenElement(10, "root"); + oldTree.AddText(11, "Text that will change"); + oldTree.OpenElement(12, "Subtree that will not change"); + oldTree.OpenElement(13, "Another"); + oldTree.AddText(14, "Text that will not change"); + oldTree.CloseElement(); + oldTree.CloseElement(); + oldTree.CloseElement(); + + newTree.OpenElement(10, "root"); + newTree.AddText(11, "Text that has changed"); + newTree.OpenElement(12, "Subtree that will not change"); + newTree.OpenElement(13, "Another"); + newTree.AddText(14, "Text that will not change"); + newTree.CloseElement(); + newTree.CloseElement(); + newTree.CloseElement(); + + // Act + var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); + + // Assert + Assert.Collection(result, + entry => Assert.Equal(RenderTreeDiffEntryType.StepIn, entry.Type), + entry => + { + Assert.Equal(RenderTreeDiffEntryType.UpdateText, entry.Type); + Assert.Equal(1, entry.NewTreeIndex); + }, + entry => Assert.Equal(RenderTreeDiffEntryType.StepOut, entry.Type)); + } + + [Fact] + public void SkipsUnmodifiedTrailingSiblings() + { + // Arrange + var oldTree = new RenderTreeBuilder(new FakeRenderer()); + var newTree = new RenderTreeBuilder(new FakeRenderer()); + var diff = new RenderTreeDiff(); + oldTree.AddText(10, "text1"); + oldTree.AddText(11, "text2"); + oldTree.AddText(12, "text3"); + oldTree.AddText(13, "text4"); + newTree.AddText(10, "text1"); + newTree.AddText(11, "text2modified"); + newTree.AddText(12, "text3"); + newTree.AddText(13, "text4"); + + // Act + var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); + + // Assert + Assert.Collection(result, + entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), + entry => + { + Assert.Equal(RenderTreeDiffEntryType.UpdateText, entry.Type); + Assert.Equal(1, entry.NewTreeIndex); + }); + } + private class FakeRenderer : Renderer { internal protected override void UpdateDisplay(int componentId, ArraySegment renderTree)