Skip to content

Commit faefcd9

Browse files
kittenmeta-codesync[bot]
authored andcommitted
refactor(metro-file-map): Drop "native find binary" crawler (and forceNodeFilesystemAPI flag) (#1693)
Summary: This is a proposal to drop the native find binary crawler code path entirely. We've recently dropped the native find binary crawler in Expo, which was accidentally activated in some cases. We discovered (which is particularly obvious on large projects/repos and `expo/expo` itself) that it's consistently much slower than the Node FS crawler. To get into the "correct" preference and use the Node crawler, we're currently forcing `useWatchman: null`, which then defaults to use watchman, if it's installed, but falls back to `forceNodeFilesystemAPI: true`. This is outside of what the types specify as valid, but is the cleanest way to disable the native find binary crawling. This crawler basically just adds overhead over the Node.js implementation and is consistently slower, and less efficient, while the Node crawler is pretty optimised already (and has very low overhead once #1677 is merged) The timings of the native find binary crawler in `expo/expo` was also observed to scale with the size of this repo, compared to a minimal reproduction, so the overhead isn't just limited to the child process call and result parsing. Changelog: [Internal] Drop native find binary crawler and remove `forceNodeFilesystemAPI` flag Pull Request resolved: #1693 Test Plan: - Existing tests should pass - Quick generated script to play around with and assess the crawlers' comparative performance: kitten@74dffd4 Reviewed By: huntie Differential Revision: D101985455 Pulled By: robhogan fbshipit-source-id: efe2ab02fc5c0edac4d68f037db57c635fef8e50
1 parent 0e9d7aa commit faefcd9

16 files changed

Lines changed: 28 additions & 374 deletions

File tree

packages/metro-file-map/src/Watcher.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ type WatcherOptions = {
4949
console: Console,
5050
enableSymlinks: boolean,
5151
extensions: ReadonlyArray<string>,
52-
forceNodeFilesystemAPI: boolean,
5352
healthCheckFilePrefix: string,
5453
ignoreForCrawl: (filePath: string) => boolean,
5554
ignorePatternForWatch: RegExp,
@@ -131,7 +130,6 @@ export class Watcher extends EventEmitter {
131130
console: options.console,
132131
includeSymlinks: options.enableSymlinks,
133132
extensions: options.extensions,
134-
forceNodeFilesystemAPI: options.forceNodeFilesystemAPI,
135133
ignore: ignoreForCrawl,
136134
onStatus: status => {
137135
this.emit('status', status);

packages/metro-file-map/src/cache/__tests__/DiskCacheManager-test.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ const buildParameters: BuildParameters = {
4242
cacheBreaker: '',
4343
computeSha1: true,
4444
enableSymlinks: false,
45-
forceNodeFilesystemAPI: true,
4645
ignorePattern: /ignored/,
4746
retainAllFiles: false,
4847
extensions: ['js', 'json'],

packages/metro-file-map/src/crawlers/__tests__/integration-test.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,10 @@ const isWatchmanOnPath = () => {
3434
}
3535
};
3636

37-
const mockUseNativeFind = jest.fn();
38-
jest.mock('../node/hasNativeFindSupport', () => () => mockUseNativeFind());
39-
4037
type Crawler = typeof nodeCrawl | typeof watchmanCrawl;
4138

4239
const CRAWLERS: {[key: string]: ?Crawler} = {
43-
'node-find': opts => {
44-
mockUseNativeFind.mockResolvedValue(true);
45-
return nodeCrawl(opts);
46-
},
4740
'node-recursive': opts => {
48-
mockUseNativeFind.mockResolvedValue(false);
4941
return nodeCrawl(opts);
5042
},
5143
watchman: isWatchmanOnPath() ? watchmanCrawl : null,
@@ -140,7 +132,6 @@ describe.each(Object.keys(CRAWLERS))(
140132
rootDir: FIXTURES_DIR,
141133
abortSignal: null,
142134
computeSha1: false,
143-
forceNodeFilesystemAPI: false,
144135
onStatus: () => {},
145136
});
146137

packages/metro-file-map/src/crawlers/__tests__/node-test.js

Lines changed: 14 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,6 @@ import TreeFS from '../../lib/TreeFS';
1212

1313
jest.useRealTimers();
1414

15-
jest.mock('child_process', () => ({
16-
spawn: jest.fn((cmd, args) => {
17-
let closeCallback;
18-
return {
19-
on: jest.fn().mockImplementation((event, callback) => {
20-
if (event === 'exit') {
21-
callback(mockSpawnExit, null);
22-
}
23-
}),
24-
stdout: {
25-
on: jest.fn().mockImplementation((event, callback) => {
26-
if (event === 'data') {
27-
setTimeout(() => {
28-
callback(mockResponse);
29-
setTimeout(closeCallback, 0);
30-
}, 0);
31-
} else if (event === 'close') {
32-
closeCallback = callback;
33-
}
34-
}),
35-
setEncoding: jest.fn(),
36-
},
37-
};
38-
}),
39-
}));
40-
4115
jest.mock('graceful-fs', () => {
4216
const slash = require('slash');
4317
let mtime = 32;
@@ -126,96 +100,37 @@ const createMap = obj =>
126100
const rootDir = '/project';
127101
const emptyFS = new TreeFS({rootDir, files: new Map()});
128102
const getFS = (files: FileData) => new TreeFS({rootDir, files});
129-
let mockResponse;
130-
let mockSpawnExit;
131103
let nodeCrawl;
132-
let childProcess;
133104

134105
describe('node crawler', () => {
135106
beforeEach(() => {
136107
jest.resetModules();
137-
138-
mockResponse = [
139-
'/project/fruits/pear.js',
140-
'/project/fruits/strawberry.js',
141-
'/project/fruits/tomato.js',
142-
].join('\n');
143-
144-
mockSpawnExit = 0;
145-
});
146-
147-
test('crawls for files based on patterns', async () => {
148-
childProcess = require('child_process');
149-
nodeCrawl = require('../node').default;
150-
151-
mockResponse = [
152-
'/project/fruits/pear.js',
153-
'/project/fruits/strawberry.js',
154-
'/project/fruits/tomato.js',
155-
'/project/vegetables/melon.json',
156-
].join('\n');
157-
158-
const {changedFiles, removedFiles} = await nodeCrawl({
159-
previousState: {fileSystem: emptyFS},
160-
extensions: ['js', 'json'],
161-
ignore: pearMatcher,
162-
rootDir,
163-
roots: ['/project/fruits', '/project/vegtables'],
164-
});
165-
166-
expect(childProcess.spawn).lastCalledWith('find', [
167-
'/project/fruits',
168-
'/project/vegtables',
169-
'(',
170-
'(',
171-
'-type',
172-
'f',
173-
'(',
174-
'-iname',
175-
'*.js',
176-
'-o',
177-
'-iname',
178-
'*.json',
179-
')',
180-
')',
181-
')',
182-
]);
183-
184-
expect(changedFiles).not.toBe(null);
185-
186-
expect(changedFiles).toEqual(
187-
createMap({
188-
'fruits/strawberry.js': [32, 42, 0, null, 0, null],
189-
'fruits/tomato.js': [33, 42, 0, null, 0, null],
190-
'vegetables/melon.json': [34, 42, 0, null, 0, null],
191-
}),
192-
);
193-
194-
expect(removedFiles).toEqual(new Set());
195108
});
196109

197110
test('updates only changed files', async () => {
198111
nodeCrawl = require('../node').default;
199112

200-
// In this test sample, strawberry is changed and tomato is unchanged
201-
const tomato = [33, 42, 1, null, 0, null];
113+
// The readdir mock returns tomato.js (mtime=32) and
114+
// directory/strawberry.js (mtime=33). In this test, tomato is unchanged
115+
// and strawberry is changed.
202116
const files = createMap({
203-
'fruits/strawberry.js': [30, 40, 1, null, 0, null],
204-
'fruits/tomato.js': tomato,
117+
'fruits/directory/strawberry.js': [30, 40, 1, null, 0, null],
118+
'fruits/tomato.js': [32, 42, 1, null, 0, null],
205119
});
206120

207121
const {changedFiles, removedFiles} = await nodeCrawl({
122+
console: global.console,
208123
previousState: {fileSystem: getFS(files)},
209124
extensions: ['js'],
210125
ignore: pearMatcher,
211126
rootDir,
212127
roots: ['/project/fruits'],
213128
});
214129

215-
// Tomato is not included because it is unchanged
130+
// Tomato is not included because its mtime is unchanged
216131
expect(changedFiles).toEqual(
217132
createMap({
218-
'fruits/strawberry.js': [32, 42, 0, null, 0, null],
133+
'fruits/directory/strawberry.js': [33, 42, 0, null, 0, null],
219134
}),
220135
);
221136

@@ -225,117 +140,34 @@ describe('node crawler', () => {
225140
test('returns removed files', async () => {
226141
nodeCrawl = require('../node').default;
227142

228-
// In this test sample, previouslyExisted was present before and will not be
229-
// when crawling this directory.
143+
// In this test sample, previouslyExisted was present before and will not
144+
// be found when crawling this directory.
230145
const files = createMap({
231146
'fruits/previouslyExisted.js': [30, 40, 1, null, 0, null],
232-
'fruits/strawberry.js': [33, 42, 0, null, 0, null],
147+
'fruits/directory/strawberry.js': [33, 42, 0, null, 0, null],
233148
'fruits/tomato.js': [32, 42, 0, null, 0, null],
234149
});
235150

236151
const {changedFiles, removedFiles} = await nodeCrawl({
152+
console: global.console,
237153
previousState: {fileSystem: getFS(files)},
238154
extensions: ['js'],
239155
ignore: pearMatcher,
240156
rootDir,
241157
roots: ['/project/fruits'],
242158
});
243159

244-
expect(changedFiles).toEqual(
245-
createMap({
246-
'fruits/strawberry.js': [32, 42, 0, null, 0, null],
247-
'fruits/tomato.js': [33, 42, 0, null, 0, null],
248-
}),
249-
);
160+
expect(changedFiles).toEqual(new Map());
250161
expect(removedFiles).toEqual(new Set(['fruits/previouslyExisted.js']));
251162
});
252163

253-
test('uses node fs APIs with incompatible find binary', async () => {
254-
mockResponse = '';
255-
mockSpawnExit = 1;
256-
childProcess = require('child_process');
257-
258-
nodeCrawl = require('../node').default;
259-
260-
const {changedFiles, removedFiles} = await nodeCrawl({
261-
previousState: {fileSystem: emptyFS},
262-
extensions: ['js'],
263-
ignore: pearMatcher,
264-
rootDir,
265-
roots: ['/project/fruits'],
266-
});
267-
268-
expect(childProcess.spawn).lastCalledWith(
269-
'find',
270-
['.', '-type', 'f', '(', '-iname', '*.ts', '-o', '-iname', '*.js', ')'],
271-
{cwd: expect.any(String)},
272-
);
273-
expect(changedFiles).toEqual(
274-
createMap({
275-
'fruits/directory/strawberry.js': [33, 42, 0, null, 0, null],
276-
'fruits/tomato.js': [32, 42, 0, null, 0, null],
277-
}),
278-
);
279-
expect(removedFiles).toEqual(new Set());
280-
});
281-
282-
test('uses node fs APIs without find binary', async () => {
283-
childProcess = require('child_process');
284-
childProcess.spawn.mockImplementationOnce(() => {
285-
throw new Error();
286-
});
287-
nodeCrawl = require('../node').default;
288-
289-
const {changedFiles, removedFiles} = await nodeCrawl({
290-
console: global.console,
291-
previousState: {fileSystem: emptyFS},
292-
extensions: ['js'],
293-
ignore: pearMatcher,
294-
rootDir,
295-
roots: ['/project/fruits'],
296-
});
297-
298-
expect(changedFiles).toEqual(
299-
createMap({
300-
'fruits/directory/strawberry.js': [33, 42, 0, null, 0, null],
301-
'fruits/tomato.js': [32, 42, 0, null, 0, null],
302-
}),
303-
);
304-
expect(removedFiles).toEqual(new Set());
305-
});
306-
307-
test('uses node fs APIs if "forceNodeFilesystemAPI" is set to true, regardless of platform', async () => {
308-
childProcess = require('child_process');
309-
nodeCrawl = require('../node').default;
310-
311-
const {changedFiles, removedFiles} = await nodeCrawl({
312-
console: global.console,
313-
previousState: {fileSystem: emptyFS},
314-
extensions: ['js'],
315-
forceNodeFilesystemAPI: true,
316-
ignore: pearMatcher,
317-
rootDir,
318-
roots: ['/project/fruits'],
319-
});
320-
321-
expect(childProcess.spawn).toHaveBeenCalledTimes(0);
322-
expect(changedFiles).toEqual(
323-
createMap({
324-
'fruits/directory/strawberry.js': [33, 42, 0, null, 0, null],
325-
'fruits/tomato.js': [32, 42, 0, null, 0, null],
326-
}),
327-
);
328-
expect(removedFiles).toEqual(new Set());
329-
});
330-
331164
test('completes with empty roots', async () => {
332165
nodeCrawl = require('../node').default;
333166

334167
const {changedFiles, removedFiles} = await nodeCrawl({
335168
console: global.console,
336169
previousState: {fileSystem: emptyFS},
337170
extensions: ['js'],
338-
forceNodeFilesystemAPI: true,
339171
ignore: pearMatcher,
340172
rootDir,
341173
roots: [],
@@ -357,7 +189,6 @@ describe('node crawler', () => {
357189
console: mockConsole,
358190
previousState: {fileSystem: emptyFS},
359191
extensions: ['js'],
360-
forceNodeFilesystemAPI: true,
361192
ignore: pearMatcher,
362193
rootDir,
363194
roots: ['/error'],
@@ -378,7 +209,6 @@ describe('node crawler', () => {
378209
console: global.console,
379210
previousState: {fileSystem: emptyFS},
380211
extensions: ['js'],
381-
forceNodeFilesystemAPI: true,
382212
ignore: pearMatcher,
383213
rootDir,
384214
roots: ['/project/fruits'],
@@ -440,7 +270,7 @@ describe('node crawler', () => {
440270
extensions: ['js', 'json'],
441271
ignore: pearMatcher,
442272
rootDir,
443-
roots: ['/project/fruits', '/project/vegtables'],
273+
roots: ['/project/fruits'],
444274
}),
445275
).rejects.toThrow(err);
446276
});

packages/metro-file-map/src/crawlers/node/hasNativeFindSupport.js

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)