-
-
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
Re-inject native Node modules #4970
Conversation
Does this fix #4630? |
Nope; but I'll work on the |
packages/jest-jasmine2/src/index.js
Outdated
// reference after adding some variables to it; but you still need to pass | ||
// it when calling "jasmine2". | ||
if (!environment) { | ||
throw new ReferenceError('Please pass a valid environment object'); |
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.
- valid environment object
+ valid Jest Environment
packages/jest-jasmine2/src/index.js
Outdated
@@ -85,12 +92,21 @@ async function jasmine2( | |||
if (config.resetMocks) { | |||
runtime.resetAllMocks(); | |||
|
|||
if (!environment) { | |||
throw new ReferenceError('Environment should be cleaned at the end'); | |||
} |
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 this be more descriptive?
packages/jest-runtime/src/index.js
Outdated
moduleRegistry = this._moduleRegistry; | ||
} | ||
|
||
let modulePath; |
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 move this to the top?
packages/jest-runtime/src/index.js
Outdated
switch (moduleName) { | ||
case 'buffer': // Causes issues when passing buffers to another context. | ||
case 'module': // Calls into native_module, which is not mockable. | ||
case 'os': // Pure native module. |
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 do we maintain this whitelist?
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 way 😄 . There is no way of knowing that os
is a native module, and that module
cannot be re-evaluated. Essentially trial/error.
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.
Actually, os
module should probably be wrapped with an object on top. Once we get this merged I will open a task for that.
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, so I did a bunch of things in the last commit:
- I added a test suite that requires all native modules, so this will help us to semi-automate this list.
- I also deep copy
os
andv8
modules so that they are sandboxed as well (i.e. if someone modifies them, they won't leak). - And finally I fixed the V8 crash for Node 8 (
async_hooks
can't be re-required).
import createProcessObject from '../create_process_object'; | ||
|
||
it('creates a process object that looks like the original one', () => { | ||
const fakeProcess = createProcessObject(); | ||
|
||
// "process" inherits from EventEmitter through the prototype chain. | ||
expect(fakeProcess instanceof EventEmitter).toBe(true); | ||
expect(typeof fakeProcess.on).toBe('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.
So fakeProcess instanceof EventEmitter
returns false now? Maybe worth to update the comment.
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 unfortunate, but yes. Because EventEmitter
is re-evaluated now, it is not the parent EventEmitter
anymore. In order to get that check right we should build the process object inside the VM context. However, this requires being able to do require('events')
which overcomplicates all the current flow.
The comment is still valid, since we check the existence of the on
& various methods; but I can clarify it.
@@ -314,6 +316,8 @@ exports[`works with node assert 1`] = ` | |||
Message: | |||
Missing expected exception. | |||
|
|||
at assert:1:1 |
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 don't want this in the stack at all, do we?
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.
Since these modules are now re-injected, they will appear in the stack. I don't think it's critical, though.
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 not useful though, so why not remove it? ref #4695
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 do you decide what comes from an internal and what not? Notice that now internals are not internals anymore, because we re-evaluate them 😉
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.
IMO internal === jest or node core
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, but I'm unsure on how can we remove an arbitrary number of stack frames from the stack. How are we doing that now? I can maybe adapt my filenames to the existing way.
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.
Yeah these guys have to go. We need a way to annotate node libs so they don't come up in stack traces.
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.
@SimenB this looks promising! I can inject native modules with native
on the file name, and I think I can get rid of them.
packages/jest-runtime/src/index.js
Outdated
case 'os': // Pure native module. | ||
case 'v8': // Contains invalid references. | ||
// $FlowFixMe: dynamic require needed. | ||
localModule.exports = deepCyclicCopy(require(moduleName)); |
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.
Does it have a noticeable impact on perf? And are we sure there's nothing to blacklist there?
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 checked both; v8
is just a set of functions, and os
has some additional properties (os.constants
), but it simple enough to be copied.
const result = runJest.json('require-all-modules').json; | ||
|
||
if (!result.success) { | ||
console.warn(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.
for debugging? should it be removed?
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 added it so that if a module throws when required, we can know who was based on the console 🙂
@@ -0,0 +1,9 @@ | |||
#!/usr/bin/env node --inspect-brk |
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.
fancy :D
runtime: Runtime, | ||
testPath: string, | ||
): Promise<TestResult> { | ||
// The "environment" parameter is nullable just so that we can clean its |
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.
will this help pinpointing #4710?
Failing on appveyor 🙁 |
Yes, I just debugged it. What is happening is that To be precise, what fails is the execution of any IMO this reporters should be injected outside the VM context (i.e. on the main context); but I wonder if doing so can create other undesirable side effects. cc @cpojer, @SimenB, @thymikee. |
I guess it makes sense to create another hook which works outside of VM context and put jasmine reporters there. |
Yeah, we'll need to fix that issue first before we can merge this PR. |
@@ -14,7 +14,8 @@ const runJest = require('../runJest'); | |||
|
|||
const dir = path.resolve(__dirname, '../failures'); | |||
|
|||
const normalizeDots = text => text.replace(/\.{1,}$/gm, '.'); | |||
const normalize = text => | |||
text.replace(/\.{1,}$/gm, '.').replace(/\bassert:\d+:\d+/g, 'assert:1:1'); |
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.
what is this change for?
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.
assert
can change from one version to another, and since now we re-inject the native module, line numbers can change, thus not making the snapshot to match. This ensures that line numbers are transformed into something consistent across Node versions.
Since I'm going to try make them disappear, that doesn't make sense anymore.
*/ | ||
|
||
it('requires all native modules to check they all work', () => { | ||
Object.keys(process.binding('natives')) |
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.
If in a version of node process.binding
changes to not return anything, this test will still pass but the sandboxing is broken. Can we change this to make sure it requires the right modules?
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.
Everything would be broken then, because we rely on process.binding
already (e.g. is_builtin_module
from jest-resolve
😄
What I'm going to do is to check that the length is greater than the current amount of modules for Node 6, which should be good signaling.
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.
Yep, I'm fine with that. If they ever remove core modules our test will fail, but that's fine and something we'd want to know about anyway.
@@ -25,10 +25,17 @@ const JASMINE = require.resolve('./jasmine/jasmine_light.js'); | |||
async function jasmine2( | |||
globalConfig: GlobalConfig, | |||
config: ProjectConfig, | |||
environment: Environment, | |||
environment: ?Environment, |
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, let's not do this please. For example by renaming the environment
variable below?
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 have to; I need to kill all environment references.
packages/jest-jasmine2/src/index.js
Outdated
@@ -85,12 +92,24 @@ async function jasmine2( | |||
if (config.resetMocks) { | |||
runtime.resetAllMocks(); | |||
|
|||
if (!environment) { | |||
throw new ReferenceError( |
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 there any way that this can actually happen? How about just doing `if (environment && config.timers === 'fake') { … } ?
if (config.timers === 'fake') { | ||
environment.fakeTimers.useFakeTimers(); | ||
} | ||
} | ||
}); | ||
|
||
// Free references to environment to avoid leaks. | ||
env.afterAll(() => { | ||
environment = null; |
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.
Who is holding on to environment
?
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 beforeAll
closure, through environment.fakeTimers.useFakeTimers()
.
packages/jest-runner/src/run_test.js
Outdated
@@ -142,6 +143,11 @@ async function runTestInternal( | |||
}); | |||
} finally { | |||
await environment.teardown(); | |||
await runtime.teardown(); | |||
|
|||
// Free references to enviroment to avoid leaks. |
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.
environment
.
import createProcessObject from '../create_process_object'; | ||
|
||
it('creates a process object that looks like the original one', () => { | ||
const fakeProcess = createProcessObject(); | ||
|
||
// "process" inherits from EventEmitter through the prototype chain. | ||
expect(fakeProcess instanceof EventEmitter).toBe(true); |
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.
Hah, I think this is a bug actually. You are creating the process object with EventEmitter from the parent but it should use runtime._requireNativeModule('events')
so that this test still passes. Changing this test isn't the right fix here imho.
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.
Exactly; I explained that to @thymikee before. The issue here is that in order to get the child EventEmitter
I need to execute in the context require('events').EventEmitter
. Unfortunately at this point neither the context, nor the require implementation are ready; so it's too complex to add the change in this PR.
Since I just re-did the process
object about a week ago, and before it did not inherit from EventEmitter
either, I think we are more or less safe to do this.
However, at the core of the issue, you are both right, the check should pass 🙂
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.
In that case let's change where we are initializing process then? It seems like that should happen later or lazily. You could make process
a lazy getter that, when first accessed, overwrites itself by doing global.process = process
. What do you think about that? We just need to make sure there is nothing in events
or its dependencies that depends on process which would produce a circular dependency.
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 tried making this work, but I ended up with a V8 crash (see below). I think we should revisit it later; but it should be kept out of the scope of this PR.
FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal.
1: node::Abort() [/Users/.../.nvm/versions/node/v8.9.1/bin/node]
2: node::FatalException(v8::Isolate*, v8::Local<v8::Value>, v8::Local<v8::Message>) [/Users/.../.nvm/versions/node/v8.9.1/bin/node]
3: v8::V8::ToLocalEmpty() [/Users/.../.nvm/versions/node/v8.9.1/bin/node]
4: node::(anonymous namespace)::ContextifyScript::RunInContext(v8::FunctionCallbackInfo<v8::Value> const&) [/Users/.../.nvm/versions/node/v8.9.1/bin/node]
5: v8::internal::FunctionCallbackArguments::Call(void (*)(v8::FunctionCallbackInfo<v8::Value> const&)) [/Users/.../.nvm/versions/node/v8.9.1/bin/node]
6: v8::internal::MaybeHandle<v8::internal::Object> v8::internal::(anonymous namespace)::HandleApiCallHelper<false>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::Handle<v8::internal::Object>, v8::internal::BuiltinArguments) [/Users/.../.nvm/versions/node/v8.9.1/bin/node]
7: v8::internal::Builtin_Impl_HandleApiCall(v8::internal::BuiltinArguments, v8::internal::Isolate*) [/Users/.../.nvm/versions/node/v8.9.1/bin/node]
8: 0x2fbc95e0463d
9: 0x2fbc95ef4048
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 file a task to fix this?
const isNativeModule = | ||
moduleName && | ||
((options && options.isNativeModule) || | ||
this._resolver.isCoreModule(moduleName)); |
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.. pretty :P
if (isNativeModule) { | ||
moduleRegistry = this._nativeModuleRegistry; | ||
} else if (options && options.isInternalModule) { | ||
moduleRegistry = this._internalModuleRegistry; |
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 internal module registry is used in Jest for Jest's own core modules like pretty-format
or chalk
. Any reason you can't reuse that one? Why do you need a third one?
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.
Because of clashes. Once you start requiring inside a Jest internal, all the child modules are required inside Jest are internal modules as well (which is fine, because you prevent clashes between test and Jest code).
If Jest requires internal/util
, internal
refers to NPM's module; but if you require it from the Native util
module, it refers to the internal file; so you need a separate require
implementation (as well as a separate registry) to avoid the conflict.
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, that makes sense.
Object.defineProperty(localModule, 'require', { | ||
value: this._createRequireImplementation(filename, options), | ||
}); | ||
|
||
const fileSource = isNativeModule | ||
? // $FlowFixMe: process.binding exists. | ||
process.binding('natives')[filename] |
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 call fast or slow?
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.
Really fast. The following code:
console.time('binding');
for (let i = 0; i < 100000; i++) {
global.fsSourceCode = process.binding('natives').fs;
}
console.timeEnd('binding');
Reports an average of 16ms
per 100,000 calls.
moduleRegistry: ModuleRegistry, | ||
from: Path, | ||
) { | ||
switch (moduleName) { |
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 like it.
packages/jest-runtime/src/index.js
Outdated
// use native resolution. Native resolution differs from standard | ||
// resolution on how requires are resolved (e.g. the "util" module | ||
// requires "internal/util", which is not the NPM "internal" module, | ||
// but something else). |
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 this comment, how about:
Use a special resolution when requiring node's internal modules. For example the "util" module requires "internal/util" which isn't a physical directory.
It's shorter and conveys the same 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.
This is great work, I'm so excited for better sandboxing and fewer memory leaks in Jest. Can you work on some of the inline comments that I left?
Before merging this, here are some more high-level questions that would be great to answer:
- Can you show results of
--logHeapUsage -i
before and after this change? - How does this affect performance of Jest?
- Can we reduce this to only two module registries (internal and user) vs. three?
@@ -33,7 +33,7 @@ export type Options = {| | |||
collectCoverage: boolean, | |||
collectCoverageFrom: Array<Glob>, | |||
collectCoverageOnlyFrom: ?{[key: string]: boolean, __proto__: null}, | |||
isCoreModule?: boolean, | |||
isNativeModule?: 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.
why change the name? "native module" in my mind is native addons (https://nodejs.org/api/addons.html), not node core modules.
I suppose it's not public API, but still
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 already use a isCoreModule
in packages/jest-resolve/src/index.js
, which does not quite match the meaning of isNativeModule
. When I noticed it I decided to rename all the new additions.
|
Huh, can you try to run again with |
As explained, it is expected based on where the memory is being measured. |
Added a new commit (also rebased over master); modifying where memory is measured. Memory consumption went down from 156 to 143 MB (i.e ~10% less memory 🙂) while measured in Jest repository. However, the memory still grows within the first tests, up until it stabilizes around 143 MB. Some simple snapshots between tests (running with |
Codecov Report
@@ Coverage Diff @@
## master #4970 +/- ##
==========================================
+ Coverage 60.25% 60.27% +0.01%
==========================================
Files 198 198
Lines 6623 6659 +36
Branches 4 3 -1
==========================================
+ Hits 3991 4014 +23
- Misses 2632 2645 +13
Continue to review full report at Codecov.
|
}; | ||
|
||
let runtime = new Runtime( | ||
config, |
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 think it's time to turn this constructor into something that takes an object.
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.
Should we make this change now? This would break Runtime
's interface; that's why I thought it was better to keep it as-is.
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.
No, that should be a follow-up but we could do it for Jest 22.
packages/jest-runner/src/run_test.js
Outdated
// Delay the resolution to allow log messages to be output. | ||
return new Promise(resolve => { | ||
setImmediate(() => resolve({leakDetector, result})); | ||
}); | ||
} finally { | ||
await environment.teardown(); | ||
environment.teardown && (await environment.teardown()); | ||
runtime.teardown && (await runtime.teardown()); |
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.
No way. Can your turn these into if statements?
packages/jest-runtime/src/index.js
Outdated
@@ -130,6 +134,8 @@ class Runtime { | |||
this._mockRegistry = Object.create(null); | |||
this._moduleMocker = this._environment.moduleMocker; | |||
this._moduleRegistry = Object.create(null); | |||
this._nativeModuleRegistry = Object.create(null); | |||
this._path = path || '<unknown test file>'; |
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 change this to "unknown entry point"? jest-runtime
can be used as standalone and doesn't have to run test files.
packages/jest-runtime/src/index.js
Outdated
@@ -267,40 +273,68 @@ class Runtime { | |||
return cliOptions; | |||
} | |||
|
|||
teardown() { |
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 this reset
and call it in the constructor also for less duplication?
packages/jest-runtime/src/index.js
Outdated
if (!this._environment) { | ||
throw new Error( | ||
`A module was required after the test suite ${this._path} finished.\n` + | ||
`This usually means that you forgot to clean or mock an async operation`, |
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.
nit: "In most cases this is because an async operation wasn't cleaned up or mocked properly."
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.
Awesome. Please rebase + remove the merged PR + fix some of the nits before merging. I'm still awaiting the memory difference of this on the two large codebases internally, mind sending me that within FB? :)
This should probably have an entry in the changelog |
* Revert "docs: update expect.anything() example (#5007)" This reverts commit ea3fabc. * Revert "Emphasise required return (#4999)" This reverts commit 4f1113c. * Revert "Makes NPM a link like Yarn is (#4998)" This reverts commit aa4f09d. * Revert "Update Timeout error message to `jest.timeout` and display current timeout value (#4990)" This reverts commit 08f8394. * Revert "fix: jest-util should not depend on jest-mock (#4992)" This reverts commit 4e2f41f. * Revert "Update Troubleshooting.md (#4988)" This reverts commit 6414f28. * Revert "Revert "Add the Yammer logo to the 'who is using this' section of the website." (#4987)" This reverts commit 91b104f. * Revert "Make "weak" optional dependency and check it at runtime (#4984)" This reverts commit e00529d. * Revert "Re-inject native Node modules (#4970)" This reverts commit ef55e89.
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. |
This PR improves the sandboxing for native modules. Every time a native require is detected, Jest will:
process.binding('natives')
; which differs from the previous approach whererequire(moduleName)
was done on the main context; and
require
logic to know it's inside a native module (thus,internal/util
will not resolve to the NPMinternal
module, but to the native Node one).This kills all memory leaks related to
process
and native module modification. I have added an integration test with the most common use cases. Executing the test (./jest leak_detection
) on a previous commit will fail because of detected leaks.It also makes the
jest
test suite leak free itself (tried with./jest -i --detectLeaks packages
).