Skip to content

Commit 254e393

Browse files
committed
Watch failed lookups recursively to reduce number of directory watches
Also we dont need to watch type roots any more
1 parent 10ea5bf commit 254e393

7 files changed

Lines changed: 115 additions & 115 deletions

File tree

src/compiler/core.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,6 +1809,8 @@ namespace ts {
18091809
* Removes a trailing directory separator from a path.
18101810
* @param path The path.
18111811
*/
1812+
export function removeTrailingDirectorySeparator(path: Path): Path;
1813+
export function removeTrailingDirectorySeparator(path: string): string;
18121814
export function removeTrailingDirectorySeparator(path: string) {
18131815
if (path.charAt(path.length - 1) === directorySeparator) {
18141816
return path.substr(0, path.length - 1);

src/compiler/resolutionCache.ts

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ namespace ts {
1414
invalidateResolutionOfFile(filePath: Path): void;
1515
createHasInvalidatedResolution(): HasInvalidatedResolution;
1616

17+
setRootDirectory(dir: string): void;
18+
1719
clear(): void;
1820
}
1921

@@ -32,13 +34,15 @@ namespace ts {
3234
export interface ResolutionCacheHost extends ModuleResolutionHost {
3335
toPath(fileName: string): Path;
3436
getCompilationSettings(): CompilerOptions;
35-
watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback): FileWatcher;
37+
watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher;
3638
onInvalidatedResolution(): void;
3739
getCachedPartialSystem?(): CachedPartialSystem;
3840
projectName?: string;
3941
getGlobalCache?(): string | undefined;
4042
}
4143

44+
const MAX_DIRPATHS_TO_RECURSE = 5;
45+
4246
export function createResolutionCache(resolutionHost: ResolutionCacheHost): ResolutionCache {
4347
let filesWithChangedSetOfUnresolvedImports: Path[] | undefined;
4448
let filesWithInvalidatedResolutions: Map<true> | undefined;
@@ -50,19 +54,37 @@ namespace ts {
5054
const resolvedTypeReferenceDirectives = createMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
5155

5256
const directoryWatchesOfFailedLookups = createMap<DirectoryWatchesOfFailedLookup>();
57+
const failedLookupLocationToDirPath = createMap<Path>();
58+
let rootDir: string;
59+
let rootPath: Path;
5360
return {
5461
startRecordingFilesWithChangedResolutions,
5562
finishRecordingFilesWithChangedResolutions,
5663
resolveModuleNames,
5764
resolveTypeReferenceDirectives,
5865
invalidateResolutionOfFile,
5966
createHasInvalidatedResolution,
67+
setRootDirectory,
6068
clear
6169
};
6270

71+
function setRootDirectory(dir: string) {
72+
Debug.assert(!resolvedModuleNames.size && !resolvedTypeReferenceDirectives.size && !directoryWatchesOfFailedLookups.size);
73+
rootDir = removeTrailingDirectorySeparator(getNormalizedAbsolutePath(dir, resolutionHost.getCurrentDirectory()));
74+
rootPath = resolutionHost.toPath(rootDir);
75+
}
76+
77+
function isInDirectoryPath(dir: Path, file: Path) {
78+
if (dir === undefined || file.length <= dir.length) {
79+
return false;
80+
}
81+
return startsWith(file, dir) && file[dir.length] === directorySeparator;
82+
}
83+
6384
function clear() {
6485
// Close all the watches for failed lookup locations, irrespective of refcounts for them since this is to clear the cache
6586
clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf);
87+
failedLookupLocationToDirPath.clear();
6688
resolvedModuleNames.clear();
6789
resolvedTypeReferenceDirectives.clear();
6890
}
@@ -196,16 +218,58 @@ namespace ts {
196218
}
197219

198220
function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path) {
199-
const dirPath = getDirectoryPath(failedLookupLocationPath);
221+
const cachedDir = failedLookupLocationToDirPath.get(failedLookupLocationPath);
222+
if (cachedDir) {
223+
watchFailedLookupLocationInDirectory(cachedDir, failedLookupLocation, failedLookupLocationPath, /*dir*/ undefined);
224+
return;
225+
}
226+
227+
if (isInDirectoryPath(rootPath, failedLookupLocationPath)) {
228+
// Watch in directory of rootPath
229+
watchFailedLookupLocationInDirectory(rootPath, failedLookupLocation, failedLookupLocationPath, rootDir);
230+
return;
231+
}
232+
233+
let dirPath = getDirectoryPath(failedLookupLocationPath);
234+
let dir = getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, resolutionHost.getCurrentDirectory()));
235+
for (let i = 0; i < MAX_DIRPATHS_TO_RECURSE; i++) {
236+
const parentPath = getDirectoryPath(dirPath);
237+
if (directoryWatchesOfFailedLookups.has(dirPath) || parentPath === dirPath) {
238+
watchFailedLookupLocationInDirectory(dirPath, failedLookupLocation, failedLookupLocationPath, dir);
239+
return;
240+
}
241+
dirPath = parentPath;
242+
dir = getDirectoryPath(dir);
243+
}
244+
245+
// Verify there are no watches in parent directory
246+
const ancestorDirPath = getAncestorDirectoryWithWatches(dirPath);
247+
// We wont need directory if we are using ancestor since its already cached
248+
watchFailedLookupLocationInDirectory(ancestorDirPath || dirPath, failedLookupLocation, failedLookupLocationPath, dir);
249+
}
250+
251+
function getAncestorDirectoryWithWatches(dirPath: Path) {
252+
for (let parentDirPath = getDirectoryPath(dirPath); parentDirPath !== dirPath; parentDirPath = getDirectoryPath(parentDirPath)) {
253+
if (directoryWatchesOfFailedLookups.has(parentDirPath)) {
254+
return parentDirPath;
255+
}
256+
dirPath = parentDirPath;
257+
}
258+
return undefined;
259+
}
260+
261+
function watchFailedLookupLocationInDirectory(dirPath: Path, failedLookupLocation: string, failedLookupLocationPath: Path, dir: string | undefined) {
262+
failedLookupLocationToDirPath.set(failedLookupLocationPath, dirPath);
200263
const watches = directoryWatchesOfFailedLookups.get(dirPath);
201264
if (watches) {
202265
watches.mapLocations.add(failedLookupLocationPath, failedLookupLocation);
203266
}
204267
else {
268+
Debug.assert(dir !== undefined);
205269
const mapLocations = createMultiMap<string>();
206270
mapLocations.add(failedLookupLocationPath, failedLookupLocation);
207271
directoryWatchesOfFailedLookups.set(dirPath, {
208-
watcher: createDirectoryWatcher(getDirectoryPath(failedLookupLocation), dirPath),
272+
watcher: createDirectoryWatcher(dir, dirPath),
209273
mapLocations
210274
});
211275
}
@@ -224,16 +288,22 @@ namespace ts {
224288
onAddOrRemoveDirectoryOfFailedLookup(dirPath);
225289
resolutionHost.onInvalidatedResolution();
226290
}
227-
else if (onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolderPath)) {
291+
else if (onFileAddOrRemoveInDirectoryOfFailedLookup(dirPath, fileOrFolderPath)) {
228292
resolutionHost.onInvalidatedResolution();
229293
}
230-
});
294+
}, WatchDirectoryFlags.Recursive);
231295
}
232296

233297
function closeFailedLookupLocationWatcher(failedLookupLocation: string, failedLookupLocationPath: Path) {
234-
const dirPath = getDirectoryPath(failedLookupLocationPath);
298+
const dirPath = failedLookupLocationToDirPath.get(failedLookupLocationPath);
235299
const watches = directoryWatchesOfFailedLookups.get(dirPath);
236300
watches.mapLocations.remove(failedLookupLocationPath, failedLookupLocation);
301+
// If this was last failed lookup location being tracked by the dir watcher,
302+
// remove the failed lookup location path to dir Path entry
303+
if (!watches.mapLocations.has(failedLookupLocationPath)) {
304+
failedLookupLocationToDirPath.delete(failedLookupLocationPath);
305+
}
306+
// If there are no more files that need this watcher alive, close the watcher
237307
if (watches.mapLocations.size === 0) {
238308
watches.watcher.close();
239309
directoryWatchesOfFailedLookups.delete(dirPath);
@@ -311,8 +381,7 @@ namespace ts {
311381
invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName);
312382
}
313383

314-
function onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolder: Path) {
315-
const dirPath = getDirectoryPath(fileOrFolder);
384+
function onFileAddOrRemoveInDirectoryOfFailedLookup(dirPath: Path, fileOrFolder: Path) {
316385
const watches = directoryWatchesOfFailedLookups.get(dirPath);
317386
const isFailedLookupFile = watches.mapLocations.has(fileOrFolder);
318387
if (isFailedLookupFile) {
@@ -324,7 +393,7 @@ namespace ts {
324393
}
325394

326395
function onAddOrRemoveDirectoryOfFailedLookup(dirPath: Path) {
327-
const isInDirPath: (location: string) => boolean = location => getDirectoryPath(resolutionHost.toPath(location)) === dirPath;
396+
const isInDirPath: (location: string) => boolean = location => isInDirectoryPath(dirPath, resolutionHost.toPath(location));
328397
invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedModuleNames, isInDirPath);
329398
invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedTypeReferenceDirectives, isInDirPath);
330399
}

src/compiler/watchedProgram.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,10 @@ namespace ts {
297297
};
298298
// Cache for the module resolution
299299
const resolutionCache = createResolutionCache(compilerHost);
300-
300+
resolutionCache.setRootDirectory(configFileName ?
301+
getDirectoryPath(getNormalizedAbsolutePath(configFileName, getCurrentDirectory())) :
302+
getCurrentDirectory()
303+
);
301304
// There is no extra check needed since we can just rely on the program to decide emit
302305
const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true);
303306

@@ -576,8 +579,8 @@ namespace ts {
576579
}
577580
}
578581

579-
function watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback) {
580-
return watchDirectory(system, directory, cb, WatchDirectoryFlags.None, writeLog);
582+
function watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) {
583+
return watchDirectory(system, directory, cb, flags, writeLog);
581584
}
582585

583586
function watchMissingFilePath(missingFilePath: Path) {

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 17 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -321,34 +321,6 @@ namespace ts.projectSystem {
321321
verifyDiagnostics(actual, []);
322322
}
323323

324-
function getPathsForTypesOrModules(base: string, rootPaths: string[], typesOrModules: string[], map: Map<true>, rootDir?: string) {
325-
while (1) {
326-
forEach(rootPaths, r => {
327-
const rp = combinePaths(base, r);
328-
forEach(typesOrModules, tm => {
329-
map.set(tm === "" ? rp : combinePaths(rp, tm), true);
330-
});
331-
});
332-
const parentDir = getDirectoryPath(base);
333-
if (base === rootDir || parentDir === base) {
334-
break;
335-
}
336-
base = parentDir;
337-
}
338-
return map;
339-
}
340-
341-
function getNodeModulesWatchedDirectories(path: string, modules: string[], map = createMap<true>()) {
342-
forEach(modules, module => {
343-
getPathsForTypesOrModules(path, ["node_modules"], ["", module, "@types", `@types/${module}`], map);
344-
});
345-
return map;
346-
}
347-
348-
function getTypesWatchedDirectories(path: string, typeRoots: string[], types: string[], map = createMap<true>()) {
349-
return getPathsForTypesOrModules(path, typeRoots, types.concat(""), map, path);
350-
}
351-
352324
describe("tsserverProjectSystem", () => {
353325
const commonFile1: FileOrFolder = {
354326
path: "/a/b/commonFile1.ts",
@@ -386,8 +358,8 @@ namespace ts.projectSystem {
386358
const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"];
387359
const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]);
388360
checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path));
389-
checkWatchedDirectories(host, ["/a/b/c"], /*recursive*/ false);
390-
checkWatchedDirectories(host, [], /*recursive*/ true);
361+
checkWatchedDirectories(host, [], /*recursive*/ false);
362+
checkWatchedDirectories(host, ["/"], /*recursive*/ true);
391363
});
392364

393365
it("can handle tsconfig file name with difference casing", () => {
@@ -4040,7 +4012,7 @@ namespace ts.projectSystem {
40404012
const calledMaps = getCallsTrackingMap();
40414013
return {
40424014
verifyNoCall,
4043-
verifyCalledOnEachEntryOnce,
4015+
verifyCalledOnEachEntryNTimes,
40444016
verifyCalledOnEachEntry,
40454017
verifyNoHostCalls,
40464018
verifyNoHostCallsExceptFileExistsOnce,
@@ -4090,8 +4062,8 @@ namespace ts.projectSystem {
40904062
});
40914063
}
40924064

4093-
function verifyCalledOnEachEntryOnce(callback: keyof CalledMaps, expectedKeys: string[]) {
4094-
return verifyCalledOnEachEntry(callback, zipToMap(expectedKeys, expectedKeys.map(() => 1)));
4065+
function verifyCalledOnEachEntryNTimes(callback: keyof CalledMaps, expectedKeys: string[], nTimes: number) {
4066+
return verifyCalledOnEachEntry(callback, zipToMap(expectedKeys, expectedKeys.map(() => nTimes)));
40954067
}
40964068

40974069
function verifyNoHostCalls() {
@@ -4101,7 +4073,7 @@ namespace ts.projectSystem {
41014073
}
41024074

41034075
function verifyNoHostCallsExceptFileExistsOnce(expectedKeys: string[]) {
4104-
verifyCalledOnEachEntryOnce("fileExists", expectedKeys);
4076+
verifyCalledOnEachEntryNTimes("fileExists", expectedKeys, 1);
41054077
verifyNoCall("directoryExists");
41064078
verifyNoCall("getDirectories");
41074079
verifyNoCall("readFile");
@@ -4253,17 +4225,7 @@ namespace ts.projectSystem {
42534225
const { configFileName } = projectService.openClientFile(file1.path);
42544226
assert.equal(configFileName, tsconfigFile.path, `should find config`);
42554227
checkNumberOfConfiguredProjects(projectService, 1);
4256-
const watchedModuleDirectories = arrayFrom(
4257-
getNodeModulesWatchedDirectories(
4258-
canonicalFrontendDir,
4259-
types,
4260-
getTypesWatchedDirectories(
4261-
canonicalFrontendDir,
4262-
typeRoots,
4263-
types
4264-
)
4265-
).keys()
4266-
);
4228+
const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, canonicalFrontendDir, "/"];
42674229

42684230
const project = projectService.configuredProjects.get(canonicalConfigPath);
42694231
verifyProjectAndWatchedDirectories();
@@ -4276,7 +4238,7 @@ namespace ts.projectSystem {
42764238
host.runQueuedTimeoutCallbacks();
42774239

42784240
const canonicalFile3Path = useCaseSensitiveFileNames ? file3.path : file3.path.toLocaleLowerCase();
4279-
callsTrackingHost.verifyCalledOnEachEntryOnce("fileExists", [canonicalFile3Path]);
4241+
callsTrackingHost.verifyCalledOnEachEntryNTimes("fileExists", [canonicalFile3Path], watchingRecursiveDirectories.length);
42804242

42814243
// Called for type root resolution
42824244
const directoryExistsCalled = createMap<number>();
@@ -4286,11 +4248,11 @@ namespace ts.projectSystem {
42864248
directoryExistsCalled.set(`/node_modules`, 2);
42874249
directoryExistsCalled.set(`${frontendDir}/types`, 2);
42884250
directoryExistsCalled.set(`${frontendDir}/node_modules/@types`, 2);
4289-
directoryExistsCalled.set(canonicalFile3Path, 1);
4251+
directoryExistsCalled.set(canonicalFile3Path, watchingRecursiveDirectories.length);
42904252
callsTrackingHost.verifyCalledOnEachEntry("directoryExists", directoryExistsCalled);
42914253

42924254
callsTrackingHost.verifyNoCall("getDirectories");
4293-
callsTrackingHost.verifyCalledOnEachEntryOnce("readFile", [file3.path]);
4255+
callsTrackingHost.verifyCalledOnEachEntryNTimes("readFile", [file3.path], 1);
42944256
callsTrackingHost.verifyNoCall("readDirectory");
42954257

42964258
checkNumberOfConfiguredProjects(projectService, 1);
@@ -4316,8 +4278,8 @@ namespace ts.projectSystem {
43164278
function verifyProjectAndWatchedDirectories() {
43174279
checkProjectActualFiles(project, map(projectFiles, f => f.path));
43184280
checkWatchedFiles(host, mapDefined(projectFiles, getFilePathIfOpen));
4319-
checkWatchedDirectories(host, [`${canonicalFrontendDir}/src`], /*recursive*/ true);
4320-
checkWatchedDirectories(host, watchedModuleDirectories, /*recursive*/ false);
4281+
checkWatchedDirectories(host, watchingRecursiveDirectories, /*recursive*/ true);
4282+
checkWatchedDirectories(host, [], /*recursive*/ false);
43214283
}
43224284
}
43234285

@@ -4372,7 +4334,7 @@ namespace ts.projectSystem {
43724334
const projectService = createProjectService(host);
43734335
const { configFileName } = projectService.openClientFile(app.path);
43744336
assert.equal(configFileName, tsconfigJson.path, `should find config`);
4375-
const watchedModuleLocations = arrayFrom(getNodeModulesWatchedDirectories(appFolder, ["lodash"]).keys());
4337+
const recursiveWatchedDirectories: string[] = [appFolder, "/"];
43764338
verifyProject();
43774339

43784340
let timeoutAfterReloadFs = timeoutDuringPartialInstallation;
@@ -4450,7 +4412,8 @@ namespace ts.projectSystem {
44504412

44514413
const lodashIndexPath = "/a/b/node_modules/@types/lodash/index.d.ts";
44524414
projectFiles.push(find(filesAndFoldersToAdd, f => f.path === lodashIndexPath));
4453-
watchedModuleLocations.length = indexOf(watchedModuleLocations, getDirectoryPath(lodashIndexPath));
4415+
// we would now not have failed lookup in the parent of appFolder since lodash is available
4416+
recursiveWatchedDirectories.length = 1;
44544417
// npm installation complete, timeout after reload fs
44554418
timeoutAfterReloadFs = true;
44564419
verifyAfterPartialOrCompleteNpmInstall(2);
@@ -4475,8 +4438,8 @@ namespace ts.projectSystem {
44754438

44764439
const filesWatched = filter(projectFilePaths, p => p !== app.path);
44774440
checkWatchedFiles(host, filesWatched);
4478-
checkWatchedDirectories(host, [appFolder], /*recursive*/ true);
4479-
checkWatchedDirectories(host, watchedModuleLocations, /*recursive*/ false);
4441+
checkWatchedDirectories(host, recursiveWatchedDirectories, /*recursive*/ true);
4442+
checkWatchedDirectories(host, [], /*recursive*/ false);
44804443
}
44814444
}
44824445

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ namespace ts.TestFSWithWatch {
141141
}
142142

143143
export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive = false) {
144-
checkMapKeys("watchedDirectories", recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories);
144+
checkMapKeys(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories);
145145
}
146146

147147
export function checkOutputContains(host: TestServerHost, expected: ReadonlyArray<string>) {

0 commit comments

Comments
 (0)