Skip to content

Commit 52db3c0

Browse files
clydinalan-agius4
authored andcommitted
perf(@angular-devkit/build-angular): minimize Angular diagnostics incremental analysis in esbuild-based builder
When using the experimental esbuild-based browser application builder, the Angular diagnostic analysis performed per rebuild is now reduced to only the affected files for that rebuild. A rebuild will now query the TypeScript compiler and the Angular compiler to determine the list of potentially affected files. The Angular compiler will then only be queried for diagnostics for this set of affected files instead of the entirety of the program.
1 parent 1518133 commit 52db3c0

File tree

2 files changed

+80
-19
lines changed

2 files changed

+80
-19
lines changed

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

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,8 @@ export function createCompilerPlugin(
173173

174174
// This uses a wrapped dynamic import to load `@angular/compiler-cli` which is ESM.
175175
// Once TypeScript provides support for retaining dynamic imports this workaround can be dropped.
176-
const compilerCli = await loadEsmModule<typeof import('@angular/compiler-cli')>(
177-
'@angular/compiler-cli',
178-
);
176+
const { GLOBAL_DEFS_FOR_TERSER_WITH_AOT, NgtscProgram, OptimizeFor, readConfiguration } =
177+
await loadEsmModule<typeof import('@angular/compiler-cli')>('@angular/compiler-cli');
179178

180179
// Temporary deep import for transformer support
181180
const {
@@ -185,7 +184,7 @@ export function createCompilerPlugin(
185184

186185
// Setup defines based on the values provided by the Angular compiler-cli
187186
build.initialOptions.define ??= {};
188-
for (const [key, value] of Object.entries(compilerCli.GLOBAL_DEFS_FOR_TERSER_WITH_AOT)) {
187+
for (const [key, value] of Object.entries(GLOBAL_DEFS_FOR_TERSER_WITH_AOT)) {
189188
if (key in build.initialOptions.define) {
190189
// Skip keys that have been manually provided
191190
continue;
@@ -202,7 +201,7 @@ export function createCompilerPlugin(
202201
rootNames,
203202
errors: configurationDiagnostics,
204203
} = profileSync('NG_READ_CONFIG', () =>
205-
compilerCli.readConfiguration(pluginOptions.tsconfig, {
204+
readConfiguration(pluginOptions.tsconfig, {
206205
noEmitOnError: false,
207206
suppressOutputPathCheck: true,
208207
outDir: undefined,
@@ -249,6 +248,7 @@ export function createCompilerPlugin(
249248
let previousBuilder: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined;
250249
let previousAngularProgram: NgtscProgram | undefined;
251250
const babelDataCache = new Map<string, string>();
251+
const diagnosticCache = new WeakMap<ts.SourceFile, ts.Diagnostic[]>();
252252

253253
build.onStart(async () => {
254254
const result: OnStartResult = {
@@ -339,12 +339,10 @@ export function createCompilerPlugin(
339339
// Create the Angular specific program that contains the Angular compiler
340340
const angularProgram = profileSync(
341341
'NG_CREATE_PROGRAM',
342-
() =>
343-
new compilerCli.NgtscProgram(rootNames, compilerOptions, host, previousAngularProgram),
342+
() => new NgtscProgram(rootNames, compilerOptions, host, previousAngularProgram),
344343
);
345344
previousAngularProgram = angularProgram;
346345
const angularCompiler = angularProgram.compiler;
347-
const { ignoreForDiagnostics } = angularCompiler;
348346
const typeScriptProgram = angularProgram.getTsProgram();
349347
augmentProgramWithVersioning(typeScriptProgram);
350348

@@ -366,12 +364,16 @@ export function createCompilerPlugin(
366364
yield* builder.getGlobalDiagnostics();
367365

368366
// Collect source file specific diagnostics
369-
const OptimizeFor = compilerCli.OptimizeFor;
367+
const affectedFiles = findAffectedFiles(builder, angularCompiler);
368+
const optimizeFor =
369+
affectedFiles.size > 1 ? OptimizeFor.WholeProgram : OptimizeFor.SingleFile;
370370
for (const sourceFile of builder.getSourceFiles()) {
371-
if (ignoreForDiagnostics.has(sourceFile)) {
371+
if (angularCompiler.ignoreForDiagnostics.has(sourceFile)) {
372372
continue;
373373
}
374374

375+
// TypeScript will use cached diagnostics for files that have not been
376+
// changed or affected for this build when using incremental building.
375377
yield* profileSync(
376378
'NG_DIAGNOSTICS_SYNTACTIC',
377379
() => builder.getSyntacticDiagnostics(sourceFile),
@@ -383,12 +385,22 @@ export function createCompilerPlugin(
383385
true,
384386
);
385387

386-
const angularDiagnostics = profileSync(
387-
'NG_DIAGNOSTICS_TEMPLATE',
388-
() => angularCompiler.getDiagnosticsForFile(sourceFile, OptimizeFor.WholeProgram),
389-
true,
390-
);
391-
yield* angularDiagnostics;
388+
// Only request Angular template diagnostics for affected files to avoid
389+
// overhead of template diagnostics for unchanged files.
390+
if (affectedFiles.has(sourceFile)) {
391+
const angularDiagnostics = profileSync(
392+
'NG_DIAGNOSTICS_TEMPLATE',
393+
() => angularCompiler.getDiagnosticsForFile(sourceFile, optimizeFor),
394+
true,
395+
);
396+
diagnosticCache.set(sourceFile, angularDiagnostics);
397+
yield* angularDiagnostics;
398+
} else {
399+
const angularDiagnostics = diagnosticCache.get(sourceFile);
400+
if (angularDiagnostics) {
401+
yield* angularDiagnostics;
402+
}
403+
}
392404
}
393405
}
394406

@@ -408,7 +420,7 @@ export function createCompilerPlugin(
408420
mergeTransformers(angularCompiler.prepareEmit().transformers, {
409421
before: [replaceBootstrap(() => builder.getProgram().getTypeChecker())],
410422
}),
411-
(sourceFile) => angularCompiler.incrementalDriver.recordSuccessfulEmit(sourceFile),
423+
(sourceFile) => angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile),
412424
);
413425

414426
return result;
@@ -590,3 +602,52 @@ async function transformWithBabel(
590602

591603
return result?.code ?? data;
592604
}
605+
606+
function findAffectedFiles(
607+
builder: ts.EmitAndSemanticDiagnosticsBuilderProgram,
608+
{ ignoreForDiagnostics, ignoreForEmit, incrementalCompilation }: NgtscProgram['compiler'],
609+
): Set<ts.SourceFile> {
610+
const affectedFiles = new Set<ts.SourceFile>();
611+
612+
// eslint-disable-next-line no-constant-condition
613+
while (true) {
614+
const result = builder.getSemanticDiagnosticsOfNextAffectedFile(undefined, (sourceFile) => {
615+
// If the affected file is a TTC shim, add the shim's original source file.
616+
// This ensures that changes that affect TTC are typechecked even when the changes
617+
// are otherwise unrelated from a TS perspective and do not result in Ivy codegen changes.
618+
// For example, changing @Input property types of a directive used in another component's
619+
// template.
620+
// A TTC shim is a file that has been ignored for diagnostics and has a filename ending in `.ngtypecheck.ts`.
621+
if (ignoreForDiagnostics.has(sourceFile) && sourceFile.fileName.endsWith('.ngtypecheck.ts')) {
622+
// This file name conversion relies on internal compiler logic and should be converted
623+
// to an official method when available. 15 is length of `.ngtypecheck.ts`
624+
const originalFilename = sourceFile.fileName.slice(0, -15) + '.ts';
625+
const originalSourceFile = builder.getSourceFile(originalFilename);
626+
if (originalSourceFile) {
627+
affectedFiles.add(originalSourceFile);
628+
}
629+
630+
return true;
631+
}
632+
633+
return false;
634+
});
635+
636+
if (!result) {
637+
break;
638+
}
639+
640+
affectedFiles.add(result.affected as ts.SourceFile);
641+
}
642+
643+
// A file is also affected if the Angular compiler requires it to be emitted
644+
for (const sourceFile of builder.getSourceFiles()) {
645+
if (ignoreForEmit.has(sourceFile) || incrementalCompilation.safeToSkipEmit(sourceFile)) {
646+
continue;
647+
}
648+
649+
affectedFiles.add(sourceFile);
650+
}
651+
652+
return affectedFiles;
653+
}

packages/angular_devkit/build_angular/src/builders/browser-esbuild/profiling.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function logCumulativeDurations(): void {
2121

2222
for (const [name, duration] of cumulativeDurations) {
2323
// eslint-disable-next-line no-console
24-
console.log(`DURATION[${name}]: ${duration} seconds`);
24+
console.log(`DURATION[${name}]: ${duration.toFixed(9)} seconds`);
2525
}
2626
}
2727

@@ -32,7 +32,7 @@ function recordDuration(name: string, startTime: bigint, cumulative?: boolean):
3232
cumulativeDurations.set(name, (cumulativeDurations.get(name) ?? 0) + duration);
3333
} else {
3434
// eslint-disable-next-line no-console
35-
console.log(`DURATION[${name}]: ${duration} seconds`);
35+
console.log(`DURATION[${name}]: ${duration.toFixed(9)} seconds`);
3636
}
3737
}
3838

0 commit comments

Comments
 (0)