From b03b9eefeac77b93931803de208118e3a6c5a928 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 15 Dec 2021 13:24:43 +0100 Subject: [PATCH] perf(@ngtools/webpack): reduce redudant module rebuilds when cache is restored With this change we reduce the redundant module rebuilds when Webpack's FS cache is restored. Previously, when the cache was restored we caused all modules to be rebuild even though their contents didn't change. This is because the file emit history wasn't persisted to disk. This also caused the side effect that Webpack will create additional cache `pack` files due to the unnecessary module rebuilds, which ultimatly causes increase of the size of the cache on disk. --- packages/ngtools/webpack/src/ivy/plugin.ts | 58 +++++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/packages/ngtools/webpack/src/ivy/plugin.ts b/packages/ngtools/webpack/src/ivy/plugin.ts index ae771f3817ee..aaaf00a20a7b 100644 --- a/packages/ngtools/webpack/src/ivy/plugin.ts +++ b/packages/ngtools/webpack/src/ivy/plugin.ts @@ -90,13 +90,14 @@ function initializeNgccProcessor( return { processor, errors, warnings }; } -function hashContent(content: string): Uint8Array { - return createHash('md5').update(content).digest(); -} - const PLUGIN_NAME = 'angular-compiler'; const compilationFileEmitters = new WeakMap(); +interface FileEmitHistoryItem { + length: number; + hash: Uint8Array; +} + export class AngularWebpackPlugin { private readonly pluginOptions: AngularWebpackPluginOptions; private compilerCliModule?: typeof import('@angular/compiler-cli'); @@ -105,10 +106,11 @@ export class AngularWebpackPlugin { private ngtscNextProgram?: NgtscProgram; private builder?: ts.EmitAndSemanticDiagnosticsBuilderProgram; private sourceFileCache?: SourceFileCache; + private webpackCache?: ReturnType; private readonly fileDependencies = new Map>(); private readonly requiredFilesToEmit = new Set(); private readonly requiredFilesToEmitCache = new Map(); - private readonly fileEmitHistory = new Map(); + private readonly fileEmitHistory = new Map(); constructor(options: Partial = {}) { this.pluginOptions = { @@ -136,6 +138,7 @@ export class AngularWebpackPlugin { return this.pluginOptions; } + // eslint-disable-next-line max-lines-per-function apply(compiler: Compiler): void { const { NormalModuleReplacementPlugin, util } = compiler.webpack; @@ -177,9 +180,13 @@ export class AngularWebpackPlugin { compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { // Register plugin to ensure deterministic emit order in multi-plugin usage const emitRegistration = this.registerWithCompilation(compilation); - this.watchMode = compiler.watchMode; + // Initialize webpack cache + if (!this.webpackCache && compilation.options.cache) { + this.webpackCache = compilation.getCache(PLUGIN_NAME); + } + // Initialize the resource loader if not already setup if (!resourceLoader) { resourceLoader = new WebpackResourceLoader(this.watchMode); @@ -377,7 +384,7 @@ export class AngularWebpackPlugin { const filesToRebuild = new Set(); for (const requiredFile of this.requiredFilesToEmit) { - const history = this.fileEmitHistory.get(requiredFile); + const history = await this.getFileEmitHistory(requiredFile); if (history) { const emitResult = await fileEmitter(requiredFile); if ( @@ -706,12 +713,8 @@ export class AngularWebpackPlugin { onAfterEmit?.(sourceFile); - let hash; - if (content !== undefined && this.watchMode) { - // Capture emit history info for Angular rebuild analysis - hash = hashContent(content); - this.fileEmitHistory.set(filePath, { length: content.length, hash }); - } + // Capture emit history info for Angular rebuild analysis + const hash = content ? (await this.addFileEmitHistory(filePath, content)).hash : undefined; const dependencies = [ ...(this.fileDependencies.get(filePath) || []), @@ -737,4 +740,33 @@ export class AngularWebpackPlugin { this.compilerCliModule = await new Function(`return import('@angular/compiler-cli');`)(); this.compilerNgccModule = await new Function(`return import('@angular/compiler-cli/ngcc');`)(); } + + private async addFileEmitHistory( + filePath: string, + content: string, + ): Promise { + const historyData: FileEmitHistoryItem = { + length: content.length, + hash: createHash('md5').update(content).digest(), + }; + + if (this.webpackCache) { + const history = await this.getFileEmitHistory(filePath); + if (!history || Buffer.compare(history.hash, historyData.hash) !== 0) { + // Hash doesn't match or item doesn't exist. + await this.webpackCache.storePromise(filePath, null, historyData); + } + } else if (this.watchMode) { + // The in memory file emit history is only required during watch mode. + this.fileEmitHistory.set(filePath, historyData); + } + + return historyData; + } + + private async getFileEmitHistory(filePath: string): Promise { + return this.webpackCache + ? this.webpackCache.getPromise(filePath, null) + : this.fileEmitHistory.get(filePath); + } }