Conversation
…transformer TransformCache read every module's source through the runtime's FileCache before calling ScriptTransformer#transform. The transformer memoizes its results per worker (projectCaches, keyed on path and mtime) and ignores the source on a hit, while the FileCache is per test file. So every test file re-read from disk every module it required, and on every read but the first in a worker the text was discarded. On a miss the transformer reads the source itself into the same cacheFS the runtime shares with it, so leaving the read to it changes nothing but the redundant reads. Internal modules, which bypass the transformer, still read through the FileCache.
✅ Deploy Preview for jestjs ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
babel-jest
babel-plugin-jest-hoist
babel-preset-jest
create-jest
@jest/diff-sequences
expect
@jest/expect-utils
jest
jest-changed-files
jest-circus
jest-cli
jest-config
@jest/console
@jest/core
@jest/create-cache-key-function
jest-diff
jest-docblock
jest-each
@jest/environment
jest-environment-jsdom
@jest/environment-jsdom-abstract
jest-environment-node
@jest/expect
@jest/fake-timers
@jest/get-type
@jest/globals
jest-haste-map
jest-jasmine2
jest-leak-detector
jest-matcher-utils
jest-message-util
jest-mock
@jest/pattern
jest-phabricator
jest-regex-util
@jest/reporters
jest-resolve
jest-resolve-dependencies
jest-runner
jest-runtime
@jest/schemas
jest-snapshot
@jest/snapshot-utils
@jest/source-map
@jest/test-result
@jest/test-sequencer
@jest/transform
@jest/types
jest-util
jest-validate
jest-watcher
jest-worker
pretty-format
commit: |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TransformCache#transform(and#transformAsync) reads a module's source through the runtime'sFileCachebefore callingScriptTransformer#transform:The two caches have different lifetimes:
FileCachewraps thecacheFSmap thatrunTestcreates per test file;ScriptTransformer#transformfirst looks the module up inprojectCaches[...].transformedFiles, which is module-level and lives for the whole worker, keyed on path + mtime (+ instrumentation and caller flags). On a hit it returns without looking atfileSourceat all. On a miss,_transformAndBuildScriptusesfileSource ?? this._cacheFS.get(filename)and otherwise reads the file itself into that samecacheFS.So in a worker that runs many test files, each test file reads from disk every module it requires, and every one of those reads after the first is thrown away. A module required by 1,000 test files is read about 1,000 times and transformed once.
This PR stops reading up front and leaves the read to the transformer. Internal modules, which skip the transformer, still read through the
FileCache.transformJsonis unchanged: it has no per-worker memo, so it does need the source.How this differs from #13419
#13419 (2022) proposed the same deferral and was closed after the review point that
readFileand the transformer use the samecacheFS, so it looked like the same read made at a different moment. That holds within one test file. The saving is across test files:cacheFSstarts empty for each test file, but the transformer'stransformedFilesmemo does not, so on a memo hit nothing needs the source. The mtime check behind that memo (getScriptCacheKey, onestatSync) is unchanged, so a module edited on disk is still read again and re-transformed.What still sees the source
cacheFSconsumers in custom transformers (e.g. ts-jest's language service) are only called on a memo miss, and on that path the transformer has already read the file intocacheFS. They also fall back to disk when an entry is missing.originalCode(used by coverage) comes from the memoizedTransformResult, so it is unaffected.Measurement
Synthetic project: 300 one-function CommonJS modules behind an
index.js, and 100 test files that eachrequirethe index (default config, sobabel-jest). Same checkout and the same warm transform cache; the only difference between runs ispackages/jest-runtime/build/index.jsbuilt frommainor from this branch. Runs were interleaved A/B/B/A, 6 rounds per worker setting, on a shared development machine. A--requirehook countedfs.readFileSynccalls on the library's files in every Jest process.Median of 6 runs per variant, with the range in brackets (warm-up runs excluded):
main--runInBandreadFileSynccalls on the library's files/usr/bin/time -l)-w 2readFileSynccalls on the library's files(With
-w 2,/usr/bin/timecounts instructions for the parent process only, so that row is left out.)Reproduction
Generate the project (
./gen.sh /tmp/repro 300 100):Count the reads (
READS_DIR=/tmp/repro READS_OUT=/tmp/reads NODE_OPTIONS="--require $PWD/count-reads.cjs" node packages/jest-cli/bin/jest.js --rootDir /tmp/repro -i, then sum the lines of/tmp/reads):The read count is exact. The time figures were taken on a loaded machine, so read them as direction and rough size, not as a precise benchmark. The size of the gain depends on how many modules each test file loads compared with how much work the tests do. On a larger private TypeScript suite using
@swc/jest, the same change applied as a local patch cut total CPU time at 2 workers by about 11%.Test plan
New tests:
packages/jest-runtime/src/__tests__/runtime_source_reads.test.ts: builds twoRuntimes, each with its owncacheFSas in two test files of one worker, and countsgraceful-fs.readFileSynccalls on a fixture module.reads a module once for two test files that require it: 1 read (2 onmain).reads a module again after it changes on disk: a rewrite with a later mtime is read again, and the second runtime gets the new code.TransformCache.test.ts:leaves reading the source to the transformerfortransformandtransformAsync.forwards options through getFullTransformationOptionsno longer expects a source argument. Internal modules still read through theFileCache.Checking that the tests catch the defect:
TransformCache.tsreset tomain, exactly 4 tests fail: the read-once runtime test (Expected: 1, Received: 2), bothleaves reading the source to the transformertests, andforwards options…. The other 9 pass.getScriptCacheKeyin the built@jest/transform,reads a module again after it changes on diskfails (Expected: "after", Received: "before"). This shows the test does guard against a stale module.Commands: