Skip to content

Commit 0d97c05

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular-devkit/build-angular): add debug profiling support to esbuild angular compiler plugin
When using the experimental esbuild-based browser application builder, initial debug performance profiling information can now be output to the console by using the `NG_BUILD_DEBUG_PERF` environment variable. When enabled, duration information for elements of the Angular build pipeline will be shown on the console. Certain elements marked with an asterisk postfix represent the total parallel execution time and will not correlate directly to the total build time. This information is useful for both experimentation with build process improvements as well as diagnosing slow builds.
1 parent 8ccf873 commit 0d97c05

File tree

3 files changed

+194
-90
lines changed

3 files changed

+194
-90
lines changed

packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts

Lines changed: 124 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ import ts from 'typescript';
2424
import angularApplicationPreset from '../../babel/presets/application';
2525
import { requiresLinking } from '../../babel/webpack-loader';
2626
import { loadEsmModule } from '../../utils/load-esm';
27+
import {
28+
logCumulativeDurations,
29+
profileAsync,
30+
profileSync,
31+
resetCumulativeDurations,
32+
} from './profiling';
2733
import { BundleStylesheetOptions, bundleStylesheetFile, bundleStylesheetText } from './stylesheets';
2834

2935
interface EmitFileResult {
@@ -193,21 +199,23 @@ export function createCompilerPlugin(
193199
options: compilerOptions,
194200
rootNames,
195201
errors: configurationDiagnostics,
196-
} = compilerCli.readConfiguration(pluginOptions.tsconfig, {
197-
noEmitOnError: false,
198-
suppressOutputPathCheck: true,
199-
outDir: undefined,
200-
inlineSources: pluginOptions.sourcemap,
201-
inlineSourceMap: pluginOptions.sourcemap,
202-
sourceMap: false,
203-
mapRoot: undefined,
204-
sourceRoot: undefined,
205-
declaration: false,
206-
declarationMap: false,
207-
allowEmptyCodegenFiles: false,
208-
annotationsAs: 'decorators',
209-
enableResourceInlining: false,
210-
});
202+
} = profileSync('NG_READ_CONFIG', () =>
203+
compilerCli.readConfiguration(pluginOptions.tsconfig, {
204+
noEmitOnError: false,
205+
suppressOutputPathCheck: true,
206+
outDir: undefined,
207+
inlineSources: pluginOptions.sourcemap,
208+
inlineSourceMap: pluginOptions.sourcemap,
209+
sourceMap: false,
210+
mapRoot: undefined,
211+
sourceRoot: undefined,
212+
declaration: false,
213+
declarationMap: false,
214+
allowEmptyCodegenFiles: false,
215+
annotationsAs: 'decorators',
216+
enableResourceInlining: false,
217+
}),
218+
);
211219

212220
if (compilerOptions.target === undefined || compilerOptions.target < ts.ScriptTarget.ES2022) {
213221
// If 'useDefineForClassFields' is already defined in the users project leave the value as is.
@@ -231,6 +239,9 @@ export function createCompilerPlugin(
231239
build.onStart(async () => {
232240
const result: OnStartResult = {};
233241

242+
// Reset debug performance tracking
243+
resetCumulativeDurations();
244+
234245
// Reset stylesheet resource output files
235246
stylesheetResourceFiles = [];
236247

@@ -307,11 +318,10 @@ export function createCompilerPlugin(
307318
}
308319

309320
// Create the Angular specific program that contains the Angular compiler
310-
const angularProgram = new compilerCli.NgtscProgram(
311-
rootNames,
312-
compilerOptions,
313-
host,
314-
previousAngularProgram,
321+
const angularProgram = profileSync(
322+
'NG_CREATE_PROGRAM',
323+
() =>
324+
new compilerCli.NgtscProgram(rootNames, compilerOptions, host, previousAngularProgram),
315325
);
316326
previousAngularProgram = angularProgram;
317327
const angularCompiler = angularProgram.compiler;
@@ -327,7 +337,7 @@ export function createCompilerPlugin(
327337
);
328338
previousBuilder = builder;
329339

330-
await angularCompiler.analyzeAsync();
340+
await profileAsync('NG_ANALYZE_PROGRAM', () => angularCompiler.analyzeAsync());
331341

332342
function* collectDiagnostics(): Iterable<ts.Diagnostic> {
333343
// Collect program level diagnostics
@@ -343,25 +353,36 @@ export function createCompilerPlugin(
343353
continue;
344354
}
345355

346-
yield* builder.getSyntacticDiagnostics(sourceFile);
347-
yield* builder.getSemanticDiagnostics(sourceFile);
356+
yield* profileSync(
357+
'NG_DIAGNOSTICS_SYNTACTIC',
358+
() => builder.getSyntacticDiagnostics(sourceFile),
359+
true,
360+
);
361+
yield* profileSync(
362+
'NG_DIAGNOSTICS_SEMANTIC',
363+
() => builder.getSemanticDiagnostics(sourceFile),
364+
true,
365+
);
348366

349-
const angularDiagnostics = angularCompiler.getDiagnosticsForFile(
350-
sourceFile,
351-
OptimizeFor.WholeProgram,
367+
const angularDiagnostics = profileSync(
368+
'NG_DIAGNOSTICS_TEMPLATE',
369+
() => angularCompiler.getDiagnosticsForFile(sourceFile, OptimizeFor.WholeProgram),
370+
true,
352371
);
353372
yield* angularDiagnostics;
354373
}
355374
}
356375

357-
for (const diagnostic of collectDiagnostics()) {
358-
const message = convertTypeScriptDiagnostic(diagnostic, host);
359-
if (diagnostic.category === ts.DiagnosticCategory.Error) {
360-
(result.errors ??= []).push(message);
361-
} else {
362-
(result.warnings ??= []).push(message);
376+
profileSync('NG_DIAGNOSTICS_TOTAL', () => {
377+
for (const diagnostic of collectDiagnostics()) {
378+
const message = convertTypeScriptDiagnostic(diagnostic, host);
379+
if (diagnostic.category === ts.DiagnosticCategory.Error) {
380+
(result.errors ??= []).push(message);
381+
} else {
382+
(result.warnings ??= []).push(message);
383+
}
363384
}
364-
}
385+
});
365386

366387
fileEmitter = createFileEmitter(
367388
builder,
@@ -376,74 +397,87 @@ export function createCompilerPlugin(
376397

377398
build.onLoad(
378399
{ filter: compilerOptions.allowJs ? /\.[cm]?[jt]sx?$/ : /\.[cm]?tsx?$/ },
379-
async (args) => {
380-
assert.ok(fileEmitter, 'Invalid plugin execution order');
381-
382-
const typescriptResult = await fileEmitter(
383-
pluginOptions.fileReplacements?.[args.path] ?? args.path,
384-
);
385-
if (!typescriptResult) {
386-
// No TS result indicates the file is not part of the TypeScript program.
387-
// If allowJs is enabled and the file is JS then defer to the next load hook.
388-
if (compilerOptions.allowJs && /\.[cm]?js$/.test(args.path)) {
389-
return undefined;
390-
}
391-
392-
// Otherwise return an error
393-
return {
394-
errors: [
395-
{
396-
text: `File '${args.path}' is missing from the TypeScript compilation.`,
397-
notes: [
400+
(args) =>
401+
profileAsync(
402+
'NG_EMIT_TS*',
403+
async () => {
404+
assert.ok(fileEmitter, 'Invalid plugin execution order');
405+
406+
const typescriptResult = await fileEmitter(
407+
pluginOptions.fileReplacements?.[args.path] ?? args.path,
408+
);
409+
if (!typescriptResult) {
410+
// No TS result indicates the file is not part of the TypeScript program.
411+
// If allowJs is enabled and the file is JS then defer to the next load hook.
412+
if (compilerOptions.allowJs && /\.[cm]?js$/.test(args.path)) {
413+
return undefined;
414+
}
415+
416+
// Otherwise return an error
417+
return {
418+
errors: [
398419
{
399-
text: `Ensure the file is part of the TypeScript program via the 'files' or 'include' property.`,
420+
text: `File '${args.path}' is missing from the TypeScript compilation.`,
421+
notes: [
422+
{
423+
text: `Ensure the file is part of the TypeScript program via the 'files' or 'include' property.`,
424+
},
425+
],
400426
},
401427
],
402-
},
403-
],
404-
};
405-
}
406-
407-
const data = typescriptResult.content ?? '';
408-
// The pre-transformed data is used as a cache key. Since the cache is memory only,
409-
// the options cannot change and do not need to be represented in the key. If the
410-
// cache is later stored to disk, then the options that affect transform output
411-
// would need to be added to the key as well.
412-
let contents = babelDataCache.get(data);
413-
if (contents === undefined) {
414-
contents = await transformWithBabel(args.path, data, pluginOptions);
415-
babelDataCache.set(data, contents);
416-
}
417-
418-
return {
419-
contents,
420-
loader: 'js',
421-
};
422-
},
428+
};
429+
}
430+
431+
const data = typescriptResult.content ?? '';
432+
// The pre-transformed data is used as a cache key. Since the cache is memory only,
433+
// the options cannot change and do not need to be represented in the key. If the
434+
// cache is later stored to disk, then the options that affect transform output
435+
// would need to be added to the key as well.
436+
let contents = babelDataCache.get(data);
437+
if (contents === undefined) {
438+
contents = await transformWithBabel(args.path, data, pluginOptions);
439+
babelDataCache.set(data, contents);
440+
}
441+
442+
return {
443+
contents,
444+
loader: 'js',
445+
};
446+
},
447+
true,
448+
),
423449
);
424450

425-
build.onLoad({ filter: /\.[cm]?js$/ }, async (args) => {
426-
// The filename is currently used as a cache key. Since the cache is memory only,
427-
// the options cannot change and do not need to be represented in the key. If the
428-
// cache is later stored to disk, then the options that affect transform output
429-
// would need to be added to the key as well as a check for any change of content.
430-
let contents = pluginOptions.sourceFileCache?.babelFileCache.get(args.path);
431-
if (contents === undefined) {
432-
const data = await fs.readFile(args.path, 'utf-8');
433-
contents = await transformWithBabel(args.path, data, pluginOptions);
434-
pluginOptions.sourceFileCache?.babelFileCache.set(args.path, contents);
435-
}
451+
build.onLoad({ filter: /\.[cm]?js$/ }, (args) =>
452+
profileAsync(
453+
'NG_EMIT_JS*',
454+
async () => {
455+
// The filename is currently used as a cache key. Since the cache is memory only,
456+
// the options cannot change and do not need to be represented in the key. If the
457+
// cache is later stored to disk, then the options that affect transform output
458+
// would need to be added to the key as well as a check for any change of content.
459+
let contents = pluginOptions.sourceFileCache?.babelFileCache.get(args.path);
460+
if (contents === undefined) {
461+
const data = await fs.readFile(args.path, 'utf-8');
462+
contents = await transformWithBabel(args.path, data, pluginOptions);
463+
pluginOptions.sourceFileCache?.babelFileCache.set(args.path, contents);
464+
}
436465

437-
return {
438-
contents,
439-
loader: 'js',
440-
};
441-
});
466+
return {
467+
contents,
468+
loader: 'js',
469+
};
470+
},
471+
true,
472+
),
473+
);
442474

443475
build.onEnd((result) => {
444476
if (stylesheetResourceFiles.length) {
445477
result.outputFiles?.push(...stylesheetResourceFiles);
446478
}
479+
480+
logCumulativeDurations();
447481
});
448482
},
449483
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 { debugPerformance } from '../../utils/environment-options';
10+
11+
let cumulativeDurations: Map<string, number> | undefined;
12+
13+
export function resetCumulativeDurations(): void {
14+
cumulativeDurations?.clear();
15+
}
16+
17+
export function logCumulativeDurations(): void {
18+
if (!debugPerformance || !cumulativeDurations) {
19+
return;
20+
}
21+
22+
for (const [name, duration] of cumulativeDurations) {
23+
// eslint-disable-next-line no-console
24+
console.log(`DURATION[${name}]: ${duration} seconds`);
25+
}
26+
}
27+
28+
function recordDuration(name: string, startTime: bigint, cumulative?: boolean): void {
29+
const duration = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
30+
if (cumulative) {
31+
cumulativeDurations ??= new Map<string, number>();
32+
cumulativeDurations.set(name, (cumulativeDurations.get(name) ?? 0) + duration);
33+
} else {
34+
// eslint-disable-next-line no-console
35+
console.log(`DURATION[${name}]: ${duration} seconds`);
36+
}
37+
}
38+
39+
export async function profileAsync<T>(
40+
name: string,
41+
action: () => Promise<T>,
42+
cumulative?: boolean,
43+
): Promise<T> {
44+
if (!debugPerformance) {
45+
return action();
46+
}
47+
48+
const startTime = process.hrtime.bigint();
49+
try {
50+
return await action();
51+
} finally {
52+
recordDuration(name, startTime, cumulative);
53+
}
54+
}
55+
56+
export function profileSync<T>(name: string, action: () => T, cumulative?: boolean): T {
57+
if (!debugPerformance) {
58+
return action();
59+
}
60+
61+
const startTime = process.hrtime.bigint();
62+
try {
63+
return action();
64+
} finally {
65+
recordDuration(name, startTime, cumulative);
66+
}
67+
}

packages/angular_devkit/build_angular/src/utils/environment-options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,6 @@ export const useLegacySass: boolean = (() => {
9393

9494
return isEnabled(legacySassVariable);
9595
})();
96+
97+
const debugPerfVariable = process.env['NG_BUILD_DEBUG_PERF'];
98+
export const debugPerformance = isPresent(debugPerfVariable) && isEnabled(debugPerfVariable);

0 commit comments

Comments
 (0)