From 1998a06bdc6079aa97109df085e02629b69680ba Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 25 Jun 2020 09:32:09 +0100 Subject: [PATCH] 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. --- .../TestApp/Pages/GridRendering.razor | 122 ++++++++++++++++++ .../TestApp/Shared/ComplexTable/Cell.razor | 59 +++++++++ .../TestApp/Shared/ComplexTable/Row.razor | 39 ++++++ .../Shared/ComplexTable/RowCollection.razor | 19 +++ .../Shared/ComplexTable/TableComponent.razor | 46 +++++++ .../TestApp/Shared/FastGrid/Grid.razor | 48 +++++++ .../TestApp/Shared/FastGrid/GridColumn.razor | 23 ++++ .../TestApp/Shared/MainLayout.razor | 3 +- .../TestApp/Shared/PlainTable/Cell.razor | 30 +++++ .../TestApp/Shared/PlainTable/Row.razor | 35 +++++ .../Shared/PlainTable/RowCollection.razor | 19 +++ .../Shared/PlainTable/TableComponent.razor | 44 +++++++ .../TestApp/wwwroot/benchmarks/grid.js | 105 +++++++++++++++ .../TestApp/wwwroot/benchmarks/index.js | 1 + 14 files changed, 592 insertions(+), 1 deletion(-) create mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/GridRendering.razor create mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/Cell.razor create mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/Row.razor create mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/RowCollection.razor create mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/ComplexTable/TableComponent.razor create mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/FastGrid/Grid.razor create mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/FastGrid/GridColumn.razor create mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/Cell.razor create mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/Row.razor create mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/RowCollection.razor create mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/PlainTable/TableComponent.razor create mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/grid.js 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 + +

20 x 200 Grid

+ +
+ + + + + @if (forecasts != null) + { + + } +
+ +@if (forecasts == null) +{ +

(No data assigned)

+} +else if (selectedRenderMode == RenderMode.FastGrid) +{ +

FastGrid represents a minimal, optimized implementation of a grid.

+ + + @context.Date.ToShortDateString() + @context.TemperatureC + @context.TemperatureF + @context.Summary + @context.Date.ToShortDateString() + @context.TemperatureC + @context.TemperatureF + @context.Summary + @context.Date.ToShortDateString() + @context.TemperatureC + @context.TemperatureF + @context.Summary + @context.Date.ToShortDateString() + @context.TemperatureC + @context.TemperatureF + @context.Summary + @context.Date.ToShortDateString() + @context.TemperatureC + @context.TemperatureF + @context.Summary + +} +else if (selectedRenderMode == RenderMode.PlainTable) +{ +

PlainTable represents a minimal but not optimized implementation of a grid.

+ + +} +else if (selectedRenderMode == RenderMode.ComplexTable) +{ +

ComplexTable represents a maximal, not optimized implementation of a grid, using a wide range of Blazor features at once.

+ + +} + +@code { + enum RenderMode { PlainTable, ComplexTable, FastGrid } + + private RenderMode selectedRenderMode = RenderMode.FastGrid; + + private WeatherForecast[] forecasts; + public List Columns { get; set; } = new List +{ + "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 + + + @switch (Field) + { + case "Date": + @Item.Date.ToShortDateString() + break; + case "TemperatureC": + @Item.TemperatureC + break; + case "TemperatureF": + @Item.TemperatureF + break; + case "Summary": + @Item.Summary + break; + } + + +@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 OnClick { get; set; } + + private protected Dictionary Attributes + { + get + { + var attributes = new Dictionary(); + + 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 + + + @foreach (var item in Columns) + { + + + } + + + +@code { + + private bool isSelected = false; + + private string rowStyle => isSelected ? "background-color: lightblue;" : ""; + + [Parameter] + public WeatherForecast Item { get; set; } + + [Parameter] + public List Columns { get; set; } + + [Parameter] + public Func 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) +{ + +} + + +@code { + [Parameter] + public WeatherForecast[] Data { get; set; } + + [Parameter] + public List Columns { get; set; } + + [Parameter] + public Func 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 + + + + + @foreach (var item in Columns) + { + + } + + + + + + + +
@item
+ + +@code { + [Parameter] + public WeatherForecast[] Data { get; set; } + + [Parameter] + public List 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 + +@ChildContent + + + + + @foreach (var col in columns) + { + col.RenderHeader(__builder); + } + + + + @foreach (var item in Data) + { + + @foreach (var col in columns) + { + col.RenderCell(__builder, item); + } + + } + +
+ +@code { + [Parameter(CaptureUnmatchedValues = true)] public Dictionary Attributes { get; set; } + [Parameter] public ICollection Data { get; set; } + [Parameter] public RenderFragment ChildContent { get; set; } + [Parameter] public Func RowClass { get; set; } + + private List> columns = new List>(); + + internal void AddColumn(GridColumn 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 OwnerGrid { get; set; } + [Parameter] public string Title { get; set; } + [Parameter] public TRowData RowData { get; set; } + [Parameter] public RenderFragment ChildContent { get; set; } + + protected override void OnInitialized() + { + OwnerGrid.AddColumn(this); + } + + internal void RenderHeader(RenderTreeBuilder __builder) + { + @Title + } + + internal void RenderCell(RenderTreeBuilder __builder, TRowData rowData) + { + @ChildContent(rowData) + } +} 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 @@ RenderList | JSON | OrgChart | -Timer +Timer | +Grid
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 + + + @switch (Field) + { + case "Date": + @Item.Date.ToShortDateString() + break; + case "TemperatureC": + @Item.TemperatureC + break; + case "TemperatureF": + @Item.TemperatureF + break; + case "Summary": + @Item.Summary + break; + } + + +@code { + [Parameter] + public WeatherForecast Item { get; set; } + + [Parameter] + public string Field { get; set; } + + [Parameter] + public Func 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 + + + @foreach (var item in Columns) + { + + + } + + + +@code { + + private bool isSelected = false; + + private string rowStyle => isSelected ? "background-color: lightblue;" : ""; + + [Parameter] + public WeatherForecast Item { get; set; } + + [Parameter] + public List Columns { get; set; } + + [Parameter] + public Func 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) +{ + +} + + +@code { + [Parameter] + public WeatherForecast[] Data { get; set; } + + [Parameter] + public List Columns { get; set; } + + [Parameter] + public Func 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 + + + + + @foreach (var item in Columns) + { + + } + + + + + +
@item
+ + +@code { + [Parameter] + public WeatherForecast[] Data { get; set; } + + [Parameter] + public List 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');