From 1aae94a421da8db36a315b47e040547e3b0fca91 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 3 Jun 2020 17:18:46 +0100 Subject: [PATCH] perf(ngcc): cache parsed tsconfig between runs This commit will store a cached copy of the parsed tsconfig that can be reused if the tsconfig path is the same. This will improve the ngcc "noop" case, where there is no processing to do, when the entry-points have already been processed. Previously we were parsing this config every time we checked for entry-points to process, which can take up to seconds in some cases. Cherry-picked from #37417 (6e7bd939f6901539305de0917350c5ebc328e775). Resolves #36882 --- packages/compiler-cli/ngcc/index.ts | 2 +- .../compiler-cli/ngcc/src/ngcc_options.ts | 27 ++++++- .../ngcc/test/integration/ngcc_spec.ts | 5 ++ .../ngcc/test/ngcc_options_spec.ts | 78 +++++++++++++++++++ 4 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 packages/compiler-cli/ngcc/test/ngcc_options_spec.ts diff --git a/packages/compiler-cli/ngcc/index.ts b/packages/compiler-cli/ngcc/index.ts index 13c0268d3e..efd32bc07f 100644 --- a/packages/compiler-cli/ngcc/index.ts +++ b/packages/compiler-cli/ngcc/index.ts @@ -12,7 +12,7 @@ import {AsyncNgccOptions, NgccOptions, SyncNgccOptions} from './src/ngcc_options export {ConsoleLogger} from './src/logging/console_logger'; export {Logger, LogLevel} from './src/logging/logger'; -export {AsyncNgccOptions, NgccOptions, SyncNgccOptions} from './src/ngcc_options'; +export {AsyncNgccOptions, clearTsConfigCache, NgccOptions, SyncNgccOptions} from './src/ngcc_options'; export {PathMappings} from './src/path_mappings'; export function process(options: AsyncNgccOptions): Promise; diff --git a/packages/compiler-cli/ngcc/src/ngcc_options.ts b/packages/compiler-cli/ngcc/src/ngcc_options.ts index 1ada2058d2..9bdcb47947 100644 --- a/packages/compiler-cli/ngcc/src/ngcc_options.ts +++ b/packages/compiler-cli/ngcc/src/ngcc_options.ts @@ -156,7 +156,7 @@ export function getSharedSetup(options: NgccOptions): SharedSetup&RequiredNgccOp const absBasePath = absoluteFrom(options.basePath); const projectPath = fileSystem.dirname(absBasePath); const tsConfig = - options.tsConfigPath !== null ? readConfiguration(options.tsConfigPath || projectPath) : null; + options.tsConfigPath !== null ? getTsConfig(options.tsConfigPath || projectPath) : null; let { basePath, @@ -200,3 +200,28 @@ export function getSharedSetup(options: NgccOptions): SharedSetup&RequiredNgccOp new InPlaceFileWriter(fileSystem, logger, errorOnFailedEntryPoint), }; } + +let tsConfigCache: ParsedConfiguration|null = null; +let tsConfigPathCache: string|null = null; + +/** + * Get the parsed configuration object for the given `tsConfigPath`. + * + * This function will cache the previous parsed configuration object to avoid unnecessary processing + * of the tsconfig.json in the case that it is requested repeatedly. + * + * This makes the assumption, which is true as of writing, that the contents of tsconfig.json and + * its dependencies will not change during the life of the process running ngcc. + */ +function getTsConfig(tsConfigPath: string): ParsedConfiguration|null { + if (tsConfigPath !== tsConfigPathCache) { + tsConfigPathCache = tsConfigPath; + tsConfigCache = readConfiguration(tsConfigPath); + } + return tsConfigCache; +} + +export function clearTsConfigCache() { + tsConfigPathCache = null; + tsConfigCache = null; +} diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 7503eeb6e6..8a022cacd3 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -14,6 +14,7 @@ import {Folder, MockFileSystem, runInEachFileSystem, TestFile} from '../../../sr import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers'; import {getLockFilePath} from '../../src/locking/lock_file'; import {mainNgcc} from '../../src/main'; +import {clearTsConfigCache} from '../../src/ngcc_options'; import {hasBeenProcessed, markAsProcessed} from '../../src/packages/build_marker'; import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point'; import {EntryPointManifestFile} from '../../src/packages/entry_point_manifest'; @@ -41,6 +42,10 @@ runInEachFileSystem(() => { spyOn(os, 'cpus').and.returnValue([{model: 'Mock CPU'} as any]); }); + afterEach(() => { + clearTsConfigCache(); + }); + it('should run ngcc without errors for esm2015', () => { expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']})) .not.toThrow(); diff --git a/packages/compiler-cli/ngcc/test/ngcc_options_spec.ts b/packages/compiler-cli/ngcc/test/ngcc_options_spec.ts new file mode 100644 index 0000000000..b3f562a2c0 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/ngcc_options_spec.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; + +import {clearTsConfigCache, getSharedSetup, NgccOptions} from '../src/ngcc_options'; + +import {MockLogger} from './helpers/mock_logger'; + + +runInEachFileSystem(() => { + let fs: FileSystem; + let _abs: typeof absoluteFrom; + let projectPath: AbsoluteFsPath; + + beforeEach(() => { + fs = getFileSystem(); + _abs = absoluteFrom; + projectPath = _abs('/project'); + }); + + describe('getSharedSetup()', () => { + let pathToProjectTsConfig: AbsoluteFsPath; + let pathToCustomTsConfig: AbsoluteFsPath; + + beforeEach(() => { + clearTsConfigCache(); + pathToProjectTsConfig = fs.resolve(projectPath, 'tsconfig.json'); + fs.ensureDir(fs.dirname(pathToProjectTsConfig)); + fs.writeFile(pathToProjectTsConfig, '{"files": ["src/index.ts"]}'); + pathToCustomTsConfig = _abs('/path/to/tsconfig.json'); + fs.ensureDir(fs.dirname(pathToCustomTsConfig)); + fs.writeFile(pathToCustomTsConfig, '{"files": ["custom/index.ts"]}'); + }); + + it('should load the tsconfig.json at the project root if tsConfigPath is `undefined`', () => { + const setup = getSharedSetup({...createOptions()}); + expect(setup.tsConfigPath).toBeUndefined(); + expect(setup.tsConfig?.rootNames).toEqual([fs.resolve(projectPath, 'src/index.ts')]); + }); + + it('should load a specific tsconfig.json if tsConfigPath is a string', () => { + const setup = getSharedSetup({...createOptions(), tsConfigPath: pathToCustomTsConfig}); + expect(setup.tsConfigPath).toEqual(pathToCustomTsConfig); + expect(setup.tsConfig?.rootNames).toEqual([_abs('/path/to/custom/index.ts')]); + }); + + it('should not load a tsconfig.json if tsConfigPath is `null`', () => { + const setup = getSharedSetup({...createOptions(), tsConfigPath: null}); + expect(setup.tsConfigPath).toBe(null); + expect(setup.tsConfig).toBe(null); + }); + }); + + /** + * This function creates an object that contains the minimal required properties for NgccOptions. + */ + function createOptions(): NgccOptions { + return { + async: false, + basePath: fs.resolve(projectPath, 'node_modules'), + propertiesToConsider: ['es2015'], + compileAllFormats: false, + createNewEntryPointFormats: false, + logger: new MockLogger(), + fileSystem: getFileSystem(), + errorOnFailedEntryPoint: true, + enableI18nLegacyMessageIdFormat: true, + invalidateEntryPointManifest: false, + }; + } +});