Cache the read directory results so that it doesnt end up reading it all the time

This commit is contained in:
Sheetal Nandi 2017-07-11 16:10:44 -07:00
parent 48c651317e
commit 19a6a003f5
4 changed files with 74 additions and 16 deletions

View File

@ -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");

View File

@ -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<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>) {
@ -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

View File

@ -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<FileSystemEntries>();
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<FileSystemEntries>();
}
// 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<Map<ResolvedModuleWithFailedLookupLocations>>();

View File

@ -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<WatchDirectoryFlags>) {
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<WildCardDirectoryWatchers>();
}
wildcardDirectories.forEach((flag, directory) =>
this.addWatcherForDirectory(flag, directory, /*replaceExisting*/ false));
this.addWatcherForDirectory(flag, directory, getCanonicalFileName, /*replaceExisting*/ false));
}
else {
this.stopWatchingWildCards();