Blazor WebAssembly internal profiling infrastructure (#23510)
This commit is contained in:
parent
1688b5a30b
commit
3cbe02f2fc
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="RenderTree" />
|
||||
<Compile Include="$(ComponentsSharedSourceRoot)src\WebAssemblyJSInteropInternalCalls.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -25,6 +28,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
ArrayRange<RenderTreeFrame> oldTree,
|
||||
ArrayRange<RenderTreeFrame> newTree)
|
||||
{
|
||||
ComponentsProfiling.Instance.Start();
|
||||
var editsBuffer = batchBuilder.EditsBuffer;
|
||||
var editsBufferStartLength = editsBuffer.Count;
|
||||
|
||||
|
|
@ -32,7 +36,9 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
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)
|
||||
|
|
@ -43,6 +49,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
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
|
||||
|
|
@ -293,10 +300,12 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
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;
|
||||
|
|
@ -342,6 +351,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
newStartIndex = NextSiblingIndex(frame, newStartIndex);
|
||||
}
|
||||
|
||||
ProfilingEnd();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -374,6 +384,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
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
|
||||
|
|
@ -422,6 +433,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
ref diffContext,
|
||||
oldStartIndex, oldEndIndexExcl,
|
||||
newStartIndex, newEndIndexExcl);
|
||||
ProfilingEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -447,9 +459,12 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
ref diffContext,
|
||||
oldStartIndex, oldEndIndexExcl,
|
||||
newStartIndex, newEndIndexExcl);
|
||||
ProfilingEnd();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
private static void AppendAttributeDiffEntriesForRangeSlow(
|
||||
|
|
@ -457,6 +472,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
int oldStartIndex, int oldEndIndexExcl,
|
||||
int newStartIndex, int newEndIndexExcl)
|
||||
{
|
||||
ProfilingStart();
|
||||
var oldTree = diffContext.OldTree;
|
||||
var newTree = diffContext.NewTree;
|
||||
|
||||
|
|
@ -495,6 +511,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
|
||||
// We should have processed any additions at this point. Reset for the next batch.
|
||||
diffContext.AttributeDiffSet.Clear();
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
private static void UpdateRetainedChildComponent(
|
||||
|
|
@ -502,6 +519,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
int oldComponentIndex,
|
||||
int newComponentIndex)
|
||||
{
|
||||
ProfilingStart();
|
||||
var oldTree = diffContext.OldTree;
|
||||
var newTree = diffContext.NewTree;
|
||||
ref var oldComponentFrame = ref oldTree[oldComponentIndex];
|
||||
|
|
@ -528,6 +546,8 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
{
|
||||
componentState.SetDirectParameters(newParameters);
|
||||
}
|
||||
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
private static int NextSiblingIndex(in RenderTreeFrame frame, int frameIndex)
|
||||
|
|
@ -550,6 +570,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
int oldFrameIndex,
|
||||
int newFrameIndex)
|
||||
{
|
||||
ProfilingStart();
|
||||
var oldTree = diffContext.OldTree;
|
||||
var newTree = diffContext.NewTree;
|
||||
ref var oldFrame = ref oldTree[oldFrameIndex];
|
||||
|
|
@ -562,6 +583,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
{
|
||||
InsertNewFrame(ref diffContext, newFrameIndex);
|
||||
RemoveOldFrame(ref diffContext, oldFrameIndex);
|
||||
ProfilingEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -687,6 +709,8 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
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
|
||||
|
|
@ -696,6 +720,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
int oldFrameIndex,
|
||||
int newFrameIndex)
|
||||
{
|
||||
ProfilingStart();
|
||||
var oldTree = diffContext.OldTree;
|
||||
var newTree = diffContext.NewTree;
|
||||
ref var oldFrame = ref oldTree[oldFrameIndex];
|
||||
|
|
@ -724,10 +749,13 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
// 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)
|
||||
|
|
@ -780,10 +808,12 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
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)
|
||||
|
|
@ -825,6 +855,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
default:
|
||||
throw new NotImplementedException($"Unexpected frame type during {nameof(RemoveOldFrame)}: {oldFrame.FrameType}");
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
private static int GetAttributesEndIndexExclusive(RenderTreeFrame[] tree, int rootIndex)
|
||||
|
|
@ -858,6 +889,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
|
||||
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++)
|
||||
|
|
@ -879,10 +911,12 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
break;
|
||||
}
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
private static void InitializeNewComponentFrame(ref DiffContext diffContext, int frameIndex)
|
||||
{
|
||||
ProfilingStart();
|
||||
var frames = diffContext.NewTree;
|
||||
ref var frame = ref frames[frameIndex];
|
||||
|
||||
|
|
@ -899,6 +933,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
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)
|
||||
|
|
@ -943,6 +978,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
|
||||
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];
|
||||
|
|
@ -955,6 +991,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
batchBuilder.DisposedEventHandlerIds.Append(frame.AttributeEventHandlerId);
|
||||
}
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -996,5 +1033,18 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Profiling;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -220,6 +221,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
/// </returns>
|
||||
public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo fieldInfo, EventArgs eventArgs)
|
||||
{
|
||||
ComponentsProfiling.Instance.Start();
|
||||
Dispatcher.AssertAccess();
|
||||
|
||||
if (!_eventBindings.TryGetValue(eventHandlerId, out var callback))
|
||||
|
|
@ -247,6 +249,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
catch (Exception e)
|
||||
{
|
||||
HandleException(e);
|
||||
ComponentsProfiling.Instance.End();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
finally
|
||||
|
|
@ -260,7 +263,9 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
|
||||
// Task completed synchronously or is still running. We already processed all of the rendering
|
||||
// work that was queued so let our error handler deal with it.
|
||||
return GetErrorHandledTask(task);
|
||||
var result = GetErrorHandledTask(task);
|
||||
ComponentsProfiling.Instance.End();
|
||||
return result;
|
||||
}
|
||||
|
||||
internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
|
||||
|
|
@ -410,6 +415,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
|
||||
private void ProcessRenderQueue()
|
||||
{
|
||||
ComponentsProfiling.Instance.Start();
|
||||
Dispatcher.AssertAccess();
|
||||
|
||||
if (_isBatchInProgress)
|
||||
|
|
@ -424,6 +430,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
{
|
||||
if (_batchBuilder.ComponentRenderQueue.Count == 0)
|
||||
{
|
||||
ComponentsProfiling.Instance.End();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -435,7 +442,9 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
}
|
||||
|
||||
var batch = _batchBuilder.ToBatch();
|
||||
ComponentsProfiling.Instance.Start(nameof(UpdateDisplayAsync));
|
||||
updateDisplayTask = UpdateDisplayAsync(batch);
|
||||
ComponentsProfiling.Instance.End(nameof(UpdateDisplayAsync));
|
||||
|
||||
// Fire off the execution of OnAfterRenderAsync, but don't wait for it
|
||||
// if there is async work to be done.
|
||||
|
|
@ -445,6 +454,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
{
|
||||
// Ensure we catch errors while running the render functions of the components.
|
||||
HandleException(e);
|
||||
ComponentsProfiling.Instance.End();
|
||||
return;
|
||||
}
|
||||
finally
|
||||
|
|
@ -462,6 +472,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
{
|
||||
ProcessRenderQueue();
|
||||
}
|
||||
ComponentsProfiling.Instance.End();
|
||||
}
|
||||
|
||||
private Task InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents, Task updateDisplayTask)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Profiling;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Rendering
|
||||
|
|
@ -56,6 +57,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
public void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
|
||||
{
|
||||
ComponentsProfiling.Instance.Start();
|
||||
// A component might be in the render queue already before getting disposed by an
|
||||
// earlier entry in the render queue. In that case, rendering is a no-op.
|
||||
if (_componentWasDisposed)
|
||||
|
|
@ -67,7 +69,9 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
(CurrentRenderTree, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, CurrentRenderTree);
|
||||
|
||||
CurrentRenderTree.Clear();
|
||||
ComponentsProfiling.Instance.Start("BuildRenderTree");
|
||||
renderFragment(CurrentRenderTree);
|
||||
ComponentsProfiling.Instance.End("BuildRenderTree");
|
||||
|
||||
var diff = RenderTreeDiffBuilder.ComputeDiff(
|
||||
_renderer,
|
||||
|
|
@ -77,6 +81,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
CurrentRenderTree.GetFrames());
|
||||
batchBuilder.UpdatedComponentDiffs.Append(diff);
|
||||
batchBuilder.InvalidateParameterViews();
|
||||
ComponentsProfiling.Instance.End();
|
||||
}
|
||||
|
||||
public bool TryDisposeInBatch(RenderBatchBuilder batchBuilder, [NotNullWhen(false)] out Exception? exception)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.AspNetCore.Components.Profiling;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Rendering
|
||||
|
|
@ -43,6 +45,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="elementName">A value representing the type of the element.</param>
|
||||
public void OpenElement(int sequence, string elementName)
|
||||
{
|
||||
ProfilingStart();
|
||||
// We are entering a new scope, since we track the "duplicate attributes" per
|
||||
// element/component we might need to clean them up now.
|
||||
if (_hasSeenAddMultipleAttributes)
|
||||
|
|
@ -53,6 +56,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
_openElementIndices.Push(_entries.Count);
|
||||
Append(RenderTreeFrame.Element(sequence, elementName));
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -61,6 +65,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// </summary>
|
||||
public void CloseElement()
|
||||
{
|
||||
ProfilingStart();
|
||||
var indexOfEntryBeingClosed = _openElementIndices.Pop();
|
||||
|
||||
// We might be closing an element with only attributes, run the duplicate cleanup pass
|
||||
|
|
@ -72,6 +77,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed];
|
||||
entry = entry.WithElementSubtreeLength(_entries.Count - indexOfEntryBeingClosed);
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -80,7 +86,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
|
||||
/// <param name="markupContent">Content for the new markup frame.</param>
|
||||
public void AddMarkupContent(int sequence, string? markupContent)
|
||||
=> Append(RenderTreeFrame.Markup(sequence, markupContent ?? string.Empty));
|
||||
{
|
||||
ProfilingStart();
|
||||
Append(RenderTreeFrame.Markup(sequence, markupContent ?? string.Empty));
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a frame representing text content.
|
||||
|
|
@ -88,7 +98,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
|
||||
/// <param name="textContent">Content for the new text frame.</param>
|
||||
public void AddContent(int sequence, string? textContent)
|
||||
=> Append(RenderTreeFrame.Text(sequence, textContent ?? string.Empty));
|
||||
{
|
||||
ProfilingStart();
|
||||
Append(RenderTreeFrame.Text(sequence, textContent ?? string.Empty));
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends frames representing an arbitrary fragment of content.
|
||||
|
|
@ -97,6 +111,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="fragment">Content to append.</param>
|
||||
public void AddContent(int sequence, RenderFragment? fragment)
|
||||
{
|
||||
ProfilingStart();
|
||||
if (fragment != null)
|
||||
{
|
||||
// We surround the fragment with a region delimiter to indicate that the
|
||||
|
|
@ -107,6 +122,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
fragment(this);
|
||||
CloseRegion();
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -117,10 +133,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="value">The value used by <paramref name="fragment"/>.</param>
|
||||
public void AddContent<TValue>(int sequence, RenderFragment<TValue>? fragment, TValue value)
|
||||
{
|
||||
ProfilingStart();
|
||||
if (fragment != null)
|
||||
{
|
||||
AddContent(sequence, fragment(value));
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -153,6 +171,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="value">The value of the attribute.</param>
|
||||
public void AddAttribute(int sequence, string name, bool value)
|
||||
{
|
||||
ProfilingStart();
|
||||
AssertCanAddAttribute();
|
||||
if (_lastNonAttributeFrameType == RenderTreeFrameType.Component)
|
||||
{
|
||||
|
|
@ -168,6 +187,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
{
|
||||
TrackAttributeName(name);
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -184,6 +204,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="value">The value of the attribute.</param>
|
||||
public void AddAttribute(int sequence, string name, string? value)
|
||||
{
|
||||
ProfilingStart();
|
||||
AssertCanAddAttribute();
|
||||
if (value != null || _lastNonAttributeFrameType == RenderTreeFrameType.Component)
|
||||
{
|
||||
|
|
@ -193,6 +214,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
{
|
||||
TrackAttributeName(name);
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -209,6 +231,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="value">The value of the attribute.</param>
|
||||
public void AddAttribute(int sequence, string name, MulticastDelegate? value)
|
||||
{
|
||||
ProfilingStart();
|
||||
AssertCanAddAttribute();
|
||||
if (value != null || _lastNonAttributeFrameType == RenderTreeFrameType.Component)
|
||||
{
|
||||
|
|
@ -218,6 +241,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
{
|
||||
TrackAttributeName(name);
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -238,6 +262,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// </remarks>
|
||||
public void AddAttribute(int sequence, string name, EventCallback value)
|
||||
{
|
||||
ProfilingStart();
|
||||
AssertCanAddAttribute();
|
||||
if (_lastNonAttributeFrameType == RenderTreeFrameType.Component)
|
||||
{
|
||||
|
|
@ -262,6 +287,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
// Track the attribute name if needed since we elided the frame.
|
||||
TrackAttributeName(name);
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -282,6 +308,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// </remarks>
|
||||
public void AddAttribute<TArgument>(int sequence, string name, EventCallback<TArgument> value)
|
||||
{
|
||||
ProfilingStart();
|
||||
AssertCanAddAttribute();
|
||||
if (_lastNonAttributeFrameType == RenderTreeFrameType.Component)
|
||||
{
|
||||
|
|
@ -306,6 +333,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
// Track the attribute name if needed since we elided the frame.
|
||||
TrackAttributeName(name);
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -319,6 +347,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="value">The value of the attribute.</param>
|
||||
public void AddAttribute(int sequence, string name, object? value)
|
||||
{
|
||||
ProfilingStart();
|
||||
// This looks a bit daunting because we need to handle the boxed/object version of all of the
|
||||
// types that AddAttribute special cases.
|
||||
if (_lastNonAttributeFrameType == RenderTreeFrameType.Element)
|
||||
|
|
@ -371,6 +400,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
// This is going to throw. Calling it just to get a consistent exception message.
|
||||
AssertCanAddAttribute();
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -385,6 +415,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="frame">A <see cref="RenderTreeFrame"/> holding the name and value of the attribute.</param>
|
||||
public void AddAttribute(int sequence, in RenderTreeFrame frame)
|
||||
{
|
||||
ProfilingStart();
|
||||
if (frame.FrameType != RenderTreeFrameType.Attribute)
|
||||
{
|
||||
throw new ArgumentException($"The {nameof(frame.FrameType)} must be {RenderTreeFrameType.Attribute}.");
|
||||
|
|
@ -392,6 +423,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
AssertCanAddAttribute();
|
||||
Append(frame.WithAttributeSequence(sequence));
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -401,6 +433,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="attributes">A collection of key-value pairs representing attributes.</param>
|
||||
public void AddMultipleAttributes(int sequence, IEnumerable<KeyValuePair<string, object>>? attributes)
|
||||
{
|
||||
ProfilingStart();
|
||||
// Calling this up-front just to make sure we validate before mutating anything.
|
||||
AssertCanAddAttribute();
|
||||
|
||||
|
|
@ -417,6 +450,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
AddAttribute(sequence, attribute.Key, attribute.Value);
|
||||
}
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -433,6 +467,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="updatesAttributeName">The name of another attribute whose value can be updated when the event handler is executed.</param>
|
||||
public void SetUpdatesAttributeName(string updatesAttributeName)
|
||||
{
|
||||
ProfilingStart();
|
||||
if (_entries.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No preceding attribute frame exists.");
|
||||
|
|
@ -445,6 +480,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
}
|
||||
|
||||
prevFrame = prevFrame.WithAttributeEventUpdatesAttributeName(updatesAttributeName);
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -476,10 +512,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="value">The value for the key.</param>
|
||||
public void SetKey(object? value)
|
||||
{
|
||||
ProfilingStart();
|
||||
if (value == null)
|
||||
{
|
||||
// Null is equivalent to not having set a key, which is valuable because Razor syntax doesn't have an
|
||||
// easy way to have conditional directive attributes
|
||||
ProfilingEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -502,10 +540,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
default:
|
||||
throw new InvalidOperationException($"Cannot set a key on a frame of type {parentFrame.FrameType}.");
|
||||
}
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
private void OpenComponentUnchecked(int sequence, Type componentType)
|
||||
{
|
||||
ProfilingStart();
|
||||
// We are entering a new scope, since we track the "duplicate attributes" per
|
||||
// element/component we might need to clean them up now.
|
||||
if (_hasSeenAddMultipleAttributes)
|
||||
|
|
@ -516,6 +556,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
_openElementIndices.Push(_entries.Count);
|
||||
Append(RenderTreeFrame.ChildComponent(sequence, componentType));
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -524,6 +565,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// </summary>
|
||||
public void CloseComponent()
|
||||
{
|
||||
ProfilingStart();
|
||||
var indexOfEntryBeingClosed = _openElementIndices.Pop();
|
||||
|
||||
// We might be closing a component with only attributes. Run the attribute cleanup pass
|
||||
|
|
@ -535,6 +577,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed];
|
||||
entry = entry.WithComponentSubtreeLength(_entries.Count - indexOfEntryBeingClosed);
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -544,12 +587,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="elementReferenceCaptureAction">An action to be invoked whenever the reference value changes.</param>
|
||||
public void AddElementReferenceCapture(int sequence, Action<ElementReference> elementReferenceCaptureAction)
|
||||
{
|
||||
ProfilingStart();
|
||||
if (GetCurrentParentFrameType() != RenderTreeFrameType.Element)
|
||||
{
|
||||
throw new InvalidOperationException($"Element reference captures may only be added as children of frames of type {RenderTreeFrameType.Element}");
|
||||
}
|
||||
|
||||
Append(RenderTreeFrame.ElementReferenceCapture(sequence, elementReferenceCaptureAction));
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -559,6 +604,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="componentReferenceCaptureAction">An action to be invoked whenever the reference value changes.</param>
|
||||
public void AddComponentReferenceCapture(int sequence, Action<object?> componentReferenceCaptureAction)
|
||||
{
|
||||
ProfilingStart();
|
||||
var parentFrameIndex = GetCurrentParentFrameIndex();
|
||||
if (!parentFrameIndex.HasValue)
|
||||
{
|
||||
|
|
@ -572,6 +618,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
}
|
||||
|
||||
Append(RenderTreeFrame.ComponentReferenceCapture(sequence, componentReferenceCaptureAction, parentFrameIndexValue));
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -580,6 +627,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
|
||||
public void OpenRegion(int sequence)
|
||||
{
|
||||
ProfilingStart();
|
||||
// We are entering a new scope, since we track the "duplicate attributes" per
|
||||
// element/component we might need to clean them up now.
|
||||
if (_hasSeenAddMultipleAttributes)
|
||||
|
|
@ -590,6 +638,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
_openElementIndices.Push(_entries.Count);
|
||||
Append(RenderTreeFrame.Region(sequence));
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -598,9 +647,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// </summary>
|
||||
public void CloseRegion()
|
||||
{
|
||||
ProfilingStart();
|
||||
var indexOfEntryBeingClosed = _openElementIndices.Pop();
|
||||
ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed];
|
||||
entry = entry.WithRegionSubtreeLength(_entries.Count - indexOfEntryBeingClosed);
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
private void AssertCanAddAttribute()
|
||||
|
|
@ -628,24 +679,29 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
ProfilingStart();
|
||||
_entries.Clear();
|
||||
_openElementIndices.Clear();
|
||||
_lastNonAttributeFrameType = null;
|
||||
_hasSeenAddMultipleAttributes = false;
|
||||
_seenAttributeNames?.Clear();
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
// internal because this should only be used during the post-event tree patching logic
|
||||
// It's expensive because it involves copying all the subsequent memory in the array
|
||||
internal void InsertAttributeExpensive(int insertAtIndex, int sequence, string attributeName, object? attributeValue)
|
||||
{
|
||||
ProfilingStart();
|
||||
// Replicate the same attribute omission logic as used elsewhere
|
||||
if ((attributeValue == null) || (attributeValue is bool boolValue && !boolValue))
|
||||
{
|
||||
ProfilingEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
_entries.InsertExpensive(insertAtIndex, RenderTreeFrame.Attribute(sequence, attributeName, attributeValue));
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -669,6 +725,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
// Internal for testing
|
||||
internal void ProcessDuplicateAttributes(int first)
|
||||
{
|
||||
ProfilingStart();
|
||||
Debug.Assert(_hasSeenAddMultipleAttributes);
|
||||
|
||||
// When AddMultipleAttributes method has been called, we need to postprocess attributes while closing
|
||||
|
|
@ -750,6 +807,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
seenAttributeNames.Clear();
|
||||
_hasSeenAddMultipleAttributes = false;
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
// Internal for testing
|
||||
|
|
@ -766,7 +824,22 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
ProfilingStart();
|
||||
_entries.Dispose();
|
||||
ProfilingEnd();
|
||||
}
|
||||
|
||||
// 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_RenderTreeBuilder compiler symbol, otherwise the calls are compiled out entirely.
|
||||
// Enabling detailed profiling adds about 5% to rendering benchmark times.
|
||||
|
||||
[Conditional("Profile_RenderTreeBuilder")]
|
||||
private static void ProfilingStart([CallerMemberName] string? name = null)
|
||||
=> ComponentsProfiling.Instance.Start(name);
|
||||
|
||||
[Conditional("Profile_RenderTreeBuilder")]
|
||||
private static void ProfilingEnd([CallerMemberName] string? name = null)
|
||||
=> ComponentsProfiling.Instance.End(name);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace WebAssembly.JSInterop
|
||||
|
|
@ -21,6 +22,6 @@ namespace WebAssembly.JSInterop
|
|||
public static extern string InvokeJSMarshalled(out string exception, ref long asyncHandle, string functionIdentifier, string argsJson);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
public static extern TRes InvokeJSUnmarshalled<T0, T1, T2, TRes>(out string exception, string functionIdentifier, T0 arg0, T1 arg1, T2 arg2);
|
||||
public static extern TRes InvokeJSUnmarshalled<T0, T1, T2, TRes>(out string exception, string functionIdentifier, [AllowNull] T0 arg0, [AllowNull] T1 arg1, [AllowNull] T2 arg2);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -12,6 +12,7 @@ import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
|
|||
import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
|
||||
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
|
||||
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
|
||||
import { initializeProfiling } from './Platform/Profiling';
|
||||
|
||||
let renderingFailed = false;
|
||||
let started = false;
|
||||
|
|
@ -21,6 +22,7 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
|
|||
throw new Error('Blazor has already started.');
|
||||
}
|
||||
started = true;
|
||||
initializeProfiling(null);
|
||||
|
||||
// Establish options to be used
|
||||
const options = resolveOptions(userOptions);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { WebAssemblyConfigLoader } from './Platform/WebAssemblyConfigLoader';
|
|||
import { BootConfigResult } from './Platform/BootConfig';
|
||||
import { Pointer } from './Platform/Platform';
|
||||
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
|
||||
import { profileStart, profileEnd } from './Platform/Profiling';
|
||||
|
||||
let started = false;
|
||||
|
||||
|
|
@ -27,7 +28,9 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
|
|||
const platform = Environment.setPlatform(monoPlatform);
|
||||
window['Blazor'].platform = platform;
|
||||
window['Blazor']._internal.renderBatch = (browserRendererId: number, batchAddress: Pointer) => {
|
||||
profileStart('renderBatch');
|
||||
renderBatch(browserRendererId, new SharedMemoryRenderBatch(batchAddress));
|
||||
profileEnd('renderBatch');
|
||||
};
|
||||
|
||||
// Configure navigation via JS Interop
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { navigateTo, internalFunctions as navigationManagerInternalFunctions } from './Services/NavigationManager';
|
||||
import { attachRootComponentToElement } from './Rendering/Renderer';
|
||||
import { domFunctions } from './DomWrapper';
|
||||
import { setProfilingEnabled } from './Platform/Profiling';
|
||||
|
||||
// Make the following APIs available in global scope for invocation from JS
|
||||
window['Blazor'] = {
|
||||
|
|
@ -10,5 +11,6 @@ window['Blazor'] = {
|
|||
attachRootComponentToElement,
|
||||
navigationManager: navigationManagerInternalFunctions,
|
||||
domWrapper: domFunctions,
|
||||
setProfilingEnabled: setProfilingEnabled,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { WebAssemblyResourceLoader, LoadingResource } from '../WebAssemblyResour
|
|||
import { Platform, System_Array, Pointer, System_Object, System_String } from '../Platform';
|
||||
import { loadTimezoneData } from './TimezoneDataFile';
|
||||
import { WebAssemblyBootResourceType } from '../WebAssemblyStartOptions';
|
||||
import { initializeProfiling } from '../Profiling';
|
||||
|
||||
let mono_string_get_utf8: (managedString: System_String) => Pointer;
|
||||
let mono_wasm_add_assembly: (name: string, heapAddress: number, length: number) => void;
|
||||
|
|
@ -34,6 +35,10 @@ export const monoPlatform: Platform = {
|
|||
start: function start(resourceLoader: WebAssemblyResourceLoader) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
attachDebuggerHotkey(resourceLoader);
|
||||
initializeProfiling(isCapturing => {
|
||||
const setCapturingMethod = bindStaticMethod('Microsoft.AspNetCore.Components', 'Microsoft.AspNetCore.Components.Profiling.WebAssemblyComponentsProfiling', 'SetCapturing');
|
||||
setCapturingMethod(isCapturing);
|
||||
});
|
||||
|
||||
// dotnet.js assumes the existence of this
|
||||
window['Browser'] = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
import { System_String } from './Platform';
|
||||
|
||||
interface TimingEntry {
|
||||
// To minimize overhead, don't even decode the strings that arrive from .NET. Assume they are compile-time constants
|
||||
// and hence the memory address will be fixed, so we can just store the pointer value.
|
||||
name: string | System_String;
|
||||
type: 'start' | 'end';
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface TraceEvent {
|
||||
// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
|
||||
name: string;
|
||||
cat: string; // Category
|
||||
ph: 'B' | 'E'; // Phase
|
||||
ts: number; // Timestamp in microseconds
|
||||
pid: number; // Process ID
|
||||
tid: number; // Thread ID
|
||||
}
|
||||
|
||||
let updateCapturingStateInHost: (isCapturing: boolean) => void;
|
||||
let captureStartTime = 0;
|
||||
const blazorProfilingEnabledKey = 'blazorProfilingEnabled';
|
||||
const profilingEnabled = !!localStorage[blazorProfilingEnabledKey];
|
||||
const entryLog: TimingEntry[] = [];
|
||||
const openRegionsStack: (string | System_String)[] = [];
|
||||
|
||||
export function setProfilingEnabled(enabled: boolean) {
|
||||
// We only wire up the hotkeys etc. if the following localStorage entry is present during startup
|
||||
// This is to ensure we're not interfering with any other hotkeys that developers might want to
|
||||
// use for different purposes, plus it gives us a single point where we can notify .NET code during
|
||||
// startup about whether profiling should be enabled.
|
||||
localStorage[blazorProfilingEnabledKey] = (enabled !== false);
|
||||
location.reload();
|
||||
}
|
||||
|
||||
export function initializeProfiling(setCapturingCallback: ((isCapturing: boolean) => void) | null) {
|
||||
if (!profilingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateCapturingStateInHost = setCapturingCallback || (() => {});
|
||||
|
||||
// Attach hotkey (alt/cmd)+shift+p
|
||||
const altKeyName = navigator.platform.match(/^Mac/i) ? 'Cmd' : 'Alt';
|
||||
console.info(`Profiling hotkey: Shift+${altKeyName}+P (when application has focus)`);
|
||||
document.addEventListener('keydown', evt => {
|
||||
if (evt.shiftKey && (evt.metaKey || evt.altKey) && evt.code === 'KeyP') {
|
||||
toggleCaptureEnabled();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function profileStart(name: System_String | string) {
|
||||
if (!captureStartTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
openRegionsStack.push(name);
|
||||
entryLog.push({ name: name, type: 'start', timestamp: startTime });
|
||||
}
|
||||
|
||||
export function profileEnd(name: System_String | string) {
|
||||
if (!captureStartTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const poppedRegionName = openRegionsStack.pop();
|
||||
if (!poppedRegionName) {
|
||||
throw new Error(`Profiling mismatch: tried to end profiling for '${readJsString(name)}', but the stack was empty.`);
|
||||
} else if (poppedRegionName !== name) {
|
||||
throw new Error(`Profiling mismatch: tried to end profiling for '${readJsString(name)}', but the top stack item was '${readJsString(poppedRegionName)}'.`);
|
||||
}
|
||||
|
||||
entryLog.push({ name: name, type: 'end', timestamp: endTime });
|
||||
}
|
||||
|
||||
function profileReset() {
|
||||
openRegionsStack.length = 0;
|
||||
entryLog.length = 0;
|
||||
captureStartTime = 0;
|
||||
updateCapturingStateInHost(false);
|
||||
}
|
||||
|
||||
function profileExport() {
|
||||
const traceEvents: TraceEvent[] = entryLog.map(entry => ({
|
||||
name: readJsString(entry.name)!,
|
||||
cat: 'PERF',
|
||||
ph: entry.type === 'start' ? 'B': 'E',
|
||||
ts: (entry.timestamp - captureStartTime) * 1000,
|
||||
pid: 0,
|
||||
tid: 0,
|
||||
}));
|
||||
const traceEventsJson = JSON.stringify(traceEvents);
|
||||
const traceEventsBuffer = new TextEncoder().encode(traceEventsJson);
|
||||
const anchorElement = document.createElement('a');
|
||||
anchorElement.href = URL.createObjectURL(new Blob([traceEventsBuffer]));
|
||||
anchorElement.setAttribute('download', 'trace.json');
|
||||
anchorElement.click();
|
||||
URL.revokeObjectURL(anchorElement.href);
|
||||
}
|
||||
|
||||
function toggleCaptureEnabled() {
|
||||
if (!captureStartTime) {
|
||||
displayOverlayMessage('Started capturing performance profile...');
|
||||
updateCapturingStateInHost(true);
|
||||
captureStartTime = performance.now();
|
||||
} else {
|
||||
displayOverlayMessage('Finished capturing performance profile');
|
||||
profileExport();
|
||||
profileReset();
|
||||
}
|
||||
}
|
||||
|
||||
function displayOverlayMessage(message: string) {
|
||||
const elem = document.createElement('div');
|
||||
elem.textContent = message;
|
||||
elem.setAttribute('style', 'position: absolute; z-index: 99999; font-family: \'Sans Serif\'; top: 0; left: 0; padding: 4px; font-size: 12px; background-color: purple; color: white;');
|
||||
document.body.appendChild(elem);
|
||||
setTimeout(() => document.body.removeChild(elem), 3000);
|
||||
}
|
||||
|
||||
function readJsString(str: string | System_String) {
|
||||
// This is expensive, so don't do it while capturing timings. Only do it as part of the export process.
|
||||
return typeof str === 'string' ? str : BINDING.conv_string(str);
|
||||
}
|
||||
|
||||
// These globals deliberately differ from our normal conventions for attaching functions inside Blazor.*
|
||||
// because the intention is to minimize overhead in all reasonable ways. Having any dot-separators in the
|
||||
// name would cause extra string allocations on every invocation.
|
||||
window['_blazorProfileStart'] = profileStart;
|
||||
window['_blazorProfileEnd'] = profileEnd;
|
||||
|
|
@ -6,6 +6,7 @@ import { applyCaptureIdToElement } from './ElementReferenceCapture';
|
|||
import { EventFieldInfo } from './EventFieldInfo';
|
||||
import { dispatchEvent } from './RendererEventDispatcher';
|
||||
import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager';
|
||||
import { profileEnd, profileStart } from '../Platform/Profiling';
|
||||
const selectValuePropname = '_blazorSelectValue';
|
||||
const sharedTemplateElemForParsing = document.createElement('template');
|
||||
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
|
|
@ -40,6 +41,8 @@ export class BrowserRenderer {
|
|||
}
|
||||
|
||||
public updateComponent(batch: RenderBatch, componentId: number, edits: ArrayBuilderSegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>): void {
|
||||
profileStart('updateComponent');
|
||||
|
||||
const element = this.childComponentLocations[componentId];
|
||||
if (!element) {
|
||||
throw new Error(`No element is currently associated with component ${componentId}`);
|
||||
|
|
@ -67,6 +70,8 @@ export class BrowserRenderer {
|
|||
if ((activeElementBefore instanceof HTMLElement) && ownerDocument && ownerDocument.activeElement !== activeElementBefore) {
|
||||
activeElementBefore.focus();
|
||||
}
|
||||
|
||||
profileEnd('updateComponent');
|
||||
}
|
||||
|
||||
public disposeComponent(componentId: number) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(ComponentsSharedSourceRoot)src\WebAssemblyJSInteropInternalCalls.cs" />
|
||||
<Reference Include="Microsoft.JSInterop" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue