Skip to content

Commit 76c9307

Browse files
robhoganfacebook-github-bot
authored andcommittedSep 26, 2022
Make Watchman existence check lazy and async
Summary: (This is a change I originally made "upstream" to `jest-haste-map` in jestjs/jest#12675) Currently, `metro-file-map` checks for Watchman existence at import time (top level) with a blocking `execSync` call. This has a few downsides: - Blocking at the top level holds up main thread (by 20-200ms) at a time when there's CPU work to be done. - `execSync` spawns a shell, which isn't necessary here - `execFile` is more efficient. - The exec happens totally unnecessarily even if the consumer specifies `useWatchman: false`. This diff extracts this to an async utility function, executed lazily and memoized by the core class. Reviewed By: huntie Differential Revision: D39772133 fbshipit-source-id: 7f7d5acd3506cb0c27a8f67e8813e867f25a83ba
1 parent ab58166 commit 76c9307

File tree

4 files changed

+93
-29
lines changed

4 files changed

+93
-29
lines changed
 

‎packages/metro-file-map/src/__tests__/index-test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ function mockHashContents(contents) {
1717
return crypto.createHash('sha1').update(contents).digest('hex');
1818
}
1919

20-
jest.mock('child_process', () => ({
21-
// If this does not throw, we'll use the (mocked) watchman crawler
22-
execSync() {},
20+
jest.mock('../lib/canUseWatchman', () => ({
21+
__esModule: true,
22+
default: async () => true,
2323
}));
2424

2525
jest.mock('jest-worker', () => ({

‎packages/metro-file-map/src/index.js

+25-26
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {DiskCacheManager} from './cache/DiskCacheManager';
3636
import H from './constants';
3737
import getMockName from './getMockName';
3838
import HasteFS from './HasteFS';
39+
import canUseWatchman from './lib/canUseWatchman';
3940
import deepCloneInternalData from './lib/deepCloneInternalData';
4041
import * as fastPath from './lib/fast_path';
4142
import getPlatformExtension from './lib/getPlatformExtension';
@@ -48,7 +49,6 @@ import NodeWatcher from './watchers/NodeWatcher';
4849
// $FlowFixMe[untyped-import] - WatchmanWatcher
4950
import WatchmanWatcher from './watchers/WatchmanWatcher';
5051
import {getSha1, worker} from './worker';
51-
import {execSync} from 'child_process';
5252
import EventEmitter from 'events';
5353
import invariant from 'invariant';
5454
// $FlowFixMe[untyped-import] - jest-regex-util
@@ -145,14 +145,6 @@ const VCS_DIRECTORIES = ['.git', '.hg']
145145
.map(vcs => escapePathForRegex(path.sep + vcs + path.sep))
146146
.join('|');
147147

148-
const canUseWatchman = ((): boolean => {
149-
try {
150-
execSync('watchman --version', {stdio: ['ignore']});
151-
return true;
152-
} catch {}
153-
return false;
154-
})();
155-
156148
/**
157149
* HasteMap is a JavaScript implementation of Facebook's haste module system.
158150
*
@@ -233,6 +225,7 @@ const canUseWatchman = ((): boolean => {
233225
export default class HasteMap extends EventEmitter {
234226
_buildPromise: ?Promise<InternalDataObject>;
235227
_cachePath: Path;
228+
_canUseWatchmanPromise: Promise<boolean>;
236229
_changeInterval: ?IntervalID;
237230
_console: Console;
238231
_options: InternalOptions;
@@ -787,7 +780,7 @@ export default class HasteMap extends EventEmitter {
787780
return this._worker;
788781
}
789782

790-
_crawl(hasteMap: InternalData): Promise<?(
783+
async _crawl(hasteMap: InternalData): Promise<?(
791784
| Promise<{
792785
changedFiles?: FileData,
793786
hasteMap: InternalData,
@@ -798,8 +791,7 @@ export default class HasteMap extends EventEmitter {
798791
this._options.perfLogger?.point('crawl_start');
799792
const options = this._options;
800793
const ignore = (filePath: string) => this._ignore(filePath);
801-
const crawl =
802-
canUseWatchman && this._options.useWatchman ? watchmanCrawl : nodeCrawl;
794+
const crawl = (await this._shouldUseWatchman()) ? watchmanCrawl : nodeCrawl;
803795
const crawlerOptions: CrawlerOptions = {
804796
abortSignal: this._crawlerAbortController.signal,
805797
computeSha1: options.computeSha1,
@@ -851,11 +843,11 @@ export default class HasteMap extends EventEmitter {
851843
/**
852844
* Watch mode
853845
*/
854-
_watch(hasteMap: InternalData): Promise<void> {
846+
async _watch(hasteMap: InternalData): Promise<void> {
855847
this._options.perfLogger?.point('watch_start');
856848
if (!this._options.watch) {
857849
this._options.perfLogger?.point('watch_end');
858-
return Promise.resolve();
850+
return;
859851
}
860852

861853
// In watch mode, we'll only warn about module collisions and we'll retain
@@ -864,12 +856,11 @@ export default class HasteMap extends EventEmitter {
864856
this._options.retainAllFiles = true;
865857

866858
// WatchmanWatcher > FSEventsWatcher > sane.NodeWatcher
867-
const WatcherImpl =
868-
canUseWatchman && this._options.useWatchman
869-
? WatchmanWatcher
870-
: FSEventsWatcher.isSupported()
871-
? FSEventsWatcher
872-
: NodeWatcher;
859+
const WatcherImpl = (await this._shouldUseWatchman())
860+
? WatchmanWatcher
861+
: FSEventsWatcher.isSupported()
862+
? FSEventsWatcher
863+
: NodeWatcher;
873864

874865
const extensions = this._options.extensions;
875866
const ignorePattern = this._options.ignorePattern;
@@ -1067,12 +1058,10 @@ export default class HasteMap extends EventEmitter {
10671058

10681059
this._changeInterval = setInterval(emitChange, CHANGE_INTERVAL);
10691060

1070-
return Promise.all(this._options.roots.map(createWatcher)).then(
1071-
watchers => {
1072-
this._watchers = watchers;
1073-
this._options.perfLogger?.point('watch_end');
1074-
},
1075-
);
1061+
await Promise.all(this._options.roots.map(createWatcher)).then(watchers => {
1062+
this._watchers = watchers;
1063+
this._options.perfLogger?.point('watch_end');
1064+
});
10761065
}
10771066

10781067
/**
@@ -1163,6 +1152,16 @@ export default class HasteMap extends EventEmitter {
11631152
);
11641153
}
11651154

1155+
async _shouldUseWatchman(): Promise<boolean> {
1156+
if (!this._options.useWatchman) {
1157+
return false;
1158+
}
1159+
if (!this._canUseWatchmanPromise) {
1160+
this._canUseWatchmanPromise = canUseWatchman();
1161+
}
1162+
return this._canUseWatchmanPromise;
1163+
}
1164+
11661165
_createEmptyMap(): InternalData {
11671166
return {
11681167
clocks: new Map(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow strict
9+
*/
10+
11+
import canUseWatchman from '../canUseWatchman';
12+
13+
const mockExecFile = jest.fn();
14+
jest.mock('child_process', () => ({
15+
execFile: (...args) => mockExecFile(...args),
16+
}));
17+
18+
describe('canUseWatchman', () => {
19+
beforeEach(() => {
20+
jest.clearAllMocks();
21+
});
22+
23+
it('executes watchman --version and returns true on success', async () => {
24+
mockExecFile.mockImplementation((file, args, cb) => {
25+
expect(file).toBe('watchman');
26+
expect(args).toStrictEqual(['--version']);
27+
cb(null, {stdout: 'v123'});
28+
});
29+
expect(await canUseWatchman()).toBe(true);
30+
expect(mockExecFile).toHaveBeenCalledWith(
31+
'watchman',
32+
['--version'],
33+
expect.any(Function),
34+
);
35+
});
36+
37+
it('returns false when execFile fails', async () => {
38+
mockExecFile.mockImplementation((file, args, cb) => {
39+
cb(new Error());
40+
});
41+
expect(await canUseWatchman()).toBe(false);
42+
expect(mockExecFile).toHaveBeenCalled();
43+
});
44+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow strict
9+
*/
10+
11+
import {execFile} from 'child_process';
12+
import {promisify} from 'util';
13+
14+
export default async function canUseWatchman(): Promise<boolean> {
15+
try {
16+
await promisify(execFile)('watchman', ['--version']);
17+
return true;
18+
} catch {
19+
return false;
20+
}
21+
}

0 commit comments

Comments
 (0)
Please sign in to comment.