E2E benchmarks (#1307)

This commit is contained in:
Steve Sanderson 2018-08-14 13:21:19 +01:00 committed by GitHub
parent fd5426943f
commit d3bc28de55
31 changed files with 1105 additions and 1 deletions

View File

@ -115,6 +115,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorServerSide-CSharp.App
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorServerSide-CSharp.Server", "src\Microsoft.AspNetCore.Blazor.Templates\content\BlazorServerSide-CSharp\BlazorServerSide-CSharp.Server\BlazorServerSide-CSharp.Server.csproj", "{72004416-E278-4787-B84F-40C7E5668D74}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Blazor.E2EPerformance", "benchmarks\Microsoft.AspNetCore.Blazor.E2EPerformance\Microsoft.AspNetCore.Blazor.E2EPerformance.csproj", "{CCEC81C4-1A3C-40DC-952F-074712C46180}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -422,6 +424,14 @@ Global
{72004416-E278-4787-B84F-40C7E5668D74}.Release|Any CPU.Build.0 = Release|Any CPU
{72004416-E278-4787-B84F-40C7E5668D74}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{72004416-E278-4787-B84F-40C7E5668D74}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{CCEC81C4-1A3C-40DC-952F-074712C46180}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CCEC81C4-1A3C-40DC-952F-074712C46180}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CCEC81C4-1A3C-40DC-952F-074712C46180}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{CCEC81C4-1A3C-40DC-952F-074712C46180}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{CCEC81C4-1A3C-40DC-952F-074712C46180}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CCEC81C4-1A3C-40DC-952F-074712C46180}.Release|Any CPU.Build.0 = Release|Any CPU
{CCEC81C4-1A3C-40DC-952F-074712C46180}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{CCEC81C4-1A3C-40DC-952F-074712C46180}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -474,6 +484,7 @@ Global
{6BDD3018-3961-488E-97D3-1FB7320A8AC6} = {E8EBA72C-D555-43AE-BC98-F0B2D05F6A07}
{49F0DC0E-D8E6-4E74-8A3F-F024EAAECB8B} = {6BDD3018-3961-488E-97D3-1FB7320A8AC6}
{72004416-E278-4787-B84F-40C7E5668D74} = {6BDD3018-3961-488E-97D3-1FB7320A8AC6}
{CCEC81C4-1A3C-40DC-952F-074712C46180} = {36A7DEB7-5F88-4BFB-B57E-79EEC9950E25}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}

View File

@ -0,0 +1 @@
<Router AppAssembly=typeof(Program).Assembly />

View File

@ -0,0 +1,17 @@
// 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 Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Blazor.E2EPerformance
{
public static class BenchmarkEvent
{
public static void Send(string name)
{
((IJSInProcessRuntime)JSRuntime.Current).Invoke<object>(
"receiveBenchmarkEvent",
name);
}
}
}

View File

@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<!-- Workaround for #261 which requires the bin and obj directory be in the project directory -->
<DisableArcadeProjectLayout>true</DisableArcadeProjectLayout>
</PropertyGroup>
<Import Project="..\..\Directory.Build.props" />
</Project>

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<!-- Local alternative to <RunArguments>blazor serve</RunArguments> -->
<RunCommand>dotnet</RunCommand>
<RunArguments>run --project ../../src/Microsoft.AspNetCore.Blazor.Cli serve</RunArguments>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Blazor\Microsoft.AspNetCore.Blazor.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Blazor.Browser\Microsoft.AspNetCore.Blazor.Browser.csproj" />
</ItemGroup>
<!-- Local alternative to <PackageReference Include="Microsoft.AspNetCore.Blazor.Build" /> -->
<Import Project="..\..\src\Microsoft.AspNetCore.Blazor.Build\ReferenceFromSource.props" />
</Project>

View File

@ -0,0 +1,10 @@
@page "/"
Hello, world!
@functions {
protected override void OnAfterRender()
{
BenchmarkEvent.Send("Rendered index.cshtml");
}
}

View File

@ -0,0 +1,97 @@
@page "/json"
<h2>JSON performance</h2>
<p><button id="reset-all" onclick=@Reset>Reset</button></p>
<button id="serialize-small" onclick=@SerializeSmall>Serialize (small)</button>
<button id="serialize-large" onclick=@SerializeLarge>Serialize (large)</button>
<p><pre style="border: 1px solid black; overflow: scroll;">@serializedValue</pre></p>
@if (serializedValue != null)
{
<p>Serialized length: <strong id="serialized-length">@serializedValue.Length</strong> chars</p>
}
<button id="deserialize-small" onclick=@DeserializeSmall>Deserialize (small)</button>
<button id="deserialize-large" onclick=@DeserializeLarge>Deserialize (large)</button>
@if (numPeopleDeserialized > 0)
{
<p>Deserialized <strong id="deserialized-count">@numPeopleDeserialized</strong> people</p>
}
@functions {
static string[] Clearances = new[] { "Alpha", "Beta", "Gamma", "Delta", "Epsilon" };
Person smallOrgChart = GenerateOrgChart(1, 4);
Person largeOrgChart = GenerateOrgChart(5, 4);
string smallOrgChartJson;
string largeOrgChartJson;
int numPeopleDeserialized;
protected override void OnInit()
{
smallOrgChartJson = Microsoft.JSInterop.Json.Serialize(smallOrgChart);
largeOrgChartJson = Microsoft.JSInterop.Json.Serialize(largeOrgChart);
}
protected override void OnAfterRender()
{
BenchmarkEvent.Send("Finished JSON processing");
}
string serializedValue;
void Reset()
{
serializedValue = null;
numPeopleDeserialized = 0;
}
void SerializeSmall()
=> serializedValue = Microsoft.JSInterop.Json.Serialize(smallOrgChart);
void SerializeLarge()
=> serializedValue = Microsoft.JSInterop.Json.Serialize(largeOrgChart);
void DeserializeSmall()
=> numPeopleDeserialized = Deserialize(smallOrgChartJson);
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 = Microsoft.JSInterop.Json.Deserialize<Person>(json);
return CountPeople(ceo);
}
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,74 @@
@page "/renderlist"
<h2>Render List</h2>
Number of items: <input id="num-items" type="number" bind=@numItems />
<button id="show-list" onclick=@Show>Show</button>
<button id="hide-list" onclick=@Hide>Hide</button>
@if (show)
{
<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 GenerateForecasts(numItems))
{
<tr>
<td>@forecast.DateFormatted</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@functions {
int numItems = 10;
bool show = false;
void Hide()
{
show = false;
}
void Show()
{
show = true;
}
protected override void OnAfterRender()
{
BenchmarkEvent.Send("Finished rendering list");
}
static IEnumerable<WeatherForecast> GenerateForecasts(int count)
{
for (var i = 0; i < count; i++)
{
yield return new WeatherForecast
{
DateFormatted = DateTime.Now.AddDays(i).ToShortDateString(),
TemperatureC = i % 100,
TemperatureF = (int)((i % 100) * 1.8) + 32,
Summary = $"Item {i}",
};
}
}
class WeatherForecast
{
public string DateFormatted { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF { get; set; }
public string Summary { get; set; }
}
}

View File

@ -0,0 +1 @@
@layout MainLayout

View File

@ -0,0 +1,19 @@
// 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 Microsoft.AspNetCore.Blazor.Hosting;
namespace Microsoft.AspNetCore.Blazor.E2EPerformance
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
BlazorWebAssemblyHost.CreateDefaultBuilder()
.UseBlazorStartup<Startup>();
}
}

View File

@ -0,0 +1,13 @@
@inherits BlazorLayoutComponent
<h1>E2E Performance</h1>
<a href="">Home</a> |
<a href="renderlist">RenderList</a> |
<a href="json">JSON</a>
<hr/>
<div>
@Body
</div>

View File

@ -0,0 +1,20 @@
// 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 Microsoft.AspNetCore.Blazor.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Blazor.E2EPerformance
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
}
public void Configure(IBlazorApplicationBuilder app)
{
app.AddComponent<App>("app");
}
}
}

View File

@ -0,0 +1,6 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Blazor.Layouts
@using Microsoft.AspNetCore.Blazor.Routing
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Blazor.E2EPerformance
@using Microsoft.AspNetCore.Blazor.E2EPerformance.Shared

View File

@ -0,0 +1,15 @@
import { group, benchmark } from './lib/minibench/minibench.js';
import { BlazorApp } from './util/BlazorApp.js';
group('App Startup', () => {
benchmark('Time to first UI', async () => {
const app = new BlazorApp();
try {
await app.start();
} finally {
app.dispose();
}
});
});

View File

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

View File

@ -0,0 +1,57 @@
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';
import { largeJsonToDeserialize, largeObjectToSerialize } from './jsonHandlingData.js';
group('JSON handling', () => {
let app;
setup(async () => {
app = new BlazorApp();
await app.start();
app.navigateTo('json');
});
teardown(() => app.dispose());
benchmark('Serialize 1kb', () =>
benchmarkJson(app, '#serialize-small', '#serialized-length', 935));
benchmark('Serialize 340kb', () =>
benchmarkJson(app, '#serialize-large', '#serialized-length', 339803));
benchmark('Deserialize 1kb', () =>
benchmarkJson(app, '#deserialize-small', '#deserialized-count', 5));
benchmark('Deserialize 340kb', () =>
benchmarkJson(app, '#deserialize-large', '#deserialized-count', 1365));
benchmark('Serialize 340kb (JavaScript)', () => {
const json = JSON.stringify(largeObjectToSerialize);
if (json.length !== 339803) {
throw new Error(`Incorrect length: ${json.length}`);
}
});
benchmark('Deserialize 340kb (JavaScript)', () => {
const parsed = JSON.parse(largeJsonToDeserialize);
if (parsed.name !== 'CEO - Subordinate 0') {
throw new Error('Incorrect result');
}
});
});
async function benchmarkJson(app, buttonSelector, resultSelector, expectedResult) {
const appDocument = app.window.document;
appDocument.querySelector('#reset-all').click();
let nextRenderCompletion = receiveEvent('Finished JSON processing');
appDocument.querySelector(buttonSelector).click();
await nextRenderCompletion;
const resultElem = appDocument.querySelector(resultSelector);
if (resultElem.textContent != expectedResult.toString()) {
throw new Error(`Incorrect result: ${resultElem.textContent}`);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,17 @@
# Minibench
A simple harness for benchmarking JavaScript code.
Supports both synchronous and asynchronous code being benchmarked, plus both sync/async setup and teardown logic.
## Sample
See `sample-benchmarks.js`. To run it, serve `sample.html` from a local webserver.
## Caveats
Absolutely no support provided.
## License
MIT

View File

@ -0,0 +1,486 @@
/** minibench - https://github.com/SteveSanderson/minibench */
class EventEmitter {
constructor() {
this.eventListeners = {};
}
on(eventName, callback, options) {
const listeners = this.eventListeners[eventName] = this.eventListeners[eventName] || [];
const handler = argsArray => {
if (options && options.once) {
const thisIndex = listeners.indexOf(handler);
listeners.splice(thisIndex, 1);
}
callback.apply(null, argsArray);
};
listeners.push(handler);
}
once(eventName, callback) {
this.on(eventName, callback, { once: true });
}
_emit(eventName, ...args) {
const listeners = this.eventListeners[eventName];
listeners && listeners.forEach(l => l.call(null, args));
}
}
let currentPromise = new Promise(resolve => resolve());
function addToWorkQueue(fn) {
const cancelHandle = new CancelHandle();
currentPromise = currentPromise.then(() => cancelHandle.isCancelled || fn());
return cancelHandle;
}
class CancelHandle {
cancel() {
this.isCancelled = true;
}
}
const queue = [];
const messageIdentifier = 'nextTick-' + Math.random();
function nextTick(callback) {
queue.push(callback);
window.postMessage(messageIdentifier, '*');
}
function nextTickPromise() {
return new Promise(resolve => nextTick(resolve));
}
window.addEventListener('message', evt => {
if (evt.data === messageIdentifier) {
evt.stopPropagation();
const callback = queue.shift();
callback && callback();
}
});
/*
To work around browsers' current nonsupport for high-resolution timers
(since Spectre etc.), the approach used here is to group executions into
blocks of roughly fixed duration.
- In each block, we execute the test code as many times as we can until
the end of the block duration, without even yielding the thread if
it's a synchronous call. We count how many executions completed. It
will always be at least 1, even if the single call duration is longer
than the intended block duration.
- Since each block is of a significant duration (e.g., 0.5 sec), the low
resolution of the timer doesn't matter. We can divide the measured block
duration by the measured number of executions to estimate the per-call
duration.
- Each block will give us a different estimate. We want to return the *best*
timing, not the mean or median. That's the most accurate predictor of the
true execution cost, as hopefully there will have been at least one block
during which there was no unrelated GC cycle or other background contention.
- We keep running blocks until some larger timeout occurs *and* we've done
at least some minimum number of executions.
Note that this approach does *not* allow for per-execution setup/teardown
logic whose timing is separated from the code under test. Because of the
low timer precision, there would be no way to separate the setup duration
from the test code duration if they were interleaved too quickly (e.g.,
if the test code was < 1ms). We do support per-benchmark setup/teardown,
but not per-execution.
*/
const totalDurationMs = 6000;
const blockDurationMs = 400;
const minExecutions = 10;
class ExecutionTimer {
constructor(fn) {
this._fn = fn;
}
async run(progressCallback, runOptions) {
this._isAborted = false;
this.numExecutions = 0;
this.bestExecutionsPerMs = null;
// 'verify only' means just do a single execution to check it doesn't error
const targetBlockDuration = runOptions.verifyOnly ? 1 : blockDurationMs;
const targetMinExecutions = runOptions.verifyOnly ? 1 : minExecutions;
const targetTotalDuration = runOptions.verifyOnly ? 0 : totalDurationMs;
const endTime = performance.now() + targetTotalDuration;
while (performance.now() < endTime || this.numExecutions < targetMinExecutions) {
if (this._isAborted) {
this.numExecutions = 0;
this.bestExecutionsPerMs = null;
break;
}
const { blockDuration, blockExecutions } = await this._runBlock(targetBlockDuration);
this.numExecutions += blockExecutions;
const blockExecutionsPerMs = blockExecutions / blockDuration;
if (blockExecutionsPerMs > this.bestExecutionsPerMs) {
this.bestExecutionsPerMs = blockExecutionsPerMs;
}
progressCallback && progressCallback();
}
}
abort() {
this._isAborted = true;
}
async _runBlock(targetBlockDuration) {
await nextTickPromise();
const blockStartTime = performance.now();
const blockEndTime = blockStartTime + targetBlockDuration;
let executions = 0;
while ((performance.now() < blockEndTime) && !this._isAborted) {
const syncResult = this._fn();
// Only yield the thread if we really have to
if (syncResult instanceof Promise) {
await syncResult;
}
executions++;
}
return {
blockDuration: performance.now() - blockStartTime,
blockExecutions: executions
};
}
}
class Benchmark extends EventEmitter {
constructor(group, name, fn, options) {
super();
this._group = group;
this.name = name;
this._fn = fn;
this._options = options;
this._state = { status: BenchmarkStatus.idle };
}
get state() {
return this._state;
}
run(runOptions) {
this._currentRunWasAborted = false;
if (this._state.status === BenchmarkStatus.idle) {
this._updateState({ status: BenchmarkStatus.queued });
this.workQueueCancelHandle = addToWorkQueue(async () => {
try {
if (!(runOptions && runOptions.skipGroupSetup)) {
await this._group.runSetup();
}
this._updateState({ status: BenchmarkStatus.running });
this._options && this._options.setup && await this._options.setup();
await this._measureTimings(runOptions);
this._options && this._options.teardown && await this._options.teardown();
if (this._currentRunWasAborted || !(runOptions && runOptions.skipGroupTeardown)) {
await this._group.runTeardown();
}
this._updateState({ status: BenchmarkStatus.idle });
} catch (ex) {
this._updateState({ status: BenchmarkStatus.error });
console.error(ex);
}
});
}
}
stop() {
this._currentRunWasAborted = true;
this.timer && this.timer.abort();
this.workQueueCancelHandle && this.workQueueCancelHandle.cancel();
this._updateState({ status: BenchmarkStatus.idle });
}
async _measureTimings(runOptions) {
this._updateState({ numExecutions: 0, estimatedExecutionDurationMs: null });
this.timer = new ExecutionTimer(this._fn);
const updateTimingsDisplay = () => {
this._updateState({
numExecutions: this.timer.numExecutions,
estimatedExecutionDurationMs: this.timer.bestExecutionsPerMs ? 1 / this.timer.bestExecutionsPerMs : null
});
};
await this.timer.run(updateTimingsDisplay, { verifyOnly: runOptions.verifyOnly });
updateTimingsDisplay();
this.timer = null;
}
_updateState(newState) {
Object.assign(this._state, newState);
this._emit('changed', this._state);
}
}
const BenchmarkStatus = {
idle: 0,
queued: 1,
running: 2,
error: 3,
};
class Group extends EventEmitter {
constructor(name) {
super();
this.name = name;
this.benchmarks = [];
}
add(benchmark) {
this.benchmarks.push(benchmark);
benchmark.on('changed', () => this._emit('changed'));
}
runAll(runOptions) {
this.benchmarks.forEach((benchmark, index) => {
benchmark.run(Object.assign({
skipGroupSetup: index > 0,
skipGroupTeardown: index < this.benchmarks.length - 1,
}, runOptions));
});
}
stopAll() {
this.benchmarks.forEach(b => b.stop());
}
async runSetup() {
this.setup && await this.setup();
}
async runTeardown() {
this.teardown && await this.teardown();
}
get status() {
return this.benchmarks.reduce(
(prev, next) => Math.max(prev, next.state.status),
BenchmarkStatus.idle
);
}
}
const groups = [];
function group(name, configure) {
groups.push(new Group(name));
configure && configure();
}
function benchmark(name, fn, options) {
const group = groups[groups.length - 1];
group.add(new Benchmark(group, name, fn, options));
}
function setup(fn) {
groups[groups.length - 1].setup = fn;
}
function teardown(fn) {
groups[groups.length - 1].teardown = fn;
}
class BenchmarkDisplay {
constructor(htmlUi, benchmark) {
this.benchmark = benchmark;
this.elem = document.createElement('tr');
const headerCol = this.elem.appendChild(document.createElement('th'));
headerCol.className = 'pl-4';
headerCol.textContent = benchmark.name;
headerCol.setAttribute('scope', 'row');
const progressCol = this.elem.appendChild(document.createElement('td'));
this.numExecutionsText = progressCol.appendChild(document.createTextNode(''));
const timingCol = this.elem.appendChild(document.createElement('td'));
this.executionDurationText = timingCol.appendChild(document.createElement('span'));
const runCol = this.elem.appendChild(document.createElement('td'));
runCol.className = 'pr-4';
runCol.setAttribute('align', 'right');
this.runButton = document.createElement('a');
this.runButton.className = 'run-button';
runCol.appendChild(this.runButton);
this.runButton.textContent = 'Run';
this.runButton.onclick = evt => {
evt.preventDefault();
this.benchmark.run(htmlUi.globalRunOptions);
};
benchmark.on('changed', state => this.updateDisplay(state));
this.updateDisplay(this.benchmark.state);
}
updateDisplay(state) {
const benchmark = this.benchmark;
this.elem.className = rowClass(state.status);
this.runButton.textContent = runButtonText(state.status);
this.numExecutionsText.textContent = state.numExecutions
? `Executions: ${state.numExecutions}` : '';
this.executionDurationText.innerHTML = state.estimatedExecutionDurationMs
? `Duration: <b>${parseFloat(state.estimatedExecutionDurationMs.toPrecision(3))}ms</b>` : '';
if (state.status === BenchmarkStatus.idle) {
this.runButton.setAttribute('href', '');
} else {
this.runButton.removeAttribute('href');
if (state.status === BenchmarkStatus.error) {
this.numExecutionsText.textContent = 'Error - see console';
}
}
}
}
function runButtonText(status) {
switch (status) {
case BenchmarkStatus.idle:
case BenchmarkStatus.error:
return 'Run';
case BenchmarkStatus.queued:
return 'Waiting...';
case BenchmarkStatus.running:
return 'Running...';
default:
throw new Error(`Unknown status: ${status}`);
}
}
function rowClass(status) {
switch (status) {
case BenchmarkStatus.idle:
return 'benchmark-idle';
case BenchmarkStatus.queued:
return 'benchmark-waiting';
case BenchmarkStatus.running:
return 'benchmark-running';
case BenchmarkStatus.error:
return 'benchmark-error';
default:
throw new Error(`Unknown status: ${status}`);
}
}
class GroupDisplay {
constructor(htmlUi, group) {
this.group = group;
this.elem = document.createElement('div');
this.elem.className = 'my-3 py-2 bg-white rounded shadow-sm';
const headerContainer = this.elem.appendChild(document.createElement('div'));
headerContainer.className = 'd-flex align-items-baseline px-4';
const header = headerContainer.appendChild(document.createElement('h5'));
header.className = 'py-2';
header.textContent = group.name;
this.runButton = document.createElement('a');
this.runButton.className = 'ml-auto run-button';
this.runButton.setAttribute('href', '');
headerContainer.appendChild(this.runButton);
this.runButton.textContent = 'Run all';
this.runButton.onclick = evt => {
evt.preventDefault();
group.runAll(htmlUi.globalRunOptions);
};
const table = this.elem.appendChild(document.createElement('table'));
table.className = 'table mb-0 benchmarks';
const tbody = table.appendChild(document.createElement('tbody'));
group.benchmarks.forEach(benchmark => {
const benchmarkDisplay = new BenchmarkDisplay(htmlUi, benchmark);
tbody.appendChild(benchmarkDisplay.elem);
});
group.on('changed', () => this.updateDisplay());
this.updateDisplay();
}
updateDisplay() {
const canRun = this.group.status === BenchmarkStatus.idle;
this.runButton.style.display = canRun ? 'block' : 'none';
}
}
class HtmlUI {
constructor(title, selector) {
this.containerElement = document.querySelector(selector);
const headerDiv = this.containerElement.appendChild(document.createElement('div'));
headerDiv.className = 'd-flex align-items-center';
const header = headerDiv.appendChild(document.createElement('h2'));
header.className = 'mx-3 flex-grow-1';
header.textContent = title;
const verifyCheckboxLabel = document.createElement('label');
verifyCheckboxLabel.className = 'ml-auto mr-5';
headerDiv.appendChild(verifyCheckboxLabel);
this.verifyCheckbox = verifyCheckboxLabel.appendChild(document.createElement('input'));
this.verifyCheckbox.type = 'checkbox';
this.verifyCheckbox.className = 'mr-2';
verifyCheckboxLabel.appendChild(document.createTextNode('Verify only'));
this.runButton = document.createElement('button');
this.runButton.className = 'btn btn-success ml-auto px-4 run-button';
headerDiv.appendChild(this.runButton);
this.runButton.textContent = 'Run all';
this.runButton.onclick = () => {
groups.forEach(g => g.runAll(this.globalRunOptions));
};
this.stopButton = document.createElement('button');
this.stopButton.className = 'btn btn-danger ml-auto px-4 stop-button';
headerDiv.appendChild(this.stopButton);
this.stopButton.textContent = 'Stop';
this.stopButton.onclick = () => {
groups.forEach(g => g.stopAll());
};
groups.forEach(group$$1 => {
const groupDisplay = new GroupDisplay(this, group$$1);
this.containerElement.appendChild(groupDisplay.elem);
group$$1.on('changed', () => this.updateDisplay());
});
this.updateDisplay();
}
updateDisplay() {
const areAllIdle = groups.reduce(
(prev, next) => prev && next.status === BenchmarkStatus.idle,
true
);
this.runButton.style.display = areAllIdle ? 'block' : 'none';
this.stopButton.style.display = areAllIdle ? 'none' : 'block';
}
get globalRunOptions() {
return { verifyOnly: this.verifyCheckbox.checked };
}
}
/**
* minibench
* https://github.com/SteveSanderson/minibench
*/
export { group, benchmark, setup, teardown, HtmlUI };

View File

@ -0,0 +1,19 @@
body { padding: 2rem 0; background: #e8e8e8; }
.run-button::before {
content: '▶';
font-family: 'Segoe UI Symbol', sans-serif;
font-size: 90%;
margin-right: 8px;
}
.run-button:hover { text-decoration: none; }
.benchmarks th { width: 40%; }
.benchmarks td:nth-child(1) { width: 20%; color: #888; }
.benchmarks td:nth-child(2) { width: 20%; color: #888; }
.benchmarks td:nth-child(3) { width: 20%; color: #888; }
.benchmarks b { color: black; font-weight: normal; }
.benchmark-running { background-color: #fff7a1; }
.benchmark-waiting { background-color: #f1f1f1; color: #bebebe; }
.benchmark-error { background-color: #ffa1a1; }
.benchmark-running .run-button::before, .benchmark-waiting .run-button::before {
display: none;
}

View File

@ -0,0 +1,45 @@
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('Rendering list', () => {
let app;
setup(async () => {
app = new BlazorApp();
await app.start();
app.navigateTo('renderList');
});
teardown(() => {
app.dispose();
});
benchmark('Render 10 items', () => measureRenderList(app, 10));
benchmark('Render 100 items', () => measureRenderList(app, 100));
benchmark('Render 1000 items', () => measureRenderList(app, 1000));
});
async function measureRenderList(app, numItems) {
const appDocument = app.window.document;
const numItemsTextbox = appDocument.querySelector('#num-items');
setInputValue(numItemsTextbox, numItems.toString());
let nextRenderCompletion = receiveEvent('Finished rendering list');
appDocument.querySelector('#hide-list').click();
await nextRenderCompletion;
if (appDocument.querySelectorAll('tbody tr').length !== 0) {
throw new Error('Wrong number of items rendered');
}
nextRenderCompletion = receiveEvent('Finished rendering list');
appDocument.querySelector('#show-list').click();
await nextRenderCompletion;
if (appDocument.querySelectorAll('tbody tr').length !== numItems) {
throw new Error('Wrong number of items rendered');
}
}

View File

@ -0,0 +1,21 @@
const pendingCallbacksByEventName = {};
// Returns a promise that resolves the next time we receive the specified event
export function receiveEvent(name) {
let capturedResolver;
const resultPromise = new Promise(resolve => {
capturedResolver = resolve;
});
pendingCallbacksByEventName[name] = pendingCallbacksByEventName[name] || [];
pendingCallbacksByEventName[name].push(capturedResolver);
return resultPromise;
}
// Listen for messages forwarded from the child frame
window.receiveBenchmarkEvent = function (name) {
const callbacks = pendingCallbacksByEventName[name];
delete pendingCallbacksByEventName[name];
callbacks && callbacks.forEach(callback => callback());
}

View File

@ -0,0 +1,25 @@
import { receiveEvent } from './BenchmarkEvents.js';
export class BlazorApp {
constructor() {
this._frame = document.createElement('iframe');
document.body.appendChild(this._frame);
}
get window() {
return this._frame.contentWindow;
}
async start() {
this._frame.src = 'blazor-frame.html';
await receiveEvent('Rendered index.cshtml');
}
navigateTo(url) {
this.window.Blazor.navigateTo(url);
}
dispose() {
document.body.removeChild(this._frame);
}
}

View File

@ -0,0 +1,7 @@
export function setInputValue(inputElement, value) {
inputElement.value = value;
const event = document.createEvent('HTMLEvents');
event.initEvent('change', false, true);
inputElement.dispatchEvent(event);
}

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>E2EPerformance</title>
<base href="/" />
</head>
<body>
<script>
// Pass through benchmark events to the iframe parent
window.receiveBenchmarkEvent = function () {
if (window !== window.parent) { // Only if we're running in a frame
window.parent.receiveBenchmarkEvent.apply(window.parent, arguments);
}
};
// Behave as if we were loaded from / instead of /blazor-frame.html
history.pushState('', null, '/');
</script>
<app>Loading...</app>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>E2EPerformance</title>
<link href="benchmarks/lib/bootstrap.min.css" rel="stylesheet" />
<link href="benchmarks/lib/minibench/style.css" rel="stylesheet" />
</head>
<body style="overflow: scroll;">
<div class="container" id="display"></div>
<p class="container px-3">
<a href="blazor-frame.html">View benchmark app ⮕</a>
</p>
<script type="module" src="benchmarks/index.js"></script>
</body>
</html>

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -36,6 +36,7 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures
var solutionDir = FindSolutionDir();
var possibleLocations = new[]
{
Path.Combine(solutionDir, "benchmarks", projectName),
Path.Combine(solutionDir, "samples", projectName),
Path.Combine(solutionDir, "test", "testapps", projectName)
};

View File

@ -23,6 +23,9 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure
public static void True(Func<bool> actual)
=> WaitAssertCore(() => Assert.True(actual()));
public static void True(Func<bool> actual, TimeSpan timeout)
=> WaitAssertCore(() => Assert.True(actual()), timeout);
public static void False(Func<bool> actual)
=> WaitAssertCore(() => Assert.False(actual()));

View File

@ -18,6 +18,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\benchmarks\Microsoft.AspNetCore.Blazor.E2EPerformance\Microsoft.AspNetCore.Blazor.E2EPerformance.csproj" />
<ProjectReference Include="..\..\samples\HostedInAspNet.Client\HostedInAspNet.Client.csproj" />
<ProjectReference Include="..\..\samples\HostedInAspNet.Server\HostedInAspNet.Server.csproj" />
<ProjectReference Include="..\..\samples\MonoSanityClient\MonoSanityClient.csproj" />

View File

@ -0,0 +1,56 @@
// 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 Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures;
using OpenQA.Selenium;
using System;
using System.Linq;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
{
public class PerformanceTest
: ServerTestBase<DevHostServerFixture<E2EPerformance.Program>>
{
public PerformanceTest(
BrowserFixture browserFixture,
DevHostServerFixture<E2EPerformance.Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
Navigate("/", noReload: true);
}
[Fact]
public void HasTitle()
{
Assert.Equal("E2EPerformance", Browser.Title);
}
[Fact]
public void BenchmarksRunWithoutError()
{
// In CI, we only verify that the benchmarks run without throwing any
// errors. To get actual perf numbers, you must run the E2EPerformance
// site manually.
var verifyOnlyLabel = Browser.FindElement(By.XPath("//label[contains(text(), 'Verify only')]/input"));
verifyOnlyLabel.Click();
var runAllButton = Browser.FindElement(By.CssSelector("button.btn-success.run-button"));
runAllButton.Click();
// The "run" button goes away while the benchmarks execute, then it comes back
WaitAssert.False(() => runAllButton.Displayed);
WaitAssert.True(
() => runAllButton.Displayed || Browser.FindElements(By.CssSelector(".benchmark-error")).Any(),
TimeSpan.FromSeconds(60));
var finishedBenchmarks = Browser.FindElements(By.CssSelector(".benchmark-idle"));
var failedBenchmarks = Browser.FindElements(By.CssSelector(".benchmark-error"));
Assert.NotEmpty(finishedBenchmarks);
Assert.Empty(failedBenchmarks);
}
}
}