From 6269d28bb0c05c08cc1b9531768ce421d41aa27c Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Tue, 28 Mar 2017 13:32:46 -0700 Subject: [PATCH] fix(language-service): improve performance of `updateModuleAnalysis()` (#15543) --- packages/compiler-cli/src/compiler_host.ts | 29 ++++++++++++------- .../src/aot/static_symbol_resolver.ts | 21 ++++++++++++++ .../language-service/src/typescript_host.ts | 24 +++++++++++++-- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/packages/compiler-cli/src/compiler_host.ts b/packages/compiler-cli/src/compiler_host.ts index b3def2a01a..85d441ff3f 100644 --- a/packages/compiler-cli/src/compiler_host.ts +++ b/packages/compiler-cli/src/compiler_host.ts @@ -33,6 +33,7 @@ export class CompilerHost implements AotCompilerHost { private resolverCache = new Map(); private bundleIndexCache = new Map(); private bundleIndexNames = new Set(); + private moduleFileNames = new Map(); protected resolveModuleNameHost: CompilerHostContext; constructor( @@ -69,19 +70,25 @@ export class CompilerHost implements AotCompilerHost { getCanonicalFileName(fileName: string): string { return fileName; } moduleNameToFileName(m: string, containingFile: string): string|null { - if (!containingFile || !containingFile.length) { - if (m.indexOf('.') === 0) { - throw new Error('Resolution of relative paths requires a containing file.'); + const key = m + ':' + (containingFile || ''); + let result = this.moduleFileNames.get(key); + if (!result) { + if (!containingFile || !containingFile.length) { + if (m.indexOf('.') === 0) { + throw new Error('Resolution of relative paths requires a containing file.'); + } + // Any containing file gives the same result for absolute imports + containingFile = this.getCanonicalFileName(path.join(this.basePath, 'index.ts')); } - // Any containing file gives the same result for absolute imports - containingFile = this.getCanonicalFileName(path.join(this.basePath, 'index.ts')); + m = m.replace(EXT, ''); + const resolved = + ts.resolveModuleName( + m, containingFile.replace(/\\/g, '/'), this.options, this.resolveModuleNameHost) + .resolvedModule; + result = resolved ? this.getCanonicalFileName(resolved.resolvedFileName) : null; + this.moduleFileNames.set(key, result); } - m = m.replace(EXT, ''); - const resolved = - ts.resolveModuleName( - m, containingFile.replace(/\\/g, '/'), this.options, this.resolveModuleNameHost) - .resolvedModule; - return resolved ? this.getCanonicalFileName(resolved.resolvedFileName) : null; + return result; }; /** diff --git a/packages/compiler/src/aot/static_symbol_resolver.ts b/packages/compiler/src/aot/static_symbol_resolver.ts index 1861a28656..d451a961eb 100644 --- a/packages/compiler/src/aot/static_symbol_resolver.ts +++ b/packages/compiler/src/aot/static_symbol_resolver.ts @@ -59,6 +59,7 @@ export class StaticSymbolResolver { // Note: this will only contain StaticSymbols without members! private importAs = new Map(); private symbolResourcePaths = new Map(); + private symbolFromFile = new Map(); constructor( private host: StaticSymbolResolverHost, private staticSymbolCache: StaticSymbolCache, @@ -143,6 +144,25 @@ export class StaticSymbolResolver { this.importAs.set(sourceSymbol, targetSymbol); } + /** + * Invalidate all information derived from the given file. + * + * @param fileName the file to invalidate + */ + invalidateFile(fileName: string) { + this.metadataCache.delete(fileName); + this.resolvedFilePaths.delete(fileName); + const symbols = this.symbolFromFile.get(fileName); + if (symbols) { + this.symbolFromFile.delete(fileName); + for (const symbol of symbols) { + this.resolvedSymbols.delete(symbol); + this.importAs.delete(symbol); + this.symbolResourcePaths.delete(symbol); + } + } + } + private _resolveSymbolMembers(staticSymbol: StaticSymbol): ResolvedStaticSymbol { const members = staticSymbol.members; const baseResolvedSymbol = @@ -281,6 +301,7 @@ export class StaticSymbolResolver { } resolvedSymbols.forEach( (resolvedSymbol) => this.resolvedSymbols.set(resolvedSymbol.symbol, resolvedSymbol)); + this.symbolFromFile.set(filePath, resolvedSymbols.map(resolvedSymbol => resolvedSymbol.symbol)); } private createResolvedSymbol( diff --git a/packages/language-service/src/typescript_host.ts b/packages/language-service/src/typescript_host.ts index 28197c24f5..8df4c7c292 100644 --- a/packages/language-service/src/typescript_host.ts +++ b/packages/language-service/src/typescript_host.ts @@ -87,6 +87,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost { private fileToComponent: Map; private templateReferences: string[]; private collectedErrors: Map; + private fileVersions = new Map(); constructor(private host: ts.LanguageServiceHost, private tsService: ts.LanguageService) {} @@ -222,7 +223,6 @@ export class TypeScriptServiceHost implements LanguageServiceHost { if (this.modulesOutOfDate) { this.analyzedModules = null; this._reflector = null; - this._staticSymbolResolver = null; this.templateReferences = null; this.fileToComponent = null; this.ensureAnalyzedModules(); @@ -242,8 +242,28 @@ export class TypeScriptServiceHost implements LanguageServiceHost { private validate() { const program = this.program; - if (this.lastProgram != program) { + if (this._staticSymbolResolver && this.lastProgram != program) { + // Invalidate file that have changed in the static symbol resolver + const invalidateFile = (fileName: string) => + this._staticSymbolResolver.invalidateFile(fileName); this.clearCaches(); + const seen = new Set(); + for (let sourceFile of this.program.getSourceFiles()) { + const fileName = sourceFile.fileName; + seen.add(fileName); + const version = this.host.getScriptVersion(fileName); + const lastVersion = this.fileVersions.get(fileName); + if (version != lastVersion) { + this.fileVersions.set(fileName, version); + invalidateFile(fileName); + } + } + + // Remove file versions that are no longer in the file and invalidate them. + const missing = Array.from(this.fileVersions.keys()).filter(f => !seen.has(f)); + missing.forEach(f => this.fileVersions.delete(f)); + missing.forEach(invalidateFile); + this.lastProgram = program; } }