Further renderer refactoring

This commit is contained in:
Steve Sanderson 2018-02-09 20:04:18 +00:00
parent d8ae9fd478
commit e37e22aa27
11 changed files with 127 additions and 213 deletions

View File

@ -14,7 +14,7 @@ namespace HostedInAspNet.Client
{
// Temporarily render this test component until there's a proper mechanism
// for testing this.
new BrowserRenderer().AddComponent("app", new MyComponent());
new BrowserRenderer().AddComponent<MyComponent>("app");
}
}

View File

@ -9,8 +9,7 @@ namespace StandaloneApp
{
public static void Main(string[] args)
{
new BrowserRenderer()
.AddComponent("app", new Home());
new BrowserRenderer().AddComponent<Home>("app");
}
}
}

View File

@ -18,12 +18,6 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
{
private readonly int _browserRendererId;
// Ensures the explicitly-added components aren't GCed, because the browser
// will still send events referencing them by ID. We only need to store the
// top-level components, because the associated ComponentState will reference
// all the reachable descendant components of each.
private IList<IComponent> _rootComponents = new List<IComponent>();
/// <summary>
/// Constructs an instance of <see cref="BrowserRenderer"/>.
/// </summary>
@ -38,22 +32,33 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
internal void RenderNewBatchInternal(int componentId)
=> RenderNewBatch(componentId);
/// <summary>
/// Attaches a new root component to the renderer,
/// causing it to be displayed in the specified DOM element.
/// </summary>
/// <typeparam name="TComponent">The type of the component.</typeparam>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
public void AddComponent<TComponent>(string domElementSelector)
where TComponent: IComponent
{
AddComponent(typeof(TComponent), domElementSelector);
}
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="BrowserRenderer"/>,
/// causing it to be displayed in the specified DOM element.
/// </summary>
/// <param name="componentType">The type of the component.</param>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
/// <param name="component">The <see cref="IComponent"/>.</param>
public void AddComponent(string domElementSelector, IComponent component)
public void AddComponent(Type componentType, string domElementSelector)
{
var component = InstantiateComponent(componentType);
var componentId = AssignComponentId(component);
RegisteredFunction.InvokeUnmarshalled<int, string, int, object>(
"attachComponentToElement",
_browserRendererId,
domElementSelector,
componentId);
_rootComponents.Add(component);
RenderNewBatch(componentId);
}

View File

@ -10,36 +10,6 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
{
internal static class RenderTreeDiffBuilder
{
private struct DiffContext
{
// Exists only so that the various methods in this class can call each other without
// constantly building up long lists of parameters. Is private to this class, so the
// fact that it's a mutable struct is manageable.
// Always pass by ref to avoid copying, and because the 'SiblingIndex' is mutable.
public readonly Renderer Renderer;
public readonly RenderBatchBuilder BatchBuilder;
public readonly RenderTreeFrame[] OldTree;
public readonly RenderTreeFrame[] NewTree;
public readonly ArrayBuilder<RenderTreeEdit> Edits;
public readonly ArrayBuilder<RenderTreeFrame> ReferenceFrames;
public int SiblingIndex;
public DiffContext(
Renderer renderer,
RenderBatchBuilder batchBuilder,
RenderTreeFrame[] oldTree, RenderTreeFrame[] newTree)
{
Renderer = renderer;
BatchBuilder = batchBuilder;
OldTree = oldTree;
NewTree = newTree;
Edits = batchBuilder.EditsBuffer;
ReferenceFrames = batchBuilder.ReferenceFramesBuffer;
SiblingIndex = 0;
}
}
public static RenderTreeDiff ComputeDiff(
Renderer renderer,
RenderBatchBuilder batchBuilder,
@ -419,7 +389,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
{
if (oldFrame.AttributeEventHandlerId > 0)
{
diffContext.BatchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId);
diffContext.BatchBuilder.DisposedEventHandlerIds.Append(oldFrame.AttributeEventHandlerId);
}
InitializeNewAttributeFrame(ref diffContext, ref newFrame);
var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
@ -490,7 +460,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
diffContext.Edits.Append(RenderTreeEdit.RemoveAttribute(diffContext.SiblingIndex, oldFrame.AttributeName));
if (oldFrame.AttributeEventHandlerId > 0)
{
diffContext.BatchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId);
diffContext.BatchBuilder.DisposedEventHandlerIds.Append(oldFrame.AttributeEventHandlerId);
}
break;
}
@ -568,7 +538,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}");
}
diffContext.Renderer.InstantiateChildComponent(ref frame);
diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame);
var childComponentInstance = frame.Component;
// All descendants of a component are its properties
@ -604,9 +574,42 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
}
else if (frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId > 0)
{
batchBuilder.AddDisposedEventHandlerId(frame.AttributeEventHandlerId);
batchBuilder.DisposedEventHandlerIds.Append(frame.AttributeEventHandlerId);
}
}
}
/// <summary>
/// Exists only so that the various methods in this class can call each other without
/// constantly building up long lists of parameters. Is private to this class, so the
/// fact that it's a mutable struct is manageable.
///
/// Always pass by ref to avoid copying, and because the 'SiblingIndex' is mutable.
/// </summary>
private struct DiffContext
{
public readonly Renderer Renderer;
public readonly RenderBatchBuilder BatchBuilder;
public readonly RenderTreeFrame[] OldTree;
public readonly RenderTreeFrame[] NewTree;
public readonly ArrayBuilder<RenderTreeEdit> Edits;
public readonly ArrayBuilder<RenderTreeFrame> ReferenceFrames;
public int SiblingIndex;
public DiffContext(
Renderer renderer,
RenderBatchBuilder batchBuilder,
RenderTreeFrame[] oldTree,
RenderTreeFrame[] newTree)
{
Renderer = renderer;
BatchBuilder = batchBuilder;
OldTree = oldTree;
NewTree = newTree;
Edits = batchBuilder.EditsBuffer;
ReferenceFrames = batchBuilder.ReferenceFramesBuffer;
SiblingIndex = 0;
}
}
}
}

View File

@ -35,11 +35,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
_renderTreeBuilderPrevious = new RenderTreeBuilder(renderer);
}
/// <summary>
/// Regenerates the <see cref="RenderTree"/> and adds the changes to the
/// <paramref name="batchBuilder"/>.
/// </summary>
public void Render(Renderer renderer, RenderBatchBuilder batchBuilder)
public void RenderIntoBatch(RenderBatchBuilder batchBuilder)
{
if (_component is IHandlePropertiesChanged notifyableComponent)
{
@ -59,19 +55,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
_renderTreeBuilderPrevious.GetFrames(),
_renderTreeBuilderCurrent.GetFrames());
batchBuilder.UpdatedComponentDiffs.Append(diff);
// Process disposal queue now in case it causes further component renders to be enqueued
while (batchBuilder.ComponentDisposalQueue.Count > 0)
{
var disposeComponentId = batchBuilder.ComponentDisposalQueue.Dequeue();
renderer.DisposeInExistingBatch(batchBuilder, disposeComponentId);
}
}
/// <summary>
/// Notifies the component that it is being disposed.
/// </summary>
public void NotifyDisposed(RenderBatchBuilder batchBuilder)
public void DisposeInBatch(RenderBatchBuilder batchBuilder)
{
// TODO: Handle components throwing during dispose. Shouldn't break the whole render batch.
if (_component is IDisposable disposable)

View File

@ -6,45 +6,41 @@ using Microsoft.AspNetCore.Blazor.RenderTree;
namespace Microsoft.AspNetCore.Blazor.Rendering
{
/// <summary>
/// Collects the data produced by the rendering system during the course
/// of rendering a single batch. This tracks both the final output data
/// and the intermediate states (such as the queue of components still to
/// be rendered).
/// </summary>
internal class RenderBatchBuilder
{
// Primary result data
public ArrayBuilder<RenderTreeDiff> UpdatedComponentDiffs { get; } = new ArrayBuilder<RenderTreeDiff>();
public ArrayBuilder<int> DisposedComponentIds { get; } = new ArrayBuilder<int>();
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>();
// State of render pipeline
public Queue<int> ComponentRenderQueue { get; } = new Queue<int>();
public Queue<int> ComponentDisposalQueue { get; } = new Queue<int>();
public ArrayBuilder<RenderTreeDiff> UpdatedComponentDiffs { get; set; }
= new ArrayBuilder<RenderTreeDiff>();
private readonly ArrayBuilder<int> _disposedComponentIds = new ArrayBuilder<int>();
private readonly ArrayBuilder<int> _disposedEventHandlerIds = new ArrayBuilder<int>();
public ArrayRange<int> GetDisposedEventHandlerIds()
=> _disposedEventHandlerIds.ToRange();
public void Clear()
{
EditsBuffer.Clear();
ReferenceFramesBuffer.Clear();
ComponentRenderQueue.Clear();
UpdatedComponentDiffs.Clear();
_disposedComponentIds.Clear();
_disposedEventHandlerIds.Clear();
DisposedComponentIds.Clear();
DisposedEventHandlerIds.Clear();
}
public RenderBatch ToBatch()
=> new RenderBatch(
UpdatedComponentDiffs.ToRange(),
ReferenceFramesBuffer.ToRange(),
_disposedComponentIds.ToRange());
public void AddDisposedComponentId(int componentId)
=> _disposedComponentIds.Append(componentId);
public void AddDisposedEventHandlerId(int attributeEventHandlerId)
=> _disposedEventHandlerIds.Append(attributeEventHandlerId);
DisposedComponentIds.ToRange());
}
}

View File

@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.RenderTree;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Microsoft.AspNetCore.Blazor.Rendering
@ -16,15 +15,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// </summary>
public abstract class Renderer
{
// Methods for tracking associations between component IDs, instances, and states,
// without pinning any of them in memory here. The explictly GC rooted items are the
// components explicitly added to the renderer (i.e., top-level components). In turn
// these reference descendant components and associated ComponentState instances.
private readonly WeakValueDictionary<int, ComponentState> _componentStateById
= new WeakValueDictionary<int, ComponentState>();
private readonly ConditionalWeakTable<IComponent, ComponentState> _componentStateByComponent
= new ConditionalWeakTable<IComponent, ComponentState>();
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
private readonly Dictionary<int, ComponentState> _componentStateById
= new Dictionary<int, ComponentState>();
// Because rendering is currently synchronous and single-threaded, we can keep re-using the
// same RenderBatchBuilder instance to avoid reallocating
@ -35,35 +28,46 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
private readonly Dictionary<int, UIEventHandler> _eventHandlersById
= new Dictionary<int, UIEventHandler>();
/// <summary>
/// Constructs a new component of the specified type.
/// </summary>
/// <param name="componentType">The type of the component to instantiate.</param>
/// <returns>The component instance.</returns>
protected IComponent InstantiateComponent(Type componentType)
{
if (!typeof(IComponent).IsAssignableFrom(componentType))
{
throw new ArgumentException($"Must implement {nameof(IComponent)}", nameof(componentType));
}
return (IComponent)Activator.CreateInstance(componentType);
}
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
/// an identifier that is unique within the scope of the <see cref="Renderer"/>.
/// </summary>
/// <param name="component">The <see cref="IComponent"/>.</param>
/// <returns>The assigned identifier for the <see cref="IComponent"/>.</returns>
/// <param name="component">The component.</param>
/// <returns>The component's assigned identifier.</returns>
protected int AssignComponentId(IComponent component)
{
lock (_componentStateById)
{
var componentId = _nextComponentId++;
var componentState = new ComponentState(this, componentId, component);
_componentStateById.Add(componentId, componentState);
_componentStateByComponent.Add(component, componentState); // Ensure the componentState lives for at least as long as the component
return componentId;
}
var componentId = _nextComponentId++;
var componentState = new ComponentState(this, componentId, component);
_componentStateById.Add(componentId, componentState);
return componentId;
}
/// <summary>
/// Updates the visible UI.
/// </summary>
/// <param name="renderBatch">The changes to the UI since the previous call.</param>
internal protected abstract void UpdateDisplay(RenderBatch renderBatch);
protected abstract void UpdateDisplay(RenderBatch renderBatch);
/// <summary>
/// Updates the rendered state of the specified <see cref="IComponent"/>.
/// </summary>
/// <param name="componentId">The identifier of the <see cref="IComponent"/> to render.</param>
protected internal void RenderNewBatch(int componentId)
protected void RenderNewBatch(int componentId)
{
// It's very important that components' rendering logic has no side-effects, and in particular
// components must *not* trigger Render from inside their render logic, otherwise you could
@ -84,11 +88,11 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
"Render logic must not have side-effects such as manually triggering other rendering.");
}
_sharedRenderBatchBuilder.ComponentRenderQueue.Enqueue(componentId);
try
{
RenderInExistingBatch(_sharedRenderBatchBuilder, componentId);
// Process
// Process render queue until empty
while (_sharedRenderBatchBuilder.ComponentRenderQueue.Count > 0)
{
var nextComponentIdToRender = _sharedRenderBatchBuilder.ComponentRenderQueue.Dequeue();
@ -96,24 +100,15 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
}
UpdateDisplay(_sharedRenderBatchBuilder.ToBatch());
RemoveEventHandlerIds(_sharedRenderBatchBuilder.GetDisposedEventHandlerIds());
}
finally
{
RemoveEventHandlerIds(_sharedRenderBatchBuilder.DisposedEventHandlerIds.ToRange());
_sharedRenderBatchBuilder.Clear();
Interlocked.Exchange(ref _renderBatchLock, 0);
}
}
internal void RenderInExistingBatch(RenderBatchBuilder batchBuilder, int componentId)
=> GetRequiredComponentState(componentId).Render(this, batchBuilder);
internal void DisposeInExistingBatch(RenderBatchBuilder batchBuilder, int componentId)
{
GetRequiredComponentState(componentId).NotifyDisposed(batchBuilder);
batchBuilder.AddDisposedComponentId(componentId);
}
/// <summary>
/// Notifies the specified component that an event has occurred.
/// </summary>
@ -136,7 +131,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
}
}
internal void InstantiateChildComponent(ref RenderTreeFrame frame)
internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame)
{
if (frame.FrameType != RenderTreeFrameType.Component)
{
@ -148,7 +143,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
throw new ArgumentException($"The frame already has a non-null component instance", nameof(frame));
}
var newComponent = (IComponent)Activator.CreateInstance(frame.ComponentType);
var newComponent = InstantiateComponent(frame.ComponentType);
var newComponentId = AssignComponentId(newComponent);
frame = frame.WithComponentInstance(newComponentId, newComponent);
}
@ -160,6 +155,25 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
frame = frame.WithAttributeEventHandlerId(id);
}
private ComponentState GetRequiredComponentState(int componentId)
=> _componentStateById.TryGetValue(componentId, out var componentState)
? componentState
: throw new ArgumentException($"The renderer does not have a component with ID {componentId}.");
private void RenderInExistingBatch(RenderBatchBuilder batchBuilder, int componentId)
{
GetRequiredComponentState(componentId).RenderIntoBatch(batchBuilder);
// Process disposal queue now in case it causes further component renders to be enqueued
while (batchBuilder.ComponentDisposalQueue.Count > 0)
{
var disposeComponentId = batchBuilder.ComponentDisposalQueue.Dequeue();
GetRequiredComponentState(disposeComponentId).DisposeInBatch(batchBuilder);
_componentStateById.Remove(disposeComponentId);
batchBuilder.DisposedComponentIds.Append(disposeComponentId);
}
}
private void RemoveEventHandlerIds(ArrayRange<int> eventHandlerIds)
{
var array = eventHandlerIds.Array;
@ -169,10 +183,5 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
_eventHandlersById.Remove(array[i]);
}
}
private ComponentState GetRequiredComponentState(int componentId)
=> _componentStateById.TryGetValue(componentId, out var componentState)
? componentState
: throw new ArgumentException($"The renderer does not have a component with ID {componentId}.");
}
}

View File

@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class TestRenderer : Renderer
{
protected internal override void UpdateDisplay(RenderBatch renderBatch)
protected override void UpdateDisplay(RenderBatch renderBatch)
=> throw new NotImplementedException();
}
}

View File

@ -785,7 +785,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class FakeRenderer : Renderer
{
internal protected override void UpdateDisplay(RenderBatch renderBatch)
protected override void UpdateDisplay(RenderBatch renderBatch)
{
}
}

View File

@ -275,78 +275,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.True(renderer2.Batches.Single().DiffsByComponentId.ContainsKey(renderer2ComponentId));
}
[Fact]
public void ComponentsAreNotPinnedInMemory()
{
// It's important that the Renderer base class does not itself pin in memory
// any of the component instances that were attached to it (or by extension,
// their descendants). This is because as the set of active components changes
// over time, we need the GC to be able to release unused ones, and there isn't
// any other mechanism for explicitly destroying components when they stop
// being referenced.
var renderer = new NoOpRenderer();
AssertCanBeCollected(() =>
{
var component = new TestComponent(null);
renderer.AssignComponentId(component);
return component;
});
}
[Fact]
public void CannotRenderComponentsIfGCed()
{
// Arrange
var renderer = new NoOpRenderer();
// Act
var componentId = new Func<int>(() =>
{
var component = new TestComponent(builder =>
throw new NotImplementedException("Should not be invoked"));
return renderer.AssignComponentId(component);
})();
// Since there are no unit test references to 'component' here, the GC
// should be able to collect it
GC.Collect();
GC.WaitForPendingFinalizers();
// Assert
Assert.ThrowsAny<Exception>(() =>
{
renderer.RenderNewBatch(componentId);
});
}
[Fact]
public void CanRenderComponentsIfNotGCed()
{
// Arrange
var renderer = new NoOpRenderer();
var didRender = false;
// Act
var component = new TestComponent(builder =>
{
didRender = true;
});
var componentId = renderer.AssignComponentId(component);
// Unlike the preceding test, we still have a reference to the component
// instance on the stack here, so the following should not cause it to
// be collected. Then when we call RenderComponent, there should be no error.
GC.Collect();
GC.WaitForPendingFinalizers();
renderer.RenderNewBatch(componentId);
// Assert
Assert.True(didRender);
}
[Fact]
public void PreservesChildComponentInstancesWithNoAttributes()
{
@ -721,7 +649,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
public new void RenderNewBatch(int componentId)
=> base.RenderNewBatch(componentId);
protected internal override void UpdateDisplay(RenderBatch renderBatch)
protected override void UpdateDisplay(RenderBatch renderBatch)
{
}
}
@ -740,7 +668,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args)
=> base.DispatchEvent(componentId, eventHandlerId, args);
protected internal override void UpdateDisplay(RenderBatch renderBatch)
protected override void UpdateDisplay(RenderBatch renderBatch)
{
var capturedBatch = new CapturedBatch();
Batches.Add(capturedBatch);
@ -879,17 +807,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
}
void AssertCanBeCollected(Func<object> targetFactory)
{
// We have to construct the WeakReference in a separate scope
// otherwise its target won't be collected on this GC cycle
var weakRef = new Func<WeakReference>(
() => new WeakReference(targetFactory()))();
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.Null(weakRef.Target);
}
(int, T) FirstWithIndex<T>(IEnumerable<T> items, Predicate<T> predicate)
{
var index = 0;

View File

@ -19,8 +19,7 @@ namespace BasicTestApp
public static void MountTestComponent(string componentTypeName)
{
var componentType = Type.GetType(componentTypeName);
var componentInstance = (IComponent)Activator.CreateInstance(componentType);
new BrowserRenderer().AddComponent("app", componentInstance);
new BrowserRenderer().AddComponent(componentType, "app");
}
}
}