Skip rerendering child components if their params are definitely unchanged

This commit is contained in:
Steve Sanderson 2018-02-22 13:23:52 +00:00
parent 08d7b77d38
commit 25b76bc6dc
4 changed files with 161 additions and 5 deletions

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 Microsoft.AspNetCore.Blazor.RenderTree;
namespace Microsoft.AspNetCore.Blazor.Components
@ -39,5 +40,99 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// <returns>The enumerator.</returns>
public ParameterEnumerator GetEnumerator()
=> new ParameterEnumerator(_frames, _ownerIndex);
// It's internal because there isn't a known use case for user code comparing
// ParameterCollection instances, and even if there was, it's unlikely it should
// use these equality rules which are designed for their effect on rendering.
internal bool DefinitelyEquals(ParameterCollection oldParameters)
{
// In general we can't detect mutations on arbitrary objects. We can't trust
// things like .Equals or .GetHashCode because they usually only tell us about
// shallow changes, not deep mutations. So we return false if both:
// [1] All the parameters are known to be immutable (i.e., Type.IsPrimitive
// or is in a known set of common immutable types)
// [2] And all the parameter values are equal to their previous values
// Otherwise be conservative and return false.
// To make this check cheaper, since parameters are virtually always generated in
// a deterministic order, we don't bother to account for reordering, so if any
// of the names don't match sequentially we just return false too.
//
// The logic here may look kind of epic, and would certainly be simpler if we
// used ParameterEnumerator.GetEnumerator(), but it's perf-critical and this
// implementation requires a lot fewer instructions than a GetEnumerator-based one.
var oldIndex = oldParameters._ownerIndex;
var newIndex = _ownerIndex;
var oldEndIndexExcl = oldIndex + oldParameters._frames[oldIndex].ComponentSubtreeLength;
var newEndIndexExcl = newIndex + _frames[newIndex].ComponentSubtreeLength;
while (true)
{
// First, stop if we've reached the end of either subtree
oldIndex++;
newIndex++;
var oldFinished = oldIndex == oldEndIndexExcl;
var newFinished = newIndex == newEndIndexExcl;
if (oldFinished || newFinished)
{
return oldFinished == newFinished; // Same only if we have same number of parameters
}
else
{
// Since neither subtree has finished, it's safe to read the next frame from both
ref var oldFrame = ref oldParameters._frames[oldIndex];
ref var newFrame = ref _frames[newIndex];
// Stop if we've reached the end of either subtree's sequence of attributes
oldFinished = oldFrame.FrameType != RenderTreeFrameType.Attribute;
newFinished = newFrame.FrameType != RenderTreeFrameType.Attribute;
if (oldFinished || newFinished)
{
return oldFinished == newFinished; // Same only if we have same number of parameters
}
else
{
if (!string.Equals(oldFrame.AttributeName, newFrame.AttributeName, StringComparison.Ordinal))
{
return false; // Different names
}
var oldValue = oldFrame.AttributeValue;
var newValue = newFrame.AttributeValue;
var oldIsNotNull = oldValue != null;
var newIsNotNull = newValue != null;
if (oldIsNotNull != newIsNotNull)
{
return false; // One's null and the other isn't, so different
}
else if (oldIsNotNull) // i.e., both are not null (considering previous check)
{
var oldValueType = oldValue.GetType();
var newValueType = newValue.GetType();
if (oldValueType != newValueType // Definitely different
|| !IsKnownImmutableType(oldValueType) // Maybe different
|| !oldValue.Equals(newValue)) // Somebody says they are different
{
return false;
}
}
else
{
// Both null, hence equal, so continue
}
}
}
}
}
// The contents of this list need to trade off false negatives against computation
// time. So we don't want a huge list of types to check (or would have to move to
// a hashtable lookup, which is differently expensive). It's better not to include
// uncommon types here even if they are known to be immutable.
private bool IsKnownImmutableType(Type type)
=> type.IsPrimitive
|| type == typeof(string)
|| type == typeof(DateTime)
|| type == typeof(RenderFragment)
|| type == typeof(decimal);
}
}

View File

@ -153,13 +153,22 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
// Preserve the actual componentInstance
newComponentFrame = newComponentFrame.WithComponentInstance(componentId, componentInstance);
// Supply latest parameters. They might not have changed, but it's up to the
// recipient to decide what "changed" means. Currently we only supply the new
// As an important rendering optimization, we want to skip parameter update
// notifications if we know for sure they haven't changed/mutated. The
// "MayHaveChangedSince" logic is conservative, in that it returns true if
// any parameter is of a type we don't know is immutable. In this case
// we call SetParameters and it's up to the recipient to implement
// whatever change-detection logic they want. Currently we only supply the new
// set of parameters and assume the recipient has enough info to do whatever
// comparisons it wants with the old values. Later we could choose to pass the
// old parameter values if we wanted.
// old parameter values if we wanted. By default, components always rerender
// after any SetParameters call, which is safe but now always optimal for perf.
var oldParameters = new ParameterCollection(oldTree, oldComponentIndex);
var newParameters = new ParameterCollection(newTree, newComponentIndex);
componentInstance.SetParameters(newParameters);
if (!newParameters.DefinitelyEquals(oldParameters))
{
componentInstance.SetParameters(newParameters);
}
}
private static int NextSiblingIndex(RenderTreeFrame frame, int frameIndex)

View File

@ -832,6 +832,44 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Same(objectWillNotChange, newComponentInstance.ObjectProperty);
}
[Fact]
public void SkipsUpdatingParametersOnChildComponentsIfAllAreDefinitelyImmutableAndUnchanged()
{
// We only know that types are immutable if either Type.IsPrimitive, or it's one of
// a known set of common immutable types.
// Arrange: Populate old and new with equivalent content
RenderFragment fragmentWillNotChange = builder => throw new NotImplementedException();
var dateTimeWillNotChange = DateTime.Now;
foreach (var tree in new[] { oldTree, newTree })
{
tree.OpenComponent<CaptureSetParametersComponent>(0);
tree.AddAttribute(1, "MyString", "Some fixed string");
tree.AddAttribute(1, "MyByte", (byte)123);
tree.AddAttribute(1, "MyInt", int.MaxValue);
tree.AddAttribute(1, "MyLong", long.MaxValue);
tree.AddAttribute(1, "MyBool", true);
tree.AddAttribute(1, "MyFloat", float.MaxValue);
tree.AddAttribute(1, "MyDouble", double.MaxValue);
tree.AddAttribute(1, "MyDecimal", decimal.MinusOne);
tree.AddAttribute(1, "MyDate", dateTimeWillNotChange);
tree.AddAttribute(1, "MyFragment", fragmentWillNotChange); // Treat fragments as primitive
tree.CloseComponent();
}
RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
var originalComponentInstance = (CaptureSetParametersComponent)oldTree.GetFrames().Array[0].Component;
Assert.Equal(1, originalComponentInstance.SetParametersCallCount);
// Act
var renderBatch = GetRenderedBatch();
var newComponentInstance = (CaptureSetParametersComponent)oldTree.GetFrames().Array[0].Component;
// Assert
Assert.Same(originalComponentInstance, newComponentInstance);
Assert.Equal(1, originalComponentInstance.SetParametersCallCount); // Received no further parameter change notification
}
[Fact]
public void QueuesRemovedChildComponentsForDisposal()
{
@ -915,6 +953,20 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
}
private class CaptureSetParametersComponent : IComponent
{
public int SetParametersCallCount { get; private set; }
public void Init(RenderHandle renderHandle)
{
}
public void SetParameters(ParameterCollection parameters)
{
SetParametersCallCount++;
}
}
private class DisposableComponent : IComponent, IDisposable
{
public int DisposalCount { get; private set; }

View File

@ -341,7 +341,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(0, edit.ReferenceFrameIndex);
});
AssertFrame.Text(batch.ReferenceFrames[0], "Modified message");
Assert.Empty(batch.DiffsByComponentId[nestedComponentFrame.ComponentId].Single().Edits);
Assert.False(batch.DiffsByComponentId.ContainsKey(nestedComponentFrame.ComponentId));
}
[Fact]