Skip to content

Commit 3cbe02f

Browse files
Blazor WebAssembly internal profiling infrastructure (#23510)
1 parent 1688b5a commit 3cbe02f

18 files changed

+389
-11
lines changed

src/Components/Components/src/Microsoft.AspNetCore.Components.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFrameworks>netstandard2.0;$(DefaultNetCoreTargetFramework)</TargetFrameworks>
@@ -11,6 +11,7 @@
1111

1212
<ItemGroup>
1313
<Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="RenderTree" />
14+
<Compile Include="$(ComponentsSharedSourceRoot)src\WebAssemblyJSInteropInternalCalls.cs" />
1415
</ItemGroup>
1516

1617
<ItemGroup>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Runtime.CompilerServices;
5+
using System.Runtime.InteropServices;
6+
7+
namespace Microsoft.AspNetCore.Components.Profiling
8+
{
9+
internal abstract class ComponentsProfiling
10+
{
11+
// For now, this is only intended for use on Blazor WebAssembly, and will have no effect
12+
// when running on Blazor Server. The reason for having the ComponentsProfiling abstraction
13+
// is so that if we later have two different implementations (one for WebAssembly, one for
14+
// Server), the execution characteristics of calling Start/End will be unchanged and historical
15+
// perf data will still be comparable to newer data.
16+
public static readonly ComponentsProfiling Instance;
17+
18+
static ComponentsProfiling()
19+
{
20+
Instance = RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"))
21+
? new WebAssemblyComponentsProfiling()
22+
: (ComponentsProfiling)new NoOpComponentsProfiling();
23+
}
24+
25+
public abstract void Start([CallerMemberName] string? name = null);
26+
public abstract void End([CallerMemberName] string? name = null);
27+
}
28+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Components.Profiling
5+
{
6+
internal class NoOpComponentsProfiling : ComponentsProfiling
7+
{
8+
public override void Start(string? name)
9+
{
10+
}
11+
12+
public override void End(string? name)
13+
{
14+
}
15+
}
16+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using WebAssembly.JSInterop;
5+
6+
namespace Microsoft.AspNetCore.Components.Profiling
7+
{
8+
// Later on, we will likely want to move this into the WebAssembly package. However it needs to
9+
// be inlined into the Components package directly until we're ready to make the underlying
10+
// ComponentsProfile abstraction into a public API. It's possible that this API will never become
11+
// public, or that it will be replaced by something more standard for .NET, if it's possible to
12+
// make that work performantly on WebAssembly.
13+
14+
internal class WebAssemblyComponentsProfiling : ComponentsProfiling
15+
{
16+
static bool IsCapturing = false;
17+
18+
public static void SetCapturing(bool isCapturing)
19+
{
20+
IsCapturing = isCapturing;
21+
}
22+
23+
public override void Start(string? name)
24+
{
25+
if (IsCapturing)
26+
{
27+
InternalCalls.InvokeJSUnmarshalled<string, object, object, object>(
28+
out _, "_blazorProfileStart", name, null, null);
29+
}
30+
}
31+
32+
public override void End(string? name)
33+
{
34+
if (IsCapturing)
35+
{
36+
InternalCalls.InvokeJSUnmarshalled<string, object, object, object>(
37+
out _, "_blazorProfileEnd", name, null, null);
38+
}
39+
}
40+
}
41+
}

src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
using System;
77
using System.Collections.Generic;
8+
using System.Diagnostics;
9+
using System.Runtime.CompilerServices;
10+
using Microsoft.AspNetCore.Components.Profiling;
811
using Microsoft.AspNetCore.Components.Rendering;
912

1013
namespace Microsoft.AspNetCore.Components.RenderTree
@@ -25,14 +28,17 @@ public static RenderTreeDiff ComputeDiff(
2528
ArrayRange<RenderTreeFrame> oldTree,
2629
ArrayRange<RenderTreeFrame> newTree)
2730
{
31+
ComponentsProfiling.Instance.Start();
2832
var editsBuffer = batchBuilder.EditsBuffer;
2933
var editsBufferStartLength = editsBuffer.Count;
3034

3135
var diffContext = new DiffContext(renderer, batchBuilder, componentId, oldTree.Array, newTree.Array);
3236
AppendDiffEntriesForRange(ref diffContext, 0, oldTree.Count, 0, newTree.Count);
3337

3438
var editsSegment = editsBuffer.ToSegment(editsBufferStartLength, editsBuffer.Count);
35-
return new RenderTreeDiff(componentId, editsSegment);
39+
var result = new RenderTreeDiff(componentId, editsSegment);
40+
ComponentsProfiling.Instance.End();
41+
return result;
3642
}
3743

3844
public static void DisposeFrames(RenderBatchBuilder batchBuilder, ArrayRange<RenderTreeFrame> frames)
@@ -43,6 +49,7 @@ private static void AppendDiffEntriesForRange(
4349
int oldStartIndex, int oldEndIndexExcl,
4450
int newStartIndex, int newEndIndexExcl)
4551
{
52+
ProfilingStart();
4653
// This is deliberately a very large method. Parts of it could be factored out
4754
// into other private methods, but doing so comes at a consequential perf cost,
4855
// because it involves so much parameter passing. You can think of the code here
@@ -293,10 +300,12 @@ private static void AppendDiffEntriesForRange(
293300
diffContext.KeyedItemInfoDictionaryPool.Return(keyedItemInfos);
294301
}
295302
}
303+
ProfilingEnd();
296304
}
297305

298306
private static Dictionary<object, KeyedItemInfo> BuildKeyToInfoLookup(DiffContext diffContext, int oldStartIndex, int oldEndIndexExcl, int newStartIndex, int newEndIndexExcl)
299307
{
308+
ProfilingStart();
300309
var result = diffContext.KeyedItemInfoDictionaryPool.Get();
301310
var oldTree = diffContext.OldTree;
302311
var newTree = diffContext.NewTree;
@@ -342,6 +351,7 @@ private static Dictionary<object, KeyedItemInfo> BuildKeyToInfoLookup(DiffContex
342351
newStartIndex = NextSiblingIndex(frame, newStartIndex);
343352
}
344353

354+
ProfilingEnd();
345355
return result;
346356
}
347357

@@ -374,6 +384,7 @@ private static void AppendAttributeDiffEntriesForRange(
374384
int oldStartIndex, int oldEndIndexExcl,
375385
int newStartIndex, int newEndIndexExcl)
376386
{
387+
ProfilingStart();
377388
// The overhead of the dictionary used by AppendAttributeDiffEntriesForRangeSlow is
378389
// significant, so we want to try and do a merge-join if possible, but fall back to
379390
// a hash-join if not. We'll do a merge join until we hit a case we can't handle and
@@ -422,6 +433,7 @@ private static void AppendAttributeDiffEntriesForRange(
422433
ref diffContext,
423434
oldStartIndex, oldEndIndexExcl,
424435
newStartIndex, newEndIndexExcl);
436+
ProfilingEnd();
425437
return;
426438
}
427439

@@ -447,16 +459,20 @@ private static void AppendAttributeDiffEntriesForRange(
447459
ref diffContext,
448460
oldStartIndex, oldEndIndexExcl,
449461
newStartIndex, newEndIndexExcl);
462+
ProfilingEnd();
450463
return;
451464
}
452465
}
466+
467+
ProfilingEnd();
453468
}
454469

455470
private static void AppendAttributeDiffEntriesForRangeSlow(
456471
ref DiffContext diffContext,
457472
int oldStartIndex, int oldEndIndexExcl,
458473
int newStartIndex, int newEndIndexExcl)
459474
{
475+
ProfilingStart();
460476
var oldTree = diffContext.OldTree;
461477
var newTree = diffContext.NewTree;
462478

@@ -495,13 +511,15 @@ private static void AppendAttributeDiffEntriesForRangeSlow(
495511

496512
// We should have processed any additions at this point. Reset for the next batch.
497513
diffContext.AttributeDiffSet.Clear();
514+
ProfilingEnd();
498515
}
499516

500517
private static void UpdateRetainedChildComponent(
501518
ref DiffContext diffContext,
502519
int oldComponentIndex,
503520
int newComponentIndex)
504521
{
522+
ProfilingStart();
505523
var oldTree = diffContext.OldTree;
506524
var newTree = diffContext.NewTree;
507525
ref var oldComponentFrame = ref oldTree[oldComponentIndex];
@@ -528,6 +546,8 @@ private static void UpdateRetainedChildComponent(
528546
{
529547
componentState.SetDirectParameters(newParameters);
530548
}
549+
550+
ProfilingEnd();
531551
}
532552

533553
private static int NextSiblingIndex(in RenderTreeFrame frame, int frameIndex)
@@ -550,6 +570,7 @@ private static void AppendDiffEntriesForFramesWithSameSequence(
550570
int oldFrameIndex,
551571
int newFrameIndex)
552572
{
573+
ProfilingStart();
553574
var oldTree = diffContext.OldTree;
554575
var newTree = diffContext.NewTree;
555576
ref var oldFrame = ref oldTree[oldFrameIndex];
@@ -562,6 +583,7 @@ private static void AppendDiffEntriesForFramesWithSameSequence(
562583
{
563584
InsertNewFrame(ref diffContext, newFrameIndex);
564585
RemoveOldFrame(ref diffContext, oldFrameIndex);
586+
ProfilingEnd();
565587
return;
566588
}
567589

@@ -687,6 +709,8 @@ private static void AppendDiffEntriesForFramesWithSameSequence(
687709
default:
688710
throw new NotImplementedException($"Encountered unsupported frame type during diffing: {newTree[newFrameIndex].FrameType}");
689711
}
712+
713+
ProfilingEnd();
690714
}
691715

692716
// This should only be called for attributes that have the same name. This is an
@@ -696,6 +720,7 @@ private static void AppendDiffEntriesForAttributeFrame(
696720
int oldFrameIndex,
697721
int newFrameIndex)
698722
{
723+
ProfilingStart();
699724
var oldTree = diffContext.OldTree;
700725
var newTree = diffContext.NewTree;
701726
ref var oldFrame = ref oldTree[oldFrameIndex];
@@ -724,10 +749,13 @@ private static void AppendDiffEntriesForAttributeFrame(
724749
// since it was unchanged.
725750
newFrame = oldFrame;
726751
}
752+
753+
ProfilingEnd();
727754
}
728755

729756
private static void InsertNewFrame(ref DiffContext diffContext, int newFrameIndex)
730757
{
758+
ProfilingStart();
731759
var newTree = diffContext.NewTree;
732760
ref var newFrame = ref newTree[newFrameIndex];
733761
switch (newFrame.FrameType)
@@ -780,10 +808,12 @@ private static void InsertNewFrame(ref DiffContext diffContext, int newFrameInde
780808
default:
781809
throw new NotImplementedException($"Unexpected frame type during {nameof(InsertNewFrame)}: {newFrame.FrameType}");
782810
}
811+
ProfilingEnd();
783812
}
784813

785814
private static void RemoveOldFrame(ref DiffContext diffContext, int oldFrameIndex)
786815
{
816+
ProfilingStart();
787817
var oldTree = diffContext.OldTree;
788818
ref var oldFrame = ref oldTree[oldFrameIndex];
789819
switch (oldFrame.FrameType)
@@ -825,6 +855,7 @@ private static void RemoveOldFrame(ref DiffContext diffContext, int oldFrameInde
825855
default:
826856
throw new NotImplementedException($"Unexpected frame type during {nameof(RemoveOldFrame)}: {oldFrame.FrameType}");
827857
}
858+
ProfilingEnd();
828859
}
829860

830861
private static int GetAttributesEndIndexExclusive(RenderTreeFrame[] tree, int rootIndex)
@@ -858,6 +889,7 @@ private static void AppendStepOut(ref DiffContext diffContext)
858889

859890
private static void InitializeNewSubtree(ref DiffContext diffContext, int frameIndex)
860891
{
892+
ProfilingStart();
861893
var frames = diffContext.NewTree;
862894
var endIndexExcl = frameIndex + frames[frameIndex].ElementSubtreeLength;
863895
for (var i = frameIndex; i < endIndexExcl; i++)
@@ -879,10 +911,12 @@ private static void InitializeNewSubtree(ref DiffContext diffContext, int frameI
879911
break;
880912
}
881913
}
914+
ProfilingEnd();
882915
}
883916

884917
private static void InitializeNewComponentFrame(ref DiffContext diffContext, int frameIndex)
885918
{
919+
ProfilingStart();
886920
var frames = diffContext.NewTree;
887921
ref var frame = ref frames[frameIndex];
888922

@@ -899,6 +933,7 @@ private static void InitializeNewComponentFrame(ref DiffContext diffContext, int
899933
var initialParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
900934
var initialParameters = new ParameterView(initialParametersLifetime, frames, frameIndex);
901935
childComponentState.SetDirectParameters(initialParameters);
936+
ProfilingEnd();
902937
}
903938

904939
private static void InitializeNewAttributeFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame)
@@ -943,6 +978,7 @@ private static void InitializeNewComponentReferenceCaptureFrame(ref DiffContext
943978

944979
private static void DisposeFramesInRange(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int startIndex, int endIndexExcl)
945980
{
981+
ProfilingStart();
946982
for (var i = startIndex; i < endIndexExcl; i++)
947983
{
948984
ref var frame = ref frames[i];
@@ -955,6 +991,7 @@ private static void DisposeFramesInRange(RenderBatchBuilder batchBuilder, Render
955991
batchBuilder.DisposedEventHandlerIds.Append(frame.AttributeEventHandlerId);
956992
}
957993
}
994+
ProfilingEnd();
958995
}
959996

960997
/// <summary>
@@ -996,5 +1033,18 @@ public DiffContext(
9961033
SiblingIndex = 0;
9971034
}
9981035
}
1036+
1037+
// Having too many calls to ComponentsProfiling.Instance.Start/End has a measurable perf impact
1038+
// even when capturing is disabled. So, to enable detailed profiling for this class, define the
1039+
// Profile_RenderTreeDiffBuilder compiler symbol, otherwise the calls are compiled out entirely.
1040+
// Enabling detailed profiling adds about 5% to rendering benchmark times.
1041+
1042+
[Conditional("Profile_RenderTreeDiffBuilder")]
1043+
private static void ProfilingStart([CallerMemberName] string? name = null)
1044+
=> ComponentsProfiling.Instance.Start(name);
1045+
1046+
[Conditional("Profile_RenderTreeDiffBuilder")]
1047+
private static void ProfilingEnd([CallerMemberName] string? name = null)
1048+
=> ComponentsProfiling.Instance.End(name);
9991049
}
10001050
}

0 commit comments

Comments
 (0)