perf(ngcc): introduce cache for sharing data across entry-points (#38840)

ngcc creates typically two `ts.Program` instances for each entry-point,
one for processing sources and another one for processing the typings.
The creation of these programs is somewhat expensive, as it concerns
module resolution and parsing of source files.

This commit implements several layers of caching to optimize the
creation of programs:

1. A shared module resolution cache across all entry-points within a
   single invocation of ngcc. Both the sources and typings program
   benefit from this cache.
2. Sharing the parsed `ts.SourceFile` for a single entry-point between
   the sources and typings program.
3. Sharing parsed `ts.SourceFile`s of TypeScript's default libraries
   across all entry-points within a single invocation. Some of these
   default library typings are large and therefore expensive to parse,
   so sharing the parsed source files across all entry-points offers
   a significant performance improvement.

Using a bare CLI app created using `ng new` + `ng add @angular/material`,
the above changes offer a 3-4x improvement in ngcc's processing time
when running synchronously and ~2x improvement for asynchronous runs.

PR Close #38840
This commit is contained in:
JoostK
2020-09-14 14:08:29 +02:00
committed by Andrew Kushnir
parent b9ca6d20cc
commit f0688b4d18
8 changed files with 509 additions and 20 deletions

View File

@ -10,6 +10,7 @@ import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {EntryPoint} from '../../src/packages/entry_point';
import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
import {createModuleResolutionCache, SharedFileCache} from '../../src/packages/source_file_cache';
runInEachFileSystem(() => {
describe('entry point bundle', () => {
@ -180,7 +181,10 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const esm5bundle = makeEntryPointBundle(fs, entryPoint, './index.js', false, 'esm5', true);
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm5', true);
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
.toEqual(jasmine.arrayWithExactContents([
@ -291,8 +295,11 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm5',
/* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
expect(esm5bundle.src.program.getSourceFiles().map(sf => _(sf.fileName)))
@ -328,8 +335,11 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm5',
/* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
.toContain(absoluteFrom('/node_modules/test/internal.js'));
@ -351,8 +361,11 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, './esm2015/index.js', false, 'esm2015', /* transformDts */ true,
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache,
'./esm2015/index.js', false, 'esm2015',
/* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
.toContain(absoluteFrom('/node_modules/internal/esm2015/src/internal.js'));
@ -374,8 +387,11 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm5',
/* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ false);
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
.toContain(absoluteFrom('/node_modules/test/internal.js'));
@ -398,8 +414,11 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const bundle = makeEntryPointBundle(
fs, entryPoint, './index.js', false, 'esm2015', /* transformDts */ true,
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm2015',
/* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
expect(bundle.rootDirs).toEqual([absoluteFrom('/node_modules/primary')]);
});

View File

@ -0,0 +1,223 @@
/**
* @license
* Copyright Google LLC 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 * as ts from 'typescript';
import {absoluteFrom, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {EntryPointFileCache, isAngularDts, isDefaultLibrary, SharedFileCache} from '../../src/packages/source_file_cache';
runInEachFileSystem(() => {
describe('caching', () => {
let _: typeof absoluteFrom;
let fs: FileSystem;
beforeEach(() => {
_ = absoluteFrom;
fs = getFileSystem();
loadTestFiles([
{
name: _('/node_modules/typescript/lib/lib.es5.d.ts'),
contents: `export declare interface Array {}`,
},
{
name: _('/node_modules/typescript/lib/lib.dom.d.ts'),
contents: `export declare interface Window {}`,
},
{
name: _('/node_modules/@angular/core/core.d.ts'),
contents: `export declare interface Component {}`,
},
{
name: _('/node_modules/@angular/common/common.d.ts'),
contents: `export declare interface NgIf {}`,
},
{
name: _('/index.ts'),
contents: `export const index = true;`,
},
{
name: _('/main.ts'),
contents: `export const main = true;`,
},
]);
});
describe('SharedFileCache', () => {
it('should cache a parsed source file for default libraries', () => {
const cache = new SharedFileCache(fs);
const libEs5 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.es5.d.ts')!;
expect(libEs5).not.toBeUndefined();
expect(libEs5.text).toContain('Array');
const libDom = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.dom.d.ts')!;
expect(libDom).not.toBeUndefined();
expect(libDom.text).toContain('Window');
const libEs5_2 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.es5.d.ts')!;
expect(libEs5_2).toBe(libEs5);
const libDom_2 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.dom.d.ts')!;
expect(libDom_2).toBe(libDom);
});
it('should cache a parsed source file for @angular scoped packages', () => {
const cache = new SharedFileCache(fs);
const core = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
expect(core).not.toBeUndefined();
expect(core.text).toContain('Component');
const common = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
expect(common).not.toBeUndefined();
expect(common.text).toContain('NgIf');
const core_2 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
expect(core_2).toBe(core);
const common_2 = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
expect(common_2).toBe(common);
});
it('should reparse @angular d.ts files when they change', () => {
const cache = new SharedFileCache(fs);
const core = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
expect(core).not.toBeUndefined();
expect(core.text).toContain('Component');
const common = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
expect(common).not.toBeUndefined();
expect(common.text).toContain('NgIf');
fs.writeFile(
_('/node_modules/@angular/core/core.d.ts'), `export declare interface Directive {}`);
const core_2 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
expect(core_2).not.toBe(core);
expect(core_2.text).toContain('Directive');
const core_3 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
expect(core_3).toBe(core_2);
const common_2 = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
expect(common_2).toBe(common);
});
it('should not cache files that are not default library files inside of the typescript package',
() => {
const cache = new SharedFileCache(fs);
expect(cache.getCachedSourceFile('/node_modules/typescript/lib/typescript.d.ts'))
.toBeUndefined();
expect(cache.getCachedSourceFile('/typescript/lib.es5.d.ts')).toBeUndefined();
});
});
describe('isDefaultLibrary()', () => {
it('should accept lib files inside of the typescript package', () => {
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es5.d.ts'), fs)).toBe(true);
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.dom.d.ts'), fs)).toBe(true);
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es2015.core.d.ts'), fs))
.toBe(true);
expect(isDefaultLibrary(_('/root/node_modules/typescript/lib/lib.es5.d.ts'), fs))
.toBe(true);
});
it('should reject non lib files inside of the typescript package', () => {
expect(isDefaultLibrary(_('/node_modules/typescript/lib/typescript.d.ts'), fs)).toBe(false);
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es5.ts'), fs)).toBe(false);
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.d.ts'), fs)).toBe(false);
expect(isDefaultLibrary(_('/node_modules/typescript/lib.es5.d.ts'), fs)).toBe(false);
});
it('should reject lib files outside of the typescript package', () => {
expect(isDefaultLibrary(_('/node_modules/ttypescript/lib/lib.es5.d.ts'), fs)).toBe(false);
expect(isDefaultLibrary(_('/node_modules/ttypescript/lib/lib.es5.d.ts'), fs)).toBe(false);
expect(isDefaultLibrary(_('/typescript/lib/lib.es5.d.ts'), fs)).toBe(false);
});
});
describe('isAngularDts()', () => {
it('should accept .d.ts files inside of the @angular scope', () => {
expect(isAngularDts(_('/node_modules/@angular/core/core.d.ts'), fs)).toBe(true);
expect(isAngularDts(_('/node_modules/@angular/common/common.d.ts'), fs)).toBe(true);
});
it('should reject non-.d.ts files inside @angular scoped packages', () => {
expect(isAngularDts(_('/node_modules/@angular/common/src/common.ts'), fs)).toBe(false);
});
it('should reject .d.ts files nested deeply inside @angular scoped packages', () => {
expect(isAngularDts(_('/node_modules/@angular/common/src/common.d.ts'), fs)).toBe(false);
});
it('should reject .d.ts files directly inside the @angular scope', () => {
expect(isAngularDts(_('/node_modules/@angular/common.d.ts'), fs)).toBe(false);
});
it('should reject files that are not inside node_modules', () => {
expect(isAngularDts(_('/@angular/core/core.d.ts'), fs)).toBe(false);
});
});
describe('EntryPointFileCache', () => {
let sharedFileCache: SharedFileCache;
beforeEach(() => {
sharedFileCache = new SharedFileCache(fs);
});
it('should prefer source files cached in SharedFileCache', () => {
const cache1 = new EntryPointFileCache(fs, sharedFileCache);
const libEs5_1 = cache1.getCachedSourceFile(
'/node_modules/typescript/lib/lib.es5.d.ts', ts.ScriptTarget.ESNext)!;
expect(libEs5_1).not.toBeUndefined();
expect(libEs5_1.text).toContain('Array');
expect(libEs5_1.languageVersion).toBe(ts.ScriptTarget.ES2015);
const cache2 = new EntryPointFileCache(fs, sharedFileCache);
const libEs5_2 = cache2.getCachedSourceFile(
'/node_modules/typescript/lib/lib.es5.d.ts', ts.ScriptTarget.ESNext)!;
expect(libEs5_1).toBe(libEs5_2);
});
it('should cache source files that are not default library files', () => {
const cache = new EntryPointFileCache(fs, sharedFileCache);
const index = cache.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
expect(index).not.toBeUndefined();
expect(index.text).toContain('index');
expect(index.languageVersion).toBe(ts.ScriptTarget.ESNext);
const main = cache.getCachedSourceFile('/main.ts', ts.ScriptTarget.ESNext)!;
expect(main).not.toBeUndefined();
expect(main.text).toContain('main');
expect(main.languageVersion).toBe(ts.ScriptTarget.ESNext);
const index_2 = cache.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
expect(index_2).toBe(index);
const main_2 = cache.getCachedSourceFile('/main.ts', ts.ScriptTarget.ESNext)!;
expect(main_2).toBe(main);
});
it('should not share non-library files across multiple cache instances', () => {
const cache1 = new EntryPointFileCache(fs, sharedFileCache);
const cache2 = new EntryPointFileCache(fs, sharedFileCache);
const index1 = cache1.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
const index2 = cache2.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
expect(index1).not.toBe(index2);
});
it('should return undefined if the file does not exist', () => {
const cache = new EntryPointFileCache(fs, sharedFileCache);
expect(cache.getCachedSourceFile('/nonexistent.ts', ts.ScriptTarget.ESNext))
.toBeUndefined();
});
it('should return undefined if the path is a directory', () => {
const cache = new EntryPointFileCache(fs, sharedFileCache);
expect(cache.getCachedSourceFile('/node_modules', ts.ScriptTarget.ESNext)).toBeUndefined();
});
});
});
});