Blazor grid performance scenarios (#23301)

* Add complex table benchmark

* Add FastGrid scenario too

* Make the two grids consistent with each other

* Add scenario for PlainTable

* Empty commit to trigger re-rerun on CI. Clicking retry doesn't seem to be working.
This commit is contained in:
Steve Sanderson 2020-06-25 09:32:09 +01:00 committed by GitHub
parent 38f9b9abdb
commit 1998a06bdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 592 additions and 1 deletions

View File

@ -0,0 +1,122 @@
@page "/gridrendering"
@inject IJSRuntime JSRuntime
@using Wasm.Performance.TestApp.Shared.FastGrid
<h1>20 x 200 Grid</h1>
<fieldset>
<select id="render-mode" @bind="selectedRenderMode">
<option>@RenderMode.FastGrid</option>
<option>@RenderMode.PlainTable</option>
<option>@RenderMode.ComplexTable</option>
</select>
<button id="show" @onclick="Show">Show</button>
<button id="hide" @onclick="Hide">Hide</button>
@if (forecasts != null)
{
<button id="change-page" @onclick="ChangePage">Switch pages</button>
}
</fieldset>
@if (forecasts == null)
{
<p><em>(No data assigned)</em></p>
}
else if (selectedRenderMode == RenderMode.FastGrid)
{
<p>FastGrid represents a minimal, optimized implementation of a grid.</p>
<Grid Data="@forecasts">
<GridColumn TRowData="WeatherForecast" Title="Date">@context.Date.ToShortDateString()</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="TemperatureC">@context.TemperatureC</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="TemperatureF">@context.TemperatureF</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="Summary">@context.Summary</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="Date">@context.Date.ToShortDateString()</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="TemperatureC">@context.TemperatureC</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="TemperatureF">@context.TemperatureF</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="Summary">@context.Summary</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="Date">@context.Date.ToShortDateString()</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="TemperatureC">@context.TemperatureC</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="TemperatureF">@context.TemperatureF</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="Summary">@context.Summary</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="Date">@context.Date.ToShortDateString()</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="TemperatureC">@context.TemperatureC</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="TemperatureF">@context.TemperatureF</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="Summary">@context.Summary</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="Date">@context.Date.ToShortDateString()</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="TemperatureC">@context.TemperatureC</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="TemperatureF">@context.TemperatureF</GridColumn>
<GridColumn TRowData="WeatherForecast" Title="Summary">@context.Summary</GridColumn>
</Grid>
}
else if (selectedRenderMode == RenderMode.PlainTable)
{
<p>PlainTable represents a minimal but not optimized implementation of a grid.</p>
<Wasm.Performance.TestApp.Shared.PlainTable.TableComponent Data="@forecasts" Columns="@Columns" />
}
else if (selectedRenderMode == RenderMode.ComplexTable)
{
<p>ComplexTable represents a maximal, not optimized implementation of a grid, using a wide range of Blazor features at once.</p>
<Wasm.Performance.TestApp.Shared.ComplexTable.TableComponent Data="@forecasts" Columns="@Columns" />
}
@code {
enum RenderMode { PlainTable, ComplexTable, FastGrid }
private RenderMode selectedRenderMode = RenderMode.FastGrid;
private WeatherForecast[] forecasts;
public List<string> Columns { get; set; } = new List<string>
{
"Date", "TemperatureC", "TemperatureF", "Summary",
"Date", "TemperatureC", "TemperatureF", "Summary",
"Date", "TemperatureC", "TemperatureF", "Summary",
"Date", "TemperatureC", "TemperatureF", "Summary",
"Date", "TemperatureC", "TemperatureF", "Summary",
};
private static string[] sampleSummaries = new[] { "Balmy", "Chilly", "Freezing", "Bracing" };
private static WeatherForecast[] staticSampleDataPage1 = Enumerable.Range(0, 200).Select(CreateSampleDataItem).ToArray();
private static WeatherForecast[] staticSampleDataPage2 = Enumerable.Range(200, 200).Select(CreateSampleDataItem).ToArray();
private static WeatherForecast CreateSampleDataItem(int index) => new WeatherForecast
{
Date = DateTime.Now.Date.AddDays(index),
Summary = sampleSummaries[index % sampleSummaries.Length],
TemperatureC = index,
};
void Show()
{
forecasts = staticSampleDataPage1;
}
void Hide()
{
forecasts = null;
}
void ChangePage()
{
forecasts = (forecasts == staticSampleDataPage1) ? staticSampleDataPage2 : staticSampleDataPage1;
}
protected override void OnAfterRender(bool firstRender)
{
BenchmarkEvent.Send(JSRuntime, "Finished rendering table");
}
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@ -0,0 +1,59 @@
@using WeatherForecast = Pages.GridRendering.WeatherForecast
<td @attributes="@Attributes"
@onclick="@(() => OnClick.Invoke(CellIndex))"
>
@switch (Field)
{
case "Date":
@Item.Date.ToShortDateString()
break;
case "TemperatureC":
@Item.TemperatureC
break;
case "TemperatureF":
@Item.TemperatureF
break;
case "Summary":
@Item.Summary
break;
}
</td>
@code {
[Parameter]
public WeatherForecast Item { get; set; }
[CascadingParameter]
public TableComponent ParentTable { get; set; }
[Parameter]
public string Field { get; set; }
[Parameter]
public int CellIndex { get; set; }
[Parameter]
public int RowIndex { get; set; }
[Parameter]
public bool Selected { get; set; }
[Parameter]
public string FormatString { get; set; }
[Parameter]
public Func<int, Task> OnClick { get; set; }
private protected Dictionary<string, object> Attributes
{
get
{
var attributes = new Dictionary<string, object>();
attributes["tabindex"] = CellIndex;
return attributes;
}
}
}

View File

@ -0,0 +1,39 @@
@using WeatherForecast = Pages.GridRendering.WeatherForecast
<tr class="complex-table-row" style="@rowStyle">
@foreach (var item in Columns)
{
<Cell Item="@Item"
Field="@item"
CellIndex="1"
RowIndex="2"
Selected="@isSelected"
FormatString="foo"
OnClick="@OnCellClick">
</Cell>
}
</tr>
@code {
private bool isSelected = false;
private string rowStyle => isSelected ? "background-color: lightblue;" : "";
[Parameter]
public WeatherForecast Item { get; set; }
[Parameter]
public List<string> Columns { get; set; }
[Parameter]
public Func<int, Task> OnClick { get; set; }
Task OnCellClick(int args)
{
isSelected = !isSelected;
return OnClick.Invoke(args);
}
}

View File

@ -0,0 +1,19 @@
@using WeatherForecast = Pages.GridRendering.WeatherForecast
@foreach (var item in Data)
{
<Row Item="@item" Columns="@Columns"
OnClick="@OnClick"></Row>
}
@code {
[Parameter]
public WeatherForecast[] Data { get; set; }
[Parameter]
public List<string> Columns { get; set; }
[Parameter]
public Func<int, Task> OnClick { get; set; }
}

View File

@ -0,0 +1,46 @@
@using WeatherForecast = Pages.GridRendering.WeatherForecast
<table class="table">
<thead>
<tr>
@foreach (var item in Columns)
{
<th>@item</th>
}
</tr>
</thead>
<tbody>
<CascadingValue Value="@this">
<RowCollection Data="@Data"
Columns="@Columns"
OnClick="@RefreshComponent"></RowCollection>
</CascadingValue>
</tbody>
</table>
@code {
[Parameter]
public WeatherForecast[] Data { get; set; }
[Parameter]
public List<string> Columns { get; set; }
DateTime t1;
DateTime t2;
Task RefreshComponent(int index)
{
t1 = DateTime.Now;
StateHasChanged();
return Task.CompletedTask;
}
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
{
t2 = DateTime.Now;
Console.WriteLine("Refresh Time " + (t2 - t1).TotalMilliseconds);
}
return base.OnAfterRenderAsync(firstRender);
}
}

View File

@ -0,0 +1,48 @@
@typeparam TRowData
<CascadingValue IsFixed="true" Value="this">@ChildContent</CascadingValue>
<table @attributes="@Attributes">
<thead>
<tr>
@foreach (var col in columns)
{
col.RenderHeader(__builder);
}
</tr>
</thead>
<tbody>
@foreach (var item in Data)
{
<tr @key="item.GetHashCode()" class="@(RowClass?.Invoke(item))">
@foreach (var col in columns)
{
col.RenderCell(__builder, item);
}
</tr>
}
</tbody>
</table>
@code {
[Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> Attributes { get; set; }
[Parameter] public ICollection<TRowData> Data { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public Func<TRowData, string> RowClass { get; set; }
private List<GridColumn<TRowData>> columns = new List<GridColumn<TRowData>>();
internal void AddColumn(GridColumn<TRowData> column)
{
columns.Add(column);
}
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
// On the first render, we collect the list of columns, then we have to render them.
StateHasChanged();
}
}
}

View File

@ -0,0 +1,23 @@
@typeparam TRowData
@using Microsoft.AspNetCore.Components.Rendering
@code {
[CascadingParameter] public Grid<TRowData> OwnerGrid { get; set; }
[Parameter] public string Title { get; set; }
[Parameter] public TRowData RowData { get; set; }
[Parameter] public RenderFragment<TRowData> ChildContent { get; set; }
protected override void OnInitialized()
{
OwnerGrid.AddColumn(this);
}
internal void RenderHeader(RenderTreeBuilder __builder)
{
<th>@Title</th>
}
internal void RenderCell(RenderTreeBuilder __builder, TRowData rowData)
{
<td>@ChildContent(rowData)</td>
}
}

View File

@ -6,7 +6,8 @@
<a href="renderlist">RenderList</a> |
<a href="json">JSON</a> |
<a href="orgchart">OrgChart</a> |
<a href="timer">Timer</a>
<a href="timer">Timer</a> |
<a href="gridrendering">Grid</a>
<hr />

View File

@ -0,0 +1,30 @@
@using WeatherForecast = Pages.GridRendering.WeatherForecast
<td @onclick="@(() => OnClick.Invoke(1))">
@switch (Field)
{
case "Date":
@Item.Date.ToShortDateString()
break;
case "TemperatureC":
@Item.TemperatureC
break;
case "TemperatureF":
@Item.TemperatureF
break;
case "Summary":
@Item.Summary
break;
}
</td>
@code {
[Parameter]
public WeatherForecast Item { get; set; }
[Parameter]
public string Field { get; set; }
[Parameter]
public Func<int, Task> OnClick { get; set; }
}

View File

@ -0,0 +1,35 @@
@using WeatherForecast = Pages.GridRendering.WeatherForecast
<tr style="@rowStyle">
@foreach (var item in Columns)
{
<Cell Item="@Item"
Field="@item"
OnClick="@OnCellClick">
</Cell>
}
</tr>
@code {
private bool isSelected = false;
private string rowStyle => isSelected ? "background-color: lightblue;" : "";
[Parameter]
public WeatherForecast Item { get; set; }
[Parameter]
public List<string> Columns { get; set; }
[Parameter]
public Func<int, Task> OnClick { get; set; }
Task OnCellClick(int args)
{
isSelected = !isSelected;
return OnClick.Invoke(args);
}
}

View File

@ -0,0 +1,19 @@
@using WeatherForecast = Pages.GridRendering.WeatherForecast
@foreach (var item in Data)
{
<Row Item="@item" Columns="@Columns"
OnClick="@OnClick"></Row>
}
@code {
[Parameter]
public WeatherForecast[] Data { get; set; }
[Parameter]
public List<string> Columns { get; set; }
[Parameter]
public Func<int, Task> OnClick { get; set; }
}

View File

@ -0,0 +1,44 @@
@using WeatherForecast = Pages.GridRendering.WeatherForecast
<table class="table">
<thead>
<tr>
@foreach (var item in Columns)
{
<th>@item</th>
}
</tr>
</thead>
<tbody>
<RowCollection Data="@Data"
Columns="@Columns"
OnClick="@RefreshComponent"></RowCollection>
</tbody>
</table>
@code {
[Parameter]
public WeatherForecast[] Data { get; set; }
[Parameter]
public List<string> Columns { get; set; }
DateTime t1;
DateTime t2;
Task RefreshComponent(int index)
{
t1 = DateTime.Now;
StateHasChanged();
return Task.CompletedTask;
}
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
{
t2 = DateTime.Now;
Console.WriteLine("Refresh Time " + (t2 - t1).TotalMilliseconds);
}
return base.OnAfterRenderAsync(firstRender);
}
}

View File

@ -0,0 +1,105 @@
import { group, benchmark, setup, teardown } from './lib/minibench/minibench.js';
import { receiveEvent } from './util/BenchmarkEvents.js';
import { BlazorApp } from './util/BlazorApp.js';
import { setInputValue } from './util/DOM.js';
group('Grid', () => {
let app;
setup(async () => {
app = new BlazorApp();
await app.start();
app.navigateTo('gridrendering');
});
teardown(() => {
app.dispose();
});
benchmark('PlainTable: From blank', () => measureRenderGridFromBlank(app), {
setup: () => prepare(app, 'PlainTable', false),
descriptor: {
name: 'blazorwasm/render-plaintable-from-blank',
description: 'Time to render plain table from blank (ms)'
}
});
benchmark('PlainTable: Switch pages', () => measureRenderGridSwitchPages(app), {
setup: () => prepare(app, 'PlainTable', true),
descriptor: {
name: 'blazorwasm/render-plaintable-switch-pages',
description: 'Time to render plain table change of page (ms)'
}
});
benchmark('ComplexTable: From blank', () => measureRenderGridFromBlank(app), {
setup: () => prepare(app, 'ComplexTable', false),
descriptor: {
name: 'blazorwasm/render-complextable-from-blank',
description: 'Time to render complex table from blank (ms)'
}
});
benchmark('ComplexTable: Switch pages', () => measureRenderGridSwitchPages(app), {
setup: () => prepare(app, 'ComplexTable', true),
descriptor: {
name: 'blazorwasm/render-complextable-switch-pages',
description: 'Time to render complex table change of page (ms)'
}
});
benchmark('FastGrid: From blank', () => measureRenderGridFromBlank(app), {
setup: () => prepare(app, 'FastGrid', false),
descriptor: {
name: 'blazorwasm/render-fastgrid-from-blank',
description: 'Time to render fast grid from blank (ms)'
}
});
benchmark('FastGrid: Switch pages', () => measureRenderGridSwitchPages(app), {
setup: () => prepare(app, 'FastGrid', true),
descriptor: {
name: 'blazorwasm/render-fastgrid-switch-pages',
description: 'Time to render fast grid change of page (ms)'
}
});
});
async function prepare(app, renderMode, populateTable) {
const renderModeSelect = app.window.document.querySelector('#render-mode');
setInputValue(renderModeSelect, renderMode);
if (populateTable) {
let nextRenderCompletion = receiveEvent('Finished rendering table');
app.window.document.querySelector(populateTable ? '#show' : '#hide').click();
await nextRenderCompletion;
}
}
async function measureRenderGridFromBlank(app) {
const appDocument = app.window.document;
let nextRenderCompletion = receiveEvent('Finished rendering table');
appDocument.querySelector('#hide').click();
await nextRenderCompletion;
if (appDocument.querySelectorAll('tbody tr').length !== 0) {
throw new Error('Wrong number of rows rendered');
}
nextRenderCompletion = receiveEvent('Finished rendering table');
appDocument.querySelector('#show').click();
await nextRenderCompletion;
if (appDocument.querySelectorAll('tbody tr').length !== 200) {
throw new Error('Wrong number of rows rendered');
}
}
async function measureRenderGridSwitchPages(app) {
const appDocument = app.window.document;
let nextRenderCompletion = receiveEvent('Finished rendering table');
appDocument.querySelector('#change-page').click();
await nextRenderCompletion;
}

View File

@ -4,6 +4,7 @@ import './appStartup.js';
import './renderList.js';
import './jsonHandling.js';
import './orgChart.js';
import './grid.js';
import { getBlazorDownloadSize } from './blazorDownloadSize.js';
new HtmlUI('E2E Performance', '#display');