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

Slow startup time in monorepos (single test/project) #10833

Open
beckerei opened this issue Nov 16, 2020 · 39 comments
Open

Slow startup time in monorepos (single test/project) #10833

beckerei opened this issue Nov 16, 2020 · 39 comments

Comments

@beckerei
Copy link

All of this is done on OSX with 3,5 GHz Dual-Core Intel Core i7 and 1 jest worker.

Problem

We have a monorepo with currently around 26 packages. We currently run with yarn (1) workspaces. So the code base is not exactly small, but once you work on it you mostly work on one of those packages at a time. The problem is that even when just running a single test, it takes about ~10 seconds for the tests to finish. This is mostly startup time because jest reports the test itself running in ~100ms.

We would like to get this time down to allow for a better developer experience. The fact that all tests together take almost ten minutes doesn't bother us that much, but running a single test should ideally finish in less than a second.

We hope that someone here can help us or at least point us in the right direction.

jest config

module.exports = {
  testRunner: 'jest-circus/runner',
  transform: {
    '^.+\\.(t|j)sx?$': 'babel-jest',
  },
  transformIgnorePatterns: ['<rootDir>/node_modules/'],
  moduleNameMapper: {
    '//': 'Here are 10 modules mapped',
  },
  clearMocks: true,
  roots: ['<rootDir>'],
  snapshotSerializers: ['jest-date-serializer'],
  testURL: 'http://www.test.com',
  moduleFileExtensions: ['tsx', 'ts', 'js', 'json'],
  testEnvironment: 'jest-environment-jsdom-sixteen',
  setupFilesAfterEnv: [
    'jest-canvas-mock',
    'jest-localstorage-mock',
    '<rootDir>/setup/index.ts',
    '<rootDir>/setup/errorCatcher.ts',
  ],
  reporters: ['default', '<rootDir>/setup/consoleErrorReporter.js'],
  rootDir: '<rootDir>/../../../',
};

consoleErrorReporter gathers information about errors written to the console.

So in order to allow a custom config for each project the final config is dynamically built:

const fs = require('fs');
const { rootDir, ...baseConfig } = require('./setup/baseJestConfig');
const packages = []; // we have a function which returns all packages

const projects = packages.map(({ location, title }) => {
  try {
    // use config if provided by a package
    fs.accessSync(`${location}/jest.config.js`);
    return `<rootDir>/${location}`;
  } catch (e) {
    // otherwise just use the base config
    return {
      ...baseConfig,
      displayName: title,
      testRegex: `${location}/.*(Test|.test)\\.(t|j)sx?$`,
    };
  }
});

module.exports = {
  ...baseConfig,
  roots: ['<rootDir>'],
  projects,
};

In the end projects would be an array of 26 configs, which mostly look the same.

What we tried so far

Different transpiler

I first thought transpilation might be a bottleneck. I tried swc and esbuild. To my surprise, it made no difference.

Define just the config for the package you are using

We initially filtered for configs we need for a run, but then found out about --selectedProjects.
Both approaches sped up startup time by a factor of three on my machine. My colleague (with slightly better hardware) could observe around 50% speedup, regardless of the total amount of tests that he ran.

How we tried to debug

Get some times

Hacked timings in jest-runtime/build/index.js like

console.time(module.filename);
compiledFunction.call(
  module.exports,
  module, // module object
  module.exports, // module exports
  module.require, // require implementation
  module.path, // __dirname
  module.filename, // __filename
  this._environment.global, // global object
  ...lastArgs.filter(notEmpty)
);
console.timeEnd(module.filename);

Most files take less than a ms, longest took around 600ms. I can see this pilling up for 7-8k files when done in sync.

Profiled the node process

We are not very familiar with how to read and interpret these reports.
Here is an excerpt from it, I cut off lines and just left the top 5 for each:

Statistical profiling result from isolate-0x10469d000-91221-v8.log, (20504 ticks, 27 unaccounted, 0 excluded).

 [Shared libraries]:
   ticks  total  nonlib   name
    300    1.5%          /usr/lib/system/libsystem_platform.dylib
     65    0.3%          /usr/lib/system/libsystem_pthread.dylib
     47    0.2%          /usr/lib/system/libsystem_kernel.dylib
     25    0.1%          /usr/lib/system/libsystem_malloc.dylib
      1    0.0%          /usr/lib/system/libdispatch.dylib

 [JavaScript]:
   ticks  total  nonlib   name
    116    0.6%    0.6%  RegExp: /\.git/|/\.hg/
     53    0.3%    0.3%  LazyCompile: *_ignore /Users/****/node_modules/jest-haste-map/build/index.js:1191:10
     30    0.1%    0.1%  LazyCompile: *<anonymous> /****/node_modules/jest-haste-map/build/crawlers/node.js:254:15
     22    0.1%    0.1%  RegExp: .*\/locales\/.*en\.json$
     14    0.1%    0.1%  LazyCompile: *resolve path.js:973:10

 [C++]:
   ticks  total  nonlib   name
   8465   41.3%   42.2%  T __kernelrpc_thread_policy_set
   3148   15.4%   15.7%  T __ZN2v88internal19ScriptStreamingDataC2ENSt3__110unique_ptrINS_14ScriptCompiler20ExternalSourceStreamENS2_14default_deleteIS5_EEEENS4_14StreamedSource8EncodingE
   2265   11.0%   11.3%  T node::SyncProcessRunner::Spawn(v8::FunctionCallbackInfo<v8::Value> const&)
   1135    5.5%    5.7%  T __kernelrpc_mach_vm_purgable_control_trap
    596    2.9%    3.0%  t node::fs::Read(v8::FunctionCallbackInfo<v8::Value> const&)

 [Summary]:
   ticks  total  nonlib   name
    439    2.1%    2.2%  JavaScript
  19600   95.6%   97.7%  C++
    487    2.4%    2.4%  GC
    438    2.1%          Shared libraries
     27    0.1%          Unaccounted

 [C++ entry points]:
   ticks    cpp   total   name
   4961   47.8%   24.2%  T __ZN2v88internal21Builtin_HandleApiCallEiPmPNS0_7IsolateE
   3711   35.8%   18.1%  T __ZN2v88internal19ScriptStreamingDataC2ENSt3__110unique_ptrINS_14ScriptCompiler20ExternalSourceStreamENS2_14default_deleteIS5_EEEENS4_14StreamedSource8EncodingE
    933    9.0%    4.6%  T __kernelrpc_mach_vm_purgable_control_trap
    130    1.3%    0.6%  T __ZN2v88internal30Builtin_ErrorCaptureStackTraceEiPmPNS0_7IsolateE
    129    1.2%    0.6%  T _open$NOCANCEL

Interestingly the CPU profiler in node shows a lot (~4s) of "nothing" in between starting the script and executing jest:
98929895-25c4ed80-24dc-11eb-9df6-3cfa500f194f

Also the jestAdapter takes 8s before a tests starts and a total of 12s for the entire run.

Test Suites: 5 passed, 5 total
Tests:       26 passed, 26 total

As far as I can tell the "nothing" time is spent with reading files. onStreamRead and program, zoomed in:
image

Issues that might be related

#10301
#9554

@jeysal
Copy link
Contributor

jeysal commented Nov 16, 2020

Hi, just some random ideas:

  • You could try installing watchman which Jest will use by default if installed and see if that helps
  • Does it work better with more than 1 worker? Workers are used in more parts of Jest than the actual running of tests

@beckerei
Copy link
Author

You could try installing watchman which Jest will use by default if installed and see if that helps

=> Found "fb-watchman@2.0.1"
info Reasons this module exists

But I assume I must install watchman e.g. via brew. I can't observe a difference, maybe a few 100ms.

Does it work better with more than 1 worker? Workers are used in more parts of Jest than the actual running of tests

Yes and no. So general time for a single test does not matter. But sure if you run the entire suite or just a package this helps a lot.
The entire suite is mostly just run on the CI.

@beckerei beckerei changed the title Slow startup time in monorepos (single test/package) Slow startup time in monorepos (single test/project) Nov 17, 2020
@FabianSellmann
Copy link

Hi @beckerei, thank you for creating this issue as we are also in a very similar situation. We also tried out using different transpilers etc. I reckon the underlying issue lies within creating the haste map, where at some point even with watchman the whole filesystem under the rootDir is read and processed, or something similar.

@ustun
Copy link

ustun commented Dec 8, 2020

You could try installing watchman which Jest will use by default if installed and see if that helps

=> Found "fb-watchman@2.0.1"
info Reasons this module exists

But I assume I must install watchman e.g. via brew. I can't observe a difference, maybe a few 100ms.

I had the same issue (which is how I arrived at this issue url). A single test taking 12 seconds in a monorepo situation. Installing watchman via brew, the single test run now takes 500ms after the first run (which is still 12 seconds since watchman is booting up then), so it kind of solved the issue for me. Does the Activity Monitor show a watchman process running?

Here is also what I found out: When you specify an argument to jest, in the form of say jest foo, it does not mean it should run the tests in the directory foo, but anything that matches the regex /foo/ . See https://jestjs.io/docs/en/cli#jest-regexfortestfiles.

So, jest first finds all test files (using find in my case before the installation of watchman, so it executes something like find . -iname .js -iname .json which is very slow since it drills into all folders. You can verify this by running ps aux | grep find in another terminal window during that 10 seconds, find will be there. (The relevant code is here: https://github.com/facebook/jest/blob/ca47512c77e6a325bca734a35a4ad15a45dee8aa/packages/jest-haste-map/src/crawlers/node.ts#L147-L158 I think one other culprit here might be that it does not exclude node_modules, so that might be another factor for the slowdown.)

Another thing to watch out here is that when you do jest foo, it will match other files in other directories that match the regex foo, as in bar/foo. So, you should also specify the absolute path to make it more explicit (this won't necessarily make it faster, but more reliable, so something like jest $(pwd)/foo. In our repository, we have a folder named ai, so we were running jest ai, but since the repo name is braid, there is a regex match and it was running all the tests instead :)

In any event, I think even using watchman covers the root cause, in that to properly specify which folders it should look at, you should specify the directories as projects, from what I have understood from the documentation. In that case, I believe it will just look at those directories you want.

Related issue: #9862

@ustun
Copy link

ustun commented Dec 9, 2020

Here is a way to speed up the initial run when watchman is installed.

Create a file named .watchmanconfig with the following contents:

{
  "ignore_dirs": ["build", "node_modules", "thirdparty"]
}

Now watchman will ignore those folders, resulting in a significant change (4-5 seconds as opposed to 10-12 seconds in our case).

Also, in case where watchman is not installed, find could be modified to ignore node_modules, that would help too. (or even use fd tool if that is installed, that is very fast as well and ignores .gitignore)

fd '\.snap?$|\.jsx?$|\.tsx?$|\.json$|\.node$' is orders of magnitude faster than find that jest uses by default.

@beckerei
Copy link
Author

I still don't see a change wether I have a watchman config or not. Might be because we already did some optimizations with running different projects depending on what param you pass to jest.

For myself I now only run single test files with --runTestsByPath ind watch mode. This give me okayish feedback loops when developing or changing stuff. I mostly run our suite in the CI.

I might find some time after christmas to play around a bit more. I'll defiantly keep the thread updated with our findings.

OT:

@FabianSellmann I think you are located close to us (according to your profile pic ;)). We did run a react meetup before the pandemic. We might continue when everything is back to normal. Would be nice to connect. https://www.meetup.com/de-DE/ReactJS-Meetup-Dusseldorf/

@ustun
Copy link

ustun commented Dec 14, 2020

For myself I now only run single test files with --runTestsByPath ind watch mode.

Wasn't aware of that option, nice find! (I thought jest already did that when one runs jest foo, which was how I arrived at this particular issue and found out that is not the case.)

@beckerei
Copy link
Author

Jest perf is even worth when using testPattern. As it has to not only search for every single test file but also search each test description. Both makes sense, but I would not recommend to use anything except path or single file runs.

@FabianSellmann
Copy link

Anyone looked into using a different test environment (e.g. node if no dom required, or something like happy dom as an alternative) curious if its worth going through tests and change the environment where possible?

@beckerei
Haha would have loved to, but I am based in asia now.

@beckerei
Copy link
Author

Almost all of our tests are utilising the DOM in some way. I have happy dom a try a few times.
It works (with some caveats), but I could not observe any meaningful speed improvement.

@robatwilliams
Copy link

Seems --runTestsByPath doesn't speed things up at all, by my experimentation and confirmed here #7031.

Our codebase has 3000 test files, and on the VM I work on it takes 2.5 minutes to start Jest in watch mode for a single file with one test in it.

@beckerei
Copy link
Author

beckerei commented Apr 7, 2021

@robatwilliams when using watch mode having installed https://facebook.github.io/watchman/ with a proper config for the repo (e.g. exclude node_modules, dist, ...) is a must.

Whenever I have time at work I look for ways to improve our test performance. However I haven't found much despite what is already said in this thread.

@dreyks
Copy link

dreyks commented May 6, 2021

I've hit this issue after we've switched to yarn workspaces. It was working fine with a monolithic app but now with the monorepo it takes 10+ seconds on each change before it actually starts running the changed files

so this is definitely related to workspaces. we're using nohoist so each of the packages has its own node_modules, maybe this is the case? some of the node_modules aren't ignored? adding watchPathIgnorePatterns: ['<rootDir>/node_modules/']doesn't help though

@AlonMiz
Copy link

AlonMiz commented Sep 3, 2021

we experience that as well in a pretty big monorepo.
i was playing with https://github.com/aelbore/esbuild-jest, it reduced the time by a big factor 104s => 28s
but im not sure about the startup time. my guess is that it's mostly improved the require/import part and the transpile time
image

@joshribakoff-sm
Copy link

joshribakoff-sm commented Apr 9, 2022

I also have bad performance in a monorepo

Jest internally uses this "haste" module system, which works by doing a depth first search of all your imports, apparently

For example, if I create a simple test:

import { get } from 'lodash-es';
it('', () => {
  get({}, 'test');
});

and then write a custom resolver https://jestjs.io/docs/configuration#resolver-string that logs out every file jest is resolving, I can see its crawling a bunch of unrelated files:

./_baseXor.js
./xorBy.js
./xorWith.js
./zip.js
./zipObject.js
./_baseZipObject.js
./zipObjectDeep.js
./zipWith.js
./lodash.default.js
./array.js
./array.default.js
./collection.js
./collection.default.js
./date.js
./date.default.js
./function.js
./function.default.js
./lang.js
./lang.default.js
./math.js
./math.default.js
./number.js
./number.default.js
./object.js
./object.default.js

In my monorepo, its a similar situation but exponentially worse. I have a simple component I wrote a test for, and it imports from other packages in my monorepo. Each package has an index.ts which exports a ton of stuff, most packages depend on other packages. Unfortunately, Jest ends up doing what seems to be a giant DFS on nearly every file, and a ton of node_modules unrelated to my test.

For example, I'm unit testing a button. This button imports some remedial thing from a "shared" lib (think a simple string or something). Since it imports from a barrel export and the index.ts in my "shared" lib exports other things which depend on heavy node modules, jest seems to be crawling those heavy node modules, even though its totally unrelated to the button I am testing which is dead simple. For example, looking at flame charts of the profiling I did, I can see my button caused everything in my "shared" to be crawled, including all of framer-motion, which has nothing to do with the button I am testing. It seems my "shared" lib just imports some remedial item from that library, like a smaller helper, but now jest has to crawl every file in that lib on every test run.

@MichaelrMentele
Copy link

joshribakoff-sm want to share your custom resolver?

@joshribakoff-sm
Copy link

I'm using https://nx.dev/

The custom resolver is this one: https://github.com/nrwl/nx/blob/master/packages/jest/plugins/resolver.ts and I just added a console.log() in order to make the above observations.

I'm also now realizing its possible the custom resolver is part of the problem, but I also think that if Jest could avoid eagerly crawling the filesystem (somehow), and instead do it "just in time", that would probably boost performance a lot :)

@emiriel
Copy link

emiriel commented May 17, 2022

Hello,

I may have some new insights on this issue. It could be related to throat.js which provides a mutex mechamism.

Here is how i came to this :

  • i tried to run jest with inspect on node and chrome debugger to see what's going, but ended with a low cpu usage after profiling which didn't meet my expectations
  • ran a codemod to compute execution times of all functions of my monorepo including node_modules ; this codemod calls a function which sends data to an express endpoint which then saves results in a sqlite database
  • ran tests on my workspace and manually ended the script when the tests really began
  • made en endpoint to give me the number of calls, total time and mean time per function as csv

Please note that i could not apply the codemod to all js files, since it failed for half of them (some parsing issue, like ts code in js file) so it may not be representative of what's really going on.

Here is an extract of the data i got (all functions with total time > 500ms):

function nb_calls total_time mean_time
node_modules/throat/index.js.module.exports_1 60 26434 440.56666666666666
node_modules/jest-haste-map/node_modules/jest-worker/build/base/BaseWorkerPool.js.BaseWorkerPool.workerExitPromises_13 11 4306 391.45454545454544
node_modules/merge-stream/index.js.module.exports_1 180 2403 13.35
node_modules/@jest/core/build/cli/index.js.buildContextsAndHasteMaps_23 1 2149 2149
node_modules/jest-haste-map/build/index.js.HasteMap.undefined._buildPromise_16 1 1536 1536
node_modules/jest-haste-map/build/crawlers/watchman.js.module.exports.watchmanCrawl_7 3 1403 467.6666666666667
node_modules/jest-haste-map/node_modules/jest-worker/build/workers/processChild.js.messageListener_2 520 640 1.2307692307692308
node_modules/@jest/core/build/cli/index.js.buildContextsAndHasteMaps.contexts_24 1 606 606
node_modules/emittery/index.js.Emittery_18 19 590 31.05263157894737
node_modules/jest-haste-map/build/crawlers/watchman.js.module.exports.watchmanCrawl.queryWatchmanForDirs_9 1 583 583

It seems the culprit is throat which provides mutex mechanism, so it looks like a deadlock issue

Which fits with the lot (~4s) of "nothing" @beckerei mentioned.

@joshribakoff-sm
Copy link

low cpu usage after profiling which didn't meet my expectations

@emiriel On my end the process is using 100% CPU during the "slow server start". perhaps these are separate issues?

ran a codemod to compute execution times

I'm not sure I understand, codemods are usually like lint rules that transform code on disk, how does a codemod gain information about the speed of the code at runtime?

It seems the culprit is throat which provides mutex mechanism, so it looks like a deadlock issue

Your table only shows "total time", I'm curious what the "self time" is. Perhaps this library is just calling into slower code paths, which affects the "total time" of everything in the stack call?

@gor918
Copy link

gor918 commented May 19, 2022

const jest: { globals: { "ts-jest": { "isolatedModules": true } }, }

Testing was sped up with this configuration

@joshribakoff-sm
Copy link

@gor918 Yes it generally saves time to skip type checking, but I think there is a separate problem here with jest eagerly calling it's resolver on every import statements, recursively, on startup (this affects large projects, not just monorepos, so the issue title is a bit of a misnomer)

@NickBolles
Copy link

NickBolles commented Jun 22, 2022

Hey All, I had this same issue with two repos (so far) at our company. I was able to narrow this down to the rootDir and roots.
When I have config:

config/jest.config.js

export default {
  rootDir: path.resolve(__dirname, "..")
}

startup time is > 20s

If I change it to

export default {
  rootDir: path.resolve(__dirname, ".."),
  roots: ["<rootDir>/src", "<rootDir>/test"]

startup time is <5s.

My theory is (as some others have mentioned) that jest does a depth first scan, based off of test regex, and finds all files in roots and then from that removes them based on ignore patterns. This would mean scanning the entire node_modules, building up a list, then excluding the ignore patterns. This would be very expensive and might come up with thousands of results, only to exclude them.

I can't reproduce this in a fresh repo, so I'm not sure if this has something to do with the packages installed, some other config, or what.

@joshribakoff-sm
Copy link

Note Jest scans for test files; but also does a depth first crawl of every module for purposes of the mock system. Narrowing the test file pattern/root may help, but I think under the hood jest still crawls all of the files for purposes of mocking. There can be more than one issue, too :)

@mingshenggan
Copy link

mingshenggan commented Jul 22, 2022

Took a while for me to hunt down the bugger

Eventually what worked for me is to profile the jest test and look through to identify what is causing the slow jest startup. You can use this video to help.

For me it was the @mui/icons-material library. after uninstalling it, running a single file went from 5s to 2s.

Alternative I found:

// Instead of destructuring like such:
import { ExpandMore } from "@mui/icons-material"

// Directly importing speeds up by 3s
import ExpandMore from "@mui/icons-material/ExpandMore"

This process can help you identify the root cause, but this is ultimately not considered a fix. Just another band aid.

related: mui/material-ui#12422

@joshribakoff-sm
Copy link

@mingshenggan as described in my above comment(s) it's because jest recursively follows all import statements during startup. So by importing 1 MUI icon via the barrel export, it forces jest to crawl 1000s of file(s) also imported by that barrel export.

Certainly reducing the amount of imports will help, but assuming your project actually needs to import part(s) of larger libraries, it doesn't solve the root issue within jest

@Maxim-Filimonov
Copy link

Facing the same issue as @joshribakoff-sm here.
Also using NX.dev and noticing that testing a simple component causes the whole component library to be crawled. Wondering if its actually an issue with jest or an issue with usage of export * from './lib'.
Looking at babel transpilation of that syntax it seem logical that it would require ALL the files.

@janeklb
Copy link

janeklb commented Jan 1, 2023

I'm also now realizing its possible the custom resolver is part of the problem, but I also think that if Jest could avoid eagerly crawling the filesystem (somehow), and instead do it "just in time", that would probably boost performance a lot :)

Along these lines, can anyone from the Jest team provide any insight if they believe Jest is doing more work than it needs to (by default) around the module resolution process? Or is this specifically / strictly the reason for being able to specify a custom resolver?

@joshribakoff-sm
Copy link

I think they wrote their own module system to support Jest mocks. Some minds nowadays say you should avoid mocking anyway (at least for UI layers like React components). Personally, my way of mitigating this issue was to avoid jest for React component testing. I still use it to verify pure functions, like utils and helpers, but I switched to Cypress integration tests since it tests against compiled webpack code which doesn't have any slow start issues.

@janeklb
Copy link

janeklb commented Jan 3, 2023

I think they wrote their own module system to support Jest mocks.

I don't follow, could you please elaborate?

Some minds nowadays say you should avoid mocking anyway (at least for UI layers like React components). Personally, my way of mitigating this issue was to avoid jest for React component testing. I still use it to verify pure functions, like utils and helpers, but I switched to Cypress integration tests since it tests against compiled webpack code which doesn't have any slow start issues.

That's encouraging to hear as my team is also (slowly) moving in that direction. That said, this "problem" is not just for using Jest to test UI components; it applies to all tests (though maybe it manifests most frequently in UI tests?).

@joshribakoff-sm
Copy link

joshribakoff-sm commented Jan 4, 2023

You are right it affects barrel exports and large code bases. Many larger react projects happen to exhibit both of these traits.

Internally jest does not use webpack or other "well-known" module systems. Facebook made their own module system called 'haste' and that is what enables jest mocks which makes it a breeze to test legacy code that is hard to test.

My two cents is that Facebook's solution solved the testing problem, but it is better to actually design the code under test to use inversion of control and inject your dependencies. This makes your test code portable to "real " modules and not coupled to Jest which is apparently not scalable and not maintained adequately to meet our needs.

Instead of:

jest.mock('./http-service', () => 'foo');

// implicitly "hard coded" to depend on some other module, can only be overwritten inside of testing
doStuff() 

Write:

const mockClient = () => 'foo'

 // dependency is very explicit,  dependency can be dynamic not only in test but also in production
doStuff(mockClient)

and then just don't use jest, that would be my recommendation. The former code is just worse code overall than the latter code (ignoring any separate testing related implications). If you write the "good" easy to test code, you don't really need jest anyway

@sibelius
Copy link

anybody solved this?

@joshribakoff
Copy link

joshribakoff commented Feb 26, 2023

https://mui.com/material-ui/guides/minimizing-bundle-size/#option-two-use-a-babel-plugin is a workaround, in a basic minimal react app that imports once into material UI this shaves off maybe 10-20% of the "slow start" by reducing the number of files jest has to map out on the filesystem for the material UI library. Material UI is by no means the only library that benefits from proper tree shaking though..

Jest likely needs to be rewritten to remove "haste" (Facebooks module system), and instead bundle the code using a modern bundler that supports tree shaking -- or just run the source directly via something like node / ts-node. In turn, that will require rewriting jest mocks to rely on some other mechanism other than haste.

Rather than rewriting the jest library, which seems untenable given lack of engagement from the maintainers, I would just recommend to avoid it and use some other test runner and when you need to mock use some other mocking library, like https://sinonjs.org/releases/latest/mocks/ -- some would argue the whole benefit of writing tests is to avoid doing things like depending on a global singleton in your code, which jest aims to make easier. With a library like sinon, you have to pass mocks in as arguments or use a DI framework to inject your dependencies which leads to a better more flexible design for your code and avoids this whole nonsense with the slow "haste" module system Facebook made.

@Micev
Copy link

Micev commented May 29, 2023

Same problem on my side, you can see from snapshot it's took around 18 seconds for jest adapter.
Screenshot 2023-05-29 at 13 57 42
With Jenkins build and --maxWorkers=5 thing going better, around(6-8 sec.) per test, but we have many and checking all of the test tooks us ~25min.
Will appreciate if someone can take a look.

@sibelius
Copy link

anybody wrote a custom resolver to fix this in monorepo with many files ?

@douglasjunior
Copy link

douglasjunior commented Jun 7, 2024

Thank you @gor918, isolatedModules did the trick for us.

We hare a simple monorepo with Yarn berry and 3 packages, one with nestjs and others two with vite.

Just adding:

// jest.config.ts

  transform: {
    '.*\\.tsx?$': ['ts-jest', {
      isolatedModules: true
    }],
  }

The time was reduced from 4 minutes to just 10 seconds in our CI/CD.


EDIT:

To early, adding isolatedModules: true breaks the coverage report 😓 kulshekhar/ts-jest#818

image

@joshribakoff
Copy link

joshribakoff commented Jun 7, 2024 via email

@douglasjunior
Copy link

Hi @joshribakoff, unfortunately is not cache. This is the first time that I use jest+ts in a monorepo, but I have used jest in a lot o projects in the past.

Running 2 times with each config:

isolatedModules: false

Test Suites: 38 passed, 38 total
Tests:       119 passed, 119 total
Snapshots:   29 passed, 29 total
Time:        85.273 s
Ran all test suites.
Test Suites: 38 passed, 38 total
Tests:       119 passed, 119 total
Snapshots:   29 passed, 29 total
Time:        85.926 s
Ran all test suites.

isolatedModules: true

Test Suites: 38 passed, 38 total
Tests:       119 passed, 119 total
Snapshots:   29 passed, 29 total
Time:        11.758 s
Ran all test suites.
Test Suites: 38 passed, 38 total
Tests:       119 passed, 119 total
Snapshots:   29 passed, 29 total
Time:        11.862 s
Ran all test suites.

The jest cache was cleaned before each run.

Running in a Macbook Pro M1 limited to maxWorkers = 2 and workerIdleMemoryLimit = '500MB'.

Config files:

jest.config.ts
const isCI = Boolean(process.env.CI);

/** @type {import('jest').Config} */
const config = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
  testRegex: ".*\\.spec\\.tsx?$",
  globalSetup: '<rootDir>/jest.globalSetup.ts',
  moduleNameMapper: {
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js',
    '\\.(css|less|sass|scss)$': 'identity-obj-proxy',
    '^~/(.*)': '<rootDir>/src/$1',
  },
  setupFiles: ['<rootDir>/jest.shim.ts'],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  clearMocks: true,
  workerIdleMemoryLimit: '500MB',
  // transform: {
  //   '.*\\.tsx?$': ['ts-jest', {
  //     isolatedModules: true
  //   }],
  // }
};

config.collectCoverageFrom = [
  'packages/api/src/**/*.ts?(x)',
  'packages/templates/lib/**/*.ts?(x)',
  '!packages/**/@types/**/*.ts?(x)',
  '!packages/**/assets/**/*.ts?(x)',
  '!packages/**/utils/tests/**/*.ts?(x)',
  '!packages/**/*.stories.ts?(x)',
  '!packages/**/test-utils.ts?(x)',
  '!packages/**/dist/**/*.*',
  '!packages/**/build/**/*.*',
  '!packages/**/test/**/*.*',
  '!packages/api/src/main.ts',
];
config.reporters = ['default'];
config.coverageReporters = ['clover', 'json', 'lcov', 'text'];
config.maxWorkers = '50%';

if (isCI) {
  config.reporters.push([
    'jest-junit',
    {
      suiteName: 'jest tests',
      outputDirectory: 'junit/',
      outputName: 'junit.xml',
      uniqueOutputName: 'false',
      classNameTemplate: '{classname} -> {title}',
      titleTemplate: '{classname} -> {title}',
      ancestorSeparator: ' › ',
      usePathForSuiteName: 'true',
    },
  ]);
  config.coverageReporters.push('cobertura');
  config.coverageReporters.push('html');
  config.maxWorkers = 2;
  config.cacheDirectory = './.jest/';
  config.coverageThreshold = {
    global: {
      branches: 100,
      functions: 100,
      lines: 100,
      statements: 100,
    },
  };
}

module.exports = config;
tsconfig.json
{
  "compilerOptions": {
    "target": "es2020",
    "lib": [
      "ES2020"
    ],
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": true,
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "declaration": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "jsx": "react-jsx",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noUnusedLocals": true,
    "noFallthroughCasesInSwitch": true,
    "useDefineForClassFields": true,
    "strictPropertyInitialization": false,
    "baseUrl": ".",
    "paths": {
      "templates/*": [
        "./packages/templates/*"
      ],
      "api/*": [
        "./packages/api/*"
      ],
      "preview/*": [
        "./packages/preview/*"
      ]
    }
  }
}
packages/api/tsconfig.json (nestjs project)
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",
    "allowSyntheticDefaultImports": true,
    "outDir": "./dist",
    "incremental": true,
    "strictBindCallApply": false,
    "noEmit": false,
    "noEmitOnError": false,
  },
  "exclude": [
    "dist"
  ],
}
packages/preview/tsconfig.json (vite+storybook project)
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {  
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
  },
  "include": ["src"],
}
packages/templates/tsconfig.json (pure react+jsx project)
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {  
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "include": ["lib"],
  },
}

@Arsen-Zhakypbek-Uulu
Copy link

We use bazel to run jest test and for me removing --runTestsByPath reduced the time from 60s to 3s

@AlonMiz
Copy link

AlonMiz commented Oct 24, 2024

We use bazel to run jest test and for me removing --runTestsByPath reduced the time from 60s to 3s

vitest doesn't have this issue as well, very performant

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests