-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
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
Comments
Hi, just some random ideas:
|
But I assume I must install watchman e.g. via brew. I can't observe a difference, maybe a few 100ms.
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. |
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. |
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 Another thing to watch out here is that when you do 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 |
Here is a way to speed up the initial run when watchman is installed. Create a file named
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
|
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 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/ |
Wasn't aware of that option, nice find! (I thought jest already did that when one runs |
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. |
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 |
Almost all of our tests are utilising the DOM in some way. I have happy dom a try a few times. |
@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. |
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 |
we experience that as well in a pretty big monorepo. |
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:
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:
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 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 |
joshribakoff-sm want to share your custom resolver? |
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 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 :) |
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 :
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):
It seems the culprit is Which fits with the |
@emiriel On my end the process is using 100% CPU during the "slow server start". perhaps these are separate issues?
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?
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? |
Testing was sped up with this configuration |
@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) |
Hey All, I had this same issue with two repos (so far) at our company. I was able to narrow this down to the 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 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. |
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 :) |
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 Alternative I found:
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 |
@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 |
Facing the same issue as @joshribakoff-sm here. |
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? |
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. |
I don't follow, could you please elaborate?
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?). |
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 |
anybody solved this? |
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 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. |
anybody wrote a custom resolver to fix this in monorepo with many files ? |
Thank you @gor918, We hare a simple monorepo with Yarn berry and 3 packages, one with 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 |
For me isolatedModules made it take 3x longer (when it was already
unreasonably slow). Are you sure you’re not messing up your benchmarks with
caching? My previous investigation suggested its the code paths support
jest’s mocks and not the transpiler.
Sent from Gmail Mobile
…On Fri, Jun 7, 2024 at 1:15 PM Douglas Nassif Roma Junior < ***@***.***> wrote:
Thank you @gor918 <https://github.com/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.
—
Reply to this email directly, view it on GitHub
<#10833 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AA6VYOW4CB7PHFXIN3SV4TDZGIIHNAVCNFSM6AAAAABI7H6HM2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNJVGQ4DOMRWHE>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
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:
Config files: jest.config.tsconst 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"],
},
} |
We use bazel to run jest test and for me removing |
vitest doesn't have this issue as well, very performant |
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
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:
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
andesbuild
. 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
likeMost 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:
Interestingly the CPU profiler in node shows a lot (~4s) of "nothing" in between starting the script and executing jest:
Also the jestAdapter takes 8s before a tests starts and a total of 12s for the entire run.
As far as I can tell the "nothing" time is spent with reading files.
onStreamRead
andprogram
, zoomed in:Issues that might be related
#10301
#9554
The text was updated successfully, but these errors were encountered: