-
-
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
[jest-haste-map] reduce the number of lstat calls in node crawler #9514
Changes from all commits
81c78a9
5c14c7d
819118c
4f7109a
1dd833d
6855233
f077a64
758c76a
9f0aae7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,8 @@ jest.mock('child_process', () => ({ | |
}), | ||
})); | ||
|
||
let mockHasReaddirWithFileTypesSupport = false; | ||
|
||
jest.mock('fs', () => { | ||
let mtime = 32; | ||
const size = 42; | ||
|
@@ -42,7 +44,7 @@ jest.mock('fs', () => { | |
return path.endsWith('/directory'); | ||
}, | ||
isSymbolicLink() { | ||
return false; | ||
return path.endsWith('symlink'); | ||
}, | ||
mtime: { | ||
getTime() { | ||
|
@@ -56,13 +58,66 @@ jest.mock('fs', () => { | |
}; | ||
return { | ||
lstat: jest.fn(stat), | ||
readdir: jest.fn((dir, callback) => { | ||
if (dir === '/project/fruits') { | ||
setTimeout(() => callback(null, ['directory', 'tomato.js']), 0); | ||
} else if (dir === '/project/fruits/directory') { | ||
setTimeout(() => callback(null, ['strawberry.js']), 0); | ||
} else if (dir == '/error') { | ||
setTimeout(() => callback({code: 'ENOTDIR'}, undefined), 0); | ||
readdir: jest.fn((dir, options, callback) => { | ||
// readdir has an optional `options` arg that's in the middle of the args list. | ||
// we always provide it in practice, but let's try to handle the case where it's not | ||
// provided too | ||
if (typeof callback === 'undefined') { | ||
if (typeof options === 'function') { | ||
callback = options; | ||
} | ||
throw new Error('readdir: callback is not a function!'); | ||
} | ||
|
||
if (mockHasReaddirWithFileTypesSupport) { | ||
if (dir === '/project/fruits') { | ||
setTimeout( | ||
() => | ||
callback(null, [ | ||
{ | ||
isDirectory: () => true, | ||
isSymbolicLink: () => false, | ||
name: 'directory', | ||
}, | ||
{ | ||
isDirectory: () => false, | ||
isSymbolicLink: () => false, | ||
name: 'tomato.js', | ||
}, | ||
{ | ||
isDirectory: () => false, | ||
isSymbolicLink: () => true, | ||
name: 'symlink', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also noticed that there weren't any existing tests for symlinks that I could find, so I added one here. It doesn't change the result of any existing tests (since we ignore symlinks) but will affect the lstat call-counting tests I added. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. very nice, thank you! we'll hopefully be landing some symlink support soonish, so these tests should come in handy |
||
}, | ||
]), | ||
0, | ||
); | ||
} else if (dir === '/project/fruits/directory') { | ||
setTimeout( | ||
() => | ||
callback(null, [ | ||
{ | ||
isDirectory: () => false, | ||
isSymbolicLink: () => false, | ||
name: 'strawberry.js', | ||
}, | ||
]), | ||
0, | ||
); | ||
} else if (dir == '/error') { | ||
setTimeout(() => callback({code: 'ENOTDIR'}, undefined), 0); | ||
} | ||
} else { | ||
if (dir === '/project/fruits') { | ||
setTimeout( | ||
() => callback(null, ['directory', 'tomato.js', 'symlink']), | ||
0, | ||
); | ||
} else if (dir === '/project/fruits/directory') { | ||
setTimeout(() => callback(null, ['strawberry.js']), 0); | ||
} else if (dir == '/error') { | ||
setTimeout(() => callback({code: 'ENOTDIR'}, undefined), 0); | ||
} | ||
} | ||
}), | ||
stat: jest.fn(stat), | ||
|
@@ -296,4 +351,65 @@ describe('node crawler', () => { | |
expect(removedFiles).toEqual(new Map()); | ||
}); | ||
}); | ||
|
||
describe('readdir withFileTypes support', () => { | ||
it('calls lstat for directories and symlinks if readdir withFileTypes is not supported', () => { | ||
nodeCrawl = require('../node'); | ||
const fs = require('fs'); | ||
|
||
const files = new Map(); | ||
return nodeCrawl({ | ||
data: {files}, | ||
extensions: ['js'], | ||
forceNodeFilesystemAPI: true, | ||
ignore: pearMatcher, | ||
rootDir, | ||
roots: ['/project/fruits'], | ||
}).then(({hasteMap, removedFiles}) => { | ||
expect(hasteMap.files).toEqual( | ||
createMap({ | ||
'fruits/directory/strawberry.js': ['', 33, 42, 0, '', null], | ||
'fruits/tomato.js': ['', 32, 42, 0, '', null], | ||
}), | ||
); | ||
expect(removedFiles).toEqual(new Map()); | ||
// once for /project/fruits, once for /project/fruits/directory | ||
expect(fs.readdir).toHaveBeenCalledTimes(2); | ||
// once for each of: | ||
// 1. /project/fruits/directory | ||
// 2. /project/fruits/directory/strawberry.js | ||
// 3. /project/fruits/tomato.js | ||
// 4. /project/fruits/symlink | ||
// (we never call lstat on the root /project/fruits, since we know it's a directory) | ||
expect(fs.lstat).toHaveBeenCalledTimes(4); | ||
}); | ||
}); | ||
it('avoids calling lstat for directories and symlinks if readdir withFileTypes is supported', () => { | ||
mockHasReaddirWithFileTypesSupport = true; | ||
nodeCrawl = require('../node'); | ||
const fs = require('fs'); | ||
|
||
const files = new Map(); | ||
return nodeCrawl({ | ||
data: {files}, | ||
extensions: ['js'], | ||
forceNodeFilesystemAPI: true, | ||
ignore: pearMatcher, | ||
rootDir, | ||
roots: ['/project/fruits'], | ||
}).then(({hasteMap, removedFiles}) => { | ||
expect(hasteMap.files).toEqual( | ||
createMap({ | ||
'fruits/directory/strawberry.js': ['', 33, 42, 0, '', null], | ||
'fruits/tomato.js': ['', 32, 42, 0, '', null], | ||
}), | ||
); | ||
expect(removedFiles).toEqual(new Map()); | ||
// once for /project/fruits, once for /project/fruits/directory | ||
expect(fs.readdir).toHaveBeenCalledTimes(2); | ||
// once for strawberry.js, once for tomato.js | ||
expect(fs.lstat).toHaveBeenCalledTimes(2); | ||
}); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,22 +32,42 @@ function find( | |
|
||
function search(directory: string): void { | ||
activeCalls++; | ||
fs.readdir(directory, (err, names) => { | ||
fs.readdir(directory, {withFileTypes: true}, (err, entries) => { | ||
activeCalls--; | ||
if (err) { | ||
callback(result); | ||
return; | ||
} | ||
names.forEach(file => { | ||
file = path.join(directory, file); | ||
// node < v10.10 does not support the withFileTypes option, and | ||
// entry will be a string. | ||
entries.forEach((entry: string | fs.Dirent) => { | ||
const file = path.join( | ||
directory, | ||
typeof entry === 'string' ? entry : entry.name, | ||
); | ||
|
||
if (ignore(file)) { | ||
return; | ||
} | ||
|
||
if (typeof entry !== 'string') { | ||
if (entry.isSymbolicLink()) { | ||
return; | ||
} | ||
|
||
if (entry.isDirectory()) { | ||
search(file); | ||
return; | ||
} | ||
} | ||
SimenB marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
activeCalls++; | ||
|
||
fs.lstat(file, (err, stat) => { | ||
activeCalls--; | ||
|
||
// This logic is unnecessary for node > v10.10, but leaving it in | ||
// since we need it for backwards-compatibility still. | ||
Comment on lines
+69
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @yepitschunked we're about to drop node 10 (#12220), which part of the logic does this refer to? The entire There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this PR is old, but would love your comment over in that PR 🙂 |
||
if (!err && stat && !stat.isSymbolicLink()) { | ||
if (stat.isDirectory()) { | ||
search(file); | ||
|
@@ -58,6 +78,7 @@ function find( | |
} | ||
} | ||
} | ||
|
||
if (activeCalls === 0) { | ||
callback(result); | ||
} | ||
|
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.
In case you're wondering - I mirrored the existing mocks to look like a fake fs.Dirent object here. I thought about coming up with something fancy to avoid this duplication (e.g defining the mocks in a map and then
reduce
ing it to get strings/dirents as needed) but it seemed more complicated than it was worth.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.
yeah, let's try to keep it simple here