Skip to content

Blazor WebAssembly internal profiling infrastructure #23510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="RenderTree" />
<Compile Include="$(ComponentsSharedSourceRoot)src\WebAssemblyJSInteropInternalCalls.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
28 changes: 28 additions & 0 deletions src/Components/Components/src/Profiling/ComponentsProfiling.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Microsoft.AspNetCore.Components.Profiling
{
internal abstract class ComponentsProfiling
{
// For now, this is only intended for use on Blazor WebAssembly, and will have no effect
// when running on Blazor Server. The reason for having the ComponentsProfiling abstraction
// is so that if we later have two different implementations (one for WebAssembly, one for
// Server), the execution characteristics of calling Start/End will be unchanged and historical
// perf data will still be comparable to newer data.
public static readonly ComponentsProfiling Instance;

static ComponentsProfiling()
{
Instance = RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"))
? new WebAssemblyComponentsProfiling()
: (ComponentsProfiling)new NoOpComponentsProfiling();
}

public abstract void Start([CallerMemberName] string? name = null);
public abstract void End([CallerMemberName] string? name = null);
}
}
16 changes: 16 additions & 0 deletions src/Components/Components/src/Profiling/NoOpComponentsProfiling.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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.

namespace Microsoft.AspNetCore.Components.Profiling
{
internal class NoOpComponentsProfiling : ComponentsProfiling
{
public override void Start(string? name)
{
}

public override void End(string? name)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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 WebAssembly.JSInterop;

namespace Microsoft.AspNetCore.Components.Profiling
{
// Later on, we will likely want to move this into the WebAssembly package. However it needs to
// be inlined into the Components package directly until we're ready to make the underlying
// ComponentsProfile abstraction into a public API. It's possible that this API will never become
// public, or that it will be replaced by something more standard for .NET, if it's possible to
// make that work performantly on WebAssembly.

internal class WebAssemblyComponentsProfiling : ComponentsProfiling
{
static bool IsCapturing = false;

public static void SetCapturing(bool isCapturing)
{
IsCapturing = isCapturing;
}

public override void Start(string? name)
{
if (IsCapturing)
{
InternalCalls.InvokeJSUnmarshalled<string, object, object, object>(
out _, "_blazorProfileStart", name, null, null);
}
}

public override void End(string? name)
{
if (IsCapturing)
{
InternalCalls.InvokeJSUnmarshalled<string, object, object, object>(
out _, "_blazorProfileEnd", name, null, null);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Components.Profiling;
using Microsoft.AspNetCore.Components.Rendering;

namespace Microsoft.AspNetCore.Components.RenderTree
Expand All @@ -25,14 +28,17 @@ public static RenderTreeDiff ComputeDiff(
ArrayRange<RenderTreeFrame> oldTree,
ArrayRange<RenderTreeFrame> newTree)
{
ComponentsProfiling.Instance.Start();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use ProfilingStart/ProfilingEnd here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I wanted to include this method in the coarse-grained timings.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha! OK -- I see how this call is different now.

var editsBuffer = batchBuilder.EditsBuffer;
var editsBufferStartLength = editsBuffer.Count;

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

var editsSegment = editsBuffer.ToSegment(editsBufferStartLength, editsBuffer.Count);
return new RenderTreeDiff(componentId, editsSegment);
var result = new RenderTreeDiff(componentId, editsSegment);
ComponentsProfiling.Instance.End();
return result;
}

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

private static Dictionary<object, KeyedItemInfo> BuildKeyToInfoLookup(DiffContext diffContext, int oldStartIndex, int oldEndIndexExcl, int newStartIndex, int newEndIndexExcl)
{
ProfilingStart();
var result = diffContext.KeyedItemInfoDictionaryPool.Get();
var oldTree = diffContext.OldTree;
var newTree = diffContext.NewTree;
Expand Down Expand Up @@ -342,6 +351,7 @@ private static Dictionary<object, KeyedItemInfo> BuildKeyToInfoLookup(DiffContex
newStartIndex = NextSiblingIndex(frame, newStartIndex);
}

ProfilingEnd();
return result;
}

Expand Down Expand Up @@ -374,6 +384,7 @@ private static void AppendAttributeDiffEntriesForRange(
int oldStartIndex, int oldEndIndexExcl,
int newStartIndex, int newEndIndexExcl)
{
ProfilingStart();
// The overhead of the dictionary used by AppendAttributeDiffEntriesForRangeSlow is
// significant, so we want to try and do a merge-join if possible, but fall back to
// a hash-join if not. We'll do a merge join until we hit a case we can't handle and
Expand Down Expand Up @@ -422,6 +433,7 @@ private static void AppendAttributeDiffEntriesForRange(
ref diffContext,
oldStartIndex, oldEndIndexExcl,
newStartIndex, newEndIndexExcl);
ProfilingEnd();
return;
}

Expand All @@ -447,16 +459,20 @@ private static void AppendAttributeDiffEntriesForRange(
ref diffContext,
oldStartIndex, oldEndIndexExcl,
newStartIndex, newEndIndexExcl);
ProfilingEnd();
return;
}
}

ProfilingEnd();
}

private static void AppendAttributeDiffEntriesForRangeSlow(
ref DiffContext diffContext,
int oldStartIndex, int oldEndIndexExcl,
int newStartIndex, int newEndIndexExcl)
{
ProfilingStart();
var oldTree = diffContext.OldTree;
var newTree = diffContext.NewTree;

Expand Down Expand Up @@ -495,13 +511,15 @@ private static void AppendAttributeDiffEntriesForRangeSlow(

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

private static void UpdateRetainedChildComponent(
ref DiffContext diffContext,
int oldComponentIndex,
int newComponentIndex)
{
ProfilingStart();
var oldTree = diffContext.OldTree;
var newTree = diffContext.NewTree;
ref var oldComponentFrame = ref oldTree[oldComponentIndex];
Expand All @@ -528,6 +546,8 @@ private static void UpdateRetainedChildComponent(
{
componentState.SetDirectParameters(newParameters);
}

ProfilingEnd();
}

private static int NextSiblingIndex(in RenderTreeFrame frame, int frameIndex)
Expand All @@ -550,6 +570,7 @@ private static void AppendDiffEntriesForFramesWithSameSequence(
int oldFrameIndex,
int newFrameIndex)
{
ProfilingStart();
var oldTree = diffContext.OldTree;
var newTree = diffContext.NewTree;
ref var oldFrame = ref oldTree[oldFrameIndex];
Expand All @@ -562,6 +583,7 @@ private static void AppendDiffEntriesForFramesWithSameSequence(
{
InsertNewFrame(ref diffContext, newFrameIndex);
RemoveOldFrame(ref diffContext, oldFrameIndex);
ProfilingEnd();
return;
}

Expand Down Expand Up @@ -687,6 +709,8 @@ private static void AppendDiffEntriesForFramesWithSameSequence(
default:
throw new NotImplementedException($"Encountered unsupported frame type during diffing: {newTree[newFrameIndex].FrameType}");
}

ProfilingEnd();
}

// This should only be called for attributes that have the same name. This is an
Expand All @@ -696,6 +720,7 @@ private static void AppendDiffEntriesForAttributeFrame(
int oldFrameIndex,
int newFrameIndex)
{
ProfilingStart();
var oldTree = diffContext.OldTree;
var newTree = diffContext.NewTree;
ref var oldFrame = ref oldTree[oldFrameIndex];
Expand Down Expand Up @@ -724,10 +749,13 @@ private static void AppendDiffEntriesForAttributeFrame(
// since it was unchanged.
newFrame = oldFrame;
}

ProfilingEnd();
}

private static void InsertNewFrame(ref DiffContext diffContext, int newFrameIndex)
{
ProfilingStart();
var newTree = diffContext.NewTree;
ref var newFrame = ref newTree[newFrameIndex];
switch (newFrame.FrameType)
Expand Down Expand Up @@ -780,10 +808,12 @@ private static void InsertNewFrame(ref DiffContext diffContext, int newFrameInde
default:
throw new NotImplementedException($"Unexpected frame type during {nameof(InsertNewFrame)}: {newFrame.FrameType}");
}
ProfilingEnd();
}

private static void RemoveOldFrame(ref DiffContext diffContext, int oldFrameIndex)
{
ProfilingStart();
var oldTree = diffContext.OldTree;
ref var oldFrame = ref oldTree[oldFrameIndex];
switch (oldFrame.FrameType)
Expand Down Expand Up @@ -825,6 +855,7 @@ private static void RemoveOldFrame(ref DiffContext diffContext, int oldFrameInde
default:
throw new NotImplementedException($"Unexpected frame type during {nameof(RemoveOldFrame)}: {oldFrame.FrameType}");
}
ProfilingEnd();
}

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

private static void InitializeNewSubtree(ref DiffContext diffContext, int frameIndex)
{
ProfilingStart();
var frames = diffContext.NewTree;
var endIndexExcl = frameIndex + frames[frameIndex].ElementSubtreeLength;
for (var i = frameIndex; i < endIndexExcl; i++)
Expand All @@ -879,10 +911,12 @@ private static void InitializeNewSubtree(ref DiffContext diffContext, int frameI
break;
}
}
ProfilingEnd();
}

private static void InitializeNewComponentFrame(ref DiffContext diffContext, int frameIndex)
{
ProfilingStart();
var frames = diffContext.NewTree;
ref var frame = ref frames[frameIndex];

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

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

private static void DisposeFramesInRange(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int startIndex, int endIndexExcl)
{
ProfilingStart();
for (var i = startIndex; i < endIndexExcl; i++)
{
ref var frame = ref frames[i];
Expand All @@ -955,6 +991,7 @@ private static void DisposeFramesInRange(RenderBatchBuilder batchBuilder, Render
batchBuilder.DisposedEventHandlerIds.Append(frame.AttributeEventHandlerId);
}
}
ProfilingEnd();
}

/// <summary>
Expand Down Expand Up @@ -996,5 +1033,18 @@ public DiffContext(
SiblingIndex = 0;
}
}

// Having too many calls to ComponentsProfiling.Instance.Start/End has a measurable perf impact
// even when capturing is disabled. So, to enable detailed profiling for this class, define the
// Profile_RenderTreeDiffBuilder compiler symbol, otherwise the calls are compiled out entirely.
// Enabling detailed profiling adds about 5% to rendering benchmark times.

[Conditional("Profile_RenderTreeDiffBuilder")]
private static void ProfilingStart([CallerMemberName] string? name = null)
=> ComponentsProfiling.Instance.Start(name);

[Conditional("Profile_RenderTreeDiffBuilder")]
private static void ProfilingEnd([CallerMemberName] string? name = null)
=> ComponentsProfiling.Instance.End(name);
}
}
Loading