Skip to content

Commit 6665d76

Browse files
tboschIgorMinar
authored andcommitted
perf(compiler): speed up watch mode (angular#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 angular#19275
1 parent ad7251c commit 6665d76

File tree

11 files changed

+621
-345
lines changed

11 files changed

+621
-345
lines changed

packages/compiler-cli/src/compiler_host.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export class CompilerHost extends BaseAotCompilerHost<CompilerHostContext> {
260260
this.urlResolver = createOfflineCompileUrlResolver();
261261
}
262262

263-
getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined {
263+
protected getSourceFile(filePath: string): ts.SourceFile {
264264
let sf = this.program.getSourceFile(filePath);
265265
if (!sf) {
266266
if (this.context.fileExists(filePath)) {
@@ -270,7 +270,11 @@ export class CompilerHost extends BaseAotCompilerHost<CompilerHostContext> {
270270
throw new Error(`Source file ${filePath} not present in program.`);
271271
}
272272
}
273-
return this.metadataProvider.getMetadata(sf);
273+
return sf;
274+
}
275+
276+
getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined {
277+
return this.metadataProvider.getMetadata(this.getSourceFile(filePath));
274278
}
275279

276280
toSummaryFileName(fileName: string, referringSrcFileName: string): string {

packages/compiler-cli/src/ngtools_api2.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ export interface CompilerHost extends ts.CompilerHost {
6666
export enum EmitFlags {
6767
DTS = 1 << 0,
6868
JS = 1 << 1,
69+
Codegen = 1 << 4,
6970

70-
Default = DTS | JS
71+
Default = DTS | JS | Codegen
7172
}
7273

7374
export interface CustomTransformers {
@@ -106,6 +107,7 @@ export interface Program {
106107
customTransformers?: CustomTransformers,
107108
emitCallback?: TsEmitCallback
108109
}): ts.EmitResult;
110+
getLibrarySummaries(): {fileName: string, content: string}[];
109111
}
110112

111113
// Wrapper for createProgram.

packages/compiler-cli/src/perform_watch.ts

+94-24
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,35 @@ const ChangeDiagnostics = {
3535
},
3636
};
3737

38+
function totalCompilationTimeDiagnostic(timeInMillis: number): api.Diagnostic {
39+
let duration: string;
40+
if (timeInMillis > 1000) {
41+
duration = `${(timeInMillis / 1000).toPrecision(2)}s`;
42+
} else {
43+
duration = `${timeInMillis}ms`;
44+
}
45+
return {
46+
category: ts.DiagnosticCategory.Message,
47+
messageText: `Total time: ${duration}`,
48+
code: api.DEFAULT_ERROR_CODE,
49+
source: api.SOURCE,
50+
};
51+
}
52+
3853
export enum FileChangeEvent {
3954
Change,
40-
CreateDelete
55+
CreateDelete,
56+
CreateDeleteDir,
4157
}
4258

4359
export interface PerformWatchHost {
4460
reportDiagnostics(diagnostics: Diagnostics): void;
4561
readConfiguration(): ParsedConfiguration;
4662
createCompilerHost(options: api.CompilerOptions): api.CompilerHost;
4763
createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|undefined;
48-
onFileChange(listener: (event: FileChangeEvent, fileName: string) => void):
49-
{close: () => void, ready: (cb: () => void) => void};
64+
onFileChange(
65+
options: api.CompilerOptions, listener: (event: FileChangeEvent, fileName: string) => void,
66+
ready: () => void): {close: () => void};
5067
setTimeout(callback: () => void, ms: number): any;
5168
clearTimeout(timeoutId: any): void;
5269
}
@@ -60,23 +77,17 @@ export function createPerformWatchHost(
6077
createCompilerHost: options => createCompilerHost({options}),
6178
readConfiguration: () => readConfiguration(configFileName, existingOptions),
6279
createEmitCallback: options => createEmitCallback ? createEmitCallback(options) : undefined,
63-
onFileChange: (listeners) => {
64-
const parsed = readConfiguration(configFileName, existingOptions);
65-
function stubReady(cb: () => void) { process.nextTick(cb); }
66-
if (parsed.errors && parsed.errors.length) {
67-
reportDiagnostics(parsed.errors);
68-
return {close: () => {}, ready: stubReady};
69-
}
70-
if (!parsed.options.basePath) {
80+
onFileChange: (options, listener, ready: () => void) => {
81+
if (!options.basePath) {
7182
reportDiagnostics([{
7283
category: ts.DiagnosticCategory.Error,
7384
messageText: 'Invalid configuration option. baseDir not specified',
7485
source: api.SOURCE,
7586
code: api.DEFAULT_ERROR_CODE
7687
}]);
77-
return {close: () => {}, ready: stubReady};
88+
return {close: () => {}};
7889
}
79-
const watcher = chokidar.watch(parsed.options.basePath, {
90+
const watcher = chokidar.watch(options.basePath, {
8091
// ignore .dotfiles, .js and .map files.
8192
// can't ignore other files as we e.g. want to recompile if an `.html` file changes as well.
8293
ignored: /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json)/,
@@ -86,22 +97,32 @@ export function createPerformWatchHost(
8697
watcher.on('all', (event: string, path: string) => {
8798
switch (event) {
8899
case 'change':
89-
listeners(FileChangeEvent.Change, path);
100+
listener(FileChangeEvent.Change, path);
90101
break;
91102
case 'unlink':
92103
case 'add':
93-
listeners(FileChangeEvent.CreateDelete, path);
104+
listener(FileChangeEvent.CreateDelete, path);
105+
break;
106+
case 'unlinkDir':
107+
case 'addDir':
108+
listener(FileChangeEvent.CreateDeleteDir, path);
94109
break;
95110
}
96111
});
97-
function ready(cb: () => void) { watcher.on('ready', cb); }
112+
watcher.on('ready', ready);
98113
return {close: () => watcher.close(), ready};
99114
},
100115
setTimeout: (ts.sys.clearTimeout && ts.sys.setTimeout) || setTimeout,
101116
clearTimeout: (ts.sys.setTimeout && ts.sys.clearTimeout) || clearTimeout,
102117
};
103118
}
104119

120+
interface CacheEntry {
121+
exists?: boolean;
122+
sf?: ts.SourceFile;
123+
content?: string;
124+
}
125+
105126
/**
106127
* The logic in this function is adapted from `tsc.ts` from TypeScript.
107128
*/
@@ -112,16 +133,30 @@ export function performWatchCompilation(host: PerformWatchHost):
112133
let cachedOptions: ParsedConfiguration|undefined; // CompilerOptions cached from last compilation
113134
let timerHandleForRecompilation: any; // Handle for 0.25s wait timer to trigger recompilation
114135

115-
// Watch basePath, ignoring .dotfiles
116-
const fileWatcher = host.onFileChange(watchedFileChanged);
117136
const ingoreFilesForWatch = new Set<string>();
137+
const fileCache = new Map<string, CacheEntry>();
118138

119139
const firstCompileResult = doCompilation();
120140

121-
const readyPromise = new Promise(resolve => fileWatcher.ready(resolve));
141+
// Watch basePath, ignoring .dotfiles
142+
let resolveReadyPromise: () => void;
143+
const readyPromise = new Promise(resolve => resolveReadyPromise = resolve);
144+
// Note: ! is ok as options are filled after the first compilation
145+
// Note: ! is ok as resolvedReadyPromise is filled by the previous call
146+
const fileWatcher =
147+
host.onFileChange(cachedOptions !.options, watchedFileChanged, resolveReadyPromise !);
122148

123149
return {close, ready: cb => readyPromise.then(cb), firstCompileResult};
124150

151+
function cacheEntry(fileName: string): CacheEntry {
152+
let entry = fileCache.get(fileName);
153+
if (!entry) {
154+
entry = {};
155+
fileCache.set(fileName, entry);
156+
}
157+
return entry;
158+
}
159+
125160
function close() {
126161
fileWatcher.close();
127162
if (timerHandleForRecompilation) {
@@ -139,11 +174,8 @@ export function performWatchCompilation(host: PerformWatchHost):
139174
host.reportDiagnostics(cachedOptions.errors);
140175
return cachedOptions.errors;
141176
}
177+
const startTime = Date.now();
142178
if (!cachedCompilerHost) {
143-
// TODO(chuckj): consider avoiding re-generating factories for libraries.
144-
// Consider modifying the AotCompilerHost to be able to remember the summary files
145-
// generated from previous compiliations and return false from isSourceFile for
146-
// .d.ts files for which a summary file was already generated.å
147179
cachedCompilerHost = host.createCompilerHost(cachedOptions.options);
148180
const originalWriteFileCallback = cachedCompilerHost.writeFile;
149181
cachedCompilerHost.writeFile = function(
@@ -152,6 +184,31 @@ export function performWatchCompilation(host: PerformWatchHost):
152184
ingoreFilesForWatch.add(path.normalize(fileName));
153185
return originalWriteFileCallback(fileName, data, writeByteOrderMark, onError, sourceFiles);
154186
};
187+
const originalFileExists = cachedCompilerHost.fileExists;
188+
cachedCompilerHost.fileExists = function(fileName: string) {
189+
const ce = cacheEntry(fileName);
190+
if (ce.exists == null) {
191+
ce.exists = originalFileExists.call(this, fileName);
192+
}
193+
return ce.exists !;
194+
};
195+
const originalGetSourceFile = cachedCompilerHost.getSourceFile;
196+
cachedCompilerHost.getSourceFile = function(
197+
fileName: string, languageVersion: ts.ScriptTarget) {
198+
const ce = cacheEntry(fileName);
199+
if (!ce.sf) {
200+
ce.sf = originalGetSourceFile.call(this, fileName, languageVersion);
201+
}
202+
return ce.sf !;
203+
};
204+
const originalReadFile = cachedCompilerHost.readFile;
205+
cachedCompilerHost.readFile = function(fileName: string) {
206+
const ce = cacheEntry(fileName);
207+
if (ce.content == null) {
208+
ce.content = originalReadFile.call(this, fileName);
209+
}
210+
return ce.content !;
211+
};
155212
}
156213
ingoreFilesForWatch.clear();
157214
const compileResult = performCompilation({
@@ -166,6 +223,11 @@ export function performWatchCompilation(host: PerformWatchHost):
166223
host.reportDiagnostics(compileResult.diagnostics);
167224
}
168225

226+
const endTime = Date.now();
227+
if (cachedOptions.options.diagnostics) {
228+
const totalTime = (endTime - startTime) / 1000;
229+
host.reportDiagnostics([totalCompilationTimeDiagnostic(endTime - startTime)]);
230+
}
169231
const exitCode = exitCodeFromResult(compileResult.diagnostics);
170232
if (exitCode == 0) {
171233
cachedProgram = compileResult.program;
@@ -191,11 +253,19 @@ export function performWatchCompilation(host: PerformWatchHost):
191253
path.normalize(fileName) === path.normalize(cachedOptions.project)) {
192254
// If the configuration file changes, forget everything and start the recompilation timer
193255
resetOptions();
194-
} else if (event === FileChangeEvent.CreateDelete) {
256+
} else if (
257+
event === FileChangeEvent.CreateDelete || event === FileChangeEvent.CreateDeleteDir) {
195258
// If a file was added or removed, reread the configuration
196259
// to determine the new list of root files.
197260
cachedOptions = undefined;
198261
}
262+
263+
if (event === FileChangeEvent.CreateDeleteDir) {
264+
fileCache.clear();
265+
} else {
266+
fileCache.delete(fileName);
267+
}
268+
199269
if (!ingoreFilesForWatch.has(path.normalize(fileName))) {
200270
// Ignore the file if the file is one that was written by the compiler.
201271
startTimerForRecompilation();

packages/compiler-cli/src/transformers/api.ts

+11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export function isNgDiagnostic(diagnostic: any): diagnostic is Diagnostic {
3030
}
3131

3232
export interface CompilerOptions extends ts.CompilerOptions {
33+
// Write statistics about compilation (e.g. total time, ...)
34+
// Note: this is the --diagnostics command line option from TS (which is @internal
35+
// on ts.CompilerOptions interface).
36+
diagnostics?: boolean;
37+
3338
// Absolute path to a directory where generated file structure is written.
3439
// If unspecified, generated files will be written alongside sources.
3540
// @deprecated - no effect
@@ -273,4 +278,10 @@ export interface Program {
273278
customTransformers?: CustomTransformers,
274279
emitCallback?: TsEmitCallback
275280
}): ts.EmitResult;
281+
282+
/**
283+
* Returns the .ngsummary.json files of libraries that have been compiled
284+
* in this program or previous programs.
285+
*/
286+
getLibrarySummaries(): {fileName: string, content: string}[];
276287
}

packages/compiler-cli/src/transformers/compiler_host.ts

+51-16
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
6161
constructor(
6262
private rootFiles: string[], options: CompilerOptions, context: CompilerHost,
6363
private metadataProvider: MetadataProvider,
64-
private codeGenerator: (fileName: string) => GeneratedFile[]) {
64+
private codeGenerator: (fileName: string) => GeneratedFile[],
65+
private summariesFromPreviousCompilations: Map<string, string>) {
6566
super(options, context);
6667
this.moduleResolutionCache = ts.createModuleResolutionCache(
6768
this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context));
@@ -292,7 +293,8 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
292293
}
293294
this.generatedCodeFor.add(fileName);
294295

295-
const baseNameFromGeneratedFile = this._getBaseNameForGeneratedFile(fileName);
296+
const baseNameFromGeneratedFile = this._getBaseNamesForGeneratedFile(fileName).find(
297+
fileName => this.isSourceFile(fileName) && this.fileExists(fileName));
296298
if (baseNameFromGeneratedFile) {
297299
return this.ensureCodeGeneratedFor(baseNameFromGeneratedFile);
298300
}
@@ -336,29 +338,58 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
336338
fileName = stripNgResourceSuffix(fileName);
337339
// Note: Don't rely on this.generatedSourceFiles here,
338340
// as it might not have been filled yet.
339-
if (this._getBaseNameForGeneratedFile(fileName)) {
341+
if (this._getBaseNamesForGeneratedFile(fileName).find(baseFileName => {
342+
if (this.isSourceFile(baseFileName)) {
343+
return this.fileExists(baseFileName);
344+
} else {
345+
// Note: the factories of a previous program
346+
// are not reachable via the regular fileExists
347+
// as they might be in the outDir. So we derive their
348+
// fileExist information based on the .ngsummary.json file.
349+
return this.fileExists(summaryFileName(baseFileName));
350+
}
351+
})) {
340352
return true;
341353
}
342-
return this.originalSourceFiles.has(fileName) || this.context.fileExists(fileName);
354+
return this.summariesFromPreviousCompilations.has(fileName) ||
355+
this.originalSourceFiles.has(fileName) || this.context.fileExists(fileName);
343356
}
344357

345-
private _getBaseNameForGeneratedFile(genFileName: string): string|null {
358+
private _getBaseNamesForGeneratedFile(genFileName: string): string[] {
346359
const genMatch = GENERATED_FILES.exec(genFileName);
347360
if (genMatch) {
348361
const [, base, genSuffix, suffix] = genMatch;
349-
// Note: on-the-fly generated files always have a `.ts` suffix,
350-
// but the file from which we generated it can be a `.ts`/ `.d.ts`
351-
// (see options.generateCodeForLibraries).
352-
// It can also be a `.css` file in case of a `.css.ngstyle.ts` file
353-
if (suffix === 'ts') {
354-
const baseNames =
355-
genSuffix.indexOf('ngstyle') >= 0 ? [base] : [`${base}.ts`, `${base}.d.ts`];
356-
return baseNames.find(
357-
baseName => this.isSourceFile(baseName) && this.fileExists(baseName)) ||
358-
null;
362+
let baseNames: string[] = [];
363+
if (genSuffix.indexOf('ngstyle') >= 0) {
364+
// Note: ngstlye files have names like `afile.css.ngstyle.ts`
365+
baseNames = [base];
366+
} else if (suffix === 'd.ts') {
367+
baseNames = [base + '.d.ts'];
368+
} else if (suffix === 'ts') {
369+
// Note: on-the-fly generated files always have a `.ts` suffix,
370+
// but the file from which we generated it can be a `.ts`/ `.d.ts`
371+
// (see options.generateCodeForLibraries).
372+
baseNames = [`${base}.ts`, `${base}.d.ts`];
359373
}
374+
return baseNames;
360375
}
361-
return null;
376+
return [];
377+
}
378+
379+
loadSummary(filePath: string): string|null {
380+
if (this.summariesFromPreviousCompilations.has(filePath)) {
381+
return this.summariesFromPreviousCompilations.get(filePath) !;
382+
}
383+
return super.loadSummary(filePath);
384+
}
385+
386+
isSourceFile(filePath: string): boolean {
387+
// If we have a summary from a previous compilation,
388+
// treat the file never as a source file.
389+
if (this.summariesFromPreviousCompilations.has(summaryFileName(filePath))) {
390+
return false;
391+
}
392+
return super.isSourceFile(filePath);
362393
}
363394

364395
readFile = (fileName: string) => this.context.readFile(fileName);
@@ -431,3 +462,7 @@ function stripNgResourceSuffix(fileName: string): string {
431462
function addNgResourceSuffix(fileName: string): string {
432463
return `${fileName}.$ngresource$`;
433464
}
465+
466+
function summaryFileName(fileName: string): string {
467+
return fileName.replace(EXT, '') + '.ngsummary.json';
468+
}

0 commit comments

Comments
 (0)