Skip to content

Commit

Permalink
refactor - wip - only generate markup for root components
Browse files Browse the repository at this point in the history
  • Loading branch information
egil committed May 10, 2024
1 parent 04aa5fe commit b17e9d4
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 129 deletions.
69 changes: 27 additions & 42 deletions src/bunit/Rendering/BunitRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public sealed class BunitRenderer : Renderer
{
private readonly BunitServiceProvider services;
private readonly List<Task> disposalTasks = [];
private static readonly ConcurrentDictionary<Type, ConstructorInfo> componentActivatorCache = new();
private static readonly ConcurrentDictionary<Type, ConstructorInfo> ComponentActivatorCache = new();

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_isBatchInProgress")]
private static extern ref bool GetIsBatchInProgressField(Renderer renderer);
Expand Down Expand Up @@ -224,7 +224,7 @@ protected override ComponentState CreateComponentState(int componentId, ICompone

object CreateComponentInstance()
{
var constructorInfo = componentActivatorCache.GetOrAdd(renderedComponentType, type
var constructorInfo = ComponentActivatorCache.GetOrAdd(renderedComponentType, type
=> type.GetConstructor(
[
typeof(BunitRenderer),
Expand Down Expand Up @@ -349,65 +349,50 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
for (var i = 0; i < renderBatch.UpdatedComponents.Count; i++)
{
var diff = renderBatch.UpdatedComponents.Array[i];
var componentState = GetComponentState(diff.ComponentId);
var renderedComponent = (IRenderedComponent)componentState;
var componentState = GetRenderedComponent(diff.ComponentId);
componentState.RenderCount++;

if (returnedRenderedComponentIds.Contains(diff.ComponentId))
{
renderedComponent.UpdateState(hasRendered: true, isMarkupGenerationRequired: diff.Edits.Count > 0);
}
else
componentState.IsDirty = true;

if (componentState.Root is not null)
{
renderedComponent.UpdateState(hasRendered: true, false);
componentState.Root.IsDirty = true;
}

UpdateParents(diff.Edits.Count > 0, componentState, in renderBatch);
}

return Task.CompletedTask;

void UpdateParents(bool hasChanges, ComponentState componentState, in RenderBatch renderBatch)
foreach (var item in rootComponents)
{
var parent = componentState.ParentComponentState;
if (parent is null)
var root = GetRenderedComponent(item);
if (root.IsDirty)
{
return;
}

if (!IsParentComponentAlreadyUpdated(parent.ComponentId, in renderBatch))
{
if (returnedRenderedComponentIds.Contains(parent.ComponentId))
{
((IRenderedComponent)parent).UpdateState(hasRendered: true, isMarkupGenerationRequired: hasChanges);
}
else
{
((IRenderedComponent)parent).UpdateState(hasRendered: true, false);
}

UpdateParents(hasChanges, parent, in renderBatch);
root.UpdateMarkup();
}
}

static bool IsParentComponentAlreadyUpdated(int componentId, in RenderBatch renderBatch)
foreach (var renderedComponentId in returnedRenderedComponentIds)
{
for (var i = 0; i < renderBatch.UpdatedComponents.Count; i++)
var renderedComponent = GetRenderedComponent(renderedComponentId);
if (renderedComponent.IsDirty)
{
var diff = renderBatch.UpdatedComponents.Array[i];
if (diff.ComponentId == componentId)
{
return diff.Edits.Count > 0;
}
renderedComponent.UpdateMarkup();
}

return false;
}

return Task.CompletedTask;
}

/// <inheritdoc/>
internal new ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId)
=> base.GetCurrentRenderTreeFrames(componentId);

/// <inheritdoc/>
internal IRenderedComponent GetRenderedComponent(int componentId)
=> (IRenderedComponent)GetComponentState(componentId);

/// <inheritdoc/>
internal IRenderedComponent GetRenderedComponent(IComponent component)
=> (IRenderedComponent)GetComponentState(component);

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
Expand Down Expand Up @@ -487,7 +472,7 @@ private List<IRenderedComponent<TComponent>> FindComponents<TComponent>(IRendere
FindComponentsInRenderTree(parentComponent.ComponentId);
foreach (var rc in result)
{
((IRenderedComponent)rc).UpdateState(hasRendered: false, isMarkupGenerationRequired: true);
((IRenderedComponent)rc).UpdateMarkup();
}
}

Expand Down
12 changes: 10 additions & 2 deletions src/bunit/Rendering/IRenderedComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ internal interface IRenderedComponent : IDisposable
int ComponentId { get; }

/// <summary>
/// Called by the owning <see cref="BunitRenderer"/> when it finishes a render.
/// Gets the total number times the fragment has been through its render life-cycle.
/// </summary>
void UpdateState(bool hasRendered, bool isMarkupGenerationRequired);
int RenderCount { get; set; }

void UpdateMarkup();

void SetMarkupIndices(int start, int end);

bool IsDirty { get; set; }

IRenderedComponent? Root { get; }
}

/// <summary>
Expand Down
27 changes: 21 additions & 6 deletions src/bunit/Rendering/Internal/Htmlizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,35 @@ public static string ToBlazorAttribute(string attributeName)
public static string GetHtml(int componentId, BunitRenderer renderer)
{
var context = new HtmlRenderingContext(renderer);
var componentState = renderer.GetRenderedComponent(componentId);
var frames = context.GetRenderTreeFrames(componentId);
var newPosition = RenderFrames(context, frames, 0, frames.Count);

componentState.SetMarkupIndices(0, context.Result.Length);

Debug.Assert(
newPosition == frames.Count,
$"frames.Length = {frames.Count}. newPosition = {newPosition}"
);

return context.Result.ToString();
}

private static RenderTreeFrame RenderComponent(HtmlRenderingContext context, in RenderTreeFrame frame)
{
var startIndex = context.Result.Length;
var frames = context.GetRenderTreeFrames(frame.ComponentId);
RenderFrames(context, frames, 0, frames.Count);
var endIndex = context.Result.Length;
context.GetRenderedComponent(frame.ComponentId).SetMarkupIndices(startIndex, endIndex);
return frame;
}

private static int RenderFrames(
HtmlRenderingContext context,
ArrayRange<RenderTreeFrame> frames,
int position,
int maxElements
)
int maxElements)
{
var nextPosition = position;
var endPosition = position + maxElements;
Expand Down Expand Up @@ -130,12 +144,10 @@ int position
private static int RenderChildComponent(
HtmlRenderingContext context,
ArrayRange<RenderTreeFrame> frames,
int position
)
int position)
{
var frame = frames.Array[position];
var childFrames = context.GetRenderTreeFrames(frame.ComponentId);
RenderFrames(context, childFrames, 0, childFrames.Count);
frame = RenderComponent(context, in frame);
return position + frame.ComponentSubtreeLength;
}

Expand Down Expand Up @@ -405,6 +417,9 @@ public HtmlRenderingContext(BunitRenderer renderer)
public ArrayRange<RenderTreeFrame> GetRenderTreeFrames(int componentId)
=> renderer.GetCurrentRenderTreeFrames(componentId);

public IRenderedComponent GetRenderedComponent(int componentId)
=> renderer.GetRenderedComponent(componentId);

public StringBuilder Result { get; } = new();

public string? ClosestSelectValueAsString { get; set; }
Expand Down
130 changes: 76 additions & 54 deletions src/bunit/Rendering/RenderedComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ internal sealed class RenderedComponent<TComponent> : ComponentState, IRenderedC

[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Owned by BunitServiceProvider, disposed by it.")]
private readonly BunitHtmlParser htmlParser;

private int renderCount;
private string markup = string.Empty;
private int markupStartIndex;
private int markupEndIndex;
private INodeList? latestRenderNodes;

public bool IsDirty { get; set; }

/// <summary>
/// Gets the component under test.
/// </summary>
Expand Down Expand Up @@ -53,10 +57,22 @@ public string Markup
}
}

/// <summary>
/// Adds or removes an event handler that will be triggered after
/// each render of this <see cref="RenderedComponent{T}"/>.
/// </summary>
public event EventHandler? OnAfterRender;

/// <summary>
/// An event that is raised after the markup of the
/// <see cref="RenderedComponent{T}"/> is updated.
/// </summary>
public event EventHandler? OnMarkupUpdated;

/// <summary>
/// Gets the total number times the fragment has been through its render life-cycle.
/// </summary>
public int RenderCount { get; private set; }
public int RenderCount => renderCount;

/// <summary>
/// Gets the AngleSharp <see cref="INodeList"/> based
Expand All @@ -77,6 +93,10 @@ public INodeList Nodes
/// </summary>
public IServiceProvider Services { get; }

int IRenderedComponent.RenderCount { get => renderCount; set { renderCount = value; } }

public IRenderedComponent? Root { get; }

public RenderedComponent(
BunitRenderer renderer,
int componentId,
Expand All @@ -89,57 +109,76 @@ public RenderedComponent(
this.renderer = renderer;
this.instance = (TComponent)instance;
htmlParser = Services.GetRequiredService<BunitHtmlParser>();
var parentRenderedComponent = parentComponentState as IRenderedComponent;
Root = parentRenderedComponent?.Root ?? parentRenderedComponent;
}

/// <summary>
/// Adds or removes an event handler that will be triggered after each render of this <see cref="RenderedComponent{T}"/>.
/// </summary>
public event EventHandler? OnAfterRender;
/// <inheritdoc/>
public void Dispose()
{
if (IsDisposed)
return;

/// <summary>
/// An event that is raised after the markup of the <see cref="RenderedComponent{T}"/> is updated.
/// </summary>
public event EventHandler? OnMarkupUpdated;
if (Root is not null)
Root.IsDirty = true;

IsDisposed = true;
markup = string.Empty;
OnAfterRender = null;
OnMarkupUpdated = null;
}

/// <inheritdoc/>
public override ValueTask DisposeAsync()
{
Dispose();
return base.DisposeAsync();
}

public void SetMarkupIndices(int start, int end)
{
markupStartIndex = start;
markupEndIndex = end;
IsDirty = true;
}

/// <summary>
/// Called by the owning <see cref="BunitRenderer"/> when it finishes a render.
/// </summary>
public void UpdateState(bool hasRendered, bool isMarkupGenerationRequired)
public void UpdateMarkup()
{
if (IsDisposed)
return;

if (hasRendered)
if (Root is RenderedComponent<BunitRootComponent> root)
{
RenderCount++;
var newMarkup = root.markup[markupStartIndex..markupEndIndex];
if (markup != newMarkup)
{
Volatile.Write(ref markup, newMarkup);
latestRenderNodes = null;
OnMarkupUpdated?.Invoke(this, EventArgs.Empty);
}
else
{
// no change
}
}

if (isMarkupGenerationRequired)
else
{
UpdateMarkup();
var newMarkup = Htmlizer.GetHtml(ComponentId, renderer);

// Volatile write is necessary to ensure the updated markup
// is available across CPU cores. Without it, the pointer to the
// markup string can be stored in a CPUs register and not
// get updated when another CPU changes the string.
Volatile.Write(ref markup, newMarkup);
latestRenderNodes = null;
OnMarkupUpdated?.Invoke(this, EventArgs.Empty);
}

// The order here is important, since consumers of the events
// expect that markup has indeed changed when OnAfterRender is invoked
// (assuming there are markup changes)
if (hasRendered)
OnAfterRender?.Invoke(this, EventArgs.Empty);
}

/// <summary>
/// Updates the markup of the rendered fragment.
/// </summary>
private void UpdateMarkup()
{
latestRenderNodes = null;
var newMarkup = Htmlizer.GetHtml(ComponentId, renderer);

// Volatile write is necessary to ensure the updated markup
// is available across CPU cores. Without it, the pointer to the
// markup string can be stored in a CPUs register and not
// get updated when another CPU changes the string.
Volatile.Write(ref markup, newMarkup);
IsDirty = false;
OnAfterRender?.Invoke(this, EventArgs.Empty);
}

/// <summary>
Expand All @@ -151,22 +190,5 @@ private void EnsureComponentNotDisposed()
if (IsDisposed)
throw new ComponentDisposedException(ComponentId);
}

/// <inheritdoc/>
public void Dispose()
{
if (IsDisposed)
return;

IsDisposed = true;
markup = string.Empty;
OnAfterRender = null;
OnMarkupUpdated = null;
}

public override ValueTask DisposeAsync()
{
Dispose();
return base.DisposeAsync();
}
}

Loading

0 comments on commit b17e9d4

Please sign in to comment.