Add a perf scenario involing nested components + editing

* Adds additional scenarios to address https://github.com/dotnet/aspnetcore/issues/17011
* Include commit hash so we can track the build of Blazor WASM associated with a perf run
* Port some infrastructure fixes from master

Fixes https://github.com/dotnet/aspnetcore/issues/17011
This commit is contained in:
Pranav K 2020-01-31 15:55:24 -08:00
parent c935e9aa2d
commit 31f63d9e72
14 changed files with 251 additions and 37 deletions

View File

@ -43,7 +43,7 @@
<!-- Exclude the benchmarks because they use <PackageReference>. -->
<ProjectToExclude Include="
$(RepoRoot)src\Components\benchmarkapps\**\*.csproj;
$(RepoRoot)src\Components\benchmarkapps\BlazingPizza.Server\**\*.csproj;
$(RepoRoot)src\Mvc\benchmarkapps\**\*.csproj;
$(RepoRoot)src\Servers\Kestrel\perf\PlatformBenchmarks\**\*.csproj;
$(RepoRoot)src\SignalR\perf\benchmarkapps\**\*.csproj;

View File

@ -9,6 +9,6 @@ namespace Wasm.Performance.Driver
{
public DateTime Timestamp { get; internal set; }
public string Name { get; internal set; }
public double Value { get; internal set; }
public object Value { get; internal set; }
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Text.Encodings.Web;
using System.Text.Json;
@ -93,6 +94,24 @@ namespace Wasm.Performance.Driver
});
}
// Information about the build that this was produced from
output.Metadata.Add(new BenchmarkMetadata
{
Source = "BlazorWasm",
Name = "blazorwasm/commit",
ShortDescription = "Commit Hash",
});
output.Measurements.Add(new BenchmarkMeasurement
{
Timestamp = DateTime.UtcNow,
Name = "blazorwasm/commit",
Value = typeof(Program).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(f => f.Key == "CommitHash")
?.Value,
});
// Statistics about publish sizes
output.Metadata.Add(new BenchmarkMetadata
{
@ -238,7 +257,7 @@ namespace Wasm.Performance.Driver
if (!directory.Exists)
{
return 0;
}
}
var tasks = new List<Task<long>>();
foreach (var item in directory.EnumerateFileSystemInfos())

View File

@ -1,8 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Intentionally pinned this to .NET Core 3.1 since that's the supported version in the docker image -->
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
<OutputType>exe</OutputType>

View File

@ -24,9 +24,8 @@
}
@code {
static string[] Clearances = new[] { "Alpha", "Beta", "Gamma", "Delta", "Epsilon" };
Person smallOrgChart = GenerateOrgChart(1, 4);
Person largeOrgChart = GenerateOrgChart(5, 4);
Person smallOrgChart = Person.GenerateOrgChart(1, 4);
Person largeOrgChart = Person.GenerateOrgChart(5, 4);
string smallOrgChartJson;
string largeOrgChartJson;
int numPeopleDeserialized;
@ -62,23 +61,6 @@
void DeserializeLarge()
=> numPeopleDeserialized = Deserialize(largeOrgChartJson);
static Person GenerateOrgChart(int totalDepth, int numDescendantsPerNode, int thisDepth = 0, string namePrefix = null, int siblingIndex = 0)
{
var name = $"{namePrefix ?? "CEO"} - Subordinate {siblingIndex}";
var rng = new Random(0);
return new Person
{
Name = name,
IsAdmin = siblingIndex % 2 == 0,
Salary = 10000000 / (thisDepth + 1),
SecurityClearances = Clearances
.ToDictionary(c => c, _ => (object)(rng.Next(0, 2) == 0)),
Subordinates = Enumerable.Range(0, thisDepth < totalDepth ? numDescendantsPerNode : 0)
.Select(index => GenerateOrgChart(totalDepth, numDescendantsPerNode, thisDepth + 1, name, index))
.ToList()
};
}
static int Deserialize(string json)
{
var ceo = JsonSerializer.Deserialize<Person>(json);
@ -87,13 +69,4 @@
static int CountPeople(Person root)
=> 1 + (root.Subordinates?.Sum(CountPeople) ?? 0);
class Person
{
public string Name { get; set; }
public int Salary { get; set; }
public bool IsAdmin { get; set; }
public List<Person> Subordinates { get; set; }
public Dictionary<string, object> SecurityClearances { get; set; }
}
}

View File

@ -0,0 +1,38 @@
@page "/orgchart"
@inject IJSRuntime JSRuntime
<h1>Org Chart</h1>
<fieldset>
<label>Depth: <input id="depth" type="number" @bind="depth" /></label>
<label>Subordinates: <input id="subs" type="number" @bind="subs" /></label>
<button id="show" @onclick="Show">Show</button>
<button id="hide" @onclick="Hide">Hide</button>
</fieldset>
@if (show)
{
<PersonDisplay Person="Person.GenerateOrgChart(depth, subs)" />
}
@code
{
int depth = 2;
int subs = 5;
bool show;
protected override void OnAfterRender(bool firstRender)
{
BenchmarkEvent.Send(JSRuntime, "Finished OrgChart rendering");
}
void Hide()
{
show = false;
}
void Show()
{
show = true;
}
}

View File

@ -0,0 +1,52 @@
@inject IJSRuntime JSRuntime
<div class="person">
<h2>
@Person.Name
@if (Person.IsAdmin)
{
<span>[Administrator]</span>
}
</h2>
Salary: $<h3 class="salary">@Person.Salary</h3>
<EditForm Model="Person">
<div>
<label>Salary</label>
<InputNumber @bind-Value="Person.Salary" />
</div>
<div>
<label>Adminstrator: </label>
<InputCheckbox @bind-Value="Person.IsAdmin" />
</div>
</EditForm>
<ul>
@foreach (var kvp in Person.SecurityClearances)
{
<li>@kvp.Key: @kvp.Value</li>
}
</ul>
</div>
@foreach (var person in Person.Subordinates)
{
<ul>
<li>
<PersonDisplay Person="person" />
</li>
</ul>
}
@code
{
[Parameter] public Person Person { get; set; }
protected override void OnAfterRender(bool firstRender)
{
BenchmarkEvent.Send(JSRuntime, "Finished PersonDisplay rendering");
}
}

View File

@ -0,0 +1,38 @@
// 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.Linq;
namespace Wasm.Performance.TestApp
{
public class Person
{
static readonly string[] Clearances = new[] { "Alpha", "Beta", "Gamma", "Delta", "Epsilon" };
public string Name { get; set; }
public int Salary { get; set; }
public bool IsAdmin { get; set; }
public List<Person> Subordinates { get; set; }
public Dictionary<string, object> SecurityClearances { get; set; }
public static Person GenerateOrgChart(int totalDepth, int numDescendantsPerNode, int thisDepth = 0, string namePrefix = null, int siblingIndex = 0)
{
var name = $"{namePrefix ?? "CEO"} - Subordinate {siblingIndex}";
var rng = new Random(0);
return new Person
{
Name = name,
IsAdmin = siblingIndex % 2 == 0,
Salary = 10000000 / (thisDepth + 1),
SecurityClearances = Clearances
.ToDictionary(c => c, _ => (object)(rng.Next(0, 2) == 0)),
Subordinates = Enumerable.Range(0, thisDepth < totalDepth ? numDescendantsPerNode : 0)
.Select(index => GenerateOrgChart(totalDepth, numDescendantsPerNode, thisDepth + 1, name, index))
.ToList()
};
}
}
}

View File

@ -4,7 +4,8 @@
<a href="">Home</a> |
<a href="renderlist">RenderList</a> |
<a href="json">JSON</a>
<a href="json">JSON</a> |
<a href="orgchart">OrgChart</a>
<hr/>

View File

@ -2,8 +2,11 @@
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<ReferenceBlazorBuildLocally>true</ReferenceBlazorBuildLocally>
<RazorLangVersion>3.0</RazorLangVersion>
<HasReferenceAssembly>false</HasReferenceAssembly>
<IsProjectReferenceProvider>false</IsProjectReferenceProvider>
<ReferenceBlazorBuildLocally>true</ReferenceBlazorBuildLocally>
</PropertyGroup>
<ItemGroup>

View File

@ -1,5 +1,6 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using Wasm.Performance.TestApp

View File

@ -3,6 +3,7 @@ import { HtmlUI } from './lib/minibench/minibench.ui.js';
import './appStartup.js';
import './renderList.js';
import './jsonHandling.js';
import './orgChart.js';
new HtmlUI('E2E Performance', '#display');

View File

@ -0,0 +1,88 @@
import { group, benchmark, setup, teardown } from './lib/minibench/minibench.js';
import { BlazorApp } from './util/BlazorApp.js';
import { receiveEvent } from './util/BenchmarkEvents.js';
import { setInputValue } from './util/DOM.js';
group('Nested components', () => {
let app;
setup(async () => {
app = new BlazorApp();
await app.start();
app.navigateTo('orgChart');
});
teardown(() => {
app.dispose();
});
benchmark('Render small nested component', () => measureOrgChart(app, 1, 4), {
descriptor: {
name: 'blazorwasm/orgchart-1-4-org',
description: 'Time to render a complex component with small nesting (ms)'
}
});
benchmark('Render large nested component', () => measureOrgChart(app, 3, 3), {
descriptor: {
name: 'blazorwasm/orgchart-3-3-org',
description: 'Time to render a complex component with large nesting (ms)'
}
});
benchmark('Render component with edit', () => measureOrgChartEdit(app, 3, 2), {
descriptor: {
name: 'blazorwasm/edit-orgchart-3-2',
description: 'Time to peform updates in a nested component (ms)'
}
});
});
async function measureOrgChart(app, depth, subs) {
const appDocument = app.window.document;
setInputValue(appDocument.querySelector('#depth'), depth.toString());
setInputValue(appDocument.querySelector('#subs'), subs.toString());
let nextRenderCompletion = receiveEvent('Finished OrgChart rendering');
appDocument.querySelector('#hide').click();
await nextRenderCompletion;
if (appDocument.querySelectorAll('h2').length !== 0) {
throw new Error('Wrong number of items rendered');
}
nextRenderCompletion = receiveEvent('Finished OrgChart rendering');
appDocument.querySelector('#show').click();
await nextRenderCompletion;
if (appDocument.querySelectorAll('h2').length < depth * subs) {
throw new Error('Wrong number of items rendered');
}
}
async function measureOrgChartEdit(app, depth, subs) {
const appDocument = app.window.document;
setInputValue(appDocument.querySelector('#depth'), depth.toString());
setInputValue(appDocument.querySelector('#subs'), subs.toString());
let nextRenderCompletion = receiveEvent('Finished OrgChart rendering');
appDocument.querySelector('#show').click();
await nextRenderCompletion;
const elements = appDocument.querySelectorAll('.person');
if (!elements) {
throw new Error("No person elements found.");
}
const personElement = elements.item(elements.length / 2);
const display = personElement.querySelector('.salary');
const input = personElement.querySelector('input[type=number]');
nextRenderCompletion = receiveEvent('Finished PersonDisplay rendering');
const updated = (Math.floor(Math.random() * 100000)).toString();
setInputValue(input, updated);
await nextRenderCompletion;
if (display.innerHTML != updated) {
throw new Error('Value not updated after render');
}
}

View File

@ -21,7 +21,8 @@ RUN git init \
&& git reset --hard FETCH_HEAD \
&& git submodule update --init
RUN dotnet publish -c Release -r linux-x64 -o /app ./src/Components/benchmarkapps/Wasm.Performance/Driver/Wasm.Performance.Driver.csproj
RUN ./restore.sh
RUN .dotnet/dotnet publish -c Release -r linux-x64 -o /app ./src/Components/benchmarkapps/Wasm.Performance/Driver/Wasm.Performance.Driver.csproj
RUN chmod +x /app/Wasm.Performance.Driver
WORKDIR /app