E2E benchmarks (#1307)
This commit is contained in:
parent
fd5426943f
commit
d3bc28de55
11
Blazor.sln
11
Blazor.sln
|
|
@ -115,6 +115,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorServerSide-CSharp.App
|
||||||
EndProject
|
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}"
|
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
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{72004416-E278-4787-B84F-40C7E5668D74}.ReleaseNoVSIX|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -474,6 +484,7 @@ Global
|
||||||
{6BDD3018-3961-488E-97D3-1FB7320A8AC6} = {E8EBA72C-D555-43AE-BC98-F0B2D05F6A07}
|
{6BDD3018-3961-488E-97D3-1FB7320A8AC6} = {E8EBA72C-D555-43AE-BC98-F0B2D05F6A07}
|
||||||
{49F0DC0E-D8E6-4E74-8A3F-F024EAAECB8B} = {6BDD3018-3961-488E-97D3-1FB7320A8AC6}
|
{49F0DC0E-D8E6-4E74-8A3F-F024EAAECB8B} = {6BDD3018-3961-488E-97D3-1FB7320A8AC6}
|
||||||
{72004416-E278-4787-B84F-40C7E5668D74} = {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
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}
|
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<Router AppAssembly=typeof(Program).Assembly />
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
@page "/"
|
||||||
|
|
||||||
|
Hello, world!
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
protected override void OnAfterRender()
|
||||||
|
{
|
||||||
|
BenchmarkEvent.Send("Rendered index.cshtml");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
@layout MainLayout
|
||||||
|
|
@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -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');
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -36,6 +36,7 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures
|
||||||
var solutionDir = FindSolutionDir();
|
var solutionDir = FindSolutionDir();
|
||||||
var possibleLocations = new[]
|
var possibleLocations = new[]
|
||||||
{
|
{
|
||||||
|
Path.Combine(solutionDir, "benchmarks", projectName),
|
||||||
Path.Combine(solutionDir, "samples", projectName),
|
Path.Combine(solutionDir, "samples", projectName),
|
||||||
Path.Combine(solutionDir, "test", "testapps", projectName)
|
Path.Combine(solutionDir, "test", "testapps", projectName)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure
|
||||||
public static void True(Func<bool> actual)
|
public static void True(Func<bool> actual)
|
||||||
=> WaitAssertCore(() => Assert.True(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)
|
public static void False(Func<bool> actual)
|
||||||
=> WaitAssertCore(() => Assert.False(actual()));
|
=> WaitAssertCore(() => Assert.False(actual()));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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.Client\HostedInAspNet.Client.csproj" />
|
||||||
<ProjectReference Include="..\..\samples\HostedInAspNet.Server\HostedInAspNet.Server.csproj" />
|
<ProjectReference Include="..\..\samples\HostedInAspNet.Server\HostedInAspNet.Server.csproj" />
|
||||||
<ProjectReference Include="..\..\samples\MonoSanityClient\MonoSanityClient.csproj" />
|
<ProjectReference Include="..\..\samples\MonoSanityClient\MonoSanityClient.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue