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(watchman): Overhauls how Watchman crawler works fixing Windows #5615

Merged
merged 2 commits into from
Feb 19, 2018

Conversation

BYK
Copy link
Contributor

@BYK BYK commented Feb 19, 2018

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

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

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.

Copy link
Contributor

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

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

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-io
Copy link

codecov-io commented Feb 19, 2018

Codecov Report

Merging #5615 into master will increase coverage by 0.02%.
The diff coverage is 91.37%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
packages/jest-haste-map/src/crawlers/watchman.js 91.66% <91.37%> (+2.57%) ⬆️
packages/jest-haste-map/src/worker.js 96% <0%> (ø) ⬆️
packages/jest-haste-map/src/blacklist.js

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update bea889c...69607dd. Read the comment docs.

expect(client.end).toBeCalled();
});
});

it('updates the file object when the clock is given', () => {
test('updates the file object when the clock is given', () => {
Copy link
Contributor

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?

Copy link
Contributor Author

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

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?

Copy link
Contributor

@jeanlauliac jeanlauliac left a 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

@BYK BYK closed this Feb 19, 2018
@BYK BYK reopened this Feb 19, 2018
Copy link
Contributor

@davidaurelio davidaurelio left a 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),
Copy link
Contributor

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')?

Copy link
Contributor Author

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?

Copy link
Contributor

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

Copy link
Member

Choose a reason for hiding this comment

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

path.posix.join?

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 don't think that replaces existing backslashes.

> path.posix.join('M:\\lol')
'M:\\lol'

Copy link
Member

@SimenB SimenB Feb 23, 2018

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'

Copy link
Contributor Author

@BYK BYK Feb 23, 2018

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?

Copy link
Member

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)

Copy link
Contributor Author

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

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?

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

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?

Copy link
Contributor Author

@BYK BYK Feb 22, 2018

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

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

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

@BYK
Copy link
Contributor Author

BYK commented Feb 22, 2018

Will make things parallel again, thanks for the review!

BYK added a commit that referenced this pull request Feb 22, 2018
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.
@github-actions
Copy link

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.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 12, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants