-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
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(watchman): Overhauls how Watchman crawler works fixing Windows #5615
Conversation
…sues **Summary** Watchman crawler was ignoring the `relative_path` field in the response of a `watch-project` call, requiring it to match watch roots with the actual project roots afterwards. Not only this was inefficient, it was also faulty due to the naive `.startsWith()` check in `isDescendant()`. This was causing issues both with Windows file paths (#5553) and after that with case-insensitive file systems where the names from Watchman did not match the casing of the passed roots. This patch replaces all that logic by taking the `relative_path` field into account and does some consolidation along with using `async`/`await` instead of promises. **Test plan** Run the updated test suite on all platforms and make sure it passes. I've also verified this on some internal Windows repos by manually patching the built module and making sure there are no warnings regarding duplicated haste names due to incorrect crawling of project root siblings.
`Watchman error: ${error.message.trim()}. Make sure watchman ` + | ||
`is running for this project. See ${watchmanURL}.`, | ||
); | ||
const stack = error.stack; |
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 guess I can merge stack traces instead since error.stack
should never be empty.
@@ -18,126 +18,127 @@ import H from '../constants'; | |||
const watchmanURL = | |||
'https://facebook.github.io/watchman/docs/troubleshooting.html'; | |||
|
|||
function isDescendant(root: string, child: string): boolean { | |||
return child.startsWith(root); | |||
class WatchmanError extends Error { |
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.
Reason for this overhaul was to make stack traces useful since otherwise I always ended up in the helper function which was far from being helpful.
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.
Not a fan of overriding the stack
field. Can you just mutate the message of the original error instance instead?
const ROOTS = [FRUITS, VEGETABLES]; | ||
const BANANA = path.join(FRUITS, 'banana.js'); | ||
const STRAWBERRY = path.join(FRUITS, 'strawberry.js'); | ||
const KIWI = path.join(FRUITS, 'kiwi.js'); | ||
const TOMATO = path.join(FRUITS, 'tomato.js'); | ||
const MELON = path.join(VEGETABLES, 'melon.json'); | ||
const WATCH_PROJECT_MOCK = { |
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 actually makes the common test scenarios more similar to real-world cases and increases coverage a bit.
reject(WatchmanError(error)); | ||
}); | ||
}); | ||
if (clientError) { |
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.
Hoping this approach works and is OKAY. Otherwise I'd need to wrap the whole thing in a new Promise()
block :(
Codecov Report
@@ Coverage Diff @@
## master #5615 +/- ##
==========================================
+ Coverage 60.65% 60.67% +0.02%
==========================================
Files 214 213 -1
Lines 7314 7316 +2
Branches 4 4
==========================================
+ Hits 4436 4439 +3
+ Misses 2877 2876 -1
Partials 1 1
Continue to review full report at Codecov.
|
expect(client.end).toBeCalled(); | ||
}); | ||
}); | ||
|
||
it('updates the file object when the clock is given', () => { | ||
test('updates the file object when the clock is given', () => { |
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 did you switch to test
instead of 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.
it
is not directly documented and seems like the preference is to use test
instead.
@@ -18,126 +18,127 @@ import H from '../constants'; | |||
const watchmanURL = | |||
'https://facebook.github.io/watchman/docs/troubleshooting.html'; | |||
|
|||
function isDescendant(root: string, child: string): boolean { | |||
return child.startsWith(root); | |||
class WatchmanError extends Error { |
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.
Not a fan of overriding the stack
field. Can you just mutate the message of the original error instance instead?
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.
Okay that sounds reasonnable
2fe9780
to
69607dd
Compare
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.
Thanks for fixing windows woes :-) that’s awesome!
Replacing .then()
with async functions is a good idea, but readability is not as good as it could be.
You also sacrificed the parallelism that we had previously.
I think a short try/catch block with await Promise.all(...)
would be a good improvement in both regards.
const WATCH_PROJECT_MOCK = { | ||
[FRUITS]: { | ||
relative_path: 'fruits', | ||
watch: forcePOSIXPaths(ROOT_MOCK), |
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 are you forcing posix paths here? Wouldn’t this be a result of something like path.join(__dirname, 'ab/cd/ef')
?
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.
Watchman always returns forward slashes and path.join()
uses \
as path separator on Windows. To correctly mimic Watchman responses, we need this. Am I missing something?
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.
no, I just missed that information in this context. All good :-)
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.
path.posix.join
?
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 don't think that replaces existing backslashes.
> path.posix.join('M:\\lol')
'M:\\lol'
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, good point!
Could use slash
, not sure if better, though.
> const slash = require('slash');
undefined
> slash('M:\\lol')
'M:/lol'
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.
Not sure if it is worth the effort or the extra devDependency
only for tests only to support UNC paths but if you think this would be an improvement, I can submit another 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.
Nah, it's fine (we have slash already, fwiw)
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.
we have slash already, fwiw
Didn't see it in jest-haste-map
, that's why :D
try { | ||
const watchmanRoots = new Map(); | ||
for (const root of roots) { | ||
const response = await cmd('watch-project', root); |
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 is effectively serialising watch-project
commands, did you check whether this incurs a speed penalty?
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 didn't notice any slowdowns but you are right. For some reason I thought await
in a for loop would not be blocking. I'll submit a follow-up to make this parallel again. Great catch!
|
||
const clocks = data.clocks; | ||
let files = data.files; | ||
try { |
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 is a gigantic try/catch, and really hard to read. Was it infeasible to break it up into functions?
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.
Well, it is because I tried to mimic the earlier behavior which was not very visible. This is also mostly to wrap the error in that custom text we have. I can try narrowing this down at the risk of not being fully backward compatible regarding error handling and error message customization.
} | ||
let shouldReset = false; | ||
const watchmanFileResults = new Map(); | ||
for (const [root, directoryFilters] of watchmanRoots) { |
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.
not parallelizing means that you have to wait for all watch-project commands before you can send the queries.
: // Otherwise use the `suffix` generator | ||
{expression, fields, suffix: extensions}; | ||
|
||
const response = await cmd('query', root, query); |
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.
... and all queries are serialised, too
Will make things parallel again, thanks for the review! |
This is a follow up to #5615 where it made all async watchman commands serialized when converting them from promises to `async` functions. Existing Watchman crawler tests should pass, maybe slightly faster.
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Summary
Watchman crawler was ignoring the
relative_path
field in the response ofa
watch-project
call, requiring it to match watch roots with the actualproject roots afterward. Not only this was inefficient, it was also faulty
due to the naive
.startsWith()
check inisDescendant()
. This was causingissues both with Windows file paths (#5553) and after that with case-insensitive
file systems where the names from Watchman did not match the casing of the passed
roots.
This patch replaces all that logic by taking the
relative_path
field into accountand does some consolidation along with using
async
/await
instead of promises.Test plan
Run the updated test suite on all platforms and make sure it passes. I've also
verified this on some internal Windows repos by manually patching the built module
and making sure there are no warnings regarding duplicated haste names due to incorrect
crawling of project root siblings.