diff options
author | Steve Sanderson <SteveSandersonMS@users.noreply.github.com> | 2020-06-25 11:32:09 +0300 |
---|---|---|
committer | Steve Sanderson <SteveSandersonMS@users.noreply.github.com> | 2020-06-29 14:21:03 +0300 |
commit | 7b57f3d12f93e0152a80101147cbd91548d45af4 (patch) | |
tree | afe957d51e2bf49d07ebe4e830e410127278da26 /src/Components | |
parent | f6a6e4bd07bd4bf51c6965382156ebce22540fb7 (diff) |
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.
Diffstat (limited to 'src/Components')
14 files changed, 592 insertions, 1 deletions
diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/GridRendering.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/GridRendering.razor new file mode 100644 index 0000000000..4fa366f44c --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/GridRendering.razor @@ -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); + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/Cell.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/Cell.razor new file mode 100644 index 0000000000..e384a03da3 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/Cell.razor @@ -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; + } + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/Row.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/Row.razor new file mode 100644 index 0000000000..00716a4c8c --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/Row.razor @@ -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); + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/RowCollection.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/RowCollection.razor new file mode 100644 index 0000000000..180043c5c8 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/RowCollection.razor @@ -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; } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/TableComponent.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/TableComponent.razor new file mode 100644 index 0000000000..70c95ec905 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/TableComponent.razor @@ -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); + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/FastGrid/Grid.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/FastGrid/Grid.razor new file mode 100644 index 0000000000..0806655e31 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/FastGrid/Grid.razor @@ -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(); + } + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/FastGrid/GridColumn.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/FastGrid/GridColumn.razor new file mode 100644 index 0000000000..aa9c6cfd5b --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/FastGrid/GridColumn.razor @@ -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> + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/MainLayout.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/MainLayout.razor index acf8fd2f8a..8b4e64bb66 100644 --- a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/MainLayout.razor +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/MainLayout.razor @@ -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 /> diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/Cell.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/Cell.razor new file mode 100644 index 0000000000..913436fd99 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/Cell.razor @@ -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; } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/Row.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/Row.razor new file mode 100644 index 0000000000..fc1b5c2f06 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/Row.razor @@ -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); + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/RowCollection.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/RowCollection.razor new file mode 100644 index 0000000000..180043c5c8 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/RowCollection.razor @@ -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; } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/TableComponent.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/TableComponent.razor new file mode 100644 index 0000000000..ea75beebc7 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/TableComponent.razor @@ -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); + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/grid.js b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/grid.js new file mode 100644 index 0000000000..3acd452ae0 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/grid.js @@ -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; +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/index.js b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/index.js index c69e1df192..cf923ae082 100644 --- a/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/index.js +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/index.js @@ -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'); |