Skip to content

Commit 7981aad

Browse files
AndrewKushnirdylhunn
authored andcommitted
test(core): add benchmark for hydration runtime logic (angular#52206)
This commit adds a benchmark for hydration runtime logic, which contains 2 parts: a baseline (create DOM nodes from scratch) and a main scenario (DOM matching instead of re-creating nodes). PR Close angular#52206
1 parent c76cac2 commit 7981aad

13 files changed

Lines changed: 619 additions & 0 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
load("//tools:defaults.bzl", "ng_module", "ts_library")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ng_module(
6+
name = "shared_lib",
7+
srcs = [
8+
"init.ts",
9+
"table.ts",
10+
"util.ts",
11+
],
12+
tsconfig = "//modules/benchmarks:tsconfig-build.json",
13+
deps = [
14+
"//modules/benchmarks/src:util_lib",
15+
"//packages/core",
16+
"//packages/platform-browser",
17+
],
18+
)
19+
20+
ts_library(
21+
name = "perf_tests_lib",
22+
testonly = 1,
23+
srcs = ["hydration.perf-spec.ts"],
24+
tsconfig = "//modules/benchmarks:tsconfig-e2e.json",
25+
deps = [
26+
"@npm//@angular/build-tooling/bazel/benchmark/driver-utilities",
27+
"@npm//protractor",
28+
],
29+
)
30+
31+
ts_library(
32+
name = "e2e_tests_lib",
33+
testonly = 1,
34+
srcs = ["hydration.e2e-spec.ts"],
35+
tsconfig = "//modules/benchmarks:tsconfig-e2e.json",
36+
deps = [
37+
"@npm//@angular/build-tooling/bazel/benchmark/driver-utilities",
38+
"@npm//protractor",
39+
],
40+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Hydration benchmark
2+
3+
This folder contains hydration benchmark that tests the process of matching DOM nodes at runtime.
4+
5+
There are 2 folders in this benchmark:
6+
7+
* `baseline` - renders a component without hydration, we use it as a baseline
8+
* `main` - the same code as the `baseline`, but Angular uses hydration and matches existing DOM nodes instead of creating new ones
9+
10+
The benchmarks are based on `largetable` benchmarks.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
load("//tools:defaults.bzl", "app_bundle", "http_server", "ng_module")
2+
load("@npm//@angular/build-tooling/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test")
3+
load("//modules/benchmarks:e2e_test.bzl", "e2e_test")
4+
5+
package(default_visibility = ["//modules/benchmarks:__subpackages__"])
6+
7+
ng_module(
8+
name = "main",
9+
srcs = glob(["*.ts"]),
10+
tsconfig = "//modules/benchmarks:tsconfig-build.json",
11+
deps = [
12+
"//modules/benchmarks/src:util_lib",
13+
"//modules/benchmarks/src/hydration:shared_lib",
14+
"//packages/core",
15+
"//packages/platform-browser",
16+
],
17+
)
18+
19+
app_bundle(
20+
name = "bundle",
21+
entry_point = ":index.ts",
22+
deps = [
23+
":main",
24+
"@npm//rxjs",
25+
],
26+
)
27+
28+
# The script needs to be called `app_bundle` for easier syncing into g3.
29+
genrule(
30+
name = "app_bundle",
31+
srcs = [":bundle.debug.min.js"],
32+
outs = ["app_bundle.js"],
33+
cmd = "cp $< $@",
34+
)
35+
36+
http_server(
37+
name = "prodserver",
38+
srcs = ["index.html"],
39+
deps = [
40+
":app_bundle",
41+
"//packages/zone.js/bundles:zone.umd.js",
42+
],
43+
)
44+
45+
benchmark_test(
46+
name = "perf",
47+
server = ":prodserver",
48+
deps = ["//modules/benchmarks/src/hydration:perf_tests_lib"],
49+
)
50+
51+
e2e_test(
52+
name = "e2e",
53+
server = ":prodserver",
54+
deps = ["//modules/benchmarks/src/hydration:e2e_tests_lib"],
55+
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<!-- Prevent the browser from requesting any favicon. -->
6+
<link rel="icon" href="data:," />
7+
</head>
8+
<body>
9+
<!--nghm-->
10+
<h2>Params</h2>
11+
<form>
12+
Cols:
13+
<input type="number" id="cols" name="cols" value="" />
14+
<br />
15+
Rows:
16+
<input type="number" id="rows" name="rows" value="" />
17+
<br />
18+
<button>Apply</button>
19+
</form>
20+
21+
<h2>Hydration Benchmark (baseline)</h2>
22+
<p>
23+
<button id="prepare">prepare</button>
24+
<button id="createDom">createDom</button>
25+
<button id="updateDom">updateDom</button>
26+
<button id="createDomProfile">profile createDom</button>
27+
<button id="updateDomProfile">profile updateDom</button>
28+
</p>
29+
30+
<div>
31+
<app id="root"></app>
32+
</div>
33+
34+
<!-- BEGIN-EXTERNAL -->
35+
<script src="/angular/packages/zone.js/bundles/zone.umd.js"></script>
36+
<!-- END-EXTERNAL -->
37+
38+
<div id="table"></div>
39+
40+
<!-- Needs to be named `app_bundle` for sync into Google. -->
41+
<script src="/app_bundle.js"></script>
42+
</body>
43+
</html>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser';
10+
11+
import {init, syncUrlParamsToForm} from '../init';
12+
import {AppComponent} from '../table';
13+
14+
syncUrlParamsToForm();
15+
16+
bootstrapApplication(AppComponent, {
17+
providers: [
18+
provideProtractorTestingSupport(),
19+
],
20+
}).then(appRef => init(appRef, false /* insertSsrContent */));
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {openBrowser, verifyNoBrowserErrors} from '@angular/build-tooling/bazel/benchmark/driver-utilities';
10+
import {$} from 'protractor';
11+
12+
describe('hydration benchmark', () => {
13+
afterEach(verifyNoBrowserErrors);
14+
15+
it(`should render the table`, async () => {
16+
openBrowser({
17+
url: '',
18+
ignoreBrowserSynchronization: true,
19+
params: [{name: 'cols', value: 5}, {name: 'rows', value: 5}],
20+
});
21+
await $('#prepare').click();
22+
await $('#createDom').click();
23+
expect($('#table').getText()).toContain('0/0');
24+
});
25+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {runBenchmark, verifyNoBrowserErrors} from '@angular/build-tooling/bazel/benchmark/driver-utilities';
10+
import {$} from 'protractor';
11+
12+
interface Worker {
13+
id: string;
14+
prepare?(): void;
15+
work(): void;
16+
}
17+
18+
const CreateWorker: Worker = {
19+
id: 'create',
20+
prepare: () => $('#prepare').click(),
21+
work: () => $('#createDom').click()
22+
};
23+
24+
const UpdateWorker: Worker = {
25+
id: 'update',
26+
prepare: () => {
27+
$('#prepare').click();
28+
$('#createDom').click();
29+
},
30+
work: () => $('#updateDom').click()
31+
};
32+
33+
// In order to make sure that we don't change the ids of the benchmarks, we need to
34+
// determine the current test package name from the Bazel target. This is necessary
35+
// because previous to the Bazel conversion, the benchmark test ids contained the test
36+
// name. e.g. "largeTable.ng2_switch.createDestroy". We determine the name of the
37+
// Bazel package where this test runs from the current test target. The Bazel target
38+
// looks like: "//modules/benchmarks/src/largetable/{pkg_name}:{target_name}".
39+
const testPackageName = process.env['BAZEL_TARGET']!.split(':')[0].split('/').pop();
40+
41+
describe('hydration benchmark perf', () => {
42+
afterEach(verifyNoBrowserErrors);
43+
44+
[CreateWorker, UpdateWorker].forEach((worker) => {
45+
describe(worker.id, () => {
46+
it(`should run benchmark for ${testPackageName}`, async () => {
47+
await runTableBenchmark({
48+
id: `hydration.${testPackageName}.${worker.id}`,
49+
url: '/',
50+
ignoreBrowserSynchronization: true,
51+
worker,
52+
});
53+
});
54+
});
55+
});
56+
});
57+
58+
function runTableBenchmark(
59+
config: {id: string, url: string, ignoreBrowserSynchronization?: boolean, worker: Worker}) {
60+
return runBenchmark({
61+
id: config.id,
62+
url: config.url,
63+
ignoreBrowserSynchronization: config.ignoreBrowserSynchronization,
64+
params: [{name: 'cols', value: 40}, {name: 'rows', value: 200}],
65+
prepare: config.worker.prepare,
66+
work: config.worker.work
67+
});
68+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ApplicationRef, ComponentRef, createComponent, EnvironmentInjector} from '@angular/core';
10+
11+
import {bindAction, profile} from '../util';
12+
13+
import {TableComponent} from './table';
14+
import {buildTable, emptyTable, initTableUtils, TableCell} from './util';
15+
16+
const DEFAULT_COLS_COUNT = '40';
17+
const DEFAULT_ROWS_COUNT = '200';
18+
19+
function getUrlParamValue(name: string): string|null {
20+
const url = new URL(document.location.href);
21+
return url.searchParams.get(name);
22+
}
23+
24+
export function syncUrlParamsToForm(): {cols: string, rows: string} {
25+
let cols = getUrlParamValue('cols') ?? DEFAULT_COLS_COUNT;
26+
let rows = getUrlParamValue('rows') ?? DEFAULT_ROWS_COUNT;
27+
(document.getElementById('cols') as HTMLInputElement).value = cols;
28+
(document.getElementById('rows') as HTMLInputElement).value = rows;
29+
return {cols, rows};
30+
}
31+
32+
export function init(appRef: ApplicationRef, insertSsrContent = true) {
33+
let tableComponentRef: ComponentRef<TableComponent>;
34+
const injector = appRef.injector;
35+
const environmentInjector = injector.get(EnvironmentInjector);
36+
37+
let data: TableCell[][] = [];
38+
39+
const setInput = (data: TableCell[][]) => {
40+
if (tableComponentRef) {
41+
tableComponentRef.setInput('data', data);
42+
tableComponentRef.changeDetectorRef.detectChanges();
43+
}
44+
};
45+
46+
function destroyDom() {
47+
setInput(emptyTable);
48+
}
49+
50+
function updateDom() {
51+
data = buildTable();
52+
setInput(data);
53+
}
54+
55+
function createDom() {
56+
const hostElement = document.getElementById('table');
57+
tableComponentRef = createComponent(TableComponent, {environmentInjector, hostElement});
58+
setInput(data);
59+
}
60+
61+
function prepare() {
62+
destroyDom();
63+
data = buildTable();
64+
65+
if (insertSsrContent) {
66+
// Prepare DOM structure, similar to what SSR would produce.
67+
const hostElement = document.getElementById('table');
68+
hostElement.setAttribute('ngh', '0');
69+
hostElement.textContent = ''; // clear existing DOM contents
70+
hostElement.appendChild(createTableDom(data));
71+
}
72+
}
73+
74+
function noop() {}
75+
76+
initTableUtils();
77+
78+
bindAction('#prepare', prepare);
79+
bindAction('#createDom', createDom);
80+
bindAction('#updateDom', updateDom);
81+
bindAction('#createDomProfile', profile(createDom, prepare, 'create'));
82+
bindAction('#updateDomProfile', profile(updateDom, noop, 'update'));
83+
}
84+
85+
/**
86+
* Creates DOM to represent a table, similar to what'd be generated
87+
* during the SSR.
88+
*/
89+
function createTableDom(data: TableCell[][]) {
90+
const table = document.createElement('table');
91+
const tbody = document.createElement('tbody');
92+
table.appendChild(tbody);
93+
this._renderCells = [];
94+
for (let r = 0; r < data.length; r++) {
95+
const dataRow = data[r];
96+
const tr = document.createElement('tr');
97+
// Mark created DOM nodes, so that we can verify that
98+
// they were *not* re-created during hydration.
99+
(tr as any).__existing = true;
100+
tbody.appendChild(tr);
101+
const renderRow = [];
102+
for (let c = 0; c < dataRow.length; c++) {
103+
const dataCell = dataRow[c];
104+
const renderCell = document.createElement('td');
105+
// Mark created DOM nodes, so that we can verify that
106+
// they were *not* re-created during hydration.
107+
(renderCell as any).__existing = true;
108+
if (r % 2 === 0) {
109+
renderCell.style.backgroundColor = 'grey';
110+
}
111+
tr.appendChild(renderCell);
112+
renderRow[c] = renderCell;
113+
renderCell.textContent = dataCell.value;
114+
}
115+
// View container anchor
116+
const comment = document.createComment('');
117+
tr.appendChild(comment);
118+
}
119+
// View container anchor
120+
const comment = document.createComment('');
121+
tbody.appendChild(comment);
122+
return table;
123+
}

0 commit comments

Comments
 (0)