You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
dd-trace 2.13.0 and 3.0.0 cause jest memory leak #2297
When upgrading between versions of dd-trace < 2.13.0 to 2.13.0 or 3.0.0, I didn't expect any impact on the memory usage of my jest test suite.
Actual behaviour
When I opened a PR updating dd-trace from 2.5.0 to 3.0.0, I noticed that my jest test suite started using lots more memory. This caused the test suite to hang forever, causing CI failures.
Just by introducing dd-trace in 2.12.1 resulted in a 94% increase in runtime. I'm not even initializing the tracer, just require-ing it, so this is very unexpected. These are single runs on GitHub actions, so it might not be as dramatic as this makes it seem, but something to look into, for sure.
hi @blimmer ! Thanks for reporting this and creating a reproducible case. I'm currently working on a PR that should hopefully fix this. I'll keep you updated
hi @blimmer ! I jumped to a conclusion too early. I thought this was an issue with the jest plugin but it isn't. The plugin does not start in the example.
We've identified the actual issue though. While we find a solution there's a workaround that fixes the memory leak. You may set the testEnvironment config parameter (https://jestjs.io/docs/configuration#testenvironment-string) to this file:
// testEnvironment.jsrequire('dd-trace')module.exports=require('jest-environment-node')// this is jest's default but you may have something else! (like jsdom)
This testEnvironment can be passed as a cli parameter like:
jest --testEnvironment=./testEnvironment.js --no-cache --logHeapUsage --maxWorkers 1
With testEnvironment.js being on the root of the project.
This should stop 'dd-trace' from being instantiated on every test.
I looked into this a bit more and the problem is that Jest runs tests somewhat in isolation, so the tracer is re-imported on every test. This was already true before, but with the new plugin manager that is based on diagnostics_channel, it means that we keep registering new subscribers for each module we instrument, and these are never removed since Jest keeps built-in modules untouched by default. This is not the root cause however and is just exposing the actual issue more.
The main issue in fixing this is that we're basically fighting Jest. Since we can't rely on global actually being global, we'd need to store the tracer elsewhere, for example in a built-in module since these are actually global even with Jest. However, Jest is also able to mock those, which would yet again break the tracer. The same is true for any other module that the tracer relies on, especially since we patch the internals that may not be correct if mocked.
The best way to handle this right now is to make sure the tracer is loaded before Jest, so instead of loading dd-trace programatically it should be loaded with NODE_OPTIONS='-r dd-trace/init' (or NODE_OPTIONS='-r dd-trace/ci/init' when using CI Visibility. This will ensure that dd-trace is loaded before Jest and the same instance is included in every test.
@juan-fernandez This is not something that we can realistically handle since multiple instances of the tracer will always be problematic. Should we instead disable tracing completely when the load order is incorrect?
@juan-fernandez This is not something that we can realistically handle since multiple instances of the tracer will always be problematic. Should we instead disable tracing completely when the load order is incorrect?
how would we know it's incorrect? Checking if we're in jest and global._ddtrace is undefined?
how would we know it's incorrect? Checking if we're in jest and global._ddtrace is undefined?
@juan-fernandez Yes, when running inside of a Jest runner there is a global jest variable available, and when we are imported before Jest then it's not there and Jest copies existing globals including dd-trace to all workers.
This is now fixed in the latest release. The tracer is now a no-op in Jest workers unless it was required at the process level to avoid it being re-initialized many times.
I just re-ran it on my sample repo and it looks like the runtime is back down to 374.427 s, which is what I'd expect! We'll test on our real application as well, thank you!
Activity
juan-fernandez commentedon Aug 18, 2022
hi @blimmer ! Thanks for reporting this and creating a reproducible case. I'm currently working on a PR that should hopefully fix this. I'll keep you updated
juan-fernandez commentedon Aug 18, 2022
hi @blimmer ! I jumped to a conclusion too early. I thought this was an issue with the
jestplugin but it isn't. The plugin does not start in the example.We've identified the actual issue though. While we find a solution there's a workaround that fixes the memory leak. You may set the
testEnvironmentconfig parameter (https://jestjs.io/docs/configuration#testenvironment-string) to this file:This
testEnvironmentcan be passed as a cli parameter like:With
testEnvironment.jsbeing on the root of the project.This should stop
'dd-trace'from being instantiated on every test.We'll let you know once we fix the issue! 😄
rochdev commentedon Aug 19, 2022
I looked into this a bit more and the problem is that Jest runs tests somewhat in isolation, so the tracer is re-imported on every test. This was already true before, but with the new plugin manager that is based on
diagnostics_channel, it means that we keep registering new subscribers for each module we instrument, and these are never removed since Jest keeps built-in modules untouched by default. This is not the root cause however and is just exposing the actual issue more.The main issue in fixing this is that we're basically fighting Jest. Since we can't rely on
globalactually being global, we'd need to store the tracer elsewhere, for example in a built-in module since these are actually global even with Jest. However, Jest is also able to mock those, which would yet again break the tracer. The same is true for any other module that the tracer relies on, especially since we patch the internals that may not be correct if mocked.The best way to handle this right now is to make sure the tracer is loaded before Jest, so instead of loading dd-trace programatically it should be loaded with
NODE_OPTIONS='-r dd-trace/init'(orNODE_OPTIONS='-r dd-trace/ci/init'when using CI Visibility. This will ensure that dd-trace is loaded before Jest and the same instance is included in every test.@juan-fernandez This is not something that we can realistically handle since multiple instances of the tracer will always be problematic. Should we instead disable tracing completely when the load order is incorrect?
juan-fernandez commentedon Aug 19, 2022
how would we know it's incorrect? Checking if we're in
jestandglobal._ddtraceis undefined?rochdev commentedon Aug 19, 2022
@juan-fernandez Yes, when running inside of a Jest runner there is a global
jestvariable available, and when we are imported before Jest then it's not there and Jest copies existing globals including dd-trace to all workers.juan-fernandez commentedon Aug 29, 2022
this should be fixed in #2316. Pinging @rochdev to confirm.
@blimmer there's going to be a release soon for you to check this 😄
rochdev commentedon Aug 29, 2022
This is now fixed in the latest release. The tracer is now a no-op in Jest workers unless it was required at the process level to avoid it being re-initialized many times.
blimmer commentedon Aug 29, 2022
I just re-ran it on my sample repo and it looks like the runtime is back down to 374.427 s, which is what I'd expect! We'll test on our real application as well, thank you!