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
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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