Skip to content

Commit

Permalink
refactor(platform-server): Add an ssr benchmark setup. (#57647)
Browse files Browse the repository at this point in the history
In order to investigate the performances of SSR, this commit introduces a benchmark suite which will measure several step of the rendering.

PR Close #57647
  • Loading branch information
JeanMeche authored and AndrewKushnir committed Oct 4, 2024
1 parent 58bfb4a commit 84b6896
Show file tree
Hide file tree
Showing 48 changed files with 7,262 additions and 88 deletions.
3 changes: 2 additions & 1 deletion .pullapprove.yml
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,8 @@ groups:
conditions:
- >
contains_any_globs(files, [
'modules/benchmarks/**/{*,.*}'
'modules/benchmarks/**/{*,.*}',
'modules/ssr-benchmarks/**/{*,.*}'
])
reviewers:
users:
Expand Down
1 change: 1 addition & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ nodejs_binary(
entry_point = ".yarn/releases/yarn-1.22.19.cjs",
visibility = [
"//integration:__subpackages__",
"//modules/ssr-benchmarks:__subpackages__",
],
)

Expand Down
2 changes: 2 additions & 0 deletions integration/npm_package_archives.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ NPM_PACKAGE_ARCHIVES = [
"@rollup/plugin-commonjs",
"check-side-effects",
"jasmine",
"http-server",
"typescript",
"rxjs",
"systemjs",
"tslib",
"patch-package",
"protractor",
"terser",
"rollup",
Expand Down
61 changes: 61 additions & 0 deletions modules/ssr-benchmarks/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
load("//integration:index.bzl", "ng_integration_test")

ng_integration_test(
name = "run",
commands = [
"yarn install --cache-folder ./.yarn_local_cache",
"yarn ng build",
"node ./dist/ssr-benchmarks/server/server.mjs",
],
)

## This target is intended to run the benchmark in a browser
## For this the benchmark script is loaded by an html page serve the http-server script
## Note: This target doesn't end until the http-server is stopped. It should not run as part of our testset.
ng_integration_test(
name = "run_browser",
commands = [
"yarn install --cache-folder ./.yarn_local_cache",

# The patch only applies for the browser target (to remove the DOM Emulation polyfills and browser incompatible code)
"yarn patch",
#
"NG_BUILD_MANGLE=0 yarn ng build --configuration production,browser",
"yarn http-server ./dist",
],
)

# This target is mostly intended for investigating via the devTools using the flamechart
ng_integration_test(
name = "run_browser_emulated_dom",
commands = [
"yarn install --cache-folder ./.yarn_local_cache",
# We keep the emulated dom in this target
# But still need to drop the node import which doesn't work in browsers.
"git apply patches/@angular-devkit+build-angular++@angular+build+19.0.0-next.6+require.patch",

# We keep the symbols with the NG_BUILD_MANGLE flag
"NG_BUILD_MANGLE=0 yarn ng build",
"yarn http-server ./dist",
],
)

# This is a target to investigate with deopt explorer (https://github.com/microsoft/deoptexplorer-vscode)
# The v8 log file will be generated in the test directory
ng_integration_test(
name = "run_deopt",
commands = [
"yarn install --cache-folder ./.yarn_local_cache",
"NG_BUILD_MANGLE=0 yarn ng build",
"yarn node --prof \
--log-deopt \
--log-ic \
--log-maps \
--log-maps-details \
--log-internal-timer-events \
--log-code \
--log-source-code \
--detailed-line-info \
./dist/ssr-benchmarks/server/server.mjs narrowRun",
],
)
60 changes: 60 additions & 0 deletions modules/ssr-benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
## Intro

This small benchmark suite is dedicated to mesure & describe how compute time is spent when rendering an application like in SSR.

## Struture

* `./main.ts` is the entry point to run the benchmark
* `./src` contains a sample app that exports a `render` function.
* This app render a table of variable size, which depends on data (`initData()`)
* This app is then rendered X numbers of times

* Individual function calls are measured with `startMeasuring()`/`stopMeasuring()` from the core package.
* If you add a new measure, make sure to add it also to the `levels` map for it to be represented correctly in the result

## Build & run

`yarn bazel run //modules/ssr-benchmarks:run`


### Running the benchmark in a browser environment

`yarn bazel run //modules/ssr-benchmarks:run_browser`

This bazel target will build the benchmark, start a http-server with a html that will load the benckmark script.
The benchmark script with this target will have DOM Emulation disabled.
The result will be visible in the devtools console.

Note: Due to the CLI adding some polyfills, @angular/build is patched to disable DOM emulation and running server code inside a browser:
1. removing an import from `node:module` in `polyfills.server.mjs` (with `tail ...`)
2. removing the import of `platform-server/init`.

To run create a usable flame chart, prepare a narrowed run (like `benchmarkRun(10000, 20);`).
Then in the performance tab of the devtools, trigger "Record & Reload" to generate a profile.

### Deopt Explorer

A target is dedicated to generate a v8 log that can be fed to the [Deopt Explorer extension](https://github.com/microsoft/deoptexplorer-vscode).

1. Run `yarn bazel run //modules/ssr-benchmarks:run_deopt`,
2. open the project generated at the path after `Successfully ran all commands in test directory:`,
3. open the logfile in the extension

## Result example

=== table with 10000 rows, with 1000 renders ===
┌─────────┬──────────────────────────────────────┬──────────┬──────────┬────────────┬───────────┐
│ (index) │ name │ min │ average │ percentage │ max │
├─────────┼──────────────────────────────────────┼──────────┼──────────┼────────────┼───────────┤
│ 0 │ ' renderApplication ' │ '77.0ms' │ '86.4ms' │ '100.0%' │ '259.2ms' │
│ 1 │ ' └ createServerPlatform ' │ '0.0ms' │ '0.1ms' │ '0.1%' │ '3.7ms' │
│ 2 │ ' └ bootstrap ' │ '35.9ms' │ '42.6ms' │ '49.3%' │ '138.4ms' │
│ 3 │ ' └ _render ' │ '39.7ms' │ '43.8ms' │ '50.7%' │ '124.9ms' │
│ 4 │ ' └ whenStable ' │ '0.0ms' │ '0.0ms' │ '0.0%' │ '0.0ms' │
│ 5 │ ' └ prepareForHydration ' │ '13.1ms' │ '14.8ms' │ '17.1%' │ '53.4ms' │
│ 6 │ ' └ insertEventRecordScript ' │ '0.0ms' │ '0.0ms' │ '0.0%' │ '0.0ms' │
│ 7 │ ' └ serializeTransferStateFactory' │ '0.0ms' │ '0.0ms' │ '0.0%' │ '0.1ms' │
│ 8 │ ' └ renderToString ' │ '7.3ms' │ '8.9ms' │ '10.3%' │ '41.8ms' │
└─────────┴──────────────────────────────────────┴──────────┴──────────┴────────────┴───────────┘

Note: The max measure is often an outlier of the first few measures, probably before the JIT optimisation happens
76 changes: 76 additions & 0 deletions modules/ssr-benchmarks/angular.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"ssr-benchmarks": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/ssr-benchmarks",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [],
"scripts": [],
"server": "src/main.server.ts",
"prerender": false,
"ssr": {
"entry": "run-benchmark.ts"
}
},
"configurations": {
"production": {
"outputHashing": "none",
"define": {
"DISABLE_DOM_EMULATION": "false"
}
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
},
"browser": {
"define": {
"DISABLE_DOM_EMULATION": "true"
}
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "ssr-new:build:production"
},
"development": {
"buildTarget": "ssr-new:build:development"
}
},
"defaultConfiguration": "development"
}
}
}
},
"cli": {
"analytics": "8b89251b-0b82-4190-a832-e19ddedba643",
"cache": {
"enabled": false
}
}
}
8 changes: 8 additions & 0 deletions modules/ssr-benchmarks/dist/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<script type="module" src="./ssr-benchmarks/server/server.mjs"></script>
<body>
Check the console.
<app-root></app-root>
</body>

</html>
28 changes: 28 additions & 0 deletions modules/ssr-benchmarks/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "ssr-benchmark",
"version": "0.0.0",
"scripts": {
"patch": "yarn patch-package",
"ng": "ng",
"http-server": "http-server"
},
"license": "MIT",
"dependencies": {
"@angular-devkit/build-angular": "file:../../node_modules/@angular-devkit/build-angular",
"@angular/cli": "file:../../node_modules/@angular/cli",
"@angular/animations": "file:../../dist/packages-dist/animations",
"@angular/common": "file:../../dist/packages-dist/common",
"@angular/compiler": "file:../../dist/packages-dist/compiler",
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
"@angular/core": "file:../../dist/packages-dist/core",
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",
"@angular/platform-server": "file:../../dist/packages-dist/platform-server",
"@angular/router": "file:../../dist/packages-dist/router",
"@angular/ssr": "file:../../dist/node_modules/@angular/ssr",
"rxjs": "file:../../node_modules/rxjs",
"typescript": "file:../../node_modules/typescript",
"http-server": "file:../../node_modules/http-server",
"patch-package": "file:../../node_modules/patch-package"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
diff --git a/node_modules/@angular-devkit/build-angular/node_modules/@angular/build/src/tools/esbuild/application-code-bundle.js b/node_modules/@angular-devkit/build-angular/node_modules/@angular/build/src/tools/esbuild/application-code-bundle.js
index f6fb661..5e5aabb 100755
--- a/node_modules/@angular-devkit/build-angular/node_modules/@angular/build/src/tools/esbuild/application-code-bundle.js
+++ b/node_modules/@angular-devkit/build-angular/node_modules/@angular/build/src/tools/esbuild/application-code-bundle.js
@@ -144,8 +144,8 @@ function createServerPolyfillBundleOptions(options, target, sourceFileCache) {
js: [
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
// See: https://github.com/evanw/esbuild/issues/1921.
- `import { createRequire } from 'node:module';`,
- `globalThis['require'] ??= createRequire(import.meta.url);`,
+ // `import { createRequire } from 'node:module';`,
+ // `globalThis['require'] ??= createRequire(import.meta.url);`,
].join('\n'),
},
target,
@@ -197,15 +197,17 @@ function createServerMainCodeBundleOptions(options, target, sourceFileCache) {
}
// Mark manifest and polyfills file as external as these are generated by a different bundle step.
(buildOptions.external ??= []).push(...utils_1.SERVER_GENERATED_EXTERNALS);
- buildOptions.plugins.push((0, virtual_module_plugin_1.createVirtualModulePlugin)({
- namespace: mainServerInjectPolyfillsNamespace,
- cache: sourceFileCache?.loadResultCache,
- loadContent: () => ({
- contents: `import './polyfills.server.mjs';`,
- loader: 'js',
- resolveDir: workspaceRoot,
- }),
- }), (0, virtual_module_plugin_1.createVirtualModulePlugin)({
+ buildOptions.plugins.push(
+ // (0, virtual_module_plugin_1.createVirtualModulePlugin)({
+ // namespace: mainServerInjectPolyfillsNamespace,
+ // cache: sourceFileCache?.loadResultCache,
+ // loadContent: () => ({
+ // contents: `import './polyfills.server.mjs';`,
+ // loader: 'js',
+ // resolveDir: workspaceRoot,
+ // }),
+ // }),
+ (0, virtual_module_plugin_1.createVirtualModulePlugin)({
namespace: mainServerInjectManifestNamespace,
cache: sourceFileCache?.loadResultCache,
loadContent: async () => {
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
diff --git a/node_modules/@angular-devkit/build-angular/node_modules/@angular/build/src/tools/esbuild/application-code-bundle.js b/node_modules/@angular-devkit/build-angular/node_modules/@angular/build/src/tools/esbuild/application-code-bundle.js
index f6fb661..5e5aabb 100755
--- a/node_modules/@angular-devkit/build-angular/node_modules/@angular/build/src/tools/esbuild/application-code-bundle.js
+++ b/node_modules/@angular-devkit/build-angular/node_modules/@angular/build/src/tools/esbuild/application-code-bundle.js
@@ -121,7 +121,7 @@ function createServerPolyfillBundleOptions(options, target, sourceFileCache) {
polyfillsFromConfig.has('@angular/localize/init')) {
serverPolyfills.push('@angular/localize/init');
}
- serverPolyfills.push('@angular/platform-server/init');
+ // serverPolyfills.push('@angular/platform-server/init');
const namespace = 'angular:polyfills-server';
const polyfillBundleOptions = getEsBuildCommonPolyfillsOptions({
...options,
@@ -144,8 +144,8 @@ function createServerPolyfillBundleOptions(options, target, sourceFileCache) {
js: [
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
// See: https://github.com/evanw/esbuild/issues/1921.
- `import { createRequire } from 'node:module';`,
- `globalThis['require'] ??= createRequire(import.meta.url);`,
+ // `import { createRequire } from 'node:module';`,
+ // `globalThis['require'] ??= createRequire(import.meta.url);`,
].join('\n'),
},
target,
@@ -197,15 +197,17 @@ function createServerMainCodeBundleOptions(options, target, sourceFileCache) {
}
// Mark manifest and polyfills file as external as these are generated by a different bundle step.
(buildOptions.external ??= []).push(...utils_1.SERVER_GENERATED_EXTERNALS);
- buildOptions.plugins.push((0, virtual_module_plugin_1.createVirtualModulePlugin)({
- namespace: mainServerInjectPolyfillsNamespace,
- cache: sourceFileCache?.loadResultCache,
- loadContent: () => ({
- contents: `import './polyfills.server.mjs';`,
- loader: 'js',
- resolveDir: workspaceRoot,
- }),
- }), (0, virtual_module_plugin_1.createVirtualModulePlugin)({
+ buildOptions.plugins.push(
+ // (0, virtual_module_plugin_1.createVirtualModulePlugin)({
+ // namespace: mainServerInjectPolyfillsNamespace,
+ // cache: sourceFileCache?.loadResultCache,
+ // loadContent: () => ({
+ // contents: `import './polyfills.server.mjs';`,
+ // loader: 'js',
+ // resolveDir: workspaceRoot,
+ // }),
+ // }),
+ (0, virtual_module_plugin_1.createVirtualModulePlugin)({
namespace: mainServerInjectManifestNamespace,
cache: sourceFileCache?.loadResultCache,
loadContent: async () => {
Loading

0 comments on commit 84b6896

Please sign in to comment.