Skip to content

Commit 9c6c333

Browse files
committed
Caches the recursive directory watchers so we do not have to traverse and recreate more children watches
Helps with #25018
1 parent 0abefec commit 9c6c333

File tree

5 files changed

+92
-39
lines changed

5 files changed

+92
-39
lines changed

src/compiler/sys.ts

+65-31
Original file line numberDiff line numberDiff line change
@@ -332,9 +332,9 @@ namespace ts {
332332
/*@internal*/
333333
export interface RecursiveDirectoryWatcherHost {
334334
watchDirectory: HostWatchDirectory;
335+
useCaseSensitiveFileNames: boolean;
335336
getAccessibleSortedChildDirectories(path: string): ReadonlyArray<string>;
336337
directoryExists(dir: string): boolean;
337-
filePathComparer: Comparer<string>;
338338
realpath(s: string): string;
339339
}
340340

@@ -345,60 +345,94 @@ namespace ts {
345345
*/
346346
/*@internal*/
347347
export function createRecursiveDirectoryWatcher(host: RecursiveDirectoryWatcherHost): (directoryName: string, callback: DirectoryWatcherCallback) => FileWatcher {
348-
type ChildWatches = ReadonlyArray<DirectoryWatcher>;
349-
interface DirectoryWatcher extends FileWatcher {
350-
childWatches: ChildWatches;
348+
interface ChildDirectoryWatcher extends FileWatcher {
351349
dirName: string;
352350
}
351+
type ChildWatches = ReadonlyArray<ChildDirectoryWatcher>;
352+
interface HostDirectoryWatcher {
353+
watcher: FileWatcher;
354+
childWatches: ChildWatches;
355+
refCount: number;
356+
}
357+
358+
const cache = createMap<HostDirectoryWatcher>();
359+
const callbackCache = createMultiMap<DirectoryWatcherCallback>();
360+
const filePathComparer = getStringComparer(!host.useCaseSensitiveFileNames);
361+
const toCanonicalFilePath = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
353362

354363
return createDirectoryWatcher;
355364

356365
/**
357366
* Create the directory watcher for the dirPath.
358367
*/
359-
function createDirectoryWatcher(dirName: string, callback: DirectoryWatcherCallback): DirectoryWatcher {
360-
const watcher = host.watchDirectory(dirName, fileName => {
361-
// Call the actual callback
362-
callback(fileName);
368+
function createDirectoryWatcher(dirName: string, callback?: DirectoryWatcherCallback): ChildDirectoryWatcher {
369+
const dirPath = toCanonicalFilePath(dirName) as Path;
370+
let directoryWatcher = cache.get(dirPath);
371+
if (directoryWatcher) {
372+
directoryWatcher.refCount++;
373+
}
374+
else {
375+
directoryWatcher = {
376+
watcher: host.watchDirectory(dirName, fileName => {
377+
// Call the actual callback
378+
callbackCache.forEach((callbacks, rootDirName) => {
379+
if (rootDirName === dirPath || (startsWith(dirPath, rootDirName) && dirPath[rootDirName.length] === directorySeparator)) {
380+
callbacks.forEach(callback => callback(fileName));
381+
}
382+
});
363383

364-
// Iterate through existing children and update the watches if needed
365-
updateChildWatches(result, callback);
366-
});
384+
// Iterate through existing children and update the watches if needed
385+
updateChildWatches(dirName, dirPath);
386+
}),
387+
refCount: 1,
388+
childWatches: emptyArray
389+
};
390+
cache.set(dirPath, directoryWatcher);
391+
updateChildWatches(dirName, dirPath);
392+
}
367393

368-
let result: DirectoryWatcher = {
369-
close: () => {
370-
watcher.close();
371-
result.childWatches.forEach(closeFileWatcher);
372-
result = undefined!;
373-
},
394+
if (callback) {
395+
callbackCache.add(dirPath, callback);
396+
}
397+
398+
return {
374399
dirName,
375-
childWatches: emptyArray
400+
close: () => {
401+
const directoryWatcher = Debug.assertDefined(cache.get(dirPath));
402+
if (callback) callbackCache.remove(dirPath, callback);
403+
directoryWatcher.refCount--;
404+
405+
if (directoryWatcher.refCount) return;
406+
407+
cache.delete(dirPath);
408+
closeFileWatcherOf(directoryWatcher);
409+
directoryWatcher.childWatches.forEach(closeFileWatcher);
410+
}
376411
};
377-
updateChildWatches(result, callback);
378-
return result;
379412
}
380413

381-
function updateChildWatches(watcher: DirectoryWatcher, callback: DirectoryWatcherCallback) {
414+
function updateChildWatches(dirName: string, dirPath: Path) {
382415
// Iterate through existing children and update the watches if needed
383-
if (watcher) {
384-
watcher.childWatches = watchChildDirectories(watcher.dirName, watcher.childWatches, callback);
416+
const parentWatcher = cache.get(dirPath);
417+
if (parentWatcher) {
418+
parentWatcher.childWatches = watchChildDirectories(dirName, parentWatcher.childWatches);
385419
}
386420
}
387421

388422
/**
389423
* Watch the directories in the parentDir
390424
*/
391-
function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, callback: DirectoryWatcherCallback): ChildWatches {
392-
let newChildWatches: DirectoryWatcher[] | undefined;
393-
enumerateInsertsAndDeletes<string, DirectoryWatcher>(
425+
function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches): ChildWatches {
426+
let newChildWatches: ChildDirectoryWatcher[] | undefined;
427+
enumerateInsertsAndDeletes<string, ChildDirectoryWatcher>(
394428
host.directoryExists(parentDir) ? mapDefined(host.getAccessibleSortedChildDirectories(parentDir), child => {
395429
const childFullName = getNormalizedAbsolutePath(child, parentDir);
396430
// Filter our the symbolic link directories since those arent included in recursive watch
397431
// which is same behaviour when recursive: true is passed to fs.watch
398-
return host.filePathComparer(childFullName, host.realpath(childFullName)) === Comparison.EqualTo ? childFullName : undefined;
432+
return filePathComparer(childFullName, normalizePath(host.realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined;
399433
}) : emptyArray,
400434
existingChildWatches,
401-
(child, childWatcher) => host.filePathComparer(child, childWatcher.dirName),
435+
(child, childWatcher) => filePathComparer(child, childWatcher.dirName),
402436
createAndAddChildDirectoryWatcher,
403437
closeFileWatcher,
404438
addChildDirectoryWatcher
@@ -410,14 +444,14 @@ namespace ts {
410444
* Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list
411445
*/
412446
function createAndAddChildDirectoryWatcher(childName: string) {
413-
const result = createDirectoryWatcher(childName, callback);
447+
const result = createDirectoryWatcher(childName);
414448
addChildDirectoryWatcher(result);
415449
}
416450

417451
/**
418452
* Add child directory watcher to the new ChildDirectoryWatcher list
419453
*/
420-
function addChildDirectoryWatcher(childWatcher: DirectoryWatcher) {
454+
function addChildDirectoryWatcher(childWatcher: ChildDirectoryWatcher) {
421455
(newChildWatches || (newChildWatches = [])).push(childWatcher);
422456
}
423457
}
@@ -710,7 +744,7 @@ namespace ts {
710744
createWatchDirectoryUsing(dynamicPollingWatchFile || createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout })) :
711745
watchDirectoryUsingFsWatch;
712746
const watchDirectoryRecursively = createRecursiveDirectoryWatcher({
713-
filePathComparer: getStringComparer(!useCaseSensitiveFileNames),
747+
useCaseSensitiveFileNames,
714748
directoryExists,
715749
getAccessibleSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories,
716750
watchDirectory,

src/compiler/watchUtilities.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ namespace ts {
398398
case WatchLogLevel.TriggerOnly:
399399
return createFileWatcherWithTriggerLogging;
400400
case WatchLogLevel.Verbose:
401-
return createFileWatcherWithLogging;
401+
return addWatch === <any>watchDirectory ? createDirectoryWatcherWithLogging : createFileWatcherWithLogging;
402402
}
403403
}
404404

@@ -413,6 +413,25 @@ namespace ts {
413413
};
414414
}
415415

416+
function createDirectoryWatcherWithLogging<H, T, U, V, X, Y>(host: H, file: string, cb: WatchCallback<U, V>, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch<H, T, U, undefined>, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo<X, Y> | undefined): FileWatcher {
417+
const watchInfo = `${watchCaption}:: Added:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`;
418+
log(watchInfo);
419+
const start = timestamp();
420+
const watcher = createFileWatcherWithTriggerLogging(host, file, cb, flags, passThrough, detailInfo1, detailInfo2, addWatch, log, watchCaption, getDetailWatchInfo);
421+
const elapsed = timestamp() - start;
422+
log(`Elapsed:: ${elapsed}ms ${watchInfo}`);
423+
return {
424+
close: () => {
425+
const watchInfo = `${watchCaption}:: Close:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`;
426+
log(watchInfo);
427+
const start = timestamp();
428+
watcher.close();
429+
const elapsed = timestamp() - start;
430+
log(`Elapsed:: ${elapsed}ms ${watchInfo}`);
431+
}
432+
};
433+
}
434+
416435
function createFileWatcherWithTriggerLogging<H, T, U, V, X, Y>(host: H, file: string, cb: WatchCallback<U, V>, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch<H, T, U, undefined>, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo<X, Y> | undefined): FileWatcher {
417436
return addWatch(host, file, (fileName, cbOptional) => {
418437
const triggerredInfo = `${watchCaption}:: Triggered with ${fileName}${cbOptional !== undefined ? cbOptional : ""}:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`;

src/harness/virtualFileSystemWithWatch.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -350,19 +350,19 @@ interface Array<T> {}`
350350
if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) {
351351
const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchFile(directory, () => cb(directory), PollingInterval.Medium);
352352
this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({
353+
useCaseSensitiveFileNames: this.useCaseSensitiveFileNames,
353354
directoryExists: path => this.directoryExists(path),
354355
getAccessibleSortedChildDirectories: path => this.getDirectories(path),
355-
filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive,
356356
watchDirectory,
357357
realpath: s => this.realpath(s)
358358
});
359359
}
360360
else if (tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory) {
361361
const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchDirectory(directory, fileName => cb(fileName), /*recursive*/ false);
362362
this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({
363+
useCaseSensitiveFileNames: this.useCaseSensitiveFileNames,
363364
directoryExists: path => this.directoryExists(path),
364365
getAccessibleSortedChildDirectories: path => this.getDirectories(path),
365-
filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive,
366366
watchDirectory,
367367
realpath: s => this.realpath(s)
368368
});
@@ -371,9 +371,9 @@ interface Array<T> {}`
371371
const watchFile = createDynamicPriorityPollingWatchFile(this);
372372
const watchDirectory: HostWatchDirectory = (directory, cb) => watchFile(directory, () => cb(directory), PollingInterval.Medium);
373373
this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({
374+
useCaseSensitiveFileNames: this.useCaseSensitiveFileNames,
374375
directoryExists: path => this.directoryExists(path),
375376
getAccessibleSortedChildDirectories: path => this.getDirectories(path),
376-
filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive,
377377
watchDirectory,
378378
realpath: s => this.realpath(s)
379379
});

src/testRunner/unittests/tsserverProjectSystem.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -8623,10 +8623,10 @@ export const x = 10;`
86238623
tscWatchDirectory === Tsc_WatchDirectory.WatchFile ?
86248624
expectedWatchedFiles :
86258625
createMap();
8626-
// For failed resolution lookup and tsconfig files
8627-
mapOfDirectories.set(projectFolder, 2);
8626+
// For failed resolution lookup and tsconfig files => cached so only watched only once
8627+
mapOfDirectories.set(projectFolder, 1);
86288628
// Through above recursive watches
8629-
mapOfDirectories.set(projectSrcFolder, 2);
8629+
mapOfDirectories.set(projectSrcFolder, 1);
86308630
// node_modules/@types folder
86318631
mapOfDirectories.set(`${projectFolder}/${nodeModulesAtTypes}`, 1);
86328632
const expectedCompletions = ["file1"];

tests/baselines/reference/api/tsserverlibrary.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4766,9 +4766,9 @@ declare namespace ts {
47664766
function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean;
47674767
interface RecursiveDirectoryWatcherHost {
47684768
watchDirectory: HostWatchDirectory;
4769+
useCaseSensitiveFileNames: boolean;
47694770
getAccessibleSortedChildDirectories(path: string): ReadonlyArray<string>;
47704771
directoryExists(dir: string): boolean;
4771-
filePathComparer: Comparer<string>;
47724772
realpath(s: string): string;
47734773
}
47744774
/**

0 commit comments

Comments
 (0)