-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
Report async tasks to V8 Inspector #13870
Conversation
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.
/cc @nodejs/async_hooks @nodejs/v8-inspector
src/async-wrap.cc
Outdated
if (!args[2]->IsBoolean()) | ||
return env->ThrowTypeError("third argument must be a boolean"); | ||
|
||
Local<String> taskName = args[0]->ToString(env->context()).ToLocalChecked(); |
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 you already checked args[0]->IsString()
, there’s no need for an explicit cast here; you can just use taskName = args[0].As<String>()
.
src/async-wrap.cc
Outdated
String::Value taskNameValue(taskName); | ||
StringView taskNameView(*taskNameValue, taskNameValue.length()); | ||
|
||
int64_t taskId = args[1]->IntegerValue(env->context()).FromJust(); |
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.
Same here, .As<Integer>()->Value()
should be fine instead of a cast.
Also, intptr_t
?
src/async-wrap.cc
Outdated
|
||
int64_t taskId = args[1]->IntegerValue(env->context()).FromJust(); | ||
void* task = reinterpret_cast<void*>(taskId); | ||
bool recurring = args[2]->BooleanValue(); |
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 you use BooleanValue()
, you’ll want the overload that takes a context
argument like above (but again, no need for casting; I’m going to stop commenting that now).
src/inspector_agent.cc
Outdated
@@ -527,6 +527,28 @@ class NodeInspectorClient : public v8_inspector::V8InspectorClient { | |||
return channel_.get(); | |||
} | |||
|
|||
// Async stack traces instrumentation. | |||
void AsyncTaskScheduled(const StringView& taskName, void* task, | |||
bool recurring) { |
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.
Style nit: Can you align the arguments vertically?
lib/async_hooks.js
Outdated
|
||
// TODO(bajtos): enable the hook when Inspector is started, | ||
// disable it when Inspector is stopped | ||
inspectorHook.enable(); |
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 this code might be better in lib/inspector.js
?
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 definitely like the idea here but we should definitely ask the question: should this be turned on by default or on demand only? Should there be an API for disabling it if it is on by default?
I'd vote for on by default, no api for disabling.
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, the hook should be always enabled when debugging with no public API to control this hook. By default, the hook should be disabled. When the Inspector is started, the hook should be enabled. When the Inspector is stopped, the hook should be disabled.
The reason why this is not implemented in my patch yet, is that I haven't figured out how to make that happen.
Here is my current thinking:
- I'll move the hook to
lib/internal/inspector-async-hook.js
and export the hook returned bycreateHook
. - Then I need to access this exported hook (a javascript object) from
src/inspector_agent.cc
- how to do this? Agent::Start
should callenable
on the exported hookAgent::Stop
should calldisable
on the exported hook
Thoughts?
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 accessing the hook; you could set up 2 persistent Function properties on the Environment (like the async_hooks_blah functions in env.h), then add a binding method to the inspector module/inspector_agent.cc that takes two parameters and sets the fields on the Environment accordingly.
One of these functions would be basically () => hook.enable()
, the other () => hook.disable()
. You could then grab those methods in Agent::Start
or Agent::Stop
off the Environment instance.
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.
@addaleax Thank you, this make sense. There is one missing piece - where to place the JS code glueing this all together. I think it should go to lib/internal/bootstrap_node.js
, right?
Since the environment always have an instance of inspector Agent
, I feel it's better to setup these two persistent Functions properties on Agent
class instead. What do you think, can you foresee any issues with 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.
There is one missing piece - where to place the JS code glueing this all together. I think it should go to
lib/internal/bootstrap_node.js
, right?
@bajtos I guess? Maybe just do it that way for now and see whether somebody has anything to comment on that.
Since the environment always have an instance of inspector
Agent
, I feel it's better to setup these two persistent Functions properties onAgent
class instead. What do you think, can you foresee any issues with that?
Not really, but I’m not as familiar with the inspector’s semantics as with the rest of the C++ code. It’s a bit easier to do it on the environment because there are already quite a few of these that can be accessed in a standard way.
src/async-wrap.cc
Outdated
env->SetMethod(target, "asyncTaskScheduled", AsyncTaskScheduled); | ||
env->SetMethod(target, "asyncTaskCanceled", AsyncTaskCanceled); | ||
env->SetMethod(target, "asyncTaskStarted", AsyncTaskStarted); | ||
env->SetMethod(target, "asyncTaskFinished", AsyncTaskFinished); |
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.
Same here, I think it would be better to keep this code in the inspector cc files?
@addaleax hey, that was fast, thank you for the review! I fixed the coding style issue you have pointed, see the two new commits above. Regarding where to keep the AsyncTask-related code: I guess it makes sense to move the C++ stuff from However, I am not so sure about moving the javascript part to Thoughts? |
Should I perhaps place the JS part into |
src/async-wrap.cc
Outdated
String::Value taskNameValue(taskName); | ||
StringView taskNameView(*taskNameValue, taskNameValue.length()); | ||
|
||
intptr_t taskId = args[1].As<Integer>()->Value(); |
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.
Style issues: snake_case, not camelCase, for locals.
src/async-wrap.cc
Outdated
|
||
intptr_t taskId = args[0].As<Integer>()->Value(); | ||
env->inspector_agent()->AsyncTaskFinished(reinterpret_cast<void*>(taskId)); | ||
} |
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.
You could DRY these a little by using a template function. As well, this probably needs to be inside a #if HAVE_INSPECTOR
guard. ./configure --without-inspector
should build/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.
You could DRY these a little by using a template function
Good point. I am not sure if I understand your idea correctly, I was able to DRY these functions with a helper function that does not require any template:
static void invokeAsyncTaskFnWithId(void (Agent::*asyncTaskFn)(void*),
const FunctionCallbackInfo<Value>& args);
Strangely enough, now that I moved this code from src/async-wrap.cc
to src/inspector_agent.cc
, ./configure --without-inspector
seems to work fine even with not #if HAVE_INSPECTOR
guard. Perhaps src/inspector_agent.cc
is excluded from compilation? Having said that, I'll probably need to modify the javascript part.
That seems perfectly fine to me. 👍 |
src/inspector_agent.cc
Outdated
if (!args[1]->IsNumber()) | ||
return env->ThrowTypeError("second argument must be a number"); | ||
if (!args[2]->IsBoolean()) | ||
return env->ThrowTypeError("third argument must be a 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.
If these errors are intended to be seen by end users:
(a) it would be better if the type checking were done in js land if possible
(b) it would be better if they used named arguments rather than "first argument", "second argument", 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.
These errors should be never seen by the user as long as async hooks work correctly. These methods are invoked by our own async hook listener from the methods invoked by async hooks (init
, before
, etc.).
(a) it would be better if the type checking were done in js land if possible
The values are coming from the async_hook module. I could add a type check to my async hook callbacks (init
, before
, etc), but that seems too defensive to me - do we really need to worry about async_hooks not honoring their API contract?
Even if we check the types in js land, I think it's still a good idea to check them in C++ land too, so that we don't crash the process (I assume that V8 will crash the process when we try to cast a JS value to an incompatible C++ type)..
(b) it would be better if they used named arguments rather than "first argument", "second argument", etc
Good point, I'll reword the messages 👍
src/inspector_agent.cc
Outdated
String::Value taskNameValue(taskName); | ||
StringView taskNameView(*taskNameValue, taskNameValue.length()); | ||
|
||
intptr_t taskId = args[1].As<Integer>()->Value(); |
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.
args[1]->IntegerValue(env->context()).ToChecked()
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.
@jasnell Huh? Even if typechecking is moved to JS, this should be correct (as long as the typechecking does its job).
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.
Correct yes, I just prefer the other style.
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 am not very familiar with V8 API and the style preferred in Node.js codebase, I am happy to follow whatever direction you agree on.
src/inspector_agent.cc
Outdated
|
||
intptr_t taskId = args[1].As<Integer>()->Value(); | ||
void* task = reinterpret_cast<void*>(taskId); | ||
bool recurring = args[2].As<Boolean>()->Value(); |
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.
args[2]->BooleanValue(env->context()).ToChecked()
src/inspector_agent.cc
Outdated
Environment* env = Environment::GetCurrent(args); | ||
|
||
if (!args[0]->IsNumber()) | ||
return env->ThrowTypeError("first argument must be a number"); |
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.
Same as above. It would be preferable if the type checking was done in js land if possible, and this should use a named argument.
@ak239 - please take a look. |
Thank you all for your feedback! I cleaned up the code based on your suggestions:
Next step for me: enable/disable the hook from inspector agent, per @addaleax suggestion in #13870 (comment) A possible next step for you, the reviewers: reach a consensus on what style I should use for converting arguments from JS to C++ (see #13870 (comment)), and where to check/assert the type of these arguments (see #13870 (comment)). |
As for typechecking, I think the idea was to After that, you can just use |
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 a lot!
const helper = require('./inspector-helper'); | ||
const assert = require('assert'); | ||
|
||
const script = 'Promise.resolve().then(() => { debugger; });'; |
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.
Inside of V8 we have own promise instrumentation, which is partially based on V8 async hooks, I'm not sure how these two instrumentations will live together, can we check msg.params.asyncStackTrace content in this test?
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.
Good point. I am aware of V8 own promise instrumentation, I think this is the relevant code:
node/deps/v8/src/inspector/v8-debugger.cc
Lines 650 to 654 in 0ba74db
// Async task events from Promises are given misaligned pointers to prevent | |
// from overlapping with other Blink task identifiers. | |
void* task = reinterpret_cast<void*>(id * 2 + 1); | |
void* parentTask = | |
parentId ? reinterpret_cast<void*>(parentId * 2 + 1) : nullptr; |
void* task = reinterpret_cast<void*>(id * 2 + 1);
void* parentTask =
parentId ? reinterpret_cast<void*>(parentId * 2 + 1) : nullptr;
IIUC, this is assigning odd numbers to async tasks related to promises. It makes me wonder whether I should assign even numbers to async tasks coming from Node.js async_hooks, to avoid the situation where the same async task id is used by built-in Promise instrumentation and by Node.js async_hooks based instrumentation?
void * task = reinterpret_cast<void*>(id * 2 + 0); // or (id << 1)
I'll double check what stack trace is reported by Promises now and beef up the assert in this test. I believe that right now, Promises call asyncTaskStarted/Finished
twice - once from built-in V8 instrumentation, second time from the new async_hooks code.
We may be able to detect when async_hooks callback was invoked for a promise event and don't call asyncTask*
API in that case, but I am not sure if it's needed and worth the effort?
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 our current approach for promises are very similar with Node.js approach so in worst case we'll call v8inspector instrumentation twice. Instrumentation on our side is faster because it doesn't need to go through additional Node.js layer. So I think it's better to detect when async_hooks called for promise and filter it here then filter it on inspector side or call it twice.
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 you don't mind, I'd rather avoid filtering out async_hook events triggered for promises, simply because I don't see a reliable way how to detect them. The only async_hook information I have is a string-based type
.
I wrote a simple program to verify what happens when both Node.js and V8 report asyncTask for the same promise:
setInterval(() => {
Promise.resolve().then(() => {
debugger;
console.log('tick')
});
}, 300);
Things seems to work well with the current implementation, see the screenshot below (my test file is called x.js
):
test/inspector/inspector-helper.js
Outdated
|
||
function runTests(harness) { | ||
// Wait for the child to start running the script | ||
setTimeout(() => run(), 1000); |
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 it common way in Node.js to wait 1 sec or we can get some notification when child process is ready?
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, the timeout should not be needed now. The code setting up the inspector session seems to handle this wait already. I'll remove the timeout shortly.
const helper = require('./inspector-helper'); | ||
const assert = require('assert'); | ||
|
||
const script = 'Promise.resolve().then(() => { debugger; });'; |
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.
And can we add a test where we create one promise and then have two different chained callback:
function foo() { debugger; }
var p = Promise.resolve();
p.then(foo);
p.then(foo);
And check that we have correct stacks on both breaks.
src/inspector_agent.cc
Outdated
if (!args[0]->IsNumber()) | ||
return env->ThrowTypeError("taskId must be a number"); | ||
|
||
intptr_t task_id = args[0].As<Integer>()->Value(); |
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 declared anywhere but in inspector we assume that when other clients use our asyncTask* API they use real pointers, or at least something aligned like real pointer, in general it means that your task_id should always be even, you can just multiply it by 2 here.
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 do, I was already thinking about this in #13870 (comment).
lib/internal/inspector_async_hook.js
Outdated
if (!Number.isInteger(asyncId)) | ||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'asyncId', 'integer'); | ||
if (typeof type !== 'string') | ||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'type', 'string'); |
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 don’t think these necessary, you can just do thingslike CHECK(args[0]->IsString());
in C++; it’s a bit shorter, and I think that if type
is ever not a string that’s an internal error in Node, so it should hard crash rather than throwing an exception.
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 these lines based on the feedback from you and @jasnell:
As for typechecking, I think the idea was to
if (…) throw errors.TypeError();
in JS land in any case (and I’m okay with that).
Having said that, I agree that if type
is not a string then it's an internal Node.js error, and I am fine with crashing the process. I'll make the changes you are proposing.
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, sorry – I didn’t realize the binding methods were only called from other internal code.
lib/async_hooks.js
Outdated
|
||
// Integration with V8 Inspector AsyncTask* API | ||
// FIXME(bajtos) move this code to src/inspector_agent.cc | ||
// Agent::Start() should enable the hook, Agent::Stop() should disable it |
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 consider this FIXME a blocker for this PR, let me know if you need any help with it.
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.
Agreed.
I have been looking into this issue in the past days, the problem is more complex than I initially thought. Typically Agent::Start
is called directly from node::Start
, long time before bootstrap_node.js
is loaded. When debugging via --inspect-brk
, bootstrap_node.js
is not started until a debugger is attached.
One option is to enable the hook at the time when a client connects (in Agent::Connect
), plus defer hook enablement until bootstrap_node.js
is called in case of --inspect-brk
, because in case of --inspect-brk
, the client connects before we can create the inspector async hook. However, I find the user experience of this solution rather poor. Because async stack trace tracing is enabled only at the time the debugger connects, any async transitions created at startup time (e.g. like setting up an HTTP server, creating timeouts, etc.) are lost.
Here is my current plan:
- Somewhere in
src/node.cc
, set a flag that can be used bylib/internal/inspector_async_hook
to decide whether the hook should be enabled at Node.js start/boot time. This should nicely handle both--inspect
and--inspect-brk
cases, as long as Node.js bootstrapping process stays synchronous. - At the same time, in order to support
process._startDebug
, I'll still need to pass the "enable" function into C++ land, so that we can enable the hook at runtime when the debugger was enabled via a signal (_startDebug
).
I estimate I'll need few more days to finish this, and since I am on vacation next week, I may not be able to push the update until the week of July 10th.
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.
Typically
Agent::Start
is called directly fromnode::Start
, long time beforebootstrap_node.js
is loaded. When debugging via--inspect-brk
,bootstrap_node.js
is not started until a debugger is attached.
To rephrase the issue, you can't create a hook until after bootstrap_node.js
has run, and this is an issue with --inspect-brk
because the hook won't be in place in time.
I find the user experience of this solution rather poor. Because async stack trace tracing is enabled only at the time the debugger connects, any async transitions created at startup time (e.g. like setting up an HTTP server, creating timeouts, etc.) are lost.
If you were to add a setupInspectorHook()
as the first call to startup()
in lib/internal/bootstrap_node.js
it would force the hook to be enabled before any possible asynchronous tasks are performed.
At the same time, in order to support
process._startDebug
, I'll still need to pass the "enable" function into C++ land, so that we can enable the hook at runtime when the debugger was enabled via a signal (_startDebug
).
To double check, this wouldn't start capturing async stack traces until after the process has been signaled, but on the same note does Chrome always record all async stack traces so they are always available after the inspector connects?
lib/internal/inspector_async_hook.js
Outdated
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'asyncId', 'integer'); | ||
if (typeof type !== 'string') | ||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'type', 'string'); | ||
// TODO(bajtos): support recurring tasks like setInterval |
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.
Am I correct to assume that https://v8docs.nodesource.com/node-8.0/df/d37/classv8__inspector_1_1_v8_inspector.html is the most “documentation” we get for the V8 inspector API? If yes, I am not sure how we would want to use it to report recurring tasks… it’s not just setInterval, it’s a lot of things that create multiple events (TCP/UDP servers, file streams, zlib, 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.
What is the consequence if we just set it to true
for all event types?
Using false
does not make sense because of setInterval
etc.. But even for setInterval
it can't be guaranteed that it will be recurring
, thus true
should always work. I guess it will just be a bit inefficient.
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 am not aware of any better documentation than the source code. I don't have any strong opinion on what's the best value to use, I am ok with using 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.
As long as Node.js calls asyncTaskCanceled, using recurring = true is fine.
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.
Biggest blocker I see is the usage of intptr_t
on 32bit builds. Hopefully I'm overlooking something that won't make that a problem.
lib/internal/inspector_async_hook.js
Outdated
}, | ||
|
||
destroy(asyncId) { | ||
if (!Number.isInteger(asyncId)) |
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 only way this could fail is if the hook callbacks are being called manually. Otherwise it's guaranteed that
asyncId > 0 && Number.isInteger(asyncId)
. Same with type
above that it guarantees
typeof type === 'string' && type.length > 0
src/inspector_agent.cc
Outdated
String::Value taskNameValue(taskName); | ||
StringView taskNameView(*taskNameValue, taskNameValue.length()); | ||
|
||
intptr_t task_id = args[1]->IntegerValue(env->context()).FromJust(); |
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.
Looks like you're using intptr_t
because of the conversion to an even void*
, but I believe this will fail quickly on 32bit builds b/c INTPTR_MAX == 2147483647
while asyncId
is internally stored as a double
. You could test this out by the following:
'use strict';
const { async_uid_fields, constants: { kAsyncUidCntr }} = process.binding('async_wrap');
const { newUid } = require('async_hooks');
async_uid_fields[kAsyncUidCntr] = -1 >>> 1;
process._rawDebug(newUid() > -1 >>> 1);
Use this code along with calling AsyncTaskScheduleWrapper()
on a 32bit build.
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.
@trevnorris Do you have any alternative suggestions? The inspector API doesn’t seem to allow anything more flexible here. I would be okay with just accepting this as a limitation.
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 get help from @nodejs/v8-inspector to make this more flexible. Ideally, it would support an arbitrary 64bit id.
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 be okay with just accepting this as a limitation.
Creating a new id every μs would cause this to run out in under 20 mins. I'm at a loss to see how we can word around this limitation and have this remotely functional.
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.
@trevnorris Not sure, my math seems to give us about 35 minutes ;) My thoughts are more in the line of: 32-bit systems are getting rarer, esp. for developers, and processes that actually create async IDs at that rate are probably not the ones that get debugged most frequently. Plus, no 32-bit system could actually handle that many async resources at a time; it’s likely that most stack traces would still be accurate.
I’ll try to figure out how to bring the inspector source code to support a larger range of values; it seems pretty doable, I may be able to get a PR up today.
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.
I'm not sure that on inspector side we'd like to support arbitrary taskId:
On Inspector side we are trying to limit async stack traces memory usage, we dedup frames, stacks and limit maximum amount of async stacks which can be stored at particular moment of time (currently it's 128*1024). We cleanup old stacks first, so it means that at moment when Node.js reports 128*1024 + 1 stack we already collected stack trace with id 1. So it means that 2^31 different async task ids should be enough (actually 2^16 should be enough). Node.js can just use nodejs_id % 2^31 as id for inspector. We can land this CL later when we figure out how we can effectively avoid cleanup of old stacks but it probably would mean that async stack implementation is moved to V8 and enabled by default not only during debugging.
(edited by @TimothyGu to fix Markdown)
test/inspector/inspector-helper.js
Outdated
}; | ||
|
||
exports.startNodeAndDebuggerViaSignal = function(callback, scriptContents) { | ||
const args = ['-e', `${scriptContents}\nconsole.error('started');`]; |
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.
Might want to use process._rawDebug
instead of console.error
because the latter will trigger an async event.
Thanks @trevnorris for joining the discussion! I'm leaving for vacation later today, I'll review and respond to your comments when I get back, after July 10th. |
Hello 👋 I have reworked the initialisation of inspector async hook:
Could you PTAL at my changes again please? |
lib/internal/inspector_async_hook.js
Outdated
}); | ||
|
||
exports.setup = function() { | ||
inspector.registerAsyncHook(() => hook.enable(), () => hook.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.
Right now, we never use the disable
callback, because AFAICT, the inspector mode is never disabled after it has been enabled. Am I missing something? Should I remove disable
callback from the C++ layer to simplify things in the spirit of YAGNI?
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 it is removed, possibly add a comment above this line explaining why disable()
is never used.
Not to be done in this PR, but just a note for the future. We should really create an object or similar that has these. e.g. if (process.flags['--inspect']) { }
// Or a call that automatically does `-_` replacement
process.hasExecFlag = function hasExecFlag(flag) {
if (typeof flag !== 'string') throw new TypeError('must be a string');
flag = flag.replace('_', '-');
for (const str of process.execArgv)
if (str === flag) return true;
return false;
};
Exposing it on |
src/inspector_agent.cc
Outdated
FatalError( | ||
"node::InspectorAgent::StartIoThread", | ||
"Cannot enable Inspector's AsyncHook, please report this."); | ||
} |
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's been discussion about standardizing how async_hooks
/async_wrap
throws (cc @AndreasMadsen @addaleax), but the behavior in Environment::AsyncHooks::pop_ids and fatalError() (lib/async_hooks.js
) is that we only abort on --abort-on-uncaught-exception
, and otherwise we forcibly exit (without chance of recovery).
I'm down for discussing how this is handled, but consistency would be good. Possibly for this PR follow the other examples, and we can fix how it's handled in another PR.
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 will add this to my list of places where we have inconsistency.
src/inspector_agent.cc
Outdated
StringView taskNameView(*taskNameValue, taskNameValue.length()); | ||
|
||
CHECK(args[1]->IsNumber()); | ||
intptr_t task_id = args[1]->IntegerValue(env->context()).FromJust(); |
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'm worried that IntegerValue()
returns a int64_t
but intptr_t
is a int32_t
on ia32 builds. This will quickly run out of space to properly store the async ids.
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.
@trevnorris Did you see #13870 (comment)?
On Inspector side we are trying to limit async stack traces memory usage, we dedup frames, stacks and limit maximum amount of async stacks which can be stored at particular moment of time (currently it's 128*1024). We cleanup old stacks first, so it means that at moment when Node.js reports 128*1024 + 1 stack we already collected stack trace with id 1. So it means that 2^31 different async task ids should be enough (actually 2^16 should be enough). Node.js can just use nodejs_id % 2^31 as id for inspector. We can land this CL later when we figure out how we can effectively avoid cleanup of old stacks but it probably would mean that async stack implementation is moved to V8 and enabled by default not only during debugging.
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 goes back to your original comment in #13870 (comment).
I acknowledge the truncation can be a problem in theory. In practice, we should be fine because of #13870 (comment). Even if it was not true, I personally think it's better to have async stack traces in the first ~20 minutes of the debugging session than to not have any async stack traces at all.
However, to make this truncation issue more obvious, I'll use int64_t
type here, and move the truncation into GetAsyncTask
function, together with an explanatory comment.
src/inspector_agent.cc
Outdated
Environment* env = Environment::GetCurrent(args); | ||
|
||
v8::Local<v8::Function> enable_function = args[0].As<Function>(); | ||
v8::Local<v8::Function> disable_function = args[1].As<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.
If either arg is not a v8::Function
then on Release builds we'll be left with a cryptic segfault. Mind adding a CHECK(args[]->IsFunction())
for each argument here?
}, | ||
|
||
destroy(asyncId) { | ||
inspector.asyncTaskCanceled(asyncId); |
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 "canceled" mean that no other "started"/"finished" calls can be made? b/c when I read that it sounds like the async task was supposed to fire a "started", but was destroyed before that happened.
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.
Good point, it looks a bit suspicious to me as well. @ak239 could you perhaps help us to understand this better?
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.
Your understanding is right. asyncTaskCanceled means that asynchronous task would never be executed again (no other started/finished calls) so on inspector side we can cleanup all related to this task data.
lib/internal/inspector_async_hook.js
Outdated
} | ||
|
||
const hook = createHook({ | ||
init(asyncId, type, triggerId, resource) { |
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.
Just to confirm that it's alright with you, while we make sure calls to init()
in core actually mean the event loop has progressed in some way since the call to init()
, the user API allows users to call before()
synchronously. There isn't much that can be done about that, and it's on the user to implement it properly.
Basically, there's nothing actionable. Just want to bring this point up.
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 am afraid I am not able to judge how this fact affects V8 Inspector asyncTask*
APIs. My knowledge of these two parts (Node.js async hooks, V8 asyncTask*) is pretty limited. I am just glueing them together, so to speak.
Rerun is green. Landed in be63c26...a0895ed 🎊 |
PR-URL: #13870 Reviewed-By: Miroslav Bajtoš <mbajtoss@gmail.com>
Implement a special async_hooks listener that forwards information about async tasks to V8Inspector asyncTask* API, thus enabling DevTools feature "async stack traces". The feature is enabled only on 64bit platforms due to a technical limitation of V8 Inspector: inspector uses a pointer as a task id, while async_hooks use 64bit numbers as ids. To avoid performance penalty of async_hooks when not debugging, the new listener is enabled only when the process enters a debug mode: - When the process is started with `--inspect` or `--inspect-brk`, the listener is enabled immediately and async stack traces lead all the way to the first tick of the event loop. - When the debug mode is enabled via SIGUSR1 or `_debugProcess()`, the listener is enabled together with the debugger. As a result, only async operations started after the signal was received will be correctly observed and reported to V8 Inspector. For example, a `setInterval()` called in the first tick of the event will not be shown in the async stack trace when the callback is invoked. This behaviour is consistent with Chrome DevTools. Last but not least, this commit fixes handling of InspectorAgent's internal property `enabled_` to ensure it's set back to `false` after the debugger is deactivated (typically via `process._debugEnd()`). Fixes: #11370 PR-URL: #13870 Reviewed-by: Timothy Gu <timothygu99@gmail.com> Reviewed-by: Anna Henningsen <anna@addaleax.net>
Awesome! I'd like to thank everybody who was involved in this long-lived pull request and helped me to make this happen. I really appreciated your help! |
Belated LGTM, sorry @bajtos, I was vacationing when you asked me to relook. |
Should this get a |
@Trott yes! added it :) |
PR-URL: #13870 Reviewed-By: Miroslav Bajtoš <mbajtoss@gmail.com>
Implement a special async_hooks listener that forwards information about async tasks to V8Inspector asyncTask* API, thus enabling DevTools feature "async stack traces". The feature is enabled only on 64bit platforms due to a technical limitation of V8 Inspector: inspector uses a pointer as a task id, while async_hooks use 64bit numbers as ids. To avoid performance penalty of async_hooks when not debugging, the new listener is enabled only when the process enters a debug mode: - When the process is started with `--inspect` or `--inspect-brk`, the listener is enabled immediately and async stack traces lead all the way to the first tick of the event loop. - When the debug mode is enabled via SIGUSR1 or `_debugProcess()`, the listener is enabled together with the debugger. As a result, only async operations started after the signal was received will be correctly observed and reported to V8 Inspector. For example, a `setInterval()` called in the first tick of the event will not be shown in the async stack trace when the callback is invoked. This behaviour is consistent with Chrome DevTools. Last but not least, this commit fixes handling of InspectorAgent's internal property `enabled_` to ensure it's set back to `false` after the debugger is deactivated (typically via `process._debugEnd()`). Fixes: #11370 PR-URL: #13870 Reviewed-by: Timothy Gu <timothygu99@gmail.com> Reviewed-by: Anna Henningsen <anna@addaleax.net>
PR-URL: #13870 Reviewed-By: Miroslav Bajtoš <mbajtoss@gmail.com>
Implement a special async_hooks listener that forwards information about async tasks to V8Inspector asyncTask* API, thus enabling DevTools feature "async stack traces". The feature is enabled only on 64bit platforms due to a technical limitation of V8 Inspector: inspector uses a pointer as a task id, while async_hooks use 64bit numbers as ids. To avoid performance penalty of async_hooks when not debugging, the new listener is enabled only when the process enters a debug mode: - When the process is started with `--inspect` or `--inspect-brk`, the listener is enabled immediately and async stack traces lead all the way to the first tick of the event loop. - When the debug mode is enabled via SIGUSR1 or `_debugProcess()`, the listener is enabled together with the debugger. As a result, only async operations started after the signal was received will be correctly observed and reported to V8 Inspector. For example, a `setInterval()` called in the first tick of the event will not be shown in the async stack trace when the callback is invoked. This behaviour is consistent with Chrome DevTools. Last but not least, this commit fixes handling of InspectorAgent's internal property `enabled_` to ensure it's set back to `false` after the debugger is deactivated (typically via `process._debugEnd()`). Fixes: #11370 PR-URL: #13870 Reviewed-by: Timothy Gu <timothygu99@gmail.com> Reviewed-by: Anna Henningsen <anna@addaleax.net>
PR-URL: #13870 Reviewed-By: Miroslav Bajtoš <mbajtoss@gmail.com>
Implement a special async_hooks listener that forwards information about async tasks to V8Inspector asyncTask* API, thus enabling DevTools feature "async stack traces". The feature is enabled only on 64bit platforms due to a technical limitation of V8 Inspector: inspector uses a pointer as a task id, while async_hooks use 64bit numbers as ids. To avoid performance penalty of async_hooks when not debugging, the new listener is enabled only when the process enters a debug mode: - When the process is started with `--inspect` or `--inspect-brk`, the listener is enabled immediately and async stack traces lead all the way to the first tick of the event loop. - When the debug mode is enabled via SIGUSR1 or `_debugProcess()`, the listener is enabled together with the debugger. As a result, only async operations started after the signal was received will be correctly observed and reported to V8 Inspector. For example, a `setInterval()` called in the first tick of the event will not be shown in the async stack trace when the callback is invoked. This behaviour is consistent with Chrome DevTools. Last but not least, this commit fixes handling of InspectorAgent's internal property `enabled_` to ensure it's set back to `false` after the debugger is deactivated (typically via `process._debugEnd()`). Fixes: #11370 PR-URL: #13870 Reviewed-by: Timothy Gu <timothygu99@gmail.com> Reviewed-by: Anna Henningsen <anna@addaleax.net>
Notable Changes * build: * Snapshots are now re-enabled in V8 #14875 * console: * Implement minimal `console.group()`. #14910 * deps: * upgrade libuv to 1.14.1 #14866 * update nghttp2 to v1.25.0 #14955 * dns: * Add `verbatim` option to dns.lookup(). When true, results from the DNS resolver are passed on as-is, without the reshuffling that Node.js otherwise does that puts IPv4 addresses before IPv6 addresses. #14731 * fs: * add fs.copyFile and fs.copyFileSync which allows for more efficient copying of files. #15034 * inspector: * Enable async stack traces #13870 * module: * Add support for ESM. This is currently behind the `--experimental-modules` flag and requires the .mjs extension. `node --experimental-modules index.mjs` #14369 * napi: * implement promise #14365 * os: * Add support for CIDR notation to the output of the networkInterfaces() method. #14307 * perf_hooks: * An initial implementation of the Performance Timing API for Node.js. This is the same Performance Timing API implemented by modern browsers with a number of Node.js specific properties. The User Timing mark() and measure() APIs are implemented, as is a Node.js specific flavor of the Frame Timing for measuring event loop duration. #14680 * tls: * multiple PFX in createSecureContext [#14793](#14793) * Added new collaborators: * BridgeAR – Ruben Bridgewater PR-URL: #15308
Notable Changes * build: * Snapshots are now re-enabled in V8 #14875 * console: * Implement minimal `console.group()`. #14910 * deps: * upgrade libuv to 1.14.1 #14866 * update nghttp2 to v1.25.0 #14955 * dns: * Add `verbatim` option to dns.lookup(). When true, results from the DNS resolver are passed on as-is, without the reshuffling that Node.js otherwise does that puts IPv4 addresses before IPv6 addresses. #14731 * fs: * add fs.copyFile and fs.copyFileSync which allows for more efficient copying of files. #15034 * inspector: * Enable async stack traces #13870 * module: * Add support for ESM. This is currently behind the `--experimental-modules` flag and requires the .mjs extension. `node --experimental-modules index.mjs` #14369 * napi: * implement promise #14365 * os: * Add support for CIDR notation to the output of the networkInterfaces() method. #14307 * perf_hooks: * An initial implementation of the Performance Timing API for Node.js. This is the same Performance Timing API implemented by modern browsers with a number of Node.js specific properties. The User Timing mark() and measure() APIs are implemented, as is a Node.js specific flavor of the Frame Timing for measuring event loop duration. #14680 * tls: * multiple PFX in createSecureContext [#14793](#14793) * Added new collaborators: * BridgeAR – Ruben Bridgewater PR-URL: #15308
Notable Changes * build: * Snapshots are now re-enabled in V8 nodejs#14875 * console: * Implement minimal `console.group()`. nodejs#14910 * deps: * upgrade libuv to 1.14.1 nodejs#14866 * update nghttp2 to v1.25.0 nodejs#14955 * dns: * Add `verbatim` option to dns.lookup(). When true, results from the DNS resolver are passed on as-is, without the reshuffling that Node.js otherwise does that puts IPv4 addresses before IPv6 addresses. nodejs#14731 * fs: * add fs.copyFile and fs.copyFileSync which allows for more efficient copying of files. nodejs#15034 * inspector: * Enable async stack traces nodejs#13870 * module: * Add support for ESM. This is currently behind the `--experimental-modules` flag and requires the .mjs extension. `node --experimental-modules index.mjs` nodejs#14369 * napi: * implement promise nodejs#14365 * os: * Add support for CIDR notation to the output of the networkInterfaces() method. nodejs#14307 * perf_hooks: * An initial implementation of the Performance Timing API for Node.js. This is the same Performance Timing API implemented by modern browsers with a number of Node.js specific properties. The User Timing mark() and measure() APIs are implemented, as is a Node.js specific flavor of the Frame Timing for measuring event loop duration. nodejs#14680 * tls: * multiple PFX in createSecureContext [nodejs#14793](nodejs#14793) * Added new collaborators: * BridgeAR – Ruben Bridgewater PR-URL: nodejs#15308
Implement a special async-hook listener that forwards information about async tasks to V8Inspector asyncTask* API.
Fixes: #11370
Note: this is an initial (incomplete) version to gather feedback early.
Here are few important items that I would like to discuss besides whatever other feedback you can provide:
Test coverage. Ideally, we should cover most/all major scenarios, not only
setTimeout
and Promises. OTOH, I'd rather avoid duplicating all async_hook/async_wrap tests, because that would add too much maintenance burden IMO. Initially, I added only two simple tests - one for promises, the second one forsetTimeout
. What other scenarios should I add?Test assertions. Right now, the tests are only checking whether async stack information is present, the correctness of this information is not verified. Do we want the tests to verify it?
How to enable/disable inspector async hook when the inspector is started/stopped? I think it's important to enable this hook only when debugging in order to avoid the performance costs of async hooks. However, I was not able to find the right place where to add enable/disable call. Can anybody point me to the right direction please?
There are two test failures right now, I believe they are caused by the fact that inspector async hook is always enabled, while the tests are relying on the async hook machinery to be disabled by default.
People that may be interested in reviewing these changes, based on the git history (the list is almost certainly incomplete):
@trevnorris @addaleax @AndreasMadsen @matthewloring @eugeneo
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
async_hooks, async_wrap, inspector