Skip to content

Commit

Permalink
test(core): add benchmark for hydration runtime logic
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
AndrewKushnir committed Oct 14, 2023
1 parent 20e7e21 commit d8dac7c
Show file tree
Hide file tree
Showing 13 changed files with 625 additions and 0 deletions.
41 changes: 41 additions & 0 deletions modules/benchmarks/src/hydration/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
load("//tools:defaults.bzl", "ng_module", "ts_library")

package(default_visibility = ["//visibility:public"])

ng_module(
name = "shared_lib",
srcs = [
"init.ts",
"table.ts",
"util.ts",
],
tsconfig = "//modules/benchmarks:tsconfig-build.json",
deps = [
"//modules/benchmarks/src:util_lib",
"//packages/common",
"//packages/core",
"//packages/platform-browser",
],
)

ts_library(
name = "perf_tests_lib",
testonly = 1,
srcs = ["hydration.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 = ["hydration.e2e-spec.ts"],
tsconfig = "//modules/benchmarks:tsconfig-e2e.json",
deps = [
"@npm//@angular/build-tooling/bazel/benchmark/driver-utilities",
"@npm//protractor",
],
)
10 changes: 10 additions & 0 deletions modules/benchmarks/src/hydration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Hydration benchmark

This folder contains hydration benchmark that tests the process of matching DOM nodes at runtime.

There are 2 folders in this benchmark:

* `baseline` - renders a component without hydration, we use it as a baseline
* `main` - the same code as the `baseline`, but Angular uses hydration and matches existing DOM nodes instead of creating new ones

The benchmarks are based on `largetable` benchmarks.
55 changes: 55 additions & 0 deletions modules/benchmarks/src/hydration/baseline/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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/hydration: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/hydration:perf_tests_lib"],
)

e2e_test(
name = "e2e",
server = ":prodserver",
deps = ["//modules/benchmarks/src/hydration:e2e_tests_lib"],
)
43 changes: 43 additions & 0 deletions modules/benchmarks/src/hydration/baseline/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- Prevent the browser from requesting any favicon. -->
<link rel="icon" href="data:," />
</head>
<body>
<!--nghm-->
<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>Hydration Benchmark (baseline)</h2>
<p>
<button id="prepare">prepare</button>
<button id="createDom">createDom</button>
<button id="updateDom">updateDom</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 -->

<div id="table"></div>

<!-- Needs to be named `app_bundle` for sync into Google. -->
<script src="/app_bundle.js"></script>
</body>
</html>
20 changes: 20 additions & 0 deletions modules/benchmarks/src/hydration/baseline/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @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 '../table';

syncUrlParamsToForm();

bootstrapApplication(AppComponent, {
providers: [
provideProtractorTestingSupport(),
],
}).then(appRef => init(appRef, false /* insertSsrContent */));
28 changes: 28 additions & 0 deletions modules/benchmarks/src/hydration/hydration.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @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('hydration 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($('#table').getText()).toContain('0/0');
await $('#createDom').click();
expect($('#table').getText()).toContain('A/A');
await $('#destroyDom').click();
expect($('#table').getText() as any).toEqual('');
});
});
68 changes: 68 additions & 0 deletions modules/benchmarks/src/hydration/hydration.perf-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @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: () => $('#prepare').click(),
work: () => $('#createDom').click()
};

const UpdateWorker: Worker = {
id: 'update',
prepare: () => {
$('#prepare').click();
$('#createDom').click();
},
work: () => $('#updateDom').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('hydration benchmark perf', () => {
afterEach(verifyNoBrowserErrors);

[CreateWorker, UpdateWorker].forEach((worker) => {
describe(worker.id, () => {
it(`should run benchmark for ${testPackageName}`, async () => {
await runTableBenchmark({
id: `hydration.${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
});
}
123 changes: 123 additions & 0 deletions modules/benchmarks/src/hydration/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* @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, ComponentRef, createComponent, EnvironmentInjector} from '@angular/core';

import {bindAction, profile} from '../util';

import {TableComponent} from './table';
import {buildTable, emptyTable, initTableUtils, TableCell} 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, insertSsrContent = true) {
let tableComponentRef: ComponentRef<TableComponent>;
const injector = appRef.injector;
const environmentInjector = injector.get(EnvironmentInjector);

let data: TableCell[][] = [];

const setInput = (data: TableCell[][]) => {
if (tableComponentRef) {
tableComponentRef.setInput('data', data);
tableComponentRef.changeDetectorRef.detectChanges();
}
};

function destroyDom() {
setInput(emptyTable);
}

function updateDom() {
data = buildTable();
setInput(data);
}

function createDom() {
const hostElement = document.getElementById('table');
tableComponentRef = createComponent(TableComponent, {environmentInjector, hostElement});
setInput(data);
}

function prepare() {
destroyDom();
data = buildTable();

if (insertSsrContent) {
// Prepare DOM structure, similar to what SSR would produce.
const hostElement = document.getElementById('table');
hostElement.setAttribute('ngh', '0');
hostElement.textContent = ''; // clear existing DOM contents
hostElement.appendChild(createTableDom(data));
}
}

function noop() {}

initTableUtils();

bindAction('#prepare', prepare);
bindAction('#createDom', createDom);
bindAction('#updateDom', updateDom);
bindAction('#createDomProfile', profile(createDom, prepare, 'create'));
bindAction('#updateDomProfile', profile(updateDom, noop, 'update'));
}

/**
* Creates DOM to represent a table, similar to what'd be generated
* during the SSR.
*/
function createTableDom(data: TableCell[][]) {
const table = document.createElement('table');
const tbody = document.createElement('tbody');
table.appendChild(tbody);
this._renderCells = [];
for (let r = 0; r < data.length; r++) {
const dataRow = data[r];
const tr = document.createElement('tr');
// Mark created DOM nodes, so that we can verify that
// they were *not* re-created during hydration.
(tr as any).__existing = true;
tbody.appendChild(tr);
const renderRow = [];
for (let c = 0; c < dataRow.length; c++) {
const dataCell = dataRow[c];
const renderCell = document.createElement('td');
// Mark created DOM nodes, so that we can verify that
// they were *not* re-created during hydration.
(renderCell as any).__existing = true;
if (r % 2 === 0) {
renderCell.style.backgroundColor = 'grey';
}
tr.appendChild(renderCell);
renderRow[c] = renderCell;
renderCell.textContent = dataCell.value;
}
// View container anchor
const comment = document.createComment('');
tr.appendChild(comment);
}
// View container anchor
const comment = document.createComment('');
tbody.appendChild(comment);
return table;
}
Loading

0 comments on commit d8dac7c

Please sign in to comment.