-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
fix: watch mode using chokidar v4 #5379
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
Conversation
Glob paths are no longer supported by chokidar starting at v4. This update works around this by resolving `watchFiles` and `watchIgnore` to valid paths and creating a list of matchers to determine if the files should be allowed or ignored through the chokidar `ignored` match function. This commit reverts changes from 8af0f1a so that the watched file changes are not fixed to the current directory. Additional note: when a `watchFile` path is removed while chokidar is watching it, recreating the `watchFile` path does not trigger events from chokidar to rerun the tests.
|
|
|
Might need to rethink this a bit. Currently, running into some issues:
|
This update fixes mochajs#5355 and refactors the previous watch mode fix attempt. Instead of only filtering the paths in the chokidar `ignored` match function, the chokidar `all` event handler now has a guard to ensure that only file paths that match the allowed patterns from `watchFiles` would trigger test reruns. Doing this solves the issue where creating/deleting directories trigger test reruns despite the watched path being a file (e.g., `**/*.xyz`) as the allowed patterns would not match with just the directory paths.
JoshuaKGoldberg
left a comment
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.
The changes seem reasonable to me at first, but they're not covered by tests at all. I think we'd want to have at least one integration test covering the behavior.
Maybe:
- Programmatically creates a big enough test case project to crash chokidar v4's watch mode
- Starts Mocha in watch mode
- Waits until it logs the expected message (and asserts it doesn't log an
EMFILEor anything like that) - Closes Mocha
WDYT?
|
Hi @JoshuaKGoldberg, thanks for the response. Actually, the changes are really only meant for handling glob paths for To recap, since chokidar v4 dropped support for globs, this caused issues where files are not correctly being watched. Before 8af0f1a, the
These are the issues that these changes aim to address which are mostly fixes for the watched files, and I think that the While the Please let me know what you think. Thanks! |
|
The assorted recent watch mode issues for reference:
If it's easiest to fix >=1 of these all together in one PR (as in, the changed areas of code intersect a lot). I'd say go for it. 🙂 |
|
The list of issues seem about right to me, where the first 2 are more or less about the watch mode taking some time to start than before and the last being the main issue. I think the changes here now should already address these issues, unless I missed something. Let me know should we need to update a few things or if there's anything to do on my end, thanks! |
|
Great! Can you add test(s) to verify the changed behavior? #5379 (review) |
- Test for watched files from outside the current working directory - Test reruns with a thousand watched files
|
Added the following tests:
Though, I'm not too confident with the approach for the last added test. Let me know if we should change how we go about it, thanks. Edit: Well, the last test seems to have failed since it kind of relied on the machine's performance. I'll work on a fix for it and limit the change to a single file. |
|
Next step here will be ensuring the test failures are nothing new (ref #5361), then I can review the code and if I approve I'll ping Josh to do the same :D |
The test is dependent on how fast the machine could create 1000 watched files, otherwise the test could fail due to a timeout error. Reducing the number of watched files would make the test pass, but doing so would make it more or less similar to already existing tests. At that point, the test does not really test the limits of possibly huge projects with thousands of watched files, so it has been removed instead.
|
Looks like the test for 1000 watched files is failing due to a timeout error (tests have a timeout of 10000). I tried reducing it to 100 files and while it seems to pass, it does not really test the limits of possibly huge projects with thousands of watched files, so it has been removed instead. Though, the test for watched files outside the current working directory is kept as it is one of the main behaviors that we probably would want to keep in check. |
|
Going to stress test this one as we saw failures in 5 out of 30 runs on main branch with our
Next steps:
|
jedwards1211
left a comment
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 think we should reduce unnecessary CPU and Memory usage in a few spots since the number of files X number of watch paths may get large.
lib/cli/watch-run.js
Outdated
| */ | ||
| function cache(map, key, value) { | ||
| if (map.size >= MAX_CACHE_SIZE) { | ||
| map.clear(); |
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.
Why clear the map instead of just removing the first key from the keys() iterator here?
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. The change would be something like:
// also make sure to update the doc comment for this function
// ...
if (map.size >= MAX_CACHE_SIZE && !map.has(key)) {
map.delete(map.keys().next().value);
}
// ...Where !map.has(key) makes sure to skip the delete if the key already exists (which wouldn't increase the map size).
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 had similar thoughts but didn't want to prematurely optimize :) if this has the same perf I'm fine with it, still very clean code :)
lib/cli/watch-run.js
Outdated
| * @type {typeof Pattern[]} The `Pattern` class is not exported by the `glob` package. | ||
| * Ref [link](../../node_modules/glob/dist/commonjs/pattern.d.ts) | ||
| */ | ||
| const patterns = globPaths.reduce((acc, globPath) => { |
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 think there were comments about this, but .flatMap is a better way to do this
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.
n may not be large here, but reduce-concat is O(n^2) whereas flatmap is just O(n)
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 was a for loop previously (5514fe2, iterating globPaths). We probably don't even need the array even if it's only a small performance cost for a slight readability improvement. Still, I'd prefer the for loop over an extra array (with .flatMap()/.reduce()) despite the additional nesting. Thoughts on reverting this change or going for flatMap @mark-wiemer?
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.
If flatMap works without introducing new dependencies let's go for it. If it's a whole new dependency just for this optimization, I'd have to see some numbers when this would have any significant impact. Given it's a simple iteration over patterns (not even matched files) I can't see it saving more than 1ms per run. That said, I do like clean code, and if flatMap is simpler / easier to maintain I'm happy to consider it. I'd just have to see them side-by-side :)
lib/cli/watch-run.js
Outdated
| } | ||
| } | ||
|
|
||
| return Array.from(pattern.globs).some(globPath => |
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 code will be run on potentially lots of files, it would be better to just iterate the set looking for a glob that matches instead of creating an array every time we test a 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.
Agreed. We can revert this back to the previous for loop (caca650), but I'll let @mark-wiemer decide what we'd want to do.
Thanks for the feedback @jedwards1211.
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 yeah I think my change here hurts performance, and since this is the number of files it could be significant. Feel free to revert if it improves perf and is still readable, maybe adding a comment like "use raw data to save time instead of copying into array"
|
I think we could afford to make those match caches bigger, too, like up to 10000 |
I don't see how reduce-concat is so much more readable than a nested for loop (or especially flatMap?) to justify its O(n^2) cost (where n is the size of the final array). It's a good pattern in FP languages based on linked lists where it's O(n); not such a good pattern in languages like JS that aren't based on linked lists. Array.from(set).some(...) is more readable but it creates a array from the set for every call, which is a needless waste for a slight increase in readability (a for loop isn't that much harder to read). |
Yeah, I may have been over-valuing readability and prioritizing my preferences as "objective." Happy to revert some of my changes, see other comments for details :) |
- Use array `flatMap` instead of `reduce` for glob path patterns - Revert glob path matching check to a `for` loop (commit caca650) - Update `MAX_CACHE_SIZE` from 1,000 to 10,000 - Update map cache to only delete the first key whenever the limit is reached - Update debug logging and formatting
|
Pushed up changes to address the recent comments. Let me know if I missed anything, thanks. Some notes:
|
| minimatch(filePath, globPath, {dot: true, windowsPathsNoEscape: true}) | ||
| ); | ||
| // loop through the set of glob paths instead of converting it into an array | ||
| for (const globPath of pattern.globs) { |
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.
| for (const globPath of pattern.globs) { | |
| // provides minor perf increase | |
| for (const globPath of pattern.globs) { |
|
any progress on getting this merged? for the last 3 months i've been using mocha 11.1.0 just so i can use the watch feature... i know youre risking a breaking change to a core feature, but it's already broken... |
I'll work with @JoshuaKGoldberg on this one. I would like to add it to v12 personally but I haven't looked at this in the past couple weeks and don't have a ton of expertise with glob matching, so I'm not confident I'd be able to identify subtle bugs here. |
|
Hey guys, any progress here? This watch is super important to my workflow, without it, life is so much slower! The solution doesn't have to work perfectly. You'll have a chance to iron out the kinks if bug reports come in, but right now the watch mode is just a dead feature on most projects. |
|
Good timing: Mark and I are now both more free than we've been in a while, and focusing heavily on getting Mocha 12 through the door (#5357). It'd be nice to have this important fix in 11 - so it's top of my queue now. |
JoshuaKGoldberg
left a comment
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.
OK! I tested this to confirm it fixes the linked issues' reproductions:
- #5355: https://github.com/zsalzbank/mocha-watch-mcve
- #5374: https://github.com/nescalante/mocha-EMFILE
There is a good bit of added logic in watch-run.js, and eventually we could (should?) probably extract it out to some shared helper, or even standalone package. But for now I think this is more than good enough. The comments are very helpful in understanding it.
Thanks for your hard work and patience with our reviews on this @Arnesfield! 🤎
| * @typedef {import('chokidar').FSWatcher} FSWatcher | ||
| * @typedef {import('glob').Glob['patterns'][number]} Pattern | ||
| * The `Pattern` class is not exported by the `glob` package. | ||
| * Ref [link](../../node_modules/glob/dist/commonjs/pattern.d.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.
Could we file an issue on glob to export it?
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.
They probably consider this internal API. I think we should ask them to export a public API like Glob.test(file: string): boolean that we can use instead of copying so much of its internal logic into createPathFilter here.
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 filed this issue, let's see what they say: isaacs/node-glob#632
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.
The comment here talks about exporting the Pattern class, but the issue talks about exporting a glob.test function, I think there was a disconnect here. Regardless, not a blocker to this PR :)
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.
Yes, I was saying above, the Pattern class seems internal (I doubt they would be willing to export it). You're welcome to ask them if they'll export it though
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 posted a comment on the issue (isaacs/node-glob#632 (comment)) and it looks like the logic that uses the Pattern class for path parsing could be improved by using minimatch directly. Not sure when I'll have time to refactor it (I'll probably create a separate issue and PR), but I think for now the current implementation should still work as intended.
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.
One thing to consider: the logic copied from glob for dealing with patterns is a bit nontrivial, and glob has hopefully tested all possible cases that can happen with it, but we probably aren't testing them all here.
This is part of why I think the logic deserves to live in a reusable package.
If I have time soon I may make a proper package for working with multiple patterns, that puts them into a tree for more efficient matching and gives us a getTopDirs method, and has thorough unit tests.
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 made some changes to the path parsing logic to use Minimatch instead of Glob Pattern and while the tests seem to pass (after handling only a few of the differences in behavior), I came to the conclusion that using Glob Patterns is better as it produces more accurate path segments, especially for Windows paths.
It adds slashes for drives (e.g., C: to C:/). Getting the path segments of C:\Users\user for example:
- Minimatch path segments:
[ 'C:', 'Users', 'user' ] - Glob
Patternpath segments:[ 'C:/', 'Users', 'user' ]
Small note: Glob uses slashes
/even for Windows.
path.resolve() results:
// via NodeJS Windows
// using path segments from minimatch
console.log(path.resolve("C:\\a\\b\\c", "C:", "Users", "user"));
// output: C:\a\b\c\Users\user
// using path segments from glob pattern (more accurate)
console.log(path.resolve("C:\\a\\b\\c", "C:/", "Users", "user"));
// output: C:\Users\userUNC paths also produce different path segments. Using \\host\share\a\b\c for example:
- Minimatch path segments:
[ '', '', 'host', 'share', 'a', 'b', 'c' ] - Glob
Patternpath segments:[ '//host/share/', 'a', 'b', 'c' ]
Glob Pattern treats //host/share/ as one segment. path.resolve() seems to handle this case as well:
// via NodeJS Windows
// using path segments from minimatch
console.log(path.resolve('C:', '', '', 'host', 'share', 'a', 'b', 'c'));
// output: C:\host\share\a\b\c
// using path segments from glob pattern (more accurate)
console.log(path.resolve('C:', '//host/share/', 'a', 'b', 'c'));
// output: \\host\share\a\b\cRefactoring to Minimatch as I previously suggested isn't the right call. Using UNC paths as watch paths probably won't make sense, but I guess the option is there so long as Chokidar is capable of watching those paths.
There may be more cases that Glob Pattern handles that I haven't looked into. In my opinion, we can keep the current way Pattern is referenced. It's hard to say if Pattern needs to be exported from the Glob package since we really only need a reference to the type and not the class itself just for explicitly typing a few variables.
Co-authored-by: Josh Goldberg ✨ <[email protected]>
|
@JoshuaKGoldberg still LGTM, I fixed the lint in #5475 and we'll work on test failures in #5361 of course. Ready to merge when you are :) just not sure if we're including this in v11 or if it'll be the first PR of v12 |
|
Let's ship now so v11 users can benefit from the important bug fix. 🚢 ! |
Glob paths are no longer supported by chokidar starting at v4.
This update works around this by resolving
watchFilesandwatchIgnoreto valid paths and creating a list of matchers to determine if the files should be allowed or ignored through the chokidarignoredmatch function.This commit reverts changes from 8af0f1a so that the watched file changes are not fixed to the current directory.
Additional note: when a
watchFilepath is removed while chokidar is watching it, recreating thewatchFilepath does not trigger events from chokidar to rerun the tests.PR Checklist
status: accepting prsOverview