-
-
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
Fix a leak in coverage #5289
Fix a leak in coverage #5289
Conversation
packages/jest-runtime/src/index.js
Outdated
getAllCoverageInfo() { | ||
return this._environment.global.__coverage__; | ||
getAllCoverageInfoCopy() { | ||
return JSON.parse(JSON.stringify(this._environment.global.__coverage__)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably the worst way to do this but I figured I would push and ask if there's a preferred way to deep copy objects in this project. I tried deepCyclicCopy
from jest-utils
but that did not work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the curious, this is what the object looks like: https://gist.github.com/rickhanlonii/337bb5310f827cef35ec2d329edbfc3b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rickhanlonii What was the issue with deepCyclicCopy
? It's true it's too basic to copy RegExp
, Date
s and such, but being JSON copy-able, it should work out of the box 😔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking at the code, what can be happening is that we create the copied object using the prototype of the original one, so the instanceof
operator still works for the copied one (this becomes especially important when creating copies of objects like process
).
But, in turn, it can be preventing the memory to release, which I guess it's why it did not work for you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that makes sense, that is what's happening
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for once again digging into this!
This is failing flow, run yarn flow
locally to see it (based on your second commit, I guess the typing is incorrect).
Can you add an entry to the changelog?
/cc @mjesun for the question about deepCyclicCopy
@SimenB what is the question regarding |
NVM, just saw the comment (collapsed after a change). |
Codecov Report
@@ Coverage Diff @@
## master #5289 +/- ##
==========================================
+ Coverage 61.22% 61.25% +0.02%
==========================================
Files 205 205
Lines 6894 6893 -1
Branches 4 3 -1
==========================================
+ Hits 4221 4222 +1
+ Misses 2672 2670 -2
Partials 1 1
Continue to review full report at Codecov.
|
Could you make a gist with the data structures that are being cloned again? The comment from earlier got lost. I'd like to take a look first. |
@cpojer here's the gist, the Flow type is defined here |
@rickhanlonii Yes, that sounds good to me. To be honest, now that I look at the implementation, we're behaving differently when cloning an
I would add the flag as you suggested, then correct the I also want to say I'm actually amazed that you found this leak. This is going to hugely improve large test collections, especially the ones relying on |
Thanks @mjesun! Updated with the change to use deepCyclicCopy I didn't update for |
Good catch on the
To avoid the issue, I would suggest making the new const newArray = options.keepPrototype
? new (Object.getPrototypeOf(array).constructor)(array.length)
: []; This will create a new All arrays have their prototype equal to |
Great point about the iFrame @mjesun Updated to that version and it works great. For testing, I used array-like objects and mocked Array.isArray. Take a look and let me know if you would recommend a different approach I also ran the before and after on React to get an idea of the memory improvement here, it's hard to compare but it seems like a pretty big improvement (note that I ran this three times to rule out normal variations, this should be a fair sample) |
To anyone following along, here's a good explination of what we're talking about for extending arrays |
LGTM! Considering the green check from @SimenB I'll merge it. Thanks a lot! |
spy.mockRestore(); | ||
}); | ||
|
||
it('does not keep the prototype of arrays when keepPrototype = false', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is a duplicate of above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One asserts the default behavior and the other with the flag explicitly passed in
Do you think that's too much?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wow good catch -- the second test should have:
const copy = deepCyclicCopy(sourceArray, {keepPrototype: false});
const length = array.length; | ||
|
||
cycles.set(array, newArray); | ||
|
||
for (let i = 0; i < length; i++) { | ||
newArray[i] = deepCyclicCopy(array[i], EMPTY, cycles); | ||
delete options.blacklist; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be good moving this out of the loop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Woah, this is a bug -- it deletes the blacklist on the first key iteration so only the first key can be blacklisted
Awesome! |
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Summary
This PR fixes a memory leak in coverage reporting
Fixes #5285
Details
From the reported issue, there's a minimal example that shows any test ran with coverage will report a leak when using
--detectLeaks
. This time I assumed the tool was correct and went directly to heapdumps:Dump Before (without coverage)
Dump After (with coverage)
As you can see, tests with coverage are leaving behind the Window object.
That convinced me that the issue was with coverage and not with
--detectLeaks
reporting. From there I basically looked over coverage related areas and started commenting them out to see if the tests would pass (in the biz this is known and plug and chug)Eventually, I found that commenting out the enclosed line passed the tests and realized that we were keeping around references to the whole test result so that we could process the coverage for each file after they all run. The fix is to make a copy of the coverage results.
Test plan
Considering this test repo:
jest --detectLeaks module.spec.js
from master will passjest --detectLeaks module.spec.js --coverage
from master will fail:On my branch this test will pass:
For the curious, here's the heapdump after the fix:
No more Window 💥