test(core): add benchmark for defer runtime logic (#52222)

This commit adds a benchmark for `@defer` runtime logic and uses `@if` as a baseline.

PR Close #52222
This commit is contained in:
Andrew Kushnir 2023-10-15 20:48:51 -07:00 committed by Dylan Hunn
parent c2560d05f2
commit c76cac25eb
14 changed files with 580 additions and 0 deletions

View file

@ -0,0 +1,39 @@
load("//tools:defaults.bzl", "ng_module", "ts_library")
package(default_visibility = ["//visibility:public"])
ng_module(
name = "shared_lib",
srcs = [
"init.ts",
"util.ts",
],
tsconfig = "//modules/benchmarks:tsconfig-build.json",
deps = [
"//modules/benchmarks/src:util_lib",
"//packages/core",
"//packages/platform-browser",
],
)
ts_library(
name = "perf_tests_lib",
testonly = 1,
srcs = ["defer.perf-spec.ts"],
tsconfig = "//modules/benchmarks:tsconfig-e2e.json",
deps = [
"@npm//@angular/build-tooling/bazel/benchmark/driver-utilities",
"@npm//protractor",
],
)
ts_library(
name = "e2e_tests_lib",
testonly = 1,
srcs = ["defer.e2e-spec.ts"],
tsconfig = "//modules/benchmarks:tsconfig-e2e.json",
deps = [
"@npm//@angular/build-tooling/bazel/benchmark/driver-utilities",
"@npm//protractor",
],
)

View file

@ -0,0 +1,10 @@
# Defer benchmark
This folder contains defer benchmark that tests the process of `@defer` block creation.
There are 2 folders in this benchmark:
* `baseline` - renders a component using an `@if` condition, we use it as a baseline
* `main` - the same code as the `baseline`, but instead of the `@if`, we use `@defer` to compare defer blocks against conditionals
The benchmarks are based on `largetable` benchmarks.

View file

@ -0,0 +1,55 @@
load("//tools:defaults.bzl", "app_bundle", "http_server", "ng_module")
load("@npm//@angular/build-tooling/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test")
load("//modules/benchmarks:e2e_test.bzl", "e2e_test")
package(default_visibility = ["//modules/benchmarks:__subpackages__"])
ng_module(
name = "main",
srcs = glob(["*.ts"]),
tsconfig = "//modules/benchmarks:tsconfig-build.json",
deps = [
"//modules/benchmarks/src:util_lib",
"//modules/benchmarks/src/defer:shared_lib",
"//packages/core",
"//packages/platform-browser",
],
)
app_bundle(
name = "bundle",
entry_point = ":index.ts",
deps = [
":main",
"@npm//rxjs",
],
)
# The script needs to be called `app_bundle` for easier syncing into g3.
genrule(
name = "app_bundle",
srcs = [":bundle.debug.min.js"],
outs = ["app_bundle.js"],
cmd = "cp $< $@",
)
http_server(
name = "prodserver",
srcs = ["index.html"],
deps = [
":app_bundle",
"//packages/zone.js/bundles:zone.umd.js",
],
)
benchmark_test(
name = "perf",
server = ":prodserver",
deps = ["//modules/benchmarks/src/defer:perf_tests_lib"],
)
e2e_test(
name = "e2e",
server = ":prodserver",
deps = ["//modules/benchmarks/src/defer:e2e_tests_lib"],
)

View file

@ -0,0 +1,55 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Input} from '@angular/core';
import {DomSanitizer, SafeStyle} from '@angular/platform-browser';
import {TableCell} from '../util';
let trustedEmptyColor: SafeStyle;
let trustedGreyColor: SafeStyle;
@Component({
standalone: true,
selector: 'app',
template: `
<table>
<tbody>
@for (row of data; track $index) {
<tr>
@for (cell of row; track $index) {
<td [style.backgroundColor]="getColor(cell.row)">
@if (condition) {
<!--
Use static text in cells to avoid the need
to run a new change detection cycle.
-->
Cell
}
</td>
}
</tr>
}
</tbody>
</table>
`,
})
export class AppComponent {
@Input() data: TableCell[][] = [];
condition = true;
constructor(sanitizer: DomSanitizer) {
trustedEmptyColor = sanitizer.bypassSecurityTrustStyle('white');
trustedGreyColor = sanitizer.bypassSecurityTrustStyle('grey');
}
getColor(row: number) {
return row % 2 ? trustedEmptyColor : trustedGreyColor;
}
}

View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- Prevent the browser from requesting any favicon. -->
<link rel="icon" href="data:," />
</head>
<body>
<h2>Params</h2>
<form>
Cols:
<input type="number" id="cols" name="cols" value="" />
<br />
Rows:
<input type="number" id="rows" name="rows" value="" />
<br />
<button>Apply</button>
</form>
<h2>Defer Benchmark (baseline)</h2>
<p>
<button id="destroyDom">destroyDom</button>
<button id="createDom">createDom</button>
<button id="createDomProfile">profile createDom</button>
<button id="updateDomProfile">profile updateDom</button>
</p>
<div>
<app id="root"></app>
</div>
<!-- BEGIN-EXTERNAL -->
<script src="/angular/packages/zone.js/bundles/zone.umd.js"></script>
<!-- END-EXTERNAL -->
<!-- Needs to be named `app_bundle` for sync into Google. -->
<script src="/app_bundle.js"></script>
</body>
</html>

View file

@ -0,0 +1,21 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser';
import {init, syncUrlParamsToForm} from '../init';
import {AppComponent} from './app.component';
syncUrlParamsToForm();
bootstrapApplication(AppComponent, {
providers: [
provideProtractorTestingSupport(),
],
}).then(init);

View file

@ -0,0 +1,24 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {openBrowser, verifyNoBrowserErrors} from '@angular/build-tooling/bazel/benchmark/driver-utilities';
import {$} from 'protractor';
describe('defer benchmark', () => {
afterEach(verifyNoBrowserErrors);
it(`should render the table`, async () => {
openBrowser({
url: '',
ignoreBrowserSynchronization: true,
params: [{name: 'cols', value: 5}, {name: 'rows', value: 5}],
});
await $('#createDom').click();
expect($('#root').getText()).toContain('Cell');
});
});

View file

@ -0,0 +1,67 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {runBenchmark, verifyNoBrowserErrors} from '@angular/build-tooling/bazel/benchmark/driver-utilities';
import {$} from 'protractor';
interface Worker {
id: string;
prepare?(): void;
work(): void;
}
const CreateWorker: Worker = {
id: 'create',
prepare: () => $('#destroyDom').click(),
work: () => $('#createDom').click()
};
const UpdateWorker: Worker = {
id: 'update',
prepare: () => {
$('#createDom').click();
},
work: () => $('#createDom').click()
};
// In order to make sure that we don't change the ids of the benchmarks, we need to
// determine the current test package name from the Bazel target. This is necessary
// because previous to the Bazel conversion, the benchmark test ids contained the test
// name. e.g. "largeTable.ng2_switch.createDestroy". We determine the name of the
// Bazel package where this test runs from the current test target. The Bazel target
// looks like: "//modules/benchmarks/src/largetable/{pkg_name}:{target_name}".
const testPackageName = process.env['BAZEL_TARGET']!.split(':')[0].split('/').pop();
describe('defer benchmark perf', () => {
afterEach(verifyNoBrowserErrors);
[CreateWorker, UpdateWorker].forEach((worker) => {
describe(worker.id, () => {
it(`should run benchmark for ${testPackageName}`, async () => {
await runTableBenchmark({
id: `defer.${testPackageName}.${worker.id}`,
url: '/',
ignoreBrowserSynchronization: true,
worker,
});
});
});
});
});
function runTableBenchmark(
config: {id: string, url: string, ignoreBrowserSynchronization?: boolean, worker: Worker}) {
return runBenchmark({
id: config.id,
url: config.url,
ignoreBrowserSynchronization: config.ignoreBrowserSynchronization,
params: [{name: 'cols', value: 40}, {name: 'rows', value: 200}],
prepare: config.worker.prepare,
work: config.worker.work
});
}

View file

@ -0,0 +1,52 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ApplicationRef} from '@angular/core';
import {bindAction, profile} from '../util';
import {buildTable, emptyTable, initTableUtils} from './util';
const DEFAULT_COLS_COUNT = '40';
const DEFAULT_ROWS_COUNT = '200';
function getUrlParamValue(name: string): string|null {
const url = new URL(document.location.href);
return url.searchParams.get(name);
}
export function syncUrlParamsToForm(): {cols: string, rows: string} {
let cols = getUrlParamValue('cols') ?? DEFAULT_COLS_COUNT;
let rows = getUrlParamValue('rows') ?? DEFAULT_ROWS_COUNT;
(document.getElementById('cols') as HTMLInputElement).value = cols;
(document.getElementById('rows') as HTMLInputElement).value = rows;
return {cols, rows};
}
export function init(appRef: ApplicationRef) {
const table = appRef.components[0].instance;
function destroyDom() {
table.data = emptyTable;
appRef.tick();
}
function createDom() {
table.data = buildTable();
appRef.tick();
}
function noop() {}
initTableUtils();
bindAction('#destroyDom', destroyDom);
bindAction('#createDom', createDom);
bindAction('#createDomProfile', profile(createDom, destroyDom, 'create'));
bindAction('#updateDomProfile', profile(createDom, noop, 'update'));
}

View file

@ -0,0 +1,55 @@
load("//tools:defaults.bzl", "app_bundle", "http_server", "ng_module")
load("@npm//@angular/build-tooling/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test")
load("//modules/benchmarks:e2e_test.bzl", "e2e_test")
package(default_visibility = ["//modules/benchmarks:__subpackages__"])
ng_module(
name = "main",
srcs = glob(["*.ts"]),
tsconfig = "//modules/benchmarks:tsconfig-build.json",
deps = [
"//modules/benchmarks/src:util_lib",
"//modules/benchmarks/src/defer:shared_lib",
"//packages/core",
"//packages/platform-browser",
],
)
app_bundle(
name = "bundle",
entry_point = ":index.ts",
deps = [
":main",
"@npm//rxjs",
],
)
# The script needs to be called `app_bundle` for easier syncing into g3.
genrule(
name = "app_bundle",
srcs = [":bundle.debug.min.js"],
outs = ["app_bundle.js"],
cmd = "cp $< $@",
)
http_server(
name = "prodserver",
srcs = ["index.html"],
deps = [
":app_bundle",
"//packages/zone.js/bundles:zone.umd.js",
],
)
benchmark_test(
name = "perf",
server = ":prodserver",
deps = ["//modules/benchmarks/src/defer:perf_tests_lib"],
)
e2e_test(
name = "e2e",
server = ":prodserver",
deps = ["//modules/benchmarks/src/defer:e2e_tests_lib"],
)

View file

@ -0,0 +1,55 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Input} from '@angular/core';
import {DomSanitizer, SafeStyle} from '@angular/platform-browser';
import {TableCell} from '../util';
let trustedEmptyColor: SafeStyle;
let trustedGreyColor: SafeStyle;
@Component({
standalone: true,
selector: 'app',
template: `
<table>
<tbody>
@for (row of data; track $index) {
<tr>
@for (cell of row; track $index) {
<td [style.backgroundColor]="getColor(cell.row)">
@defer (when condition; on immediate) {
<!--
Use static text in cells to avoid the need
to run a new change detection cycle.
-->
Cell
}
</td>
}
</tr>
}
</tbody>
</table>
`,
})
export class AppComponent {
@Input() data: TableCell[][] = [];
condition = true;
constructor(sanitizer: DomSanitizer) {
trustedEmptyColor = sanitizer.bypassSecurityTrustStyle('white');
trustedGreyColor = sanitizer.bypassSecurityTrustStyle('grey');
}
getColor(row: number) {
return row % 2 ? trustedEmptyColor : trustedGreyColor;
}
}

View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- Prevent the browser from requesting any favicon. -->
<link rel="icon" href="data:," />
</head>
<body>
<h2>Params</h2>
<form>
Cols:
<input type="number" id="cols" name="cols" value="" />
<br />
Rows:
<input type="number" id="rows" name="rows" value="" />
<br />
<button>Apply</button>
</form>
<h2>Defer Benchmark (main)</h2>
<p>
<button id="destroyDom">destroyDom</button>
<button id="createDom">createDom</button>
<button id="createDomProfile">profile createDom</button>
<button id="updateDomProfile">profile updateDom</button>
</p>
<div>
<app id="root"></app>
</div>
<!-- BEGIN-EXTERNAL -->
<script src="/angular/packages/zone.js/bundles/zone.umd.js"></script>
<!-- END-EXTERNAL -->
<!-- Needs to be named `app_bundle` for sync into Google. -->
<script src="/app_bundle.js"></script>
</body>
</html>

View file

@ -0,0 +1,21 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser';
import {init, syncUrlParamsToForm} from '../init';
import {AppComponent} from './app.component';
syncUrlParamsToForm();
bootstrapApplication(AppComponent, {
providers: [
provideProtractorTestingSupport(),
],
}).then(init);

View file

@ -0,0 +1,48 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {getIntParameter} from '../util';
export class TableCell {
constructor(public row: number, public col: number, public value: string) {}
}
let tableCreateCount: number;
let maxRow: number;
let maxCol: number;
let numberData: TableCell[][];
let charData: TableCell[][];
export function initTableUtils() {
maxRow = getIntParameter('rows');
maxCol = getIntParameter('cols');
tableCreateCount = 0;
numberData = [];
charData = [];
for (let r = 0; r < maxRow; r++) {
const numberRow: TableCell[] = [];
numberData.push(numberRow);
const charRow: TableCell[] = [];
charData.push(charRow);
for (let c = 0; c < maxCol; c++) {
numberRow.push(new TableCell(r, c, `${c}/${r}`));
charRow.push(new TableCell(r, c, `${charValue(c)}/${charValue(r)}`));
}
}
}
function charValue(i: number): string {
return String.fromCharCode('A'.charCodeAt(0) + (i % 26));
}
export const emptyTable: TableCell[][] = [];
export function buildTable(): TableCell[][] {
tableCreateCount++;
return tableCreateCount % 2 ? numberData : charData;
}