fix(language-service): Use module resolution cache (#32479)

This PR fixes a critical performance issue where the language
service makes a MASSIVE number of filesystem calls when performing
module resolution.
This is because there is no caching. To make matters worse, module
resolution is performed for **every** program change (which means every
few keystrokes trigger a massive number of fs calls).

PR Close #32479
This commit is contained in:
Keen Yee Liau
2019-09-04 12:45:48 -07:00
committed by Matias Niemelä
parent ded57245e1
commit 6052b12fb3
2 changed files with 68 additions and 16 deletions

View File

@ -48,11 +48,22 @@ class ReflectorModuleModuleResolutionHost implements ts.ModuleResolutionHost, Me
}
export class ReflectorHost implements StaticSymbolResolverHost {
private hostAdapter: ReflectorModuleModuleResolutionHost;
private metadataReaderCache = createMetadataReaderCache();
private readonly hostAdapter: ReflectorModuleModuleResolutionHost;
private readonly metadataReaderCache = createMetadataReaderCache();
private readonly moduleResolutionCache: ts.ModuleResolutionCache;
private readonly fakeContainingPath: string;
constructor(getProgram: () => ts.Program, private readonly serviceHost: ts.LanguageServiceHost) {
this.hostAdapter = new ReflectorModuleModuleResolutionHost(serviceHost, getProgram);
constructor(getProgram: () => ts.Program, private readonly tsLSHost: ts.LanguageServiceHost) {
// tsLSHost.getCurrentDirectory() returns the directory where tsconfig.json
// is located. This is not the same as process.cwd() because the language
// service host sets the "project root path" as its current directory.
const currentDir = tsLSHost.getCurrentDirectory();
this.fakeContainingPath = currentDir ? path.join(currentDir, 'fakeContainingFile.ts') : '';
this.hostAdapter = new ReflectorModuleModuleResolutionHost(tsLSHost, getProgram);
this.moduleResolutionCache = ts.createModuleResolutionCache(
currentDir,
s => s, // getCanonicalFileName
tsLSHost.getCompilationSettings());
}
getMetadataFor(modulePath: string): {[key: string]: any}[]|undefined {
@ -64,23 +75,19 @@ export class ReflectorHost implements StaticSymbolResolverHost {
if (moduleName.startsWith('.')) {
throw new Error('Resolution of relative paths requires a containing file.');
}
// serviceHost.getCurrentDirectory() returns the directory where tsconfig.json
// is located. This is not the same as process.cwd() because the language
// service host sets the "project root path" as its current directory.
const currentDirectory = this.serviceHost.getCurrentDirectory();
if (!currentDirectory) {
if (!this.fakeContainingPath) {
// If current directory is empty then the file must belong to an inferred
// project (no tsconfig.json), in which case it's not possible to resolve
// the module without the caller explicitly providing a containing file.
throw new Error(`Could not resolve '${moduleName}' without a containing file.`);
}
// Any containing file gives the same result for absolute imports
containingFile = path.join(currentDirectory, 'index.ts');
containingFile = this.fakeContainingPath;
}
const compilerOptions = this.serviceHost.getCompilationSettings();
const resolved =
ts.resolveModuleName(moduleName, containingFile, compilerOptions, this.hostAdapter)
.resolvedModule;
const compilerOptions = this.tsLSHost.getCompilationSettings();
const resolved = ts.resolveModuleName(
moduleName, containingFile, compilerOptions, this.hostAdapter,
this.moduleResolutionCache)
.resolvedModule;
return resolved ? resolved.resolvedFileName : null;
}