Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 13.8k
Fix too many watcher instances issue#6026
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
e6757445fa7bec36cc0e0d419015602cde4178b2dacaa6eb4631363f56a72178cf1a34697644c114d2bdd22626f02531af067573bdbfe862File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,18 @@ | ||
| /// <reference path="core.ts"/> | ||
| namespace ts { | ||
| export type FileWatcherCallback = (path: string, removed?: boolean) => void; | ||
| export type DirectoryWatcherCallback = (path: string) => void; | ||
| export interface System { | ||
| args: string[]; | ||
| newLine: string; | ||
| useCaseSensitiveFileNames: boolean; | ||
| write(s: string): void; | ||
| readFile(path: string, encoding?: string): string; | ||
| writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; | ||
| watchFile?(path: string, callback: (path: string, removed?: boolean) => void): FileWatcher; | ||
| watchDirectory?(path: string, callback: (path: string) => void, recursive?: boolean): FileWatcher; | ||
| watchFile?(path: Path, callback: FileWatcherCallback): FileWatcher; | ||
| watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; | ||
| resolvePath(path: string): string; | ||
| fileExists(path: string): boolean; | ||
| directoryExists(path: string): boolean; | ||
| @@ -22,15 +25,20 @@ namespace ts { | ||
| } | ||
| interface WatchedFile { | ||
| fileName: string; | ||
| callback: (fileName: string, removed?: boolean) => void; | ||
| mtime: Date; | ||
| filePath: Path; | ||
| callback: FileWatcherCallback; | ||
| mtime?: Date; | ||
| } | ||
| export interface FileWatcher { | ||
| close(): void; | ||
| } | ||
| export interface DirectoryWatcher extends FileWatcher { | ||
| directoryPath: Path; | ||
| referenceCount: number; | ||
| } | ||
| declare var require: any; | ||
| declare var module: any; | ||
| declare var process: any; | ||
| @@ -62,8 +70,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: (path: string, removed?: boolean) => void): FileWatcher; | ||
| watchDirectory?(path: string, callback: (path: string) => void, recursive?: boolean): FileWatcher; | ||
| watchFile?(path: string, callback: FileWatcherCallback): FileWatcher; | ||
| watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; | ||
| }; | ||
| export var sys: System = (function () { | ||
| @@ -221,7 +229,7 @@ namespace ts { | ||
| // average async stat takes about 30 microseconds | ||
| // set chunk size to do 30 files in < 1 millisecond | ||
| function createWatchedFileSet(interval = 2500, chunkSize = 30) { | ||
| function createPollingWatchedFileSet(interval = 2500, chunkSize = 30) { | ||
| let watchedFiles: WatchedFile[] = []; | ||
| let nextFileToCheck = 0; | ||
| let watchTimer: any; | ||
| @@ -236,13 +244,13 @@ namespace ts { | ||
| return; | ||
| } | ||
| _fs.stat(watchedFile.fileName, (err: any, stats: any) => { | ||
| _fs.stat(watchedFile.filePath, (err: any, stats: any) => { | ||
| if (err) { | ||
| watchedFile.callback(watchedFile.fileName); | ||
| watchedFile.callback(watchedFile.filePath); | ||
| } | ||
| else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) { | ||
| watchedFile.mtime = getModifiedTime(watchedFile.fileName); | ||
| watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0); | ||
| watchedFile.mtime = getModifiedTime(watchedFile.filePath); | ||
| watchedFile.callback(watchedFile.filePath, watchedFile.mtime.getTime() === 0); | ||
| } | ||
| }); | ||
| } | ||
| @@ -270,11 +278,11 @@ namespace ts { | ||
| }, interval); | ||
| } | ||
| function addFile(fileName: string, callback: (fileName: string, removed?: boolean) => void): WatchedFile { | ||
| function addFile(filePath: Path, callback: FileWatcherCallback): WatchedFile { | ||
| const file: WatchedFile = { | ||
| fileName, | ||
| filePath, | ||
| callback, | ||
| mtime: getModifiedTime(fileName) | ||
| mtime: getModifiedTime(filePath) | ||
| }; | ||
| watchedFiles.push(file); | ||
| @@ -297,6 +305,88 @@ namespace ts { | ||
| }; | ||
| } | ||
| function createWatchedFileSet() { | ||
| const dirWatchers = createFileMap<DirectoryWatcher>(); | ||
| // One file can have multiple watchers | ||
| const fileWatcherCallbacks = createFileMap<FileWatcherCallback[]>(); | ||
| return { addFile, removeFile }; | ||
| function reduceDirWatcherRefCountForFile(filePath: Path) { | ||
| const dirPath = getDirectoryPath(filePath); | ||
| if (dirWatchers.contains(dirPath)) { | ||
| const watcher = dirWatchers.get(dirPath); | ||
| watcher.referenceCount -= 1; | ||
| if (watcher.referenceCount <= 0) { | ||
| watcher.close(); | ||
| dirWatchers.remove(dirPath); | ||
| } | ||
| } | ||
| } | ||
| function addDirWatcher(dirPath: Path): void { | ||
| if (dirWatchers.contains(dirPath)) { | ||
| const watcher = dirWatchers.get(dirPath); | ||
| watcher.referenceCount += 1; | ||
| return; | ||
| } | ||
| const watcher: DirectoryWatcher = _fs.watch( | ||
| dirPath, | ||
| { persistent: true }, | ||
| (eventName: string, relativeFileName: string) => fileEventHandler(eventName, relativeFileName, dirPath) | ||
| ); | ||
| watcher.referenceCount = 1; | ||
| dirWatchers.set(dirPath, watcher); | ||
| return; | ||
| } | ||
| function addFileWatcherCallback(filePath: Path, callback: FileWatcherCallback): void { | ||
| if (fileWatcherCallbacks.contains(filePath)) { | ||
| fileWatcherCallbacks.get(filePath).push(callback); | ||
| } | ||
| else { | ||
| fileWatcherCallbacks.set(filePath, [callback]); | ||
| } | ||
| } | ||
| function addFile(filePath: Path, callback: FileWatcherCallback): WatchedFile { | ||
| addFileWatcherCallback(filePath, callback); | ||
| addDirWatcher(getDirectoryPath(filePath)); | ||
| return { filePath, callback }; | ||
| } | ||
| function removeFile(watchedFile: WatchedFile) { | ||
| removeFileWatcherCallback(watchedFile.filePath, watchedFile.callback); | ||
| reduceDirWatcherRefCountForFile(watchedFile.filePath); | ||
| } | ||
| function removeFileWatcherCallback(filePath: Path, callback: FileWatcherCallback) { | ||
| if (fileWatcherCallbacks.contains(filePath)) { | ||
| const newCallbacks = copyListRemovingItem(callback, fileWatcherCallbacks.get(filePath)); | ||
| if (newCallbacks.length === 0) { | ||
| fileWatcherCallbacks.remove(filePath); | ||
| } | ||
| else { | ||
| fileWatcherCallbacks.set(filePath, newCallbacks); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * @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); | ||
| if (eventName === "change" && fileWatcherCallbacks.contains(filePath)) { | ||
| for (const fileCallback of fileWatcherCallbacks.get(filePath)) { | ||
| fileCallback(filePath); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // REVIEW: for now this implementation uses polling. | ||
| // The advantage of polling is that it works reliably | ||
| // on all os and with network mounted files. | ||
| @@ -310,8 +400,13 @@ namespace ts { | ||
| // changes for large reference sets? If so, do we want | ||
| // to increase the chunk size or decrease the interval | ||
| // time dynamically to match the large reference set? | ||
| const pollingWatchedFileSet = createPollingWatchedFileSet(); | ||
| const watchedFileSet = createWatchedFileSet(); | ||
| function isNode4OrLater(): boolean { | ||
| return parseInt(process.version.charAt(1)) >= 4; | ||
| } | ||
| const platform: string = _os.platform(); | ||
| // win32\win64 are case insensitive platforms, MacOS (darwin) by default is also case insensitive | ||
| const useCaseSensitiveFileNames = platform !== "win32" && platform !== "win64" && platform !== "darwin"; | ||
| @@ -405,29 +500,38 @@ namespace ts { | ||
| }, | ||
| readFile, | ||
| writeFile, | ||
| watchFile: (fileName, callback) => { | ||
| watchFile: (filePath, callback) => { | ||
| // Node 4.0 stablized the `fs.watch` function on Windows which avoids polling | ||
| // and is more efficient than `fs.watchFile` (ref: https://github.com/nodejs/node/pull/2649 | ||
| // and https://github.com/Microsoft/TypeScript/issues/4643), therefore | ||
| // if the current node.js version is newer than 4, use `fs.watch` instead. | ||
| const watchedFile = watchedFileSet.addFile(fileName, callback); | ||
| const watchSet = isNode4OrLater() ? watchedFileSet : pollingWatchedFileSet; | ||
| const watchedFile = watchSet.addFile(filePath, callback); | ||
| return { | ||
| close: () => watchedFileSet.removeFile(watchedFile) | ||
| close: () => watchSet.removeFile(watchedFile) | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "watchDirectory" (line 474 below) also talks about Node 4 or later and uses the 'recursive' option, but doesn't actually check for Node 4 or later. I can see in "tsc.ts" and "editorServices.ts" it is called with "recursive: true". Are there potentially issues here if not on Node v4? Author There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tested before that if not supported then the Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure i follow about the Even if not a public API, it's a future bug waiting to happen if it ignores known failure scenarios based on assumptions about how it will be used. Please protect against it. | ||
| }; | ||
| }, | ||
| 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) | ||
| let options: any; | ||
| if (isNode4OrLater() && (process.platform === "win32" || process.platform === "darwin")) { | ||
| options = { persistent: true, recursive: !!recursive }; | ||
| } | ||
| else { | ||
| options = { persistent: true }; | ||
| } | ||
| return _fs.watch( | ||
| path, | ||
| { persistent: true, recursive: !!recursive }, | ||
| 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))); | ||
| callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(path, relativeFileName))); | ||
| }; | ||
| } | ||
| ); | ||
| @@ -511,5 +615,3 @@ namespace ts { | ||
| } | ||
| })(); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so why do we need this Polling one, why can not we just fall back to fs.watchFile?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our polling mechanism is different from
fs.watchFile.fs.watchFilepolls each file independently, so if you got many files to watch, it might increase the work load a lot. In our approach, we poll a fixed number of files (chunkSize) each number, so if thechunkSizeis 30 and you have 90 files to watch, the work load is still the same for each polling, only that changes will be detected after 3 interval time instead of just 1. This way we keep the polling work load consistent