From 52db3c00076dfe118cd39d7724229210c30665e0 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 10 Oct 2022 11:28:00 -0400 Subject: [PATCH] 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. --- .../browser-esbuild/compiler-plugin.ts | 95 +++++++++++++++---- .../src/builders/browser-esbuild/profiling.ts | 4 +- 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts index 7a949c5084cb..601de304be04 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts @@ -173,9 +173,8 @@ export function createCompilerPlugin( // This uses a wrapped dynamic import to load `@angular/compiler-cli` which is ESM. // Once TypeScript provides support for retaining dynamic imports this workaround can be dropped. - const compilerCli = await loadEsmModule( - '@angular/compiler-cli', - ); + const { GLOBAL_DEFS_FOR_TERSER_WITH_AOT, NgtscProgram, OptimizeFor, readConfiguration } = + await loadEsmModule('@angular/compiler-cli'); // Temporary deep import for transformer support const { @@ -185,7 +184,7 @@ export function createCompilerPlugin( // Setup defines based on the values provided by the Angular compiler-cli build.initialOptions.define ??= {}; - for (const [key, value] of Object.entries(compilerCli.GLOBAL_DEFS_FOR_TERSER_WITH_AOT)) { + for (const [key, value] of Object.entries(GLOBAL_DEFS_FOR_TERSER_WITH_AOT)) { if (key in build.initialOptions.define) { // Skip keys that have been manually provided continue; @@ -202,7 +201,7 @@ export function createCompilerPlugin( rootNames, errors: configurationDiagnostics, } = profileSync('NG_READ_CONFIG', () => - compilerCli.readConfiguration(pluginOptions.tsconfig, { + readConfiguration(pluginOptions.tsconfig, { noEmitOnError: false, suppressOutputPathCheck: true, outDir: undefined, @@ -249,6 +248,7 @@ export function createCompilerPlugin( let previousBuilder: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined; let previousAngularProgram: NgtscProgram | undefined; const babelDataCache = new Map(); + const diagnosticCache = new WeakMap(); build.onStart(async () => { const result: OnStartResult = { @@ -339,12 +339,10 @@ export function createCompilerPlugin( // Create the Angular specific program that contains the Angular compiler const angularProgram = profileSync( 'NG_CREATE_PROGRAM', - () => - new compilerCli.NgtscProgram(rootNames, compilerOptions, host, previousAngularProgram), + () => new NgtscProgram(rootNames, compilerOptions, host, previousAngularProgram), ); previousAngularProgram = angularProgram; const angularCompiler = angularProgram.compiler; - const { ignoreForDiagnostics } = angularCompiler; const typeScriptProgram = angularProgram.getTsProgram(); augmentProgramWithVersioning(typeScriptProgram); @@ -366,12 +364,16 @@ export function createCompilerPlugin( yield* builder.getGlobalDiagnostics(); // Collect source file specific diagnostics - const OptimizeFor = compilerCli.OptimizeFor; + const affectedFiles = findAffectedFiles(builder, angularCompiler); + const optimizeFor = + affectedFiles.size > 1 ? OptimizeFor.WholeProgram : OptimizeFor.SingleFile; for (const sourceFile of builder.getSourceFiles()) { - if (ignoreForDiagnostics.has(sourceFile)) { + if (angularCompiler.ignoreForDiagnostics.has(sourceFile)) { continue; } + // TypeScript will use cached diagnostics for files that have not been + // changed or affected for this build when using incremental building. yield* profileSync( 'NG_DIAGNOSTICS_SYNTACTIC', () => builder.getSyntacticDiagnostics(sourceFile), @@ -383,12 +385,22 @@ export function createCompilerPlugin( true, ); - const angularDiagnostics = profileSync( - 'NG_DIAGNOSTICS_TEMPLATE', - () => angularCompiler.getDiagnosticsForFile(sourceFile, OptimizeFor.WholeProgram), - true, - ); - yield* angularDiagnostics; + // Only request Angular template diagnostics for affected files to avoid + // overhead of template diagnostics for unchanged files. + if (affectedFiles.has(sourceFile)) { + const angularDiagnostics = profileSync( + 'NG_DIAGNOSTICS_TEMPLATE', + () => angularCompiler.getDiagnosticsForFile(sourceFile, optimizeFor), + true, + ); + diagnosticCache.set(sourceFile, angularDiagnostics); + yield* angularDiagnostics; + } else { + const angularDiagnostics = diagnosticCache.get(sourceFile); + if (angularDiagnostics) { + yield* angularDiagnostics; + } + } } } @@ -408,7 +420,7 @@ export function createCompilerPlugin( mergeTransformers(angularCompiler.prepareEmit().transformers, { before: [replaceBootstrap(() => builder.getProgram().getTypeChecker())], }), - (sourceFile) => angularCompiler.incrementalDriver.recordSuccessfulEmit(sourceFile), + (sourceFile) => angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile), ); return result; @@ -590,3 +602,52 @@ async function transformWithBabel( return result?.code ?? data; } + +function findAffectedFiles( + builder: ts.EmitAndSemanticDiagnosticsBuilderProgram, + { ignoreForDiagnostics, ignoreForEmit, incrementalCompilation }: NgtscProgram['compiler'], +): Set { + const affectedFiles = new Set(); + + // eslint-disable-next-line no-constant-condition + while (true) { + const result = builder.getSemanticDiagnosticsOfNextAffectedFile(undefined, (sourceFile) => { + // If the affected file is a TTC shim, add the shim's original source file. + // This ensures that changes that affect TTC are typechecked even when the changes + // are otherwise unrelated from a TS perspective and do not result in Ivy codegen changes. + // For example, changing @Input property types of a directive used in another component's + // template. + // A TTC shim is a file that has been ignored for diagnostics and has a filename ending in `.ngtypecheck.ts`. + if (ignoreForDiagnostics.has(sourceFile) && sourceFile.fileName.endsWith('.ngtypecheck.ts')) { + // This file name conversion relies on internal compiler logic and should be converted + // to an official method when available. 15 is length of `.ngtypecheck.ts` + const originalFilename = sourceFile.fileName.slice(0, -15) + '.ts'; + const originalSourceFile = builder.getSourceFile(originalFilename); + if (originalSourceFile) { + affectedFiles.add(originalSourceFile); + } + + return true; + } + + return false; + }); + + if (!result) { + break; + } + + affectedFiles.add(result.affected as ts.SourceFile); + } + + // A file is also affected if the Angular compiler requires it to be emitted + for (const sourceFile of builder.getSourceFiles()) { + if (ignoreForEmit.has(sourceFile) || incrementalCompilation.safeToSkipEmit(sourceFile)) { + continue; + } + + affectedFiles.add(sourceFile); + } + + return affectedFiles; +} diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/profiling.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/profiling.ts index 690adfa7e8bc..c7a0cceff4b5 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/profiling.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/profiling.ts @@ -21,7 +21,7 @@ export function logCumulativeDurations(): void { for (const [name, duration] of cumulativeDurations) { // eslint-disable-next-line no-console - console.log(`DURATION[${name}]: ${duration} seconds`); + console.log(`DURATION[${name}]: ${duration.toFixed(9)} seconds`); } } @@ -32,7 +32,7 @@ function recordDuration(name: string, startTime: bigint, cumulative?: boolean): cumulativeDurations.set(name, (cumulativeDurations.get(name) ?? 0) + duration); } else { // eslint-disable-next-line no-console - console.log(`DURATION[${name}]: ${duration} seconds`); + console.log(`DURATION[${name}]: ${duration.toFixed(9)} seconds`); } }