-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Changes from all commits
e675744
5fa7bec
36cc0e0
d419015
602cde4
178b2da
caa6eb4
631363f
56a7217
8cf1a34
697644c
114d2bd
d22626f
02531af
067573b
dbfe862
File filter
Filter by extension
Conversations
Jump to
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) | ||
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? 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 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.watchFile
polls 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 thechunkSize
is 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