From 19a6a003f59075049870d33f4201478a0fd15055 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 11 Jul 2017 16:10:44 -0700 Subject: [PATCH] Cache the read directory results so that it doesnt end up reading it all the time --- .../unittests/tsserverProjectSystem.ts | 6 +-- src/server/editorServices.ts | 22 ++++---- src/server/lsHost.ts | 52 +++++++++++++++++++ src/server/project.ts | 10 ++-- 4 files changed, 74 insertions(+), 16 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index bd5bfb1841d..1c368cad69c 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -574,7 +574,7 @@ namespace ts.projectSystem { const path = this.toFullPath(s); const folder = this.fs.get(path); if (isFolder(folder)) { - return map(folder.entries, x => getBaseFileName(x.fullPath)); + return mapDefined(folder.entries, entry => isFolder(entry) ? getBaseFileName(entry.fullPath) : undefined); } Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder"); return []; @@ -590,10 +590,10 @@ namespace ts.projectSystem { if (isFolder(dirEntry)) { dirEntry.entries.forEach((entry) => { if (isFolder(entry)) { - result.directories.push(entry.fullPath); + result.directories.push(getBaseFileName(entry.fullPath)); } else if (isFile(entry)) { - result.files.push(entry.fullPath); + result.files.push(getBaseFileName(entry.fullPath)); } else { Debug.fail("Unknown entry"); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5060238521f..30da6631a49 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -608,7 +608,9 @@ namespace ts.server { * @param fileName the absolute file name that changed in watched directory */ /* @internal */ - onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileName: string) { + onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileName: Path) { + project.cachedParseConfigHost.clearCacheForFile(fileName); + // If a change was made inside "folder/file", node will trigger the callback twice: // one with the fileName being "folder/file", and the other one with "folder". // We don't respond to the second one. @@ -621,7 +623,7 @@ namespace ts.server { const configFileSpecs = project.configFileSpecs; const configFilename = normalizePath(project.getConfigFilePath()); // TODO: (sheetalkamat) use the host that caches - so we dont do file exists and read directory call - const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), this.host, this.hostConfiguration.extraFileExtensions); + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), project.cachedParseConfigHost, this.hostConfiguration.extraFileExtensions); const errors = project.getAllProjectErrors(); if (result.fileNames.length === 0) { if (!configFileSpecs.filesSpecs) { @@ -945,7 +947,7 @@ namespace ts.server { return findProjectByName(projectFileName, this.externalProjects); } - private convertConfigFileContentToProjectOptions(configFilename: string) { + private convertConfigFileContentToProjectOptions(configFilename: string, cachedParseConfigHost: CachedParseConfigHost) { configFilename = normalizePath(configFilename); const configFileContent = this.host.readFile(configFilename); @@ -957,7 +959,7 @@ namespace ts.server { const errors = result.parseDiagnostics; const parsedCommandLine = parseJsonSourceFileConfigFileContent( result, - this.host, + cachedParseConfigHost, getDirectoryPath(configFilename), /*existingOptions*/ {}, configFilename, @@ -1105,7 +1107,7 @@ namespace ts.server { }); } - private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, clientFileName?: string) { + private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, cachedParseConfigHost: CachedParseConfigHost, clientFileName?: string) { const sizeLimitExceeded = this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); const project = new ConfiguredProject( configFileName, @@ -1115,7 +1117,7 @@ namespace ts.server { projectOptions.compilerOptions, /*languageServiceEnabled*/ !sizeLimitExceeded, projectOptions.compileOnSave === undefined ? false : projectOptions.compileOnSave); - + project.cachedParseConfigHost = cachedParseConfigHost; this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); project.configFileSpecs = configFileSpecs; @@ -1149,12 +1151,13 @@ namespace ts.server { } private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { - const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName); + const cachedParseConfigHost = new CachedParseConfigHost(this.host); + const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedParseConfigHost); if (success) { this.logger.info(`Opened configuration file ${configFileName}`); } - return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, clientFileName); + return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, cachedParseConfigHost, clientFileName); } private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader) { @@ -1239,7 +1242,8 @@ namespace ts.server { // note: the returned "success" is true does not mean the "configFileErrors" is empty. // because we might have tolerated the errors and kept going. So always return the configFileErrors // regardless the "success" here is true or not. - const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath()); + project.cachedParseConfigHost.clearCache(); + const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath(), project.cachedParseConfigHost); project.configFileSpecs = configFileSpecs; if (!success) { // reset project settings to default diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index c8ab7d3c0f3..720748eb5d0 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -4,6 +4,58 @@ namespace ts.server { type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean }; + + export class CachedParseConfigHost implements ParseConfigHost { + useCaseSensitiveFileNames: boolean; + private getCanonicalFileName: (fileName: string) => string; + private cachedReadDirectoryResult = createMap(); + constructor(private readonly host: ServerHost) { + this.useCaseSensitiveFileNames = host.useCaseSensitiveFileNames; + this.getCanonicalFileName = createGetCanonicalFileName(this.useCaseSensitiveFileNames); + } + + private getFileSystemEntries(rootDir: string) { + const path = ts.toPath(rootDir, this.host.getCurrentDirectory(), this.getCanonicalFileName); + const cachedResult = this.cachedReadDirectoryResult.get(path); + if (cachedResult) { + return cachedResult; + } + + const resultFromHost: FileSystemEntries = { + files: this.host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]) || [], + directories: this.host.getDirectories(rootDir) + }; + + this.cachedReadDirectoryResult.set(path, resultFromHost); + return resultFromHost; + } + + readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] { + return matchFiles(rootDir, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.host.getCurrentDirectory(), depth, path => this.getFileSystemEntries(path)); + } + + fileExists(fileName: string): boolean { + const path = ts.toPath(fileName, this.host.getCurrentDirectory(), this.getCanonicalFileName); + const result = this.getFileSystemEntries(getDirectoryPath(path)); + return contains(result.files, fileName); + } + + readFile(path: string): string { + return this.host.readFile(path); + } + + clearCacheForFile(fileName: Path) { + this.cachedReadDirectoryResult.delete(fileName); + this.cachedReadDirectoryResult.delete(getDirectoryPath(fileName)); + } + + clearCache() { + this.cachedReadDirectoryResult = createMap(); + } + + // TODO: (sheetalkamat) to cache getFileSize as well as fileExists and readFile + } + export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost { private compilationSettings: ts.CompilerOptions; private readonly resolvedModuleNames = createMap>(); diff --git a/src/server/project.ts b/src/server/project.ts index 91687faa0b9..cfd234b64ea 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -972,6 +972,7 @@ namespace ts.server { /*@internal*/ configFileSpecs: ConfigFileSpecs; + cachedParseConfigHost: CachedParseConfigHost; private plugins: PluginModule[] = []; @@ -1113,14 +1114,14 @@ namespace ts.server { this.typeRootsWatchers = watchers; } - private addWatcherForDirectory(flag: WatchDirectoryFlags, directory: string, replaceExisting: boolean) { + private addWatcherForDirectory(flag: WatchDirectoryFlags, directory: string, getCanonicalFileName: (fileName: string) => string, replaceExisting: boolean) { if (replaceExisting || !this.directoriesWatchedForWildcards.has(directory)) { const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; this.projectService.logger.info(`Add ${recursive ? "recursive " : ""} watcher for: ${directory}`); this.directoriesWatchedForWildcards.set(directory, { watcher: this.projectService.host.watchDirectory( directory, - path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), + path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, toPath(path, directory, getCanonicalFileName)), recursive ), recursive @@ -1129,6 +1130,7 @@ namespace ts.server { } watchWildcards(wildcardDirectories: Map) { + const getCanonicalFileName = createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames); if (wildcardDirectories) { if (this.directoriesWatchedForWildcards) { this.directoriesWatchedForWildcards.forEach(({ watcher, recursive }, directory) => { @@ -1145,7 +1147,7 @@ namespace ts.server { if (currentRecursive !== recursive) { this.projectService.logger.info(`Removing ${recursive ? "recursive " : ""} watcher for: ${directory}`); watcher.close(); - this.addWatcherForDirectory(currentFlag, directory, /*replaceExisting*/ true); + this.addWatcherForDirectory(currentFlag, directory, getCanonicalFileName, /*replaceExisting*/ true); } } }); @@ -1154,7 +1156,7 @@ namespace ts.server { this.directoriesWatchedForWildcards = createMap(); } wildcardDirectories.forEach((flag, directory) => - this.addWatcherForDirectory(flag, directory, /*replaceExisting*/ false)); + this.addWatcherForDirectory(flag, directory, getCanonicalFileName, /*replaceExisting*/ false)); } else { this.stopWatchingWildCards();