From caa6eb4204a8aa550e493be405ef5b22ef5eebce Mon Sep 17 00:00:00 2001 From: Zhengbo Li Date: Tue, 22 Dec 2015 15:26:21 -0800 Subject: [PATCH] Reuse watchers between 'watchDirectory' and 'watchFile' --- src/compiler/sys.ts | 184 +++++++++++++++++++++++++++++--------------- 1 file changed, 122 insertions(+), 62 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 5d723425496..4056ffd9da9 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -1,8 +1,8 @@ /// namespace ts { - export type CallbackForWatchedFile = (path: string, removed?: boolean) => void; - export type CallbackForWatchedDirectory = (path: string) => void; + export type FileWatcherCallback = (path: string, removed?: boolean) => void; + export type DirWatcherCallback = (path: string) => void; export interface System { args: string[]; @@ -11,8 +11,8 @@ namespace ts { write(s: string): void; readFile(path: string, encoding?: string): string; writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; - watchFile?(path: string, callback: CallbackForWatchedFile): FileWatcher; - watchDirectory?(path: string, callback: CallbackForWatchedDirectory, recursive?: boolean): FileWatcher; + watchFile?(path: string, callback: FileWatcherCallback): FileWatcher; + watchDirectory?(path: string, callback: DirWatcherCallback, recursive?: boolean): FileWatcher; resolvePath(path: string): string; fileExists(path: string): boolean; directoryExists(path: string): boolean; @@ -26,13 +26,17 @@ namespace ts { interface WatchedFile { fileName: string; - callback: CallbackForWatchedFile; + callback: FileWatcherCallback; mtime?: Date; } export interface FileWatcher { close(): void; } + + export interface DirWatcher extends FileWatcher { + referenceCount: number; + } declare var require: any; declare var module: any; @@ -65,8 +69,8 @@ namespace ts { readFile(path: string): string; writeFile(path: string, contents: string): void; readDirectory(path: string, extension?: string, exclude?: string[]): string[]; - watchFile?(path: string, callback: CallbackForWatchedFile): FileWatcher; - watchDirectory?(path: string, callback: CallbackForWatchedDirectory, recursive?: boolean): FileWatcher; + watchFile?(path: string, callback: FileWatcherCallback): FileWatcher; + watchDirectory?(path: string, callback: DirWatcherCallback, recursive?: boolean): FileWatcher; }; export var sys: System = (function () { @@ -274,7 +278,7 @@ namespace ts { }, interval); } - function addFile(fileName: string, callback: CallbackForWatchedFile): WatchedFile { + function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile { const file: WatchedFile = { fileName, callback, @@ -301,53 +305,124 @@ namespace ts { }; } - - function createWatchedFileSet() { - const watchedDirectories = createFileMap(); - const watchedFiles = createFileMap(); + const dirWatchers = createFileMap(); + const recursiveDirWatchers = createFileMap(); + const fileWatcherCallbacks = createFileMap(); + const dirWatcherCallbacks = createFileMap(); + const currentDirectory = process.cwd(); + return { addFile, removeFile, addDir }; - return { addFile, removeFile }; - - function addFile(fileName: string, callback: CallbackForWatchedFile): WatchedFile { - const path = toPath(fileName, currentDirectory, getCanonicalPath); - const parentDirPath = getDirectoryPath(path); - - if (!watchedDirectories.contains(parentDirPath)) { - watchedDirectories.set(parentDirPath, _fs.watch( - parentDirPath, - (eventName: string, relativeFileName: string) => fileEventHandler(eventName, relativeFileName, parentDirPath) - )); + function addDir(dirName: string, callback: DirWatcherCallback, recursive?: boolean) { + const dirPath = toPath(dirName, currentDirectory, getCanonicalPath); + dirWatcherCallbacks.set(dirPath, callback); + const { watcher, isRecursive } = addDirWatcher(dirPath, recursive); + return { + close: () => reduceDirWatcherRefCount(watcher, dirPath, isRecursive) } - watchedFiles.set(path, callback); - return { fileName, callback }; } - - function removeFile(file: WatchedFile) { - const path = toPath(file.fileName, currentDirectory, getCanonicalPath); - watchedFiles.remove(path); - - const parentDirPath = getDirectoryPath(path); - if (watchedDirectories.contains(parentDirPath)) { - let hasWatchedChildren = false; - watchedFiles.forEachValue((key, _) => { - if (ts.getDirectoryPath(key) === parentDirPath) { - hasWatchedChildren = true; - } - }); - if (!hasWatchedChildren) { - watchedDirectories.get(parentDirPath).close(); - watchedDirectories.remove(parentDirPath); + + function reduceDirWatcherRefCount(watcher: DirWatcher, dirPath: Path, isRecursive: boolean) { + watcher.referenceCount -= 1; + if (watcher.referenceCount <= 0) { + watcher.close(); + if (isRecursive) { + recursiveDirWatchers.remove(dirPath); + } else { + dirWatchers.remove(dirPath); } } } - function fileEventHandler(eventName: string, fileName: string, basePath: string) { - const path = ts.toPath(fileName, basePath, getCanonicalPath); - if (watchedFiles.contains(path)) { - const callback = watchedFiles.get(path); - callback(fileName); + function addDirWatcher(dirPath: Path, recursive?: boolean): { watcher: DirWatcher, isRecursive: boolean } { + let watchers: FileMap; + let options: { persistent: boolean, recursive?: boolean } = { persistent: true }; + + // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows + // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) + if (isNode4OrLater() && recursive === true) { + if (recursiveDirWatchers.contains(dirPath)) { + const watcher = recursiveDirWatchers.get(dirPath); + watcher.referenceCount += 1; + return { watcher, isRecursive: true }; + } + watchers = recursiveDirWatchers; + options.recursive = true; + } else { + if (dirWatchers.contains(dirPath)) { + const watcher = dirWatchers.get(dirPath); + watcher.referenceCount += 1; + return { watcher, isRecursive: false }; + } + watchers = dirWatchers; + } + + const watcher: DirWatcher = _fs.watch(dirPath, options, (eventName: string, relativeFileName: string) => fileEventHandler(eventName, relativeFileName, dirPath)); + watcher.referenceCount = 1; + watchers.set(dirPath, watcher); + return { watcher, isRecursive: false }; + } + + function findDirWatcherForFile(filePath: Path): { watcher: DirWatcher, watcherPath: Path, isRecursive: boolean } { + let watcher: DirWatcher; + let watcherPath: Path; + let isRecursive = false; + recursiveDirWatchers.forEachValue(dirPath => { + if (filePath.indexOf(dirPath) === 0) { + watcherPath = dirPath; + watcher = recursiveDirWatchers.get(dirPath); + isRecursive = true; + return; + } + }); + if (!watcher) { + const parentDirPath = getDirectoryPath(filePath); + if (dirWatchers.contains(parentDirPath)) { + watcherPath = parentDirPath; + watcher = dirWatchers.get(parentDirPath); + } + } + return { watcher, watcherPath, isRecursive }; + } + + function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile { + const filePath = toPath(fileName, currentDirectory, getCanonicalPath); + const { watcher } = findDirWatcherForFile(filePath); + if (!watcher) { + addDirWatcher(getDirectoryPath(filePath)); + } else { + watcher.referenceCount += 1; + } + fileWatcherCallbacks.set(filePath, callback); + return { fileName, callback }; + } + + function removeFile(file: WatchedFile) { + const filePath = toPath(file.fileName, currentDirectory, getCanonicalPath); + fileWatcherCallbacks.remove(filePath); + + const { watcher, watcherPath, isRecursive } = findDirWatcherForFile(filePath); + if (watcher) { + reduceDirWatcherRefCount(watcher, watcherPath, isRecursive); + } + } + + /** + * @param watcherPath is the path from which the watcher is triggered. + */ + function fileEventHandler(eventName: string, relativefileName: string, baseDirPath: Path) { + // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" + const filePath = relativefileName === undefined ? undefined : toPath(relativefileName, baseDirPath, getCanonicalPath); + // Directory callbacks are not set for file content changes, they are more often used for + // adding/removing/renaming files, which corresponds to the "rename" event + if (eventName === "rename" && dirWatcherCallbacks.contains(baseDirPath)) { + const dirCallback = dirWatcherCallbacks.get(baseDirPath); + dirCallback(filePath); + } + if (fileWatcherCallbacks.contains(filePath)) { + const fileCallback = fileWatcherCallbacks.get(filePath); + fileCallback(filePath); } } } @@ -477,22 +552,7 @@ namespace ts { }; }, watchDirectory: (path, callback, recursive) => { - // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows - // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) - const options = isNode4OrLater() ? { persistent: true } : { persistent: true, recursive: !!recursive }; - return _fs.watch( - path, - options, - (eventName: string, relativeFileName: string) => { - // In watchDirectory we only care about adding and removing files (when event name is - // "rename"); changes made within files are handled by corresponding fileWatchers (when - // event name is "change") - if (eventName === "rename") { - // When deleting a file, the passed baseFileName is null - callback(!relativeFileName ? relativeFileName : normalizePath(ts.combinePaths(path, relativeFileName))); - }; - } - ); + return watchedFileSet.addDir(path, callback, recursive); }, resolvePath: function (path: string): string { return _path.resolve(path);