Repository navigation
Conversation
This is needed because we won't mark unvisited files as changed anymore. This causes them to not be read or processed. We could exclude symlinks from this, but the lazy symlinking makes sense in conjunction with the lazy lstat change. This is a trade-off, we skip the readlinks on startup that are async, and instead run them synchronously. However, they're skipped if the symlink is never accessed, which in workspaces with isolated dependency installations can be beneficial. In most cases, this is unlikely to impact performance as much as eagerly populating symlinks, as the previous code seems to mostly assume a low number of symlinks anyway (which isn't true for isolated installations)
ad0b094 to
3c2d74c
Compare
|
This (LLM-generated) branch of a squash of this PR and #1687 combined demonstrates the rough performance when both changes are combined: https://github.com/kitten/metro/tree/%40kitten/bench/metro-file-map/crawl-and-lookup The goal (which seems to be reached, at least in the synthetic tests on the branch) is to keep performance roughly the same when all files are accessed. Performance should improve when a large amount of crawled files are never accessed. |
Resolves conflicts with react#1687 (symlink targets stored lexically resolved, as normal POSIX paths) and react#1693 (native `find` crawler removed). - The Node crawler keeps this PR's lazy `lstat` inside main's single `find` path. - Symlinks with a known `mtime` are still read eagerly by `FileMap#maybeReadLink`, which now stores main's format. A symlink crawled without an `lstat` is still `1` until a lookup traverses it, and `TreeFS#readSymlinkTarget` then reads it and stores the target in that same format, so a target read lazily and one read eagerly are indistinguishable. This replaces the `WeakMap`-based `#resolveSymlinkTargetToNormalPath`, which main removed. - The `find` binary tests are dropped along with the crawler they covered, the committed `types/` defs are dropped as on main, and the tests mock `node:fs` to match main's imports.
|
AI: I've merged Benchmarked the current head (d7e1754) against The first ten rows are
Bold where the CI excludes zero. |
|
AI: I split this into its parts to see where the numbers above come from. Same app, benchmarks and stats as above, against
Changes are the median per-round delta against
|
Resolves conflicts with react#1677, which rewrote the Node crawler: its crawler already checks the extension before `lstat` and builds normal paths per directory, so this PR's crawler change reduces to skipping the `lstat` for a file with no previous mtime, reading `previousState` from the crawler options. The crawler tests follow: new files are now recorded without an `lstat`, the filesystem root test checks the path passed to `ignore()` rather than `lstat`, and `forceNodeFilesystemAPI` (removed by react#1693) is dropped from this PR's new tests. Also updates the API snapshot for `FileSystem.getMtimeByNormalPath`.
|
High level thoughts having looked closely at this and rebased on two related, now merged ones:
Parking this for now, but hopefully we can get the benefits of lazy statting soon. |
Summary
Note
This is part of the same pick/patch-set from an experiment on the Expo repo,
like #1676, #1677, and #1687, for
metro-file-map(conflicts are expected, since changes were pulled out into separate PRs for clarity)After #1677 the highest impact on initial crawling performance with the Node crawler are the individual
lstatcalls andreadlinkcalls. The former happens for every file that's discovered and can hence get quite large for monorepos (or assets that aren't excluded with blocklists). The latter, readlinks, are expensive if we're in an isolated installation (pnpm & bun), which can contain a much larger amount of symlinks than was likely anticipated by the current code paths.This is the last (and most minimal iteration) of a few attempts to reduce the cost.
The idea is to skip the
lstatcall for new files that aren't in the previous FS snapshot (entry doesn't exist). To make this safe, these files are also excluded fromgetDifference, i.e. when both the previous and new entries have0 | nullmtime values. In both paths0 | nullindicates that the file is skipped forlstat.The
mtimeis later added via anfs.promises.lstatcall ingetOrComputeSha1i.e. when the file is actually accessed. If the file isn't accessed the mtime value remains at null.This cascades to require/allow us to lazily evaluate symlinks for mtime values of null. Their values aren't read until accessed in
#resolveSymlinkTargetToNormalPathinTreeFS. This does introduce an update on read, but lazily populates the symlink value. This trades off the asyncreadlinkcall on all symlinks for a syncreadlinkcall on a subset of symlinks.We've observed this to be an overall win, but this could be influenced further by this commit: expo/expo@af67435 (which could also be upstreamed but changes the
getContentsignature. This change can be avoided by awaiting first, but seems overall like a better change than awaitinggetContentunconditionally before plugins)Changelog: [Internal] Lazily stat Node crawled files and lazily populate symlink targets
Test plan