Add Razor TagHelper Parsing Benchmarks (#23627)

* Add Razor TagHelper Parsing Benchmarks

Benchmarks:
|                                  Method |        Mean |     Error |    StdDev |     Op/s | Allocated |
|---------------------------------------- |------------:|----------:|----------:|---------:|----------:|
|      'TagHelper Design Time Processing' | 2,331.51 us | 28.916 us | 27.048 us |    428.9 | 985.33 KB |
| 'TagHelper Component Directive Parsing' |    90.46 us |  0.472 us |  0.394 us | 11,055.1 |   3.01 KB |

Notes / Attributions:
- `BlazorServerTagHelpers` is just a demo file concatonated from the basic `BlazorServer` files
- `taghelpers.json` was updated from: 73c96f1c00/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/taghelpers.json
- `ReadTagHelpers` was copied over from fef50ba623/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectSnapshotManagerBenchmarkBase.cs (L83-L93)

Fixes: https://github.com/dotnet/aspnetcore/issues/23454
This commit is contained in:
Tanay Parikh 2020-07-06 16:32:12 -07:00 committed by GitHub
parent 17b01ae667
commit 1c2a0f4fe6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 196 additions and 11 deletions

View File

@ -247,8 +247,8 @@ namespace Microsoft.AspNetCore.Razor.Language
{
// If this is a child content tag helper, we want to add it if it's original type is in scope.
// E.g, if the type name is `Test.MyComponent.ChildContent`, we want to add it if `Test.MyComponent` is in scope.
TrySplitNamespaceAndType(typeName, out var typeNameTextSpan, out var _);
typeName = GetTextSpanContent(typeNameTextSpan, typeName);
TrySplitNamespaceAndType(typeName, out var namespaceTextSpan, out var _);
typeName = GetTextSpanContent(namespaceTextSpan, typeName);
}
if (currentNamespace != null && IsTypeInScope(typeName, currentNamespace))
@ -337,8 +337,8 @@ namespace Microsoft.AspNetCore.Razor.Language
{
// If this is a child content tag helper, we want to add it if it's original type is in scope of the given namespace.
// E.g, if the type name is `Test.MyComponent.ChildContent`, we want to add it if `Test.MyComponent` is in this namespace.
TrySplitNamespaceAndType(typeName, out var typeNameTextSpan, out var _);
typeName = GetTextSpanContent(typeNameTextSpan, typeName);
TrySplitNamespaceAndType(typeName, out var namespaceTextSpan, out var _);
typeName = GetTextSpanContent(namespaceTextSpan, typeName);
}
if (typeName != null && IsTypeInNamespace(typeName, @namespace))
{
@ -352,13 +352,13 @@ namespace Microsoft.AspNetCore.Razor.Language
internal static bool IsTypeInNamespace(string typeName, string @namespace)
{
if (!TrySplitNamespaceAndType(typeName, out var typeNamespace, out var _) || typeNamespace.Length == 0)
if (!TrySplitNamespaceAndType(typeName, out var namespaceTextSpan, out var _) || namespaceTextSpan.Length == 0)
{
// Either the typeName is not the full type name or this type is at the top level.
return true;
}
return @namespace.Length == typeNamespace.Length && 0 == string.CompareOrdinal(typeName, typeNamespace.Start, @namespace, 0, @namespace.Length);
return @namespace.Length == namespaceTextSpan.Length && 0 == string.CompareOrdinal(typeName, namespaceTextSpan.Start, @namespace, 0, @namespace.Length);
}
// Check if the given type is already in scope given the namespace of the current document.
@ -368,13 +368,13 @@ namespace Microsoft.AspNetCore.Razor.Language
// Whereas `MyComponents.SomethingElse.OtherComponent` is not in scope.
internal static bool IsTypeInScope(string typeName, string currentNamespace)
{
if (!TrySplitNamespaceAndType(typeName, out var typeNamespaceTextSpan, out var _) || typeNamespaceTextSpan.Length == 0)
if (!TrySplitNamespaceAndType(typeName, out var namespaceTextSpan, out var _) || namespaceTextSpan.Length == 0)
{
// Either the typeName is not the full type name or this type is at the top level.
return true;
}
var typeNamespace = GetTextSpanContent(typeNamespaceTextSpan, typeName);
var typeNamespace = GetTextSpanContent(namespaceTextSpan, typeName);
var typeNamespaceSegments = typeNamespace.Split(NamespaceSeparators, StringSplitOptions.RemoveEmptyEntries);
var currentNamespaceSegments = currentNamespace.Split(NamespaceSeparators, StringSplitOptions.RemoveEmptyEntries);
if (typeNamespaceSegments.Length > currentNamespaceSegments.Length)
@ -402,8 +402,8 @@ namespace Microsoft.AspNetCore.Razor.Language
{
// If this is a child content tag helper, we want to look at it's original type.
// E.g, if the type name is `Test.__generated__MyComponent.ChildContent`, we want to look at `Test.__generated__MyComponent`.
TrySplitNamespaceAndType(typeName, out var typeNameTextSpan, out var _);
typeName = GetTextSpanContent(typeNameTextSpan, typeName);
TrySplitNamespaceAndType(typeName, out var namespaceTextSpan, out var _);
typeName = GetTextSpanContent(namespaceTextSpan, typeName);
}
if (!TrySplitNamespaceAndType(typeName, out var _, out var classNameTextSpan))
{

View File

@ -0,0 +1,106 @@
@page "/BlazorServerTagHelpers"
@using blazorserver_clean.Data
@inject WeatherForecastService ForecastService
<h1>Counter</h1>
<p>Current count: @currentCount</p>
@{var someVariable = true;} @someVariable
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
<button class="btn btn-primary" @onkeypress:preventDefault @onclick="IncrementCount">Click me</button>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
}
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">blazorserver_clean</a>
<button class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</li>
</ul>
</div>
@code {
private bool collapseNavMenu = true;
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@ -16,6 +16,15 @@
<ItemGroup>
<Compile Include="$(SharedSourceRoot)BenchmarkRunner\*.cs" />
<None Include="MSN.cshtml" CopyToOutputDirectory="PreserveNewest" />
<None Include="taghelpers.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="BlazorServerTagHelpers.razor" CopyToOutputDirectory="PreserveNewest" />
<Compile Include="$(SharedSourceRoot)RazorShared\TagHelperDescriptorJsonConverter.cs">
<Link>Shared\TagHelperDescriptorJsonConverter.cs</Link>
</Compile>
<Compile Include="$(SharedSourceRoot)RazorShared\RazorDiagnosticJsonConverter.cs">
<Link>Shared\RazorDiagnosticJsonConverter.cs</Link>
</Compile>
</ItemGroup>
</Project>

View File

@ -0,0 +1,70 @@
// 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;
using System.Collections.Generic;
using System.IO;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor.Serialization;
using Microsoft.VisualStudio.LanguageServices.Razor.Serialization;
using Newtonsoft.Json;
using static Microsoft.AspNetCore.Razor.Language.DefaultRazorTagHelperBinderPhase;
namespace Microsoft.AspNetCore.Razor.Performance
{
public class RazorTagHelperParsingBenchmark
{
public RazorTagHelperParsingBenchmark()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current != null && !File.Exists(Path.Combine(current.FullName, "taghelpers.json")))
{
current = current.Parent;
}
var root = current;
var tagHelpers = ReadTagHelpers(Path.Combine(root.FullName, "taghelpers.json"));
var blazorServerTagHelpersFilePath = Path.Combine(root.FullName, "BlazorServerTagHelpers.razor");
var fileSystem = RazorProjectFileSystem.Create(root.FullName);
ProjectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, b => RazorExtensions.Register(b));
BlazorServerTagHelpersDemoFile = fileSystem.GetItem(Path.Combine(blazorServerTagHelpersFilePath), FileKinds.Legacy);
ComponentDirectiveVisitor = new ComponentDirectiveVisitor(blazorServerTagHelpersFilePath, tagHelpers, currentNamespace: null);
var codeDocument = ProjectEngine.ProcessDesignTime(BlazorServerTagHelpersDemoFile);
SyntaxTree = codeDocument.GetSyntaxTree();
}
private RazorProjectEngine ProjectEngine { get; }
private RazorProjectItem BlazorServerTagHelpersDemoFile { get; }
private ComponentDirectiveVisitor ComponentDirectiveVisitor { get; }
private RazorSyntaxTree SyntaxTree { get; }
[Benchmark(Description = "TagHelper Design Time Processing")]
public void TagHelper_ProcessDesignTime()
{
_ = ProjectEngine.ProcessDesignTime(BlazorServerTagHelpersDemoFile);
}
[Benchmark(Description = "TagHelper Component Directive Parsing")]
public void TagHelper_ComponentDirectiveVisitor()
{
ComponentDirectiveVisitor.Visit(SyntaxTree);
}
private static IReadOnlyList<TagHelperDescriptor> ReadTagHelpers(string filePath)
{
var serializer = new JsonSerializer();
serializer.Converters.Add(new RazorDiagnosticJsonConverter());
serializer.Converters.Add(new TagHelperDescriptorJsonConverter());
using (var reader = new JsonTextReader(File.OpenText(filePath)))
{
return serializer.Deserialize<IReadOnlyList<TagHelperDescriptor>>(reader);
}
}
}
}

File diff suppressed because one or more lines are too long