Skip to content

Caches the recursive directory watchers so we do not have to traverse and recreate more children watches #25464

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 65 additions & 31 deletions src/compiler/sys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,9 +332,9 @@ namespace ts {
/*@internal*/
export interface RecursiveDirectoryWatcherHost {
watchDirectory: HostWatchDirectory;
useCaseSensitiveFileNames: boolean;
getAccessibleSortedChildDirectories(path: string): ReadonlyArray<string>;
directoryExists(dir: string): boolean;
filePathComparer: Comparer<string>;
realpath(s: string): string;
}

Expand All @@ -345,60 +345,94 @@ namespace ts {
*/
/*@internal*/
export function createRecursiveDirectoryWatcher(host: RecursiveDirectoryWatcherHost): (directoryName: string, callback: DirectoryWatcherCallback) => FileWatcher {
type ChildWatches = ReadonlyArray<DirectoryWatcher>;
interface DirectoryWatcher extends FileWatcher {
childWatches: ChildWatches;
interface ChildDirectoryWatcher extends FileWatcher {
dirName: string;
}
type ChildWatches = ReadonlyArray<ChildDirectoryWatcher>;
interface HostDirectoryWatcher {
watcher: FileWatcher;
childWatches: ChildWatches;
refCount: number;
}

const cache = createMap<HostDirectoryWatcher>();
const callbackCache = createMultiMap<DirectoryWatcherCallback>();
const filePathComparer = getStringComparer(!host.useCaseSensitiveFileNames);
const toCanonicalFilePath = createGetCanonicalFileName(host.useCaseSensitiveFileNames);

return createDirectoryWatcher;

/**
* Create the directory watcher for the dirPath.
*/
function createDirectoryWatcher(dirName: string, callback: DirectoryWatcherCallback): DirectoryWatcher {
const watcher = host.watchDirectory(dirName, fileName => {
// Call the actual callback
callback(fileName);
function createDirectoryWatcher(dirName: string, callback?: DirectoryWatcherCallback): ChildDirectoryWatcher {
const dirPath = toCanonicalFilePath(dirName) as Path;
let directoryWatcher = cache.get(dirPath);
if (directoryWatcher) {
directoryWatcher.refCount++;
}
else {
directoryWatcher = {
watcher: host.watchDirectory(dirName, fileName => {
// Call the actual callback
callbackCache.forEach((callbacks, rootDirName) => {
if (rootDirName === dirPath || (startsWith(dirPath, rootDirName) && dirPath[rootDirName.length] === directorySeparator)) {
callbacks.forEach(callback => callback(fileName));
}
});

// Iterate through existing children and update the watches if needed
updateChildWatches(result, callback);
});
// Iterate through existing children and update the watches if needed
updateChildWatches(dirName, dirPath);
}),
refCount: 1,
childWatches: emptyArray
};
cache.set(dirPath, directoryWatcher);
updateChildWatches(dirName, dirPath);
}

let result: DirectoryWatcher = {
close: () => {
watcher.close();
result.childWatches.forEach(closeFileWatcher);
result = undefined!;
},
if (callback) {
callbackCache.add(dirPath, callback);
}

return {
dirName,
childWatches: emptyArray
close: () => {
const directoryWatcher = Debug.assertDefined(cache.get(dirPath));
if (callback) callbackCache.remove(dirPath, callback);
directoryWatcher.refCount--;

if (directoryWatcher.refCount) return;

cache.delete(dirPath);
closeFileWatcherOf(directoryWatcher);
directoryWatcher.childWatches.forEach(closeFileWatcher);
}
};
updateChildWatches(result, callback);
return result;
}

function updateChildWatches(watcher: DirectoryWatcher, callback: DirectoryWatcherCallback) {
function updateChildWatches(dirName: string, dirPath: Path) {
// Iterate through existing children and update the watches if needed
if (watcher) {
watcher.childWatches = watchChildDirectories(watcher.dirName, watcher.childWatches, callback);
const parentWatcher = cache.get(dirPath);
if (parentWatcher) {
parentWatcher.childWatches = watchChildDirectories(dirName, parentWatcher.childWatches);
}
}

/**
* Watch the directories in the parentDir
*/
function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, callback: DirectoryWatcherCallback): ChildWatches {
let newChildWatches: DirectoryWatcher[] | undefined;
enumerateInsertsAndDeletes<string, DirectoryWatcher>(
function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches): ChildWatches {
let newChildWatches: ChildDirectoryWatcher[] | undefined;
enumerateInsertsAndDeletes<string, ChildDirectoryWatcher>(
host.directoryExists(parentDir) ? mapDefined(host.getAccessibleSortedChildDirectories(parentDir), child => {
const childFullName = getNormalizedAbsolutePath(child, parentDir);
// Filter our the symbolic link directories since those arent included in recursive watch
// which is same behaviour when recursive: true is passed to fs.watch
return host.filePathComparer(childFullName, host.realpath(childFullName)) === Comparison.EqualTo ? childFullName : undefined;
return filePathComparer(childFullName, normalizePath(host.realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined;
}) : emptyArray,
existingChildWatches,
(child, childWatcher) => host.filePathComparer(child, childWatcher.dirName),
(child, childWatcher) => filePathComparer(child, childWatcher.dirName),
createAndAddChildDirectoryWatcher,
closeFileWatcher,
addChildDirectoryWatcher
Expand All @@ -410,14 +444,14 @@ namespace ts {
* Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list
*/
function createAndAddChildDirectoryWatcher(childName: string) {
const result = createDirectoryWatcher(childName, callback);
const result = createDirectoryWatcher(childName);
addChildDirectoryWatcher(result);
}

/**
* Add child directory watcher to the new ChildDirectoryWatcher list
*/
function addChildDirectoryWatcher(childWatcher: DirectoryWatcher) {
function addChildDirectoryWatcher(childWatcher: ChildDirectoryWatcher) {
(newChildWatches || (newChildWatches = [])).push(childWatcher);
}
}
Expand Down Expand Up @@ -710,7 +744,7 @@ namespace ts {
createWatchDirectoryUsing(dynamicPollingWatchFile || createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout })) :
watchDirectoryUsingFsWatch;
const watchDirectoryRecursively = createRecursiveDirectoryWatcher({
filePathComparer: getStringComparer(!useCaseSensitiveFileNames),
useCaseSensitiveFileNames,
directoryExists,
getAccessibleSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories,
watchDirectory,
Expand Down
21 changes: 20 additions & 1 deletion src/compiler/watchUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ namespace ts {
case WatchLogLevel.TriggerOnly:
return createFileWatcherWithTriggerLogging;
case WatchLogLevel.Verbose:
return createFileWatcherWithLogging;
return addWatch === <any>watchDirectory ? createDirectoryWatcherWithLogging : createFileWatcherWithLogging;
}
}

Expand All @@ -413,6 +413,25 @@ namespace ts {
};
}

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 {
const watchInfo = `${watchCaption}:: Added:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`;
log(watchInfo);
const start = timestamp();
const watcher = createFileWatcherWithTriggerLogging(host, file, cb, flags, passThrough, detailInfo1, detailInfo2, addWatch, log, watchCaption, getDetailWatchInfo);
const elapsed = timestamp() - start;
log(`Elapsed:: ${elapsed}ms ${watchInfo}`);
return {
close: () => {
const watchInfo = `${watchCaption}:: Close:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`;
log(watchInfo);
const start = timestamp();
watcher.close();
const elapsed = timestamp() - start;
log(`Elapsed:: ${elapsed}ms ${watchInfo}`);
}
};
}

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 {
return addWatch(host, file, (fileName, cbOptional) => {
const triggerredInfo = `${watchCaption}:: Triggered with ${fileName}${cbOptional !== undefined ? cbOptional : ""}:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`;
Expand Down
6 changes: 3 additions & 3 deletions src/harness/virtualFileSystemWithWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,19 +352,19 @@ interface Array<T> {}`
if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) {
const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchFile(directory, () => cb(directory), PollingInterval.Medium);
this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({
useCaseSensitiveFileNames: this.useCaseSensitiveFileNames,
directoryExists: path => this.directoryExists(path),
getAccessibleSortedChildDirectories: path => this.getDirectories(path),
filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive,
watchDirectory,
realpath: s => this.realpath(s)
});
}
else if (tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory) {
const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchDirectory(directory, fileName => cb(fileName), /*recursive*/ false);
this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({
useCaseSensitiveFileNames: this.useCaseSensitiveFileNames,
directoryExists: path => this.directoryExists(path),
getAccessibleSortedChildDirectories: path => this.getDirectories(path),
filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive,
watchDirectory,
realpath: s => this.realpath(s)
});
Expand All @@ -373,9 +373,9 @@ interface Array<T> {}`
const watchFile = createDynamicPriorityPollingWatchFile(this);
const watchDirectory: HostWatchDirectory = (directory, cb) => watchFile(directory, () => cb(directory), PollingInterval.Medium);
this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({
useCaseSensitiveFileNames: this.useCaseSensitiveFileNames,
directoryExists: path => this.directoryExists(path),
getAccessibleSortedChildDirectories: path => this.getDirectories(path),
filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive,
watchDirectory,
realpath: s => this.realpath(s)
});
Expand Down
6 changes: 3 additions & 3 deletions src/testRunner/unittests/tsserverProjectSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8589,10 +8589,10 @@ export const x = 10;`
tscWatchDirectory === Tsc_WatchDirectory.WatchFile ?
expectedWatchedFiles :
createMap();
// For failed resolution lookup and tsconfig files
mapOfDirectories.set(projectFolder, 2);
// For failed resolution lookup and tsconfig files => cached so only watched only once
mapOfDirectories.set(projectFolder, 1);
// Through above recursive watches
mapOfDirectories.set(projectSrcFolder, 2);
mapOfDirectories.set(projectSrcFolder, 1);
// node_modules/@types folder
mapOfDirectories.set(`${projectFolder}/${nodeModulesAtTypes}`, 1);
const expectedCompletions = ["file1"];
Expand Down
3 changes: 0 additions & 3 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1933,9 +1933,6 @@ declare namespace ts {
getAmbientModules(): Symbol[];
tryGetMemberInModuleExports(memberName: string, moduleSymbol: Symbol): Symbol | undefined;
getApparentType(type: Type): Type;
getSuggestionForNonexistentProperty(name: Identifier | string, containingType: Type): string | undefined;
getSuggestionForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): string | undefined;
getSuggestionForNonexistentExport(node: Identifier, target: Symbol): string | undefined;
getBaseConstraintOfType(type: Type): Type | undefined;
getDefaultFromTypeParameter(type: Type): Type | undefined;
/**
Expand Down