ts-jest icon indicating copy to clipboard operation
ts-jest copied to clipboard

Module caching memory leak

Open willsoto opened this issue 4 years ago ā€¢ 90 comments

šŸ› Bug Report

ts-jest seems to cache compiled modules over and over again, creating a memory leak. There is a basline suite of tests using just jest on main

To Reproduce

Steps to reproduce the behavior:

  1. git clone [email protected]:willsoto/jest-repro.git (note the branch is ts-jest-memory-leak)
  2. npm ci
  3. Run any of the test scripts to see memory usage increasing in a way that is indicative of a memory leak. You can run npm run test:force-gc to see memory usage when a garbage collection is forced.

Expected behavior

Modules should be cached once for the duration of the test run or allowed to be garbage collected after the test suite has completed.

Link to repo (highly encouraged)

https://github.com/willsoto/jest-repro/tree/ts-jest-memory-leak

Debug log:

This is the log from the full run, no forced garbage collection: npm test.

āÆ TS_JEST_LOG=ts-jest.log npm run test

> [email protected] test jest-repro
> jest --no-cache --runInBand --logHeapUsage ./test

 PASS  test/with-nest/test10.spec.ts (79 MB heap size)
 PASS  test/with-nest/test11.spec.ts (97 MB heap size)
 PASS  test/with-nest/test12.spec.ts (125 MB heap size)
 PASS  test/with-nest/test1.spec.ts (150 MB heap size)
 PASS  test/with-nest/test2.spec.ts (176 MB heap size)
 PASS  test/with-nest/test3.spec.ts (94 MB heap size)
 PASS  test/with-nest/test4.spec.ts (120 MB heap size)
 PASS  test/with-nest/test7.spec.ts (145 MB heap size)
 PASS  test/with-nest/test5.spec.ts (170 MB heap size)
 PASS  test/with-nest/test6.spec.ts (195 MB heap size)
 PASS  test/with-nest/test8.spec.ts (220 MB heap size)
 PASS  test/with-nest/test9.spec.ts (245 MB heap size)
 PASS  test/force-gc/test12.spec.ts (256 MB heap size)
 PASS  test/force-gc/test10.spec.ts (277 MB heap size)
 PASS  test/force-gc/test11.spec.ts (288 MB heap size)
 PASS  test/force-gc/test7.spec.ts (297 MB heap size)
 PASS  test/force-gc/test4.spec.ts (306 MB heap size)
 PASS  test/force-gc/test6.spec.ts (328 MB heap size)
 PASS  test/force-gc/test1.spec.ts (338 MB heap size)
 PASS  test/force-gc/test5.spec.ts (347 MB heap size)
 PASS  test/force-gc/test9.spec.ts (356 MB heap size)
 PASS  test/force-gc/test8.spec.ts (178 MB heap size)
 PASS  test/force-gc/test3.spec.ts (188 MB heap size)
 PASS  test/force-gc/test2.spec.ts (210 MB heap size)
 PASS  test/without-nest/test10.spec.ts (219 MB heap size)
 PASS  test/without-nest/test11.spec.ts (228 MB heap size)
 PASS  test/without-nest/test12.spec.ts (250 MB heap size)
 PASS  test/without-nest/test1.spec.ts (259 MB heap size)
 PASS  test/without-nest/test2.spec.ts (268 MB heap size)
 PASS  test/without-nest/test3.spec.ts (277 MB heap size)
 PASS  test/without-nest/test4.spec.ts (299 MB heap size)
 PASS  test/without-nest/test7.spec.ts (308 MB heap size)
 PASS  test/without-nest/test5.spec.ts (317 MB heap size)
 PASS  test/without-nest/test6.spec.ts (338 MB heap size)
 PASS  test/without-nest/test8.spec.ts (348 MB heap size)
 PASS  test/without-nest/test9.spec.ts (357 MB heap size)

Test Suites: 36 passed, 36 total
Tests:       3600 passed, 3600 total
Snapshots:   0 total
Time:        10.716 s
Ran all test suites matching /.\/test/i.
ts-jest.log ``` {"context":{"allowJs":false,"logLevel":20,"namespace":"jest-preset","package":"ts-jest","version":"26.3.0"},"message":"creating jest presets not handling JavaScript files","sequence":1,"time":"2020-09-18T17:00:10.883Z"} {"context":{"logLevel":20,"namespace":"Importer","package":"ts-jest","version":"26.3.0"},"message":"creating Importer singleton","sequence":2,"time":"2020-09-18T17:00:12.088Z"} {"context":{"allowJs":false,"logLevel":20,"namespace":"jest-preset","package":"ts-jest","version":"26.3.0"},"message":"creating jest presets not handling JavaScript files","sequence":3,"time":"2020-09-18T17:00:12.092Z"} {"context":{"actualVersion":"26.4.2","expectedVersion":">=26 =3.8

envinfo

  System:
    OS: macOS 10.15.6
    CPU: (8) x64 Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz
  Binaries:
    Node: 12.18.3 - ~/.volta/tools/image/node/12.18.3/bin/node
    Yarn: 1.22.4 - ~/.volta/tools/image/yarn/1.22.4/bin/yarn
    npm: 6.14.5 - ~/.volta/tools/image/npm/6.14.5/bin/npm
  npmPackages:
    jest: ^26.4.2 => 26.4.2
    ts-jest: ^26.3.0 => 26.3.0
    typescript: ^4.0.2 => 4.0.2

Additional screenshots of the heap

Screen Shot 2020-09-18 at 11 05 50 AM Screen Shot 2020-09-18 at 11 07 46 AM

willsoto avatar Sep 18 '20 17:09 willsoto

An interesting observation as I was exploring this was whether or not I was using a custom tsConfig. I'm not sure if this has a meaningful impact on smaller apps, but for large apps the difference is substantial.

When I point ts-jest at a custom tsconfig with an include: [] (an idea I got from this repo), base memory usage goes down significantly but still climbs consistent with a memory leak. When I use whatever the default is, base memory usage increases significantly.

willsoto avatar Sep 22 '20 12:09 willsoto

@ahnpnl I am more than willing to work on this issue since this is a big problem once there are hundreds of source files. We've seen heap usage climb to 1.5gb in some cases. If you can point me to possible culprits that would be great.

willsoto avatar Sep 22 '20 12:09 willsoto

When I point ts-jest at a custom tsconfig with an include: [] (an idea I got from this repo), base memory usage goes down significantly but still climbs consistent with a memory leak. When I use whatever the default is, base memory usage increases significantly.

This has been mentioned in the doc https://kulshekhar.github.io/ts-jest/user/config/isolatedModules

The reason for that is: when specifying include: [], TypeScript LanguageService will do lazy fetch modules which will reduce overhead I/O at start up.

Feel free to pick up this one @willsoto :)

ahnpnl avatar Sep 22 '20 12:09 ahnpnl

@ahnpnl can you point me to any possible places to start looking?

willsoto avatar Sep 22 '20 12:09 willsoto

You can look into this one https://github.com/kulshekhar/ts-jest/blob/master/src/compiler/language-service.ts#L175 . This is the place where I/O overhead appears.

ahnpnl avatar Sep 22 '20 12:09 ahnpnl

What would you suggest in such case for now, until investigation is happening? Downgrade to lower version? but then there is no solution right now for projects that started upgrading to typescript v4 ?

EugeneHerasymchuk avatar Sep 23 '20 14:09 EugeneHerasymchuk

The workaround is following the doc https://kulshekhar.github.io/ts-jest/user/config/isolatedModules

If using isolatedModules: true, you should combine with tsc for type checking.

ahnpnl avatar Sep 23 '20 14:09 ahnpnl

@ahnpnl That does not resolve the problem, it simply reduces growth but does not mitigate it entirely.


After looking into this all day yesterday, we managed to reduce the issue to jest + typescript. You can see a minimal reproduction here where we introduced a minimal transformer that just compiles the TS via transpileModule. The memory leak still exists.

image (1)

Our working assumption right now is that there is some negative interaction between Jest and TypeScript. Given Jest's history of memory leaks, we assume the fault is on Jest's side but are still working on a minimal reproduction.

Give this information, do you have any other thoughts on this @ahnpnl ?

willsoto avatar Sep 23 '20 15:09 willsoto

If transpileModule still produces memory leak, I also think the problem is more in Jest or TypeScript side. I think the issue might be more on Jest side.

Out of curiosity, have you tried with jest CLI flag runInBand @willsoto ? If yes, does it produce the similar leak behavior ?

ahnpnl avatar Sep 23 '20 15:09 ahnpnl

@ahnpnl You can see in the last screenshot that we are running with --runInBand. All of our tests have been using the following flags for consistency: --no-cache --runInBand --logHeapUsage

willsoto avatar Sep 23 '20 15:09 willsoto

I bet my cents on jest šŸ˜…

ahnpnl avatar Sep 23 '20 15:09 ahnpnl

Reported with Jest here. I think this issue should remain open though in case others come looking, if that is okay?

willsoto avatar Sep 23 '20 16:09 willsoto

Sure I think that is a good idea šŸ‘

ahnpnl avatar Sep 23 '20 16:09 ahnpnl

Is this being worked on? Is there anything we can do to help?

Shahor avatar Jan 06 '21 17:01 Shahor

Please check https://github.com/facebook/jest/issues/10550 because it relies on that. Besides, you can also look at the current codebase of ts-jest to see any suspicious points, here is the part using TypeScript Compiler API https://github.com/kulshekhar/ts-jest/blob/master/src/compiler/ts-compiler.ts#L72

ahnpnl avatar Jan 06 '21 17:01 ahnpnl

I don't know if this will help or not but I was having issues with my tests failing in CI due to the heap size and "occasionally" failing on my local machine.

When running with --expose-gc and --logHeapSize I was finding inconsistent results with sometimes the memory ballooning to over 6gb but others times never breaking 300mb.

The scenario I have found to make recreate it on my tests 100% of the time is to run jest with --no-cache or --clearCache before running the tests. If the cache is enabled then (obviously I guess) it can make this issue not apparent because loading the modules from the cache does not run the transformer and this does not have this leak

jcbdev avatar Jan 21 '21 14:01 jcbdev

@willsoto I don't think this is an issue with jest the more I look into it. I think it is ts-jest that is the issue. The reason being is if I switch my transformers from 'ts-jest' to 'babel-jest' my tests pass everytime. The memory never goes past 300mb on the suite.

To me this sounds like ts-jest is either responsible for the memory leak or the something in the transformer is crawling and transforming modules in a destructive manner

jcbdev avatar Jan 21 '21 15:01 jcbdev

Any progress on this issue?

MaestroJurko avatar Apr 15 '21 11:04 MaestroJurko

Currently no unfortunately :) for isolatedModules: true nothing much to do for it. For isolatedModules: false, there might be a way.

ahnpnl avatar Apr 15 '21 11:04 ahnpnl

any process on it? I set max-old-memory to 24g. But still out of memory

sinbargit avatar Apr 26 '21 12:04 sinbargit

@willsoto I don't think this is an issue with jest the more I look into it. I think it is ts-jest that is the issue. The reason being is if I switch my transformers from 'ts-jest' to 'babel-jest' my tests pass everytime. The memory never goes past 300mb on the suite.

To me this sounds like ts-jest is either responsible for the memory leak or the something in the transformer is crawling and transforming modules in a destructive manner

does babel-jest can compile ts? I like have a try

sinbargit avatar Apr 27 '21 07:04 sinbargit

yes babel-jest does support TypeScript. You just need to configure Babel to use TypeScript plugin and it will do the job. You can either use babel-jest to process TypeScript or you can use isolatedModules: true option from ts-jest to disable type checking. Type checking is the most intensive part when compiling codes with TypeScript compiler.

ahnpnl avatar Apr 27 '21 07:04 ahnpnl

Maybe support disable source map can ease this bug

sinbargit avatar Apr 27 '21 09:04 sinbargit

That is possible, Iā€™m curious how significant the improvement is when disabling source map.

ahnpnl avatar Apr 27 '21 10:04 ahnpnl

There is a similar issue in jest https://github.com/facebook/jest/pull/8331, not sure that is the true reason.

sinbargit avatar Apr 28 '21 03:04 sinbargit

@sinbargit you can now disable source map with ts-jest 27.0.0-next.12 and jest 27.0.0-next.9 by setting sourceMap: false in your test tsconfig.

ahnpnl avatar May 05 '21 14:05 ahnpnl

Adding this to jest.config.js worked for me.

  globals: {
    'ts-jest': {
      isolatedModules: true
    }
  }

jackkav avatar May 07 '21 06:05 jackkav

Adding this to jest.config.js worked for me.

  globals: {
    'ts-jest': {
      isolatedModules: true
    }
  }

I got an error **Make sure you are providing an explicit type for the xx xx ** after do that

sinbargit avatar May 07 '21 06:05 sinbargit

@sinbargit you can now disable source map with ts-jest 27.0.0-next.12 and jest 27.0.0-next.9 by setting sourceMap: false in your test tsconfig.

@ahnpnl I tried this with jest 27.0.0-next.9 and got the following warning:

ā— Validation Warning:

  Unknown option "sourceMap" with value false was found.
  This is probably a typing mistake. Fixing it will remove this message.

  Configuration Documentation:
  https://jestjs.io/docs/configuration

wcauchois avatar May 21 '21 16:05 wcauchois

Can you pls open a separate issue with a repo for it?

ahnpnl avatar May 21 '21 17:05 ahnpnl