Skip to content
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

[WIP] Restructure module for thread safety using context aware initialization #15

Merged
merged 4 commits into from Jul 3, 2020

Conversation

Widdershin
Copy link
Collaborator

@Widdershin Widdershin commented Jul 2, 2020

Previously, this module would cause an error if loaded in two separate
contexts, e.g. the main thread and a worker thread.

We can use the NODE_MODULE_INIT macro to establish our module as being
context aware.

Next, we need to remove our reliance upon static shared data, especially
any v8 objects. Accessing a v8 object across isolates causes a cryptic
runtime error.

In accordance with the Node native extensions docs, we create a separate
class to store mutable data per thread, and shift all shared v8 objects
into this structure (ProfilerData).

TODO:

  • Add tests
  • Make heap sampling thread safe
  • Extensive manual testing
  • Cross version testing
  • Check version ifdefs are still correct

I'm not a C++ dev by trade so apologies if I've made any super obvious mistakes. I'm happy to make changes based on feedback if I've taken a wrong turn somewhere.

…lization

Previously, this module would cause an error if loaded in two separate
contexts, e.g. the main thread and a worker thread.

We can use the NODE_MODULE_INIT macro to establish our module as being
context aware.

Next, we need to remove our reliance upon static shared data, especially
any v8 objects. Accessing a v8 object across isolates causes a cryptic
runtime error.

In accordance with the v8 native extensions docs, we create a separate
class to store mutable data per thread, and shift all shared v8 objects
into this structure (ProfilerData).
@hyj1991
Copy link
Owner

hyj1991 commented Jul 2, 2020

Previously, this module would cause an error if loaded in two separate
contexts, e.g. the main thread and a worker thread.

Is there a reproduce?

@Widdershin
Copy link
Collaborator Author

Widdershin commented Jul 2, 2020

const {Worker, isMainThread, parentPort} = require('worker_threads');
const profiler = require('v8-profiler-node8');

const workerCode = `
  const profiler = require('v8-profiler-node8');
  const {parentPort} = require('worker_threads');

  profiler.startProfiling('worker');

  function work() {
    for (let i = 0; i < 1e7; i++) {}
  }
  work();

  const profile = profiler.stopProfiling('worker');

  parentPort.postMessage(JSON.stringify(profile));
`;

profiler.startProfiling('main');
const worker = new Worker(workerCode, {eval: true});

worker.on('message', (m) => {
  const profile = profiler.stopProfiling('main');
  console.log('main thread profile', profile);
  console.log('worker thread profile', m);
});

worker.on('error', (e) => {
  throw e;
});

Using Node v12.18.2, results in this error:

/Users/nick/Projects/$PROJECT/test.js:30
  throw e;
  ^
internal/modules/cjs/loader.js:1188
  return process.dlopen(module, path.toNamespacedPath(filename));
                 ^

Error: Module did not self-register: '/Users/nick/Projects/$PROJECT/node_modules/v8-profiler-node8/build/binding/Release/node-v72-darwin-x64/profiler.node'.
    at Object.Module._extensions..node (internal/modules/cjs/loader.js:1188:18)
    at Module.load (internal/modules/cjs/loader.js:986:32)
    at Function.Module._load (internal/modules/cjs/loader.js:879:14)
    at Module.require (internal/modules/cjs/loader.js:1026:19)
    at require (internal/modules/cjs/helpers.js:72:18)
    at Object.<anonymous> (/Users/nick/Projects/$PROJECT/node_modules/v8-profiler-node8/v8-profiler.js:4:15)
    at Module._compile (internal/modules/cjs/loader.js:1138:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1158:10)
    at Module.load (internal/modules/cjs/loader.js:986:32)
    at Function.Module._load (internal/modules/cjs/loader.js:879:14)

With this PR applied, it successfully generates traces on both threads.

@hyj1991
Copy link
Owner

hyj1991 commented Jul 2, 2020

const {Worker, isMainThread, parentPort} = require('worker_threads');
const profiler = require('v8-profiler-node8');

const workerCode = `
  const profiler = require('v8-profiler-node8');
  const {parentPort} = require('worker_threads');

  profiler.startProfiling('worker');

  function work() {
    for (let i = 0; i < 1e7; i++) {}
  }
  work();

  const profile = profiler.stopProfiling('worker');

  parentPort.postMessage(JSON.stringify(profile));
`;

profiler.startProfiling('main');
const worker = new Worker(workerCode, {eval: true});

worker.on('message', (m) => {
  const profile = profiler.stopProfiling('main');
  console.log('main thread profile', profile);
  console.log('worker thread profile', m);
});

worker.on('error', (e) => {
  throw e;
});

Using Node v12.18.2, results in this error:

/Users/nick/Projects/$PROJECT/test.js:30
  throw e;
  ^
internal/modules/cjs/loader.js:1188
  return process.dlopen(module, path.toNamespacedPath(filename));
                 ^

Error: Module did not self-register: '/Users/nick/Projects/$PROJECT/node_modules/v8-profiler-node8/build/binding/Release/node-v72-darwin-x64/profiler.node'.
    at Object.Module._extensions..node (internal/modules/cjs/loader.js:1188:18)
    at Module.load (internal/modules/cjs/loader.js:986:32)
    at Function.Module._load (internal/modules/cjs/loader.js:879:14)
    at Module.require (internal/modules/cjs/loader.js:1026:19)
    at require (internal/modules/cjs/helpers.js:72:18)
    at Object.<anonymous> (/Users/nick/Projects/$PROJECT/node_modules/v8-profiler-node8/v8-profiler.js:4:15)
    at Module._compile (internal/modules/cjs/loader.js:1138:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1158:10)
    at Module.load (internal/modules/cjs/loader.js:986:32)
    at Function.Module._load (internal/modules/cjs/loader.js:879:14)

With this PR applied, it successfully generates traces on both threads.

Thanks, I'll take a look and get back to you :)

@Widdershin
Copy link
Collaborator Author

Heads up in my testing it looks like this PR can cause segfaults right now, still some work to do before it's stable. Will look into that tomorrow.

@hyj1991
Copy link
Owner

hyj1991 commented Jul 2, 2020

This is because dlopen result will be cached, so in the worker thread when require('v8-profiler-node8') the second time, it doesn't trigger node_module_register.

I think NODE_MODULE_INITIALIZER may be helpful.

@Widdershin
Copy link
Collaborator Author

Widdershin commented Jul 3, 2020

NODE_MODULE_INITIALIZER is definitely much better, I've swapped to that.

Adding in a segfault handler made it clear that the existing segfault remains in the currently untouched heap profiler, and disabling it fixes the segfault.

I'm going to have a go at making the same sort of changes to the heap profiler.

PID 49805 received SIGSEGV for address: 0x1721
0   segfault-handler.node               0x0000000102da0056 _ZL16segfault_handleriP9__siginfoPv + 310
1   libsystem_platform.dylib            0x00007fff70a3c42d _sigtramp + 29
2   node                                0x0000000100000000 __dso_handle + 0
3   profiler.node                       0x00000001042033a5 _ZN5nodex12HeapProfiler10InitializeEN2v85LocalINS1_6ObjectEEE + 885
4   profiler.node                       0x0000000104200e15 node_register_module_v72 + 101
5   node                                0x000000010007a275 _ZNSt3__110__function6__funcIZN4node7binding6DLOpenERKN2v820FunctionCallbackInfoINS4_5ValueEEEE3$_0NS_9allocatorISA_EEFbPNS3_4DLibEEEclEOSE_ + 821
6   node                                0x00000001000790c0 _ZN4node11Environment12TryLoadAddonEPKciRKNSt3__18functionIFbPNS_7binding4DLibEEEE + 320
7   node                                0x0000000100078e9a _ZN4node7binding6DLOpenERKN2v820FunctionCallbackInfoINS1_5ValueEEE + 634
8   node                                0x000000010024a488 _ZN2v88internal25FunctionCallbackArguments4CallENS0_15CallHandlerInfoE + 616
9   node                                0x0000000100249a49 _ZN2v88internal12_GLOBAL__N_119HandleApiCallHelperILb0EEENS0_11MaybeHandleINS0_6ObjectEEEPNS0_7IsolateENS0_6HandleINS0_10HeapObjectEEESA_NS8_INS0_20FunctionTemplateInfoEEENS8_IS4_EENS0_16BuiltinArgumentsE + 521
10  node                                0x00000001002491b2 _ZN2v88internalL26Builtin_Impl_HandleApiCallENS0_16BuiltinArgumentsEPNS0_7IsolateE + 258
11  node                                0x00000001009ce9d9 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_BuiltinExit + 57

Copy link
Owner

@hyj1991 hyj1991 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plan to release 7.x@next tag, but need to fix ci on windows/add test for worker thread first

using v8::Isolate;

ProfilerData::ProfilerData(v8::Local<Context> newContext, Isolate* isolate) {
context = newContext;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context seems not used here?

@hyj1991 hyj1991 merged commit a2c5538 into hyj1991:master Jul 3, 2020
@hyj1991
Copy link
Owner

hyj1991 commented Jul 9, 2020

This feature landed in 7330e00 with the next tag:

npm i v8-profiler-node8@next --save

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants