Files
iptvnator/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts
T
4grayandClaude Opus 5.5 d88d005062 perf(web): run the renderer zoneless and drop the zone.js polyfill
Plan item C6 step 5. app.config.ts provides
provideZonelessChangeDetection() directly and apps/web builds without the
zone.js polyfill, so a tick runs only when a signal, markForCheck, an
Angular event or a render hook asks for one, not after every macrotask.
The flag from #1824 (change-detection.providers*.ts and the *-zoneless
configurations) goes away; a build-config test fails if the polyfill or a
zone provider comes back.

NgZone.run/runOutsideAngular calls that only mattered with zone.js are
removed from the dashboard data services, the settings unload guard and
the embedded MPV position poll (with the spec case asserting it), and
the unused polyfills.ts, polyfills-test.ts and setup-jest.ts are deleted.
Unit tests keep zone.js/testing for fakeAsync and waitForAsync;
apps/remote-control-web still bootstraps with zone.js.

renderer.initialBytes: 1,596,201 -> 1,545,931 bytes (-50,270) on a local
production build of this base; the baseline is lowered to the measured
value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:44:52 +02:00

432 lines
15 KiB
TypeScript

/* eslint-disable playwright/expect-expect -- These are Node assertion-based configuration contract tests. */
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import { join } from 'node:path';
import { JOURNEY_CD_TICK_COUNTER_KEY } from './journey-renderer-probe';
interface TargetConfiguration {
configurations?: Record<string, Record<string, unknown>>;
dependsOn?: unknown;
executor?: unknown;
options?: Record<string, unknown>;
}
interface ProjectConfiguration {
targets: Record<string, TargetConfiguration>;
}
interface NxGraphTask {
outputs?: string[];
target: {
project: string;
target: string;
};
}
interface NxGraph {
tasks: {
tasks: Record<string, NxGraphTask>;
};
}
const workspaceRoot = fileURLToPath(new URL('../../../../', import.meta.url));
function readProject(relativePath: string): ProjectConfiguration {
return JSON.parse(
readFileSync(join(workspaceRoot, relativePath), 'utf8')
) as ProjectConfiguration;
}
function readResolvedWebBuildTask(): NxGraphTask | undefined {
const environment: NodeJS.ProcessEnv = {
...process.env,
FORCE_COLOR: '0',
NX_DAEMON: 'false',
};
delete environment['NO_COLOR'];
const graph = JSON.parse(
execFileSync(
process.execPath,
[
join(workspaceRoot, 'node_modules/nx/dist/bin/nx.js'),
'run',
'web:build',
'--graph=stdout',
],
{
cwd: workspaceRoot,
encoding: 'utf8',
env: environment,
}
)
) as NxGraph;
return Object.values(graph.tasks.tasks).find(
(task) =>
task.target.project === 'web' && task.target.target === 'build'
);
}
const e2eProject = readProject('apps/electron-backend-e2e/project.json');
const electronProject = readProject('apps/electron-backend/project.json');
const remoteControlProject = readProject(
'apps/remote-control-web/project.json'
);
const webProject = readProject('apps/web/project.json');
test('the performance harness target runs only Node performance specs', () => {
const target = e2eProject.targets['test-performance-harness'];
assert.ok(
target,
'electron-backend-e2e must define test-performance-harness'
);
assert.equal(target.executor, 'nx:run-commands');
assert.equal(target.options?.['cwd'], 'apps/electron-backend-e2e');
assert.equal(
target.options?.['command'],
'tsx --test "src/performance/*.spec.ts"'
);
});
// A nested `pnpm exec` under `pnpm nx` can run from the workspace root instead
// of the target cwd (an older global pnpm with version switching disabled), so
// cwd-relative globs, specs and configs are not found. run-commands already
// puts node_modules/.bin on PATH.
test('electron-backend-e2e command targets call binaries without pnpm exec', () => {
for (const [name, target] of Object.entries(e2eProject.targets)) {
const command = target.options?.['command'];
if (typeof command !== 'string') continue;
assert.doesNotMatch(command, /\bpnpm exec\b/, name);
}
});
test('the web performance build keeps production renderer behavior with profiling source maps', () => {
const build = webProject.targets['build'];
const production = build.configurations?.['production'];
const performance = build.configurations?.['electron-performance'];
assert.ok(production, 'web:build must define production');
assert.ok(performance, 'web:build must define electron-performance');
assert.equal(performance['baseHref'], './');
assert.equal(performance['serviceWorker'], false);
assert.deepEqual(performance['optimization'], production['optimization']);
assert.equal(performance['outputHashing'], production['outputHashing']);
// The only difference is the environment: production values plus the
// change-detection tick counter the journeys read.
assert.deepEqual(performance['fileReplacements'], [
{
replace: 'apps/web/src/environments/environment.ts',
with: 'apps/web/src/environments/environment.performance.ts',
},
]);
assert.deepEqual(production['fileReplacements'], [
{
replace: 'apps/web/src/environments/environment.ts',
with: 'apps/web/src/environments/environment.prod.ts',
},
]);
assert.equal(performance['sourceMap'], true);
});
test('only the web performance build installs the tick counter the journeys read', () => {
const environments = join(workspaceRoot, 'apps/web/src/environments');
const performanceEnvironment = readFileSync(
join(environments, 'environment.performance.ts'),
'utf8'
);
assert.match(
performanceEnvironment,
/export \{ AppConfig \} from '\.\/environment\.prod';/
);
assert.match(
performanceEnvironment,
/installChangeDetectionTickCounter\(\);/
);
assert.match(
readFileSync(
join(environments, 'change-detection-tick-counter.ts'),
'utf8'
),
new RegExp(
`CHANGE_DETECTION_TICK_COUNTER_KEY = '${JOURNEY_CD_TICK_COUNTER_KEY}'`
)
);
// No other configuration may reference the performance environment.
for (const [name, configuration] of Object.entries(
webProject.targets['build'].configurations ?? {}
)) {
if (name === 'electron-performance') continue;
assert.doesNotMatch(
JSON.stringify(configuration['fileReplacements'] ?? []),
/environment\.performance/,
name
);
}
});
// @ngrx/store-devtools must not reach the production, PWA or performance
// bundles (renderer.initialBytes): the default providers file is empty and
// only the development configurations swap in the devtools.
test('only development web builds provide the NgRx store devtools', () => {
const environments = join(workspaceRoot, 'apps/web/src/environments');
const devtoolsReplacement = {
replace: 'apps/web/src/environments/store-devtools.providers.ts',
with: 'apps/web/src/environments/store-devtools.providers.dev.ts',
};
const developmentConfigurations = new Set(['development', 'electron-e2e']);
assert.match(
readFileSync(join(environments, 'store-devtools.providers.ts'), 'utf8'),
/export const storeDevtoolsProviders: EnvironmentProviders\[\] = \[\];/
);
// The replacement must still provide them, or development builds would
// lose the devtools while this test passed.
assert.match(
readFileSync(
join(environments, 'store-devtools.providers.dev.ts'),
'utf8'
),
/storeDevtoolsProviders: EnvironmentProviders\[\] = \[\s*provideStoreDevtools\(/
);
assert.doesNotMatch(
readFileSync(
join(workspaceRoot, 'apps/web/src/app/app.config.ts'),
'utf8'
),
/@ngrx\/store-devtools/
);
for (const [name, configuration] of Object.entries(
webProject.targets['build'].configurations ?? {}
)) {
const replacements = (configuration['fileReplacements'] ??
[]) as unknown[];
if (developmentConfigurations.has(name)) {
assert.deepEqual(replacements[0], devtoolsReplacement, name);
} else {
assert.doesNotMatch(
JSON.stringify(replacements),
/store-devtools/,
name
);
}
}
});
// Plan item C6: the renderer runs zoneless. A zone.js polyfill or a zone
// provider would bring back a tick after every macrotask and make the
// change-detection tick counters racy again.
test('the web app bootstraps zoneless without the zone.js polyfill', () => {
const build = webProject.targets['build'];
const configurations = Object.entries(build.configurations ?? {});
assert.equal(build.options?.['polyfills'], undefined);
for (const [name, configuration] of configurations) {
assert.equal(configuration['polyfills'], undefined, name);
}
const appConfig = readFileSync(
join(workspaceRoot, 'apps/web/src/app/app.config.ts'),
'utf8'
);
assert.match(appConfig, /provideZonelessChangeDetection\(\)/);
assert.doesNotMatch(appConfig, /provideZoneChangeDetection/);
});
test('the resolved web build cache output is the renderer directory', () => {
const task = readResolvedWebBuildTask();
assert.ok(task, 'the Nx graph must include web:build');
assert.deepEqual(task.outputs, ['dist/apps/web']);
});
test('the remote-control performance build keeps optimized hashed output with profiling source maps', () => {
const build = remoteControlProject.targets['build'];
const production = build.configurations?.['production'];
const performance = build.configurations?.['electron-performance'];
assert.ok(production, 'remote-control-web:build must define production');
assert.ok(
performance,
'remote-control-web:build must define electron-performance'
);
assert.equal(performance['optimization'], true);
assert.equal(performance['outputHashing'], production['outputHashing']);
assert.equal(performance['sourceMap'], true);
});
test('the Electron performance build keeps production main-process behavior with profiling source maps', () => {
const build = electronProject.targets['build'];
const production = build.configurations?.['production'];
const performance = build.configurations?.['electron-performance'];
assert.ok(production, 'electron-backend:build must define production');
assert.ok(
performance,
'electron-backend:build must define electron-performance'
);
assert.equal(performance['optimization'], production['optimization']);
assert.equal(performance['inspect'], production['inspect']);
assert.deepEqual(
performance['fileReplacements'],
production['fileReplacements']
);
assert.equal(performance['sourceMap'], true);
});
test('the regular Electron build keeps its existing renderer dependency contract', () => {
const build = electronProject.targets['build'];
const dependencies = build.dependsOn as Array<
Record<string, unknown> | string
>;
const rendererBuild = dependencies.find(
(dependency): dependency is Record<string, unknown> =>
typeof dependency === 'object' &&
dependency !== null &&
dependency['target'] === 'build'
);
assert.deepEqual(rendererBuild, {
projects: ['web', 'remote-control-web'],
target: 'build',
});
});
test('the web performance wrapper selects the profiling configuration', () => {
const target = webProject.targets['build-performance'];
assert.ok(target, 'web must define build-performance');
assert.equal(target.executor, 'nx:run-commands');
assert.equal(
target.options?.['command'],
'pnpm nx run web:build:electron-performance'
);
});
test('the remote-control performance wrapper selects the profiling configuration', () => {
const target = remoteControlProject.targets['build-performance'];
assert.ok(target, 'remote-control-web must define build-performance');
assert.equal(target.executor, 'nx:run-commands');
assert.equal(
target.options?.['command'],
'pnpm nx run remote-control-web:build:electron-performance'
);
});
test('the Electron performance wrapper owns build dependencies before its profiled build', () => {
const target = electronProject.targets['build-performance'];
assert.ok(target, 'electron-backend must define build-performance');
assert.equal(target.executor, 'nx:run-commands');
assert.deepEqual(target.dependsOn, [
'electron-backend:build-worker-performance',
'electron-backend:build-embedded-mpv',
{
projects: ['web', 'remote-control-web'],
target: 'build-performance',
},
]);
assert.equal(
target.options?.['command'],
'pnpm nx run electron-backend:build:electron-performance --excludeTaskDependencies'
);
});
test('the Electron performance wrapper builds optimized source-mapped workers', () => {
const target = electronProject.targets['build-worker-performance'];
const source = readFileSync(
join(workspaceRoot, 'apps/electron-backend/build-worker.js'),
'utf8'
);
assert.ok(target, 'electron-backend must define build-worker-performance');
assert.equal(target.executor, 'nx:run-commands');
assert.equal(
target.options?.['command'],
'node apps/electron-backend/build-worker.js --performance'
);
assert.match(
source,
/const isPerformance = process\.argv\.includes\('--performance'\)/
);
assert.match(source, /minify: isProduction \|\| isPerformance/);
assert.match(source, /sourcemap: isPerformance \|\| !isProduction/);
});
test('the cancellation benchmark uses the production-equivalent performance build', () => {
const target = e2eProject.targets['benchmark-m3u-refresh-cancellation'];
assert.deepEqual(target.dependsOn, ['electron-backend:build-performance']);
});
test('the cancellation benchmark command is pinned to its Playwright test file', () => {
const target = e2eProject.targets['benchmark-m3u-refresh-cancellation'];
assert.equal(
target.options?.['command'],
'playwright test --config=playwright.performance.config.ts src/m3u-refresh-cancellation.performance.ts'
);
});
test('the cancellation benchmark enables preload performance capture', () => {
const source = readFileSync(
join(
workspaceRoot,
'apps/electron-backend-e2e/src/performance/m3u-refresh-cancellation.benchmark.ts'
),
'utf8'
);
assert.match(source, /IPTVNATOR_PERF_CAPTURE:\s*'1'/);
});
test('the cancellation benchmark exposes GC to Electron worker isolates', () => {
const source = readFileSync(
join(
workspaceRoot,
'apps/electron-backend-e2e/src/performance/m3u-refresh-cancellation.benchmark.ts'
),
'utf8'
);
assert.match(source, /'--js-flags=--expose-gc'/);
assert.doesNotMatch(source, /execArgv/);
});
test('the initial M3U import benchmark uses the production-equivalent performance build', () => {
const target = e2eProject.targets['benchmark-m3u-import'];
assert.ok(target, 'electron-backend-e2e must define benchmark-m3u-import');
assert.deepEqual(target.dependsOn, ['electron-backend:build-performance']);
});
test('the initial M3U import benchmark command is pinned to its Playwright test file', () => {
const target = e2eProject.targets['benchmark-m3u-import'];
assert.equal(
target.options?.['command'],
'playwright test --config=playwright.performance.config.ts src/m3u-import.performance.ts'
);
});
test('the initial M3U import benchmark enables opt-in capture and worker GC', () => {
const source = readFileSync(
join(
workspaceRoot,
'apps/electron-backend-e2e/src/performance/m3u-import.benchmark.ts'
),
'utf8'
);
assert.match(source, /IPTVNATOR_PERF_CAPTURE:\s*'1'/);
assert.match(source, /IPTVNATOR_PERF_WORKER_PROFILING:\s*'1'/);
assert.match(source, /'--js-flags=--expose-gc'/);
assert.doesNotMatch(source, /execArgv/);
});