Skip to content
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

Merged
merged 16 commits into from
Jan 14, 2016
Merged

Conversation

zhengbli
Copy link
Contributor

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.

@@ -293,6 +293,50 @@ namespace ts {
removeFile: removeFile
};
}

function createWatchedFileSet() {
let watchedDirectories: { [path: string]: FileWatcher } = {};
Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use FileMap instead.

@zhengbli
Copy link
Contributor Author

@mhegazy @vladima due to the approaching release deadline and concerns from @bpasero, I'll revert the polling watchers for all cases and send another PR to address the CR feedbacks after the release.

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ts.filter

@bpasero
Copy link
Member

bpasero commented Dec 11, 2015

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) {
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, noted. Thanks!

@zhengbli
Copy link
Contributor Author

@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.

@bpasero
Copy link
Member

bpasero commented Dec 11, 2015

👍

delete watchedDirectories[watchedPath];
}
}
watchedDirectories[pathToWatch] = _fs.watch(
Copy link
Contributor

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?

Copy link
Contributor

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

@zhengbli
Copy link
Contributor Author

@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);
Copy link
Member

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?

Copy link
Contributor

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)

Copy link
Contributor

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)
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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>();
Copy link
Contributor

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));

Copy link
Contributor Author

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;
Copy link
Contributor

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"

@zhengbli
Copy link
Contributor Author

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);
}
Copy link
Contributor

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));

@mhegazy
Copy link
Contributor

mhegazy commented Jan 14, 2016

👍

zhengbli added a commit that referenced this pull request Jan 14, 2016
Fix too many watcher instances issue
@zhengbli zhengbli merged commit d5d7a2a into microsoft:master Jan 14, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants