Skip to content

Commit

Permalink
fixup! perf(ngcc): introduce cache for sharing data across entry-points
Browse files Browse the repository at this point in the history
  • Loading branch information
JoostK committed Sep 15, 2020
1 parent 49d2d60 commit da5128c
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 77 deletions.
Expand Up @@ -14,7 +14,7 @@ import {Logger} from '../../../src/ngtsc/logging';
import {ParsedConfiguration} from '../../../src/perform_compile';
import {getEntryPointFormat} from '../packages/entry_point';
import {makeEntryPointBundle} from '../packages/entry_point_bundle';
import {TransformCache} from '../packages/transform_cache';
import {createModuleResolutionCache, SharedFileCache} from '../packages/source_file_cache';
import {PathMappings} from '../path_mappings';
import {FileWriter} from '../writing/file_writer';

Expand All @@ -31,7 +31,8 @@ export function getCreateCompileFn(
return (beforeWritingFiles, onTaskCompleted) => {
const {Transformer} = require('../packages/transformer');
const transformer = new Transformer(fileSystem, logger, tsConfig);
const transformCache = new TransformCache(fileSystem);
const sharedFileCache = new SharedFileCache(fileSystem);
const moduleResolutionCache = createModuleResolutionCache(fileSystem);

return (task: Task) => {
const {entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts} = task;
Expand All @@ -56,8 +57,8 @@ export function getCreateCompileFn(
logger.info(`Compiling ${entryPoint.name} : ${formatProperty} as ${format}`);

const bundle = makeEntryPointBundle(
fileSystem, entryPoint, transformCache, formatPath, isCore, format, processDts,
pathMappings, true, enableI18nLegacyMessageIdFormat);
fileSystem, entryPoint, sharedFileCache, moduleResolutionCache, formatPath, isCore,
format, processDts, pathMappings, true, enableI18nLegacyMessageIdFormat);

const result = transformer.transform(bundle);
if (result.success) {
Expand Down
17 changes: 10 additions & 7 deletions packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts
Expand Up @@ -11,7 +11,7 @@ import {PathMappings} from '../path_mappings';
import {BundleProgram, makeBundleProgram} from './bundle_program';
import {EntryPoint, EntryPointFormat} from './entry_point';
import {NgccDtsCompilerHost, NgccSourcesCompilerHost} from './ngcc_compiler_host';
import {EntryPointCache, TransformCache} from './transform_cache';
import {EntryPointFileCache, SharedFileCache} from './source_file_cache';

/**
* A bundle of files and paths (and TS programs) that correspond to a particular
Expand All @@ -32,7 +32,8 @@ export interface EntryPointBundle {
* Get an object that describes a formatted bundle for an entry-point.
* @param fs The current file-system being used.
* @param entryPoint The entry-point that contains the bundle.
* @param transformCache The cache to use for data that is shared across all entry-points.
* @param sharedFileCache The cache to use for source files that are shared across all entry-points.
* @param moduleResolutionCache The module resolution cache to use.
* @param formatPath The path to the source files for this bundle.
* @param isCore This entry point is the Angular core package.
* @param format The underlying format of the bundle.
Expand All @@ -44,17 +45,19 @@ export interface EntryPointBundle {
* component templates.
*/
export function makeEntryPointBundle(
fs: FileSystem, entryPoint: EntryPoint, transformCache: TransformCache, formatPath: string,
isCore: boolean, format: EntryPointFormat, transformDts: boolean, pathMappings?: PathMappings,
fs: FileSystem, entryPoint: EntryPoint, sharedFileCache: SharedFileCache,
moduleResolutionCache: ts.ModuleResolutionCache, formatPath: string, isCore: boolean,
format: EntryPointFormat, transformDts: boolean, pathMappings?: PathMappings,
mirrorDtsFromSrc: boolean = false,
enableI18nLegacyMessageIdFormat: boolean = true): EntryPointBundle {
// Create the TS program and necessary helpers.
const rootDir = entryPoint.packagePath;
const options: ts
.CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings};
const entryPointCache = new EntryPointCache(fs, transformCache);
const dtsHost = new NgccDtsCompilerHost(fs, options, entryPointCache);
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPointCache, entryPoint.packagePath);
const entryPointCache = new EntryPointFileCache(fs, sharedFileCache);
const dtsHost = new NgccDtsCompilerHost(fs, options, entryPointCache, moduleResolutionCache);
const srcHost = new NgccSourcesCompilerHost(
fs, options, entryPointCache, moduleResolutionCache, entryPoint.packagePath);

// Create the bundle programs, as necessary.
const absFormatPath = fs.resolve(entryPoint.path, formatPath);
Expand Down
13 changes: 8 additions & 5 deletions packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts
Expand Up @@ -10,7 +10,7 @@ import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
import {isWithinPackage} from '../analysis/util';
import {isRelativePath} from '../utils';
import {EntryPointCache} from './transform_cache';
import {EntryPointFileCache} from './source_file_cache';

/**
* Represents a compiler host that resolves a module import as a JavaScript source file if
Expand All @@ -20,7 +20,8 @@ import {EntryPointCache} from './transform_cache';
*/
export class NgccSourcesCompilerHost extends NgtscCompilerHost {
constructor(
fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointCache,
fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointFileCache,
private moduleResolutionCache: ts.ModuleResolutionCache,
protected packagePath: AbsoluteFsPath) {
super(fs, options);
}
Expand All @@ -34,7 +35,7 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
redirectedReference?: ts.ResolvedProjectReference): Array<ts.ResolvedModule|undefined> {
return moduleNames.map(moduleName => {
const {resolvedModule} = ts.resolveModuleName(
moduleName, containingFile, this.options, this, this.cache.moduleResolutionCache,
moduleName, containingFile, this.options, this, this.moduleResolutionCache,
redirectedReference);

// If the module request originated from a relative import in a JavaScript source file,
Expand Down Expand Up @@ -71,7 +72,9 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
* program.
*/
export class NgccDtsCompilerHost extends NgtscCompilerHost {
constructor(fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointCache) {
constructor(
fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointFileCache,
private moduleResolutionCache: ts.ModuleResolutionCache) {
super(fs, options);
}

Expand All @@ -84,7 +87,7 @@ export class NgccDtsCompilerHost extends NgtscCompilerHost {
redirectedReference?: ts.ResolvedProjectReference): Array<ts.ResolvedModule|undefined> {
return moduleNames.map(moduleName => {
const {resolvedModule} = ts.resolveModuleName(
moduleName, containingFile, this.options, this, this.cache.moduleResolutionCache,
moduleName, containingFile, this.options, this, this.moduleResolutionCache,
redirectedReference);
return resolvedModule;
});
Expand Down
Expand Up @@ -9,8 +9,8 @@ import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';

/**
* A cache that holds on to data that can be shared for processing all entry-points in a single
* invocation of ngcc. In particular, the following aspects are shared across all entry-points
* A cache that holds on to source files that can be shared for processing all entry-points in a
* single invocation of ngcc. In particular, the following files are shared across all entry-points
* through this cache:
*
* 1. Default library files such as `lib.dom.d.ts` and `lib.es5.d.ts`. These files don't change
Expand All @@ -21,26 +21,15 @@ import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
* Especially `@angular/core/core.d.ts` is large and expensive to parse repeatedly. In contrast
* to default library files, we have to account for these files to be invalidated during a single
* invocation of ngcc, as ngcc will overwrite the .d.ts files during its processing.
* 3. A module resolution cache for TypeScript to use for module resolution. Module resolution is
* an expensive operation due to the large number of filesystem accesses. During a single
* invocation of ngcc it is assumed that the filesystem layout does not change, so a single
* module resolution cache is provided for use by all entry-points.
*
* The lifecycle of this cache corresponds with a single invocation of ngcc. Separate invocations,
* e.g. the CLI's synchronous module resolution fallback will therefore all have their own cache.
* This is because module resolution results cannot be assumed to be valid across invocations, as
* modifications of the file-system may have invalidated earlier results. Additionally, it allows
* for the source file cache to be garbage collected once ngcc processing has completed.
* This allows for the source file cache to be garbage collected once ngcc processing has completed.
*/
export class TransformCache {
export class SharedFileCache {
private sfCache = new Map<AbsoluteFsPath, ts.SourceFile>();
readonly moduleResolutionCache: ts.ModuleResolutionCache;

constructor(private fs: FileSystem) {
this.moduleResolutionCache = ts.createModuleResolutionCache(fs.pwd(), fileName => {
return fs.isCaseSensitive() ? fileName : fileName.toLowerCase();
});
}
constructor(private fs: FileSystem) {}

/**
* Loads a `ts.SourceFile` if the provided `fileName` is deemed appropriate to be cached. To
Expand Down Expand Up @@ -153,20 +142,23 @@ function isFile(

/**
* A cache for processing a single entry-point. This exists to share `ts.SourceFile`s between the
* source and typing programs that are created for a single program. Additionally, it leverages the
* transform cache for module resolution and sharing of default library files.
* source and typing programs that are created for a single program.
*/
export class EntryPointCache {
export class EntryPointFileCache {
private readonly sfCache = new Map<AbsoluteFsPath, ts.SourceFile>();

constructor(private fs: FileSystem, private transformCache: TransformCache) {}

get moduleResolutionCache(): ts.ModuleResolutionCache {
return this.transformCache.moduleResolutionCache;
}
constructor(private fs: FileSystem, private sharedFileCache: SharedFileCache) {}

/**
* Returns and caches a parsed `ts.SourceFile` for the provided `fileName`. If the `fileName` is
* cached in the shared file cache, that result is used. Otherwise, the source file is cached
* internally. This method returns `undefined` id the requested file does not exist.
*
* @param fileName The path of the file to retrieve a source file for.
* @param languageVersion The language version to use for parsing the file.
*/
getCachedSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
const staticSf = this.transformCache.getCachedSourceFile(fileName);
const staticSf = this.sharedFileCache.getCachedSourceFile(fileName);
if (staticSf !== undefined) {
return staticSf;
}
Expand All @@ -192,3 +184,14 @@ function readFile(absPath: AbsoluteFsPath, fs: FileSystem): string|undefined {
}
return fs.readFile(absPath);
}

/**
* Creates a `ts.ModuleResolutionCache` that uses the provided filesystem for path operations.
*
* @param fs The filesystem to use for path operations.
*/
export function createModuleResolutionCache(fs: FileSystem): ts.ModuleResolutionCache {
return ts.createModuleResolutionCache(fs.pwd(), fileName => {
return fs.isCaseSensitive() ? fileName : fileName.toLowerCase();
});
}
8 changes: 5 additions & 3 deletions packages/compiler-cli/ngcc/test/helpers/utils.ts
Expand Up @@ -14,7 +14,7 @@ import {NgccEntryPointConfig} from '../../src/packages/configuration';
import {EntryPoint, EntryPointFormat} from '../../src/packages/entry_point';
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
import {NgccSourcesCompilerHost} from '../../src/packages/ngcc_compiler_host';
import {EntryPointCache, TransformCache} from '../../src/packages/transform_cache';
import {createModuleResolutionCache, EntryPointFileCache, SharedFileCache} from '../../src/packages/source_file_cache';

export type TestConfig = Pick<NgccEntryPointConfig, 'generateDeepReexports'>;

Expand Down Expand Up @@ -69,8 +69,10 @@ export function makeTestBundleProgram(
const rootDir = fs.dirname(entryPointPath);
const options: ts.CompilerOptions =
{allowJs: true, maxNodeModuleJsDepth: Infinity, checkJs: false, rootDir, rootDirs: [rootDir]};
const entryPointCache = new EntryPointCache(fs, new TransformCache(fs));
const host = new NgccSourcesCompilerHost(fs, options, entryPointCache, rootDir);
const moduleResolutionCache = createModuleResolutionCache(fs);
const entryPointFileCache = new EntryPointFileCache(fs, new SharedFileCache(fs));
const host =
new NgccSourcesCompilerHost(fs, options, entryPointFileCache, moduleResolutionCache, rootDir);
return makeBundleProgram(
fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles);
}
Expand Down
Expand Up @@ -10,7 +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 {TransformCache} from '../../src/packages/transform_cache';
import {createModuleResolutionCache, SharedFileCache} from '../../src/packages/source_file_cache';

runInEachFileSystem(() => {
describe('entry point bundle', () => {
Expand Down Expand Up @@ -181,8 +181,10 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, new TransformCache(fs), './index.js', false, 'esm5', true);
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm5', true);

expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
.toEqual(jasmine.arrayWithExactContents([
Expand Down Expand Up @@ -293,8 +295,10 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, new TransformCache(fs), './index.js', false, 'esm5',
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm5',
/* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);

Expand Down Expand Up @@ -331,8 +335,10 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, new TransformCache(fs), './index.js', false, 'esm5',
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))
Expand All @@ -355,8 +361,10 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, new TransformCache(fs), './esm2015/index.js', false, 'esm2015',
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))
Expand All @@ -379,8 +387,10 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, new TransformCache(fs), './index.js', false, 'esm5',
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))
Expand All @@ -404,8 +414,10 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const bundle = makeEntryPointBundle(
fs, entryPoint, new TransformCache(fs), './index.js', false, 'esm2015',
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm2015',
/* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
expect(bundle.rootDirs).toEqual([absoluteFrom('/node_modules/primary')]);
Expand Down

0 comments on commit da5128c

Please sign in to comment.