Skip to content

Commit b17e9d4

Browse files
committed
refactor - wip - only generate markup for root components
1 parent 04aa5fe commit b17e9d4

File tree

7 files changed

+146
-129
lines changed

7 files changed

+146
-129
lines changed

Diff for: src/bunit/Rendering/BunitRenderer.cs

+27-42
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public sealed class BunitRenderer : Renderer
1414
{
1515
private readonly BunitServiceProvider services;
1616
private readonly List<Task> disposalTasks = [];
17-
private static readonly ConcurrentDictionary<Type, ConstructorInfo> componentActivatorCache = new();
17+
private static readonly ConcurrentDictionary<Type, ConstructorInfo> ComponentActivatorCache = new();
1818

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

225225
object CreateComponentInstance()
226226
{
227-
var constructorInfo = componentActivatorCache.GetOrAdd(renderedComponentType, type
227+
var constructorInfo = ComponentActivatorCache.GetOrAdd(renderedComponentType, type
228228
=> type.GetConstructor(
229229
[
230230
typeof(BunitRenderer),
@@ -349,65 +349,50 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
349349
for (var i = 0; i < renderBatch.UpdatedComponents.Count; i++)
350350
{
351351
var diff = renderBatch.UpdatedComponents.Array[i];
352-
var componentState = GetComponentState(diff.ComponentId);
353-
var renderedComponent = (IRenderedComponent)componentState;
352+
var componentState = GetRenderedComponent(diff.ComponentId);
353+
componentState.RenderCount++;
354354

355-
if (returnedRenderedComponentIds.Contains(diff.ComponentId))
356-
{
357-
renderedComponent.UpdateState(hasRendered: true, isMarkupGenerationRequired: diff.Edits.Count > 0);
358-
}
359-
else
355+
componentState.IsDirty = true;
356+
357+
if (componentState.Root is not null)
360358
{
361-
renderedComponent.UpdateState(hasRendered: true, false);
359+
componentState.Root.IsDirty = true;
362360
}
363-
364-
UpdateParents(diff.Edits.Count > 0, componentState, in renderBatch);
365361
}
366362

367-
return Task.CompletedTask;
368-
369-
void UpdateParents(bool hasChanges, ComponentState componentState, in RenderBatch renderBatch)
363+
foreach (var item in rootComponents)
370364
{
371-
var parent = componentState.ParentComponentState;
372-
if (parent is null)
365+
var root = GetRenderedComponent(item);
366+
if (root.IsDirty)
373367
{
374-
return;
375-
}
376-
377-
if (!IsParentComponentAlreadyUpdated(parent.ComponentId, in renderBatch))
378-
{
379-
if (returnedRenderedComponentIds.Contains(parent.ComponentId))
380-
{
381-
((IRenderedComponent)parent).UpdateState(hasRendered: true, isMarkupGenerationRequired: hasChanges);
382-
}
383-
else
384-
{
385-
((IRenderedComponent)parent).UpdateState(hasRendered: true, false);
386-
}
387-
388-
UpdateParents(hasChanges, parent, in renderBatch);
368+
root.UpdateMarkup();
389369
}
390370
}
391371

392-
static bool IsParentComponentAlreadyUpdated(int componentId, in RenderBatch renderBatch)
372+
foreach (var renderedComponentId in returnedRenderedComponentIds)
393373
{
394-
for (var i = 0; i < renderBatch.UpdatedComponents.Count; i++)
374+
var renderedComponent = GetRenderedComponent(renderedComponentId);
375+
if (renderedComponent.IsDirty)
395376
{
396-
var diff = renderBatch.UpdatedComponents.Array[i];
397-
if (diff.ComponentId == componentId)
398-
{
399-
return diff.Edits.Count > 0;
400-
}
377+
renderedComponent.UpdateMarkup();
401378
}
402-
403-
return false;
404379
}
380+
381+
return Task.CompletedTask;
405382
}
406383

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

388+
/// <inheritdoc/>
389+
internal IRenderedComponent GetRenderedComponent(int componentId)
390+
=> (IRenderedComponent)GetComponentState(componentId);
391+
392+
/// <inheritdoc/>
393+
internal IRenderedComponent GetRenderedComponent(IComponent component)
394+
=> (IRenderedComponent)GetComponentState(component);
395+
411396
/// <inheritdoc/>
412397
protected override void Dispose(bool disposing)
413398
{
@@ -487,7 +472,7 @@ private List<IRenderedComponent<TComponent>> FindComponents<TComponent>(IRendere
487472
FindComponentsInRenderTree(parentComponent.ComponentId);
488473
foreach (var rc in result)
489474
{
490-
((IRenderedComponent)rc).UpdateState(hasRendered: false, isMarkupGenerationRequired: true);
475+
((IRenderedComponent)rc).UpdateMarkup();
491476
}
492477
}
493478

Diff for: src/bunit/Rendering/IRenderedComponent.cs

+10-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,17 @@ internal interface IRenderedComponent : IDisposable
1111
int ComponentId { get; }
1212

1313
/// <summary>
14-
/// Called by the owning <see cref="BunitRenderer"/> when it finishes a render.
14+
/// Gets the total number times the fragment has been through its render life-cycle.
1515
/// </summary>
16-
void UpdateState(bool hasRendered, bool isMarkupGenerationRequired);
16+
int RenderCount { get; set; }
17+
18+
void UpdateMarkup();
19+
20+
void SetMarkupIndices(int start, int end);
21+
22+
bool IsDirty { get; set; }
23+
24+
IRenderedComponent? Root { get; }
1725
}
1826

1927
/// <summary>

Diff for: src/bunit/Rendering/Internal/Htmlizer.cs

+21-6
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,35 @@ public static string ToBlazorAttribute(string attributeName)
5555
public static string GetHtml(int componentId, BunitRenderer renderer)
5656
{
5757
var context = new HtmlRenderingContext(renderer);
58+
var componentState = renderer.GetRenderedComponent(componentId);
5859
var frames = context.GetRenderTreeFrames(componentId);
5960
var newPosition = RenderFrames(context, frames, 0, frames.Count);
61+
62+
componentState.SetMarkupIndices(0, context.Result.Length);
63+
6064
Debug.Assert(
6165
newPosition == frames.Count,
6266
$"frames.Length = {frames.Count}. newPosition = {newPosition}"
6367
);
68+
6469
return context.Result.ToString();
6570
}
6671

72+
private static RenderTreeFrame RenderComponent(HtmlRenderingContext context, in RenderTreeFrame frame)
73+
{
74+
var startIndex = context.Result.Length;
75+
var frames = context.GetRenderTreeFrames(frame.ComponentId);
76+
RenderFrames(context, frames, 0, frames.Count);
77+
var endIndex = context.Result.Length;
78+
context.GetRenderedComponent(frame.ComponentId).SetMarkupIndices(startIndex, endIndex);
79+
return frame;
80+
}
81+
6782
private static int RenderFrames(
6883
HtmlRenderingContext context,
6984
ArrayRange<RenderTreeFrame> frames,
7085
int position,
71-
int maxElements
72-
)
86+
int maxElements)
7387
{
7488
var nextPosition = position;
7589
var endPosition = position + maxElements;
@@ -130,12 +144,10 @@ int position
130144
private static int RenderChildComponent(
131145
HtmlRenderingContext context,
132146
ArrayRange<RenderTreeFrame> frames,
133-
int position
134-
)
147+
int position)
135148
{
136149
var frame = frames.Array[position];
137-
var childFrames = context.GetRenderTreeFrames(frame.ComponentId);
138-
RenderFrames(context, childFrames, 0, childFrames.Count);
150+
frame = RenderComponent(context, in frame);
139151
return position + frame.ComponentSubtreeLength;
140152
}
141153

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

420+
public IRenderedComponent GetRenderedComponent(int componentId)
421+
=> renderer.GetRenderedComponent(componentId);
422+
408423
public StringBuilder Result { get; } = new();
409424

410425
public string? ClosestSelectValueAsString { get; set; }

Diff for: src/bunit/Rendering/RenderedComponent.cs

+76-54
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ internal sealed class RenderedComponent<TComponent> : ComponentState, IRenderedC
1616

1717
[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Owned by BunitServiceProvider, disposed by it.")]
1818
private readonly BunitHtmlParser htmlParser;
19-
19+
private int renderCount;
2020
private string markup = string.Empty;
21+
private int markupStartIndex;
22+
private int markupEndIndex;
2123
private INodeList? latestRenderNodes;
2224

25+
public bool IsDirty { get; set; }
26+
2327
/// <summary>
2428
/// Gets the component under test.
2529
/// </summary>
@@ -53,10 +57,22 @@ public string Markup
5357
}
5458
}
5559

60+
/// <summary>
61+
/// Adds or removes an event handler that will be triggered after
62+
/// each render of this <see cref="RenderedComponent{T}"/>.
63+
/// </summary>
64+
public event EventHandler? OnAfterRender;
65+
66+
/// <summary>
67+
/// An event that is raised after the markup of the
68+
/// <see cref="RenderedComponent{T}"/> is updated.
69+
/// </summary>
70+
public event EventHandler? OnMarkupUpdated;
71+
5672
/// <summary>
5773
/// Gets the total number times the fragment has been through its render life-cycle.
5874
/// </summary>
59-
public int RenderCount { get; private set; }
75+
public int RenderCount => renderCount;
6076

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

96+
int IRenderedComponent.RenderCount { get => renderCount; set { renderCount = value; } }
97+
98+
public IRenderedComponent? Root { get; }
99+
80100
public RenderedComponent(
81101
BunitRenderer renderer,
82102
int componentId,
@@ -89,57 +109,76 @@ public RenderedComponent(
89109
this.renderer = renderer;
90110
this.instance = (TComponent)instance;
91111
htmlParser = Services.GetRequiredService<BunitHtmlParser>();
112+
var parentRenderedComponent = parentComponentState as IRenderedComponent;
113+
Root = parentRenderedComponent?.Root ?? parentRenderedComponent;
92114
}
93115

94-
/// <summary>
95-
/// Adds or removes an event handler that will be triggered after each render of this <see cref="RenderedComponent{T}"/>.
96-
/// </summary>
97-
public event EventHandler? OnAfterRender;
116+
/// <inheritdoc/>
117+
public void Dispose()
118+
{
119+
if (IsDisposed)
120+
return;
98121

99-
/// <summary>
100-
/// An event that is raised after the markup of the <see cref="RenderedComponent{T}"/> is updated.
101-
/// </summary>
102-
public event EventHandler? OnMarkupUpdated;
122+
if (Root is not null)
123+
Root.IsDirty = true;
124+
125+
IsDisposed = true;
126+
markup = string.Empty;
127+
OnAfterRender = null;
128+
OnMarkupUpdated = null;
129+
}
130+
131+
/// <inheritdoc/>
132+
public override ValueTask DisposeAsync()
133+
{
134+
Dispose();
135+
return base.DisposeAsync();
136+
}
137+
138+
public void SetMarkupIndices(int start, int end)
139+
{
140+
markupStartIndex = start;
141+
markupEndIndex = end;
142+
IsDirty = true;
143+
}
103144

104145
/// <summary>
105146
/// Called by the owning <see cref="BunitRenderer"/> when it finishes a render.
106147
/// </summary>
107-
public void UpdateState(bool hasRendered, bool isMarkupGenerationRequired)
148+
public void UpdateMarkup()
108149
{
109150
if (IsDisposed)
110151
return;
111152

112-
if (hasRendered)
153+
if (Root is RenderedComponent<BunitRootComponent> root)
113154
{
114-
RenderCount++;
155+
var newMarkup = root.markup[markupStartIndex..markupEndIndex];
156+
if (markup != newMarkup)
157+
{
158+
Volatile.Write(ref markup, newMarkup);
159+
latestRenderNodes = null;
160+
OnMarkupUpdated?.Invoke(this, EventArgs.Empty);
161+
}
162+
else
163+
{
164+
// no change
165+
}
115166
}
116-
117-
if (isMarkupGenerationRequired)
167+
else
118168
{
119-
UpdateMarkup();
169+
var newMarkup = Htmlizer.GetHtml(ComponentId, renderer);
170+
171+
// Volatile write is necessary to ensure the updated markup
172+
// is available across CPU cores. Without it, the pointer to the
173+
// markup string can be stored in a CPUs register and not
174+
// get updated when another CPU changes the string.
175+
Volatile.Write(ref markup, newMarkup);
176+
latestRenderNodes = null;
120177
OnMarkupUpdated?.Invoke(this, EventArgs.Empty);
121178
}
122179

123-
// The order here is important, since consumers of the events
124-
// expect that markup has indeed changed when OnAfterRender is invoked
125-
// (assuming there are markup changes)
126-
if (hasRendered)
127-
OnAfterRender?.Invoke(this, EventArgs.Empty);
128-
}
129-
130-
/// <summary>
131-
/// Updates the markup of the rendered fragment.
132-
/// </summary>
133-
private void UpdateMarkup()
134-
{
135-
latestRenderNodes = null;
136-
var newMarkup = Htmlizer.GetHtml(ComponentId, renderer);
137-
138-
// Volatile write is necessary to ensure the updated markup
139-
// is available across CPU cores. Without it, the pointer to the
140-
// markup string can be stored in a CPUs register and not
141-
// get updated when another CPU changes the string.
142-
Volatile.Write(ref markup, newMarkup);
180+
IsDirty = false;
181+
OnAfterRender?.Invoke(this, EventArgs.Empty);
143182
}
144183

145184
/// <summary>
@@ -151,22 +190,5 @@ private void EnsureComponentNotDisposed()
151190
if (IsDisposed)
152191
throw new ComponentDisposedException(ComponentId);
153192
}
154-
155-
/// <inheritdoc/>
156-
public void Dispose()
157-
{
158-
if (IsDisposed)
159-
return;
160-
161-
IsDisposed = true;
162-
markup = string.Empty;
163-
OnAfterRender = null;
164-
OnMarkupUpdated = null;
165-
}
166-
167-
public override ValueTask DisposeAsync()
168-
{
169-
Dispose();
170-
return base.DisposeAsync();
171-
}
172193
}
194+

0 commit comments

Comments
 (0)