perf(compiler): speed up watch mode (#19275)

- don’t regenerate code for .d.ts files when
  an oldProgram is passed to `createProgram`
- cache `fileExists` / `getSourceFile` / `readFile` in watch mode
- refactor tests to share common code in `test_support`
- support `—diagnostic` command line to print total time
  used per watch mode compilation.
PR Close #19275
This commit is contained in:
Tobias Bosch
2017-09-19 11:43:34 -07:00
committed by Igor Minar
parent ad7251c8bb
commit 6665d76fbb
11 changed files with 622 additions and 346 deletions

View File

@ -30,6 +30,11 @@ export function isNgDiagnostic(diagnostic: any): diagnostic is Diagnostic {
}
export interface CompilerOptions extends ts.CompilerOptions {
// Write statistics about compilation (e.g. total time, ...)
// Note: this is the --diagnostics command line option from TS (which is @internal
// on ts.CompilerOptions interface).
diagnostics?: boolean;
// Absolute path to a directory where generated file structure is written.
// If unspecified, generated files will be written alongside sources.
// @deprecated - no effect
@ -273,4 +278,10 @@ export interface Program {
customTransformers?: CustomTransformers,
emitCallback?: TsEmitCallback
}): ts.EmitResult;
/**
* Returns the .ngsummary.json files of libraries that have been compiled
* in this program or previous programs.
*/
getLibrarySummaries(): {fileName: string, content: string}[];
}

View File

@ -61,7 +61,8 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
constructor(
private rootFiles: string[], options: CompilerOptions, context: CompilerHost,
private metadataProvider: MetadataProvider,
private codeGenerator: (fileName: string) => GeneratedFile[]) {
private codeGenerator: (fileName: string) => GeneratedFile[],
private summariesFromPreviousCompilations: Map<string, string>) {
super(options, context);
this.moduleResolutionCache = ts.createModuleResolutionCache(
this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context));
@ -292,7 +293,8 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
}
this.generatedCodeFor.add(fileName);
const baseNameFromGeneratedFile = this._getBaseNameForGeneratedFile(fileName);
const baseNameFromGeneratedFile = this._getBaseNamesForGeneratedFile(fileName).find(
fileName => this.isSourceFile(fileName) && this.fileExists(fileName));
if (baseNameFromGeneratedFile) {
return this.ensureCodeGeneratedFor(baseNameFromGeneratedFile);
}
@ -336,29 +338,58 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
fileName = stripNgResourceSuffix(fileName);
// Note: Don't rely on this.generatedSourceFiles here,
// as it might not have been filled yet.
if (this._getBaseNameForGeneratedFile(fileName)) {
if (this._getBaseNamesForGeneratedFile(fileName).find(baseFileName => {
if (this.isSourceFile(baseFileName)) {
return this.fileExists(baseFileName);
} else {
// Note: the factories of a previous program
// are not reachable via the regular fileExists
// as they might be in the outDir. So we derive their
// fileExist information based on the .ngsummary.json file.
return this.fileExists(summaryFileName(baseFileName));
}
})) {
return true;
}
return this.originalSourceFiles.has(fileName) || this.context.fileExists(fileName);
return this.summariesFromPreviousCompilations.has(fileName) ||
this.originalSourceFiles.has(fileName) || this.context.fileExists(fileName);
}
private _getBaseNameForGeneratedFile(genFileName: string): string|null {
private _getBaseNamesForGeneratedFile(genFileName: string): string[] {
const genMatch = GENERATED_FILES.exec(genFileName);
if (genMatch) {
const [, base, genSuffix, suffix] = genMatch;
// Note: on-the-fly generated files always have a `.ts` suffix,
// but the file from which we generated it can be a `.ts`/ `.d.ts`
// (see options.generateCodeForLibraries).
// It can also be a `.css` file in case of a `.css.ngstyle.ts` file
if (suffix === 'ts') {
const baseNames =
genSuffix.indexOf('ngstyle') >= 0 ? [base] : [`${base}.ts`, `${base}.d.ts`];
return baseNames.find(
baseName => this.isSourceFile(baseName) && this.fileExists(baseName)) ||
null;
let baseNames: string[] = [];
if (genSuffix.indexOf('ngstyle') >= 0) {
// Note: ngstlye files have names like `afile.css.ngstyle.ts`
baseNames = [base];
} else if (suffix === 'd.ts') {
baseNames = [base + '.d.ts'];
} else if (suffix === 'ts') {
// Note: on-the-fly generated files always have a `.ts` suffix,
// but the file from which we generated it can be a `.ts`/ `.d.ts`
// (see options.generateCodeForLibraries).
baseNames = [`${base}.ts`, `${base}.d.ts`];
}
return baseNames;
}
return null;
return [];
}
loadSummary(filePath: string): string|null {
if (this.summariesFromPreviousCompilations.has(filePath)) {
return this.summariesFromPreviousCompilations.get(filePath) !;
}
return super.loadSummary(filePath);
}
isSourceFile(filePath: string): boolean {
// If we have a summary from a previous compilation,
// treat the file never as a source file.
if (this.summariesFromPreviousCompilations.has(summaryFileName(filePath))) {
return false;
}
return super.isSourceFile(filePath);
}
readFile = (fileName: string) => this.context.readFile(fileName);
@ -431,3 +462,7 @@ function stripNgResourceSuffix(fileName: string): string {
function addNgResourceSuffix(fileName: string): string {
return `${fileName}.$ngresource$`;
}
function summaryFileName(fileName: string): string {
return fileName.replace(EXT, '') + '.ngsummary.json';
}

View File

@ -35,6 +35,9 @@ const defaultEmitCallback: TsEmitCallback =
class AngularCompilerProgram implements Program {
private metadataCache: LowerMetadataCache;
private summariesFromPreviousCompilations = new Map<string, string>();
// Note: This will be cleared out as soon as we create the _tsProgram
private oldTsProgram: ts.Program|undefined;
private _emittedGenFiles: GeneratedFile[]|undefined;
// Lazily initialized fields
@ -54,6 +57,11 @@ class AngularCompilerProgram implements Program {
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) {
throw new Error('The Angular Compiler requires TypeScript >= 2.4.');
}
this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined;
if (oldProgram) {
oldProgram.getLibrarySummaries().forEach(
({content, fileName}) => this.summariesFromPreviousCompilations.set(fileName, content));
}
this.rootNames = rootNames = rootNames.filter(r => !GENERATED_FILES.test(r));
if (options.flatModuleOutFile) {
@ -75,6 +83,23 @@ class AngularCompilerProgram implements Program {
this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit);
}
getLibrarySummaries(): {fileName: string, content: string}[] {
const emittedLibSummaries: {fileName: string, content: string}[] = [];
this.summariesFromPreviousCompilations.forEach(
(content, fileName) => emittedLibSummaries.push({fileName, content}));
if (this._emittedGenFiles) {
this._emittedGenFiles.forEach(genFile => {
if (genFile.srcFileUrl.endsWith('.d.ts') &&
genFile.genFileUrl.endsWith('.ngsummary.json')) {
// Note: ! is ok here as ngsummary.json files are always plain text, so genFile.source
// is filled.
emittedLibSummaries.push({fileName: genFile.genFileUrl, content: genFile.source !});
}
});
}
return emittedLibSummaries;
}
getTsProgram(): ts.Program { return this.tsProgram; }
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken) {
@ -284,6 +309,9 @@ class AngularCompilerProgram implements Program {
if (this._analyzedModules) {
throw new Error(`Internal Error: already initalized!`);
}
// Note: This is important to not produce a memory leak!
const oldTsProgram = this.oldTsProgram;
this.oldTsProgram = undefined;
const analyzedFiles: NgAnalyzedFile[] = [];
const codegen = (fileName: string) => {
if (this._analyzedModules) {
@ -295,15 +323,13 @@ class AngularCompilerProgram implements Program {
return this._compiler.emitBasicStubs(analyzedFile);
};
const hostAdapter = new TsCompilerAotCompilerTypeCheckHostAdapter(
this.rootNames, this.options, this.host, this.metadataCache, codegen);
this.rootNames, this.options, this.host, this.metadataCache, codegen,
this.summariesFromPreviousCompilations);
const aotOptions = getAotCompilerOptions(this.options);
this._compiler = createAotCompiler(hostAdapter, aotOptions).compiler;
this._typeCheckHost = hostAdapter;
this._structuralDiagnostics = [];
const oldTsProgram = this.oldProgram ? this.oldProgram.getTsProgram() : undefined;
// Note: This is important to not produce a memory leak!
this.oldProgram = undefined;
const tmpProgram = ts.createProgram(this.rootNames, this.options, hostAdapter, oldTsProgram);
return {tmpProgram, analyzedFiles, hostAdapter};
}