Skip to content
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

perf(jest-runtime): load chalk only once per worker #10864

Merged
merged 3 commits into from Feb 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -27,6 +27,8 @@
/website/translated_docs
/website/i18n/*

/benchmarks/*/node_modules/

/reports/*

coverage
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions benchmarks/test-file-overhead/.gitignore
@@ -0,0 +1 @@
[1-9]*.test.js
8 changes: 8 additions & 0 deletions 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', () => {});
7 changes: 7 additions & 0 deletions 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
jeysal marked this conversation as resolved.
Show resolved Hide resolved
```
5 changes: 5 additions & 0 deletions benchmarks/test-file-overhead/package.json
@@ -0,0 +1,5 @@
{
"jest": {
"testEnvironment": "node"
}
}
4 changes: 4 additions & 0 deletions 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
11 changes: 11 additions & 0 deletions 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
Expand Up @@ -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'},
Expand All @@ -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'},
Expand All @@ -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),
);
}));
});
});
10 changes: 10 additions & 0 deletions 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);
17 changes: 17 additions & 0 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -95,6 +95,17 @@ const defaultTransformOptions: InternalModuleOptions = {
type InitialModule = Omit<Module, 'require' | 'parent' | 'paths'>;
type ModuleRegistry = Map<string, InitialModule | Module>;

// 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',
);
Expand Down Expand Up @@ -660,6 +671,12 @@ export default class Runtime {

requireInternalModule<T = unknown>(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);
jeysal marked this conversation as resolved.
Show resolved Hide resolved
}
const outsideJestVmPath = decodePossibleOutsideJestVmPath(to);
if (outsideJestVmPath) {
return require(outsideJestVmPath);
Expand Down
4 changes: 2 additions & 2 deletions scripts/checkCopyrightHeaders.js
Expand Up @@ -99,15 +99,15 @@ 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$',
'^packages/jest-haste-map/src/watchers/common\\.js$',
'^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 = [
Expand Down