-
-
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
Detect memory leaks #4895
Detect memory leaks #4895
Conversation
types/TestResult.js
Outdated
@@ -138,6 +139,7 @@ export type TestResult = {| | |||
console: ?ConsoleBuffer, | |||
coverage?: RawCoverage, | |||
displayName: ?string, | |||
leaks: LeakDetector | boolean, |
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 doesn't seem like a good type. What does it represent?
'There is a number of things that can leak memory:', | ||
' - Async operations that have not finished (e.g. fs.readFile).', | ||
' - Timers not properly mocked (e.g. setInterval, setTimeout).', | ||
' - Missed global listeners (e.g. process.on).', |
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.
How about: "Global event listeners weren't cleaned up (e.g. process.on)"
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.
With #4904 that one is no longer necessary 😃
@@ -96,6 +96,23 @@ export default class TestScheduler { | |||
}); | |||
return Promise.resolve(); | |||
} | |||
if (testResult.leaks) { | |||
const message = [ | |||
'Your test suite is leaking memory! Please ensure all references are cleaned.', |
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.
I would put a bold statement on top and say "Experimental" etc. Nobody reads jest --help
.
' - Timers not properly mocked (e.g. setInterval, setTimeout).', | ||
' - Missed global listeners (e.g. process.on).', | ||
' - Keeping references to the global scope.', | ||
].join('\n'); |
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.
Can we not use an array and just use backticks + string concatenation like in the rest of Jest? :)
message, | ||
stack: new Error(message).stack, | ||
}); | ||
return Promise.resolve(); |
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.
return
is enough, no? It's already an async function.
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.
All this Promise.resolve()
(not only mine, but all others) are there because of the eslint
consistent-return
rule 😞, but TBH I prefer keeping them and not adding an eslint-disable
.
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.
it's sufficient to just return onFailure(...)
since it's already a promise (same thing can be fixed in testResult.testResults.length
block above)
packages/jest-runner/src/run_test.js
Outdated
// Keeping the core of "runTest" as a separate function (as "runTestHelper") is | ||
// key to be able to detect memory leaks. Since all variables are local to the | ||
// function, when "runTestHelper" finishes its execution, they can all be freed, | ||
// UNLESS someone else is leaking them (and that's why we can detect the leak!). |
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.
"UNLESS something else" not someone.
packages/jest-runner/src/run_test.js
Outdated
// If we had all the code in a single function, we should manually nullify all | ||
// references to verify if there is a leak, which is not maintainable and error | ||
// prone. That's why "runTestHelper" CANNOT be inlined inside "runTest". | ||
async function runTestHelper( |
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.
Can we call it _runTest
or runTestInternal
? I don't like "util" or "helper" in general :D
try { | ||
testSource = fs.readFileSync(path, 'utf8'); | ||
} catch (e) { | ||
return Promise.reject(e); |
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.
Such clowny code.
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.
The function wasn't async at the point it was written 🙂
packages/jest-runner/src/run_test.js
Outdated
@@ -66,6 +69,10 @@ export default (async function runTest( | |||
>); | |||
|
|||
const environment = new TestEnvironment(config); | |||
const environmentLeakDetector = config.detectLeaks |
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.
Can you call this variable just leakDetector
? It's soooo long.
packages/jest-runner/src/run_test.js
Outdated
@@ -66,6 +69,10 @@ export default (async function runTest( | |||
>); | |||
|
|||
const environment = new TestEnvironment(config); | |||
const environmentLeakDetector = config.detectLeaks | |||
? new LeakDetector(environment) | |||
: 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.
null?
packages/jest-runner/src/run_test.js
Outdated
return new Promise(resolve => setImmediate(() => resolve(result))); | ||
await new Promise(resolve => setImmediate(resolve)); | ||
|
||
return result; |
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.
Is this just simplifying the code or is there another reason you made this change?
packages/jest-runner/src/run_test.js
Outdated
|
||
// Resolve leak detector, so that the object is JSON serializable. | ||
if (result.leaks instanceof LeakDetector) { | ||
result.leaks = result.leaks.isLeaking(); |
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.
Ah, now I get it. This seems super hacky. How about instead of doing this we change runTestHelper
to return {leakDetector, result}
and turn result.leaks
into a boolean?
I'm almost sure the coverage report is wrong. According to it, the |
// **EXPERIMENTAL!!** Throws when the context is leaked after executing | ||
// a test. | ||
if (testResult.leaks) { | ||
const message = `Your test suite is leaking memory! Please ensure all references are cleaned. |
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.
Wait, the idea was to include the experimental text in the error message, not in a comment above the error message. The user should see this is experimental right now.
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.
😂 OK, my bad; I thought you wanted me to add it as a comment so that when people looks why it broke, they will realize it's experimental. I will add it to the message.
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, rm the comment :D
if (testResult.leaks) { | ||
const message = `Your test suite is leaking memory! Please ensure all references are cleaned. | ||
|
||
There is a number of things that can leak memory: |
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.
Can you intend this properly and use this style:
const foo = `…` +
`…`;
etc.?
); | ||
|
||
// Resolve leak detector, outside the "runTestInternal" closure. | ||
result.leaks = leakDetector ? leakDetector.isLeaking() : 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.
muuuch cleaner.
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 awesome. Feel free to merge once the text is fixed.
I think this feature can also become really handy for bug reporting. When people report bugs related to memory leaks, we can point them to this flag which will help either us or them find memory leaks in Jest or user code (custom test environments etc.).
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.
Tested this today and works nice! It actually detects some leaks coming from graceful-fs
in our codebase (interestingly and fortunately, they do not surface user tests, at least the most basic ones)
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 missing an update to the changelog.
Also missing tests, but I'm not sure if it makes sense to add any?
@thymikee Yeah, @SimenB Regarding tests, since this measures Jest leaks, the goal is to never have any test leaking; so I don't think it makes sense to make a test for that. In other words, if we create a test that leaks, we should fix what causes the leak instead of keeping it, so the test will become useless after some time. |
Codecov Report
@@ Coverage Diff @@
## master #4895 +/- ##
==========================================
- Coverage 60.36% 60.21% -0.16%
==========================================
Files 198 198
Lines 6608 6620 +12
Branches 4 4
==========================================
- Hits 3989 3986 -3
- Misses 2619 2634 +15
Continue to review full report at Codecov.
|
There's definitely something wrong with the coverage shown by codecov. After executing it locally with the |
@SimenB have you tried on Node 8? |
I'm using node 8.9.1 |
The |
So excited to get this in to track down memory leaks in Jest. |
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. |
Adding the usage of
jest-leak-detector
to the environment object. Whenever the experimental--detectLeaks
flag is passed, aLeakDetector
will be created for theenvironment
object, and it will be checked later. If the result is positive, the test suite fails.There are future plans to try improving the feedback we give to the user; I'm currently talking to someone on the V8 team see if we can do some really cool stuff parsing a heap snapshot taken immediately after. But what we currently have is just the ability of knowing if the
global
object got leaked.Side note 1: please review only the last commit. This branch is put on top of the
jest-leak-detector
branch so that I can already use it, and the changes shown also include that other branch. Once we merge the first, I will rebase that one onto master.Side note 2: if you are curious, some Jest tests actually leak. I haven't looked at them that much, but they mainly look related to file watchers.