-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Fix too many watcher instances issue #6026
Conversation
@@ -293,6 +293,50 @@ namespace ts { | |||
removeFile: removeFile | |||
}; | |||
} | |||
|
|||
function createWatchedFileSet() { | |||
let watchedDirectories: { [path: string]: FileWatcher } = {}; |
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.
watchedFiles
and watchedDirectories
do not take case sensitivity\slashes into account.
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.
use FileMap instead.
const file: WatchedFile = { fileName, callback }; | ||
const watchedPaths = Object.keys(watchedDirectories); | ||
// Try to find parent paths that are already watched. If found, don't add directory watchers | ||
const watchedParentPaths = watchedPaths.filter(path => fileName.indexOf(path) === 0); |
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.
Use ts.filter
Btw I don't really have concerns about this change it is totally the right way of doing it, but I am a bit nervous because VS Code is shipping next week with TS 1.7 and I want to make sure we are not introducing a regression late in the game. Back to polling for 1.7.x and then this right fix for 1.8 would be nice. |
// If adding new watchers, try to find children paths that are already watched. If found, close them. | ||
if (watchedParentPaths.length === 0) { | ||
const pathToWatch = ts.getDirectoryPath(fileName); | ||
for (const watchedPath in watchedDirectories) { |
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.
this part is interesting.correct me please if I'm wrong, here you are saying: "As I see nobody is watching folder 'pathToWatch' so I'm going to install watcher for it. However it is possible that somebody is already watching 'pathToWatch/'. If yes - I'm going to shutdown this watcher since watcher for 'pathToWatch' should get the same notifications".
If the answer is 'yes' then I'm curious how it works since by default fs.watch
is not recursive so if initially you had watcher for '/a/b/c/d' and then you install one for '/a/b' then after closing watcher for '/a/b/c/d' you'll receive no notifications about changes in this file
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.
You are right, I was trying to use the recursive fs.watch
but forgot to specify the {recursive: true}
option.
Though on a second thought, this logic may be too complex when I need to close a unused directory watcher later. For non-recursive watchers I can just check if the direct parent folder has any files in the watching list, while for recursive watchers it's more complex. It might make more sense to check to the non-recursive version.
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.
I would think twice before completely switching to recursive watchers or at least test it in different scenarios. Effectively it means that if user adds some file that is close to the root of the disk then watcher will receive tons of notifications about files he doesn't care which might (and will) affect performance
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.
Good point, noted. Thanks!
@bpasero @vladima @DanielRosenwasser for the approaching release 1.7.5 I sent another PR (#6066) that rolls back to polling watchers. This PR will be updated according to code reviews and tested after the release to reduce risks. |
👍 |
delete watchedDirectories[watchedPath]; | ||
} | ||
} | ||
watchedDirectories[pathToWatch] = _fs.watch( |
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.
Isn't it sloppy in regards of hasOwnProperty
, toString
that may exist as files and clash with members on watchDirectories
object?
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.
true and this is one more argument to switch to FileMap
which handles this case correctly
@vladima @DanielRosenwasser @mhegazy Updated this PR using FileMap, please let me know if you have further comments. Thanks! |
|
||
function addFile(fileName: string, callback: (fileName: string, removed?: boolean) => void): WatchedFile { | ||
const path = toPath(fileName, currentDirectory, getCanonicalPath); | ||
const parentDirPath = toPath(ts.getDirectoryPath(fileName), currentDirectory, getCanonicalPath); |
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.
Shouldn't you getDirectoryPath on the path you just created in the line above, rather than the fileName which may not be a (normalized) full path?
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.
toPath
should take care of it, however the valid point here is that separators in fileName
might still vary and getDirectoryPath
expects them to be only forward slashes. I agree with @billti that it will be simplier to just call const parentDirPath = getDirectoryPath(path)
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.
also given that getDirectoryPath
does not reshape its argument so Path
should stay Path
I think we can add an overload to getDirectoryPath
to avoid redundant conversions: smth like this:
export function getDirectoryPath(path: Path): Path;
export function getDirectoryPath(path: string): string;
export function getDirectoryPath(path: string): any {
return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(directorySeparator)));
}
In this case you can just can call getDirectoryPath(path)
and immediately get parentDirPath
return { | ||
close: () => watchedFileSet.removeFile(watchedFile) | ||
close: () => watchSet.removeFile(watchedFile) |
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.
"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 comment
The reason will be displayed to describe this comment to others. Learn more.
I tested before that if not supported then the recursive: true
would simply be ignored. The recursive directory watcher is used with tsconfig.json
without files array, so that adding/removing files in the background would be detected. The plan for this feature at the time was to support Node v4 or later only, so I didn't differentiate with the cases with older Node version.
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.
I'm not sure i follow about the plan to use the feature
. watchDirectory
is a public API (exported on interface System
). If when called, it's not going to be able to do what the API claims it does (via the recursive
parameter`), then it should error.
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.
function createWatchedFileSet() { | ||
const dirWatchers = createFileMap<DirWatcher>(); | ||
const recursiveDirWatchers = createFileMap<DirWatcher>(); | ||
const fileWatcherCallbacks = createFileMap<FileWatcherCallback>(); |
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.
as I see now we store one callback per file, a question: will this work?
const fileName = "/some/file";
ts.sys.watchFile(fileName, () => console.log(1));
ts.sys.watchFile(fileName, () => console.log(2));
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.
Ah it wouldn't work now. Maybe a file needs a list of callbacks. Will update later.
@@ -1,6 +1,9 @@ | |||
/// <reference path="core.ts"/> | |||
|
|||
namespace ts { | |||
export type FileWatcherCallback = (path: string, removed?: boolean) => void; | |||
export type DirWatcherCallback = (path: string) => void; |
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.
spell out "Dir" into "DirectoryWatcherCallback"
…omplexity, because reference counting is a lot easier in this case
After some offline discussion with @mhegazy, we decided that it would be easier not to mix the recursive watchers and non-recursive watchers together, because the logic for reference counting would be quite complex in that case. Therefore, I rolled back to the implementation with separate directory watchers and file watchers, which might have duplicated watchers for the same folder in some case, but the compatibility across platforms would be better, and the code would be more maintainable. |
const watchedDir = findWatchedDirForFile(filePath); | ||
if (watchedDir) { | ||
reduceDirWatcherRefCount(watchedDir); | ||
} |
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.
consider replacing these four lines with:
removeDirWatcher(getDirectoryPath(filePath));
👍 |
Fix too many watcher instances issue
Fix #6016.
Instead of creating a watcher for every single file, I create watchers only for the directory that the files are in, and matches the file change event with a list of registered files.