diff --git a/.gitignore b/.gitignore index d513cf11ba99..4e7db024f4c0 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,8 @@ /website/translated_docs /website/i18n/* +/benchmarks/*/node_modules/ + /reports/* coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index ea43cca7622f..6dc8f3169adf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,8 @@ ### Performance +- `[jest-runtime]` Load `chalk` only once per worker ([#10864](https://github.com/facebook/jest/pull/10864)) + ## 26.6.3 ### Fixes diff --git a/benchmarks/test-file-overhead/.gitignore b/benchmarks/test-file-overhead/.gitignore new file mode 100644 index 000000000000..a68ced87888c --- /dev/null +++ b/benchmarks/test-file-overhead/.gitignore @@ -0,0 +1 @@ +[1-9]*.test.js diff --git a/benchmarks/test-file-overhead/0.test.js b/benchmarks/test-file-overhead/0.test.js new file mode 100644 index 000000000000..bed12c324a11 --- /dev/null +++ b/benchmarks/test-file-overhead/0.test.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +it('is fast', () => {}); diff --git a/benchmarks/test-file-overhead/README.md b/benchmarks/test-file-overhead/README.md new file mode 100644 index 000000000000..d60998eeab8f --- /dev/null +++ b/benchmarks/test-file-overhead/README.md @@ -0,0 +1,7 @@ +First, run `./prepare.sh` to generate the benchmark files. On Windows, use a Bash (WSL, Git, Cygwin …) to do this, but you can use CMD for the actual benchmark run if the CMD environment is what you want to benchmark for. + +To run the benchmark, use a benchmarking tool such as [hyperfine](https://github.com/sharkdp/hyperfine): + +```sh +hyperfine -w 3 -m 10 ../../jest /tmp/other-jest-clone-to-compare-against/jest +``` diff --git a/benchmarks/test-file-overhead/package.json b/benchmarks/test-file-overhead/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/benchmarks/test-file-overhead/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/benchmarks/test-file-overhead/prepare.sh b/benchmarks/test-file-overhead/prepare.sh new file mode 100755 index 000000000000..569a785e1294 --- /dev/null +++ b/benchmarks/test-file-overhead/prepare.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +for i in {1..499}; do + cp 0.test.js $i.test.js +done diff --git a/benchmarks/test-file-overhead/yarn.lock b/benchmarks/test-file-overhead/yarn.lock new file mode 100644 index 000000000000..00246b971113 --- /dev/null +++ b/benchmarks/test-file-overhead/yarn.lock @@ -0,0 +1,11 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 4 + +"root-workspace-0b6124@workspace:.": + version: 0.0.0-use.local + resolution: "root-workspace-0b6124@workspace:." + languageName: unknown + linkType: soft diff --git a/packages/jest-runtime/src/__tests__/runtime_internal_module.test.js b/packages/jest-runtime/src/__tests__/runtime_internal_module.test.js index aeca406d3ea2..71293e89c33b 100644 --- a/packages/jest-runtime/src/__tests__/runtime_internal_module.test.js +++ b/packages/jest-runtime/src/__tests__/runtime_internal_module.test.js @@ -32,7 +32,6 @@ describe('Runtime', () => { runtime.requireModule(modulePath); }).toThrow(new Error('preprocessor must not run.')); }); - it('loads internal modules without applying transforms', async () => { const runtime = await createRuntime(__filename, { transform: {'\\.js$': './test_preprocessor'}, @@ -56,7 +55,6 @@ describe('Runtime', () => { const exports = runtime.requireModule(modulePath); expect(exports).toEqual({foo: 'foo'}); }); - it('loads internal JSON modules without applying transforms', async () => { const runtime = await createRuntime(__filename, { transform: {'\\.json$': './test_json_preprocessor'}, @@ -68,5 +66,29 @@ describe('Runtime', () => { const exports = runtime.requireInternalModule(modulePath); expect(exports).toEqual({foo: 'bar'}); }); + + const OPTIMIZED_MODULE_EXAMPLE = 'chalk'; + it('loads modules normally even if on the optimization list', () => + createRuntime(__filename).then(runtime => { + const modulePath = path.resolve( + path.dirname(runtime.__mockRootPath), + 'require-by-name.js', + ); + const requireByName = runtime.requireModule(modulePath); + expect(requireByName(OPTIMIZED_MODULE_EXAMPLE)).not.toBe( + require(OPTIMIZED_MODULE_EXAMPLE), + ); + })); + it('loads internal modules from outside if on the optimization list', () => + createRuntime(__filename).then(runtime => { + const modulePath = path.resolve( + path.dirname(runtime.__mockRootPath), + 'require-by-name.js', + ); + const requireByName = runtime.requireInternalModule(modulePath); + expect(requireByName(OPTIMIZED_MODULE_EXAMPLE)).toBe( + require(OPTIMIZED_MODULE_EXAMPLE), + ); + })); }); }); diff --git a/packages/jest-runtime/src/__tests__/test_root/require-by-name.js b/packages/jest-runtime/src/__tests__/test_root/require-by-name.js new file mode 100644 index 000000000000..793879bcfa07 --- /dev/null +++ b/packages/jest-runtime/src/__tests__/test_root/require-by-name.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +module.exports = moduleName => require(moduleName); diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index a4992f045f4f..2aa3f6739ec6 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -95,6 +95,17 @@ const defaultTransformOptions: InternalModuleOptions = { type InitialModule = Omit; type ModuleRegistry = Map; +// These are modules that we know +// * are safe to require from the outside (not stateful, not prone to errors passing in instances from different realms), and +// * take sufficiently long to require to warrant an optimization. +// When required from the outside, they use the worker's require cache and are thus +// only loaded once per worker, not once per test file. +// Use /benchmarks/test-file-overhead to measure the impact. +// Note that this only applies when they are required in an internal context; +// users who require one of these modules in their tests will still get the module from inside the VM. +// Prefer listing a module here only if it is impractical to use the jest-resolve-outside-vm-option where it is required, +// e.g. because there are many require sites spread across the dependency graph. +const INTERNAL_MODULE_REQUIRE_OUTSIDE_OPTIMIZED_MODULES = new Set(['chalk']); const JEST_RESOLVE_OUTSIDE_VM_OPTION = Symbol.for( 'jest-resolve-outside-vm-option', ); @@ -660,6 +671,12 @@ export default class Runtime { requireInternalModule(from: Config.Path, to?: string): T { if (to) { + const require = ( + nativeModule.createRequire ?? nativeModule.createRequireFromPath + )(from); + if (INTERNAL_MODULE_REQUIRE_OUTSIDE_OPTIMIZED_MODULES.has(to)) { + return require(to); + } const outsideJestVmPath = decodePossibleOutsideJestVmPath(to); if (outsideJestVmPath) { return require(outsideJestVmPath); diff --git a/scripts/checkCopyrightHeaders.js b/scripts/checkCopyrightHeaders.js index 49cd0f2e913c..b67ee285d591 100755 --- a/scripts/checkCopyrightHeaders.js +++ b/scripts/checkCopyrightHeaders.js @@ -99,7 +99,6 @@ const GENERIC_IGNORED_PATTERNS = [ const CUSTOM_IGNORED_PATTERNS = [ '\\.(example|map)$', '^examples/.*', - '^flow-typed/.*', '^packages/expect/src/jasmineUtils\\.ts$', '^packages/jest-config/src/vendor/jsonlint\\.js$', '^packages/jest-diff/src/cleanupSemantic\\.ts$', @@ -107,7 +106,8 @@ const CUSTOM_IGNORED_PATTERNS = [ '^packages/jest-haste-map/src/watchers/NodeWatcher\\.js$', '^packages/jest-haste-map/src/watchers/RecrawlWarning\\.js$', '^website/static/css/code-block-buttons\\.css$', - '^website/static/js/code-block-buttons\\.js', + '^website/static/js/code-block-buttons\\.js$', + '^benchmarks/test-file-overhead/prepare\\.sh$', ].map(createRegExp); const IGNORED_PATTERNS = [