-
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
inspector: track async stacks when necessary #16260
Conversation
10d15a4
to
ae2da88
Compare
R= @nodejs/v8-inspector |
src/inspector_agent.cc
Outdated
return; | ||
} | ||
|
||
Local<Value> method = maybe_method.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.
I’d guess you can judge this better than I can, but it seems like quite a bit of this function could be simplified by using a if (!object->Get(…).ToLocal(&var_name)) return;
pattern?
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. Fixed.
With this change, we do async stack tracking only when explicitly requested by the inspector client. This avoids unnecessary overhead for clients that might not be interested in async stack traces. Fixes: nodejs#16180
ae2da88
to
50c0bc1
Compare
src/inspector_agent.cc
Outdated
|
||
Maybe<double> maybe_depth = depth_value->NumberValue(context); | ||
if (maybe_depth.IsNothing()) { | ||
return; |
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 can’t actually be the case since you checked depths_value->IsNumber()
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.
Done.
// Basically, the logic we want to implement is: | ||
// let parsed; try { | ||
// parsed = JSON.parse(string); | ||
// } catch (e) { return; } |
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 a TryCatch
that corresponds to this conceptual try/catch
? If so, it’s not obvious where… or, rather: If there is none, what happens with the exception if JSON.parse
throws?
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 believe that the v8::JSON::Parse
API throws on a parse error (unlike the JavaScript JSON.parse
). This is the comment I am basing this on: https://cs.chromium.org/chromium/src/v8/include/v8.h?type=cs&l=1815.
/cc @nodejs/v8 if my understanding here is incorrect.
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.
Okay, tried it out myself. v8::JSON::Parse
does indeed throw. I will add a TryCatch
.
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.
@ofrobots Fwiw, as I understand it it’s a general principle of the V8 API that if a Maybe
method returns an empty value, there is a corresponding pending 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.
Done.
src/inspector_agent.cc
Outdated
MaybeLocal<String> string = | ||
String::NewFromTwoByte(isolate, message.characters16(), | ||
v8::NewStringType::kNormal, message.length()); | ||
if (string.IsEmpty()) { |
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 can’t really tell how relevant performance is here, but if it makes sense to you, maybe add an early return if message
doesn’t contain the substring Debugger.setAsync
?
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 would be good to improve performance here, but that's a non-critical optimization and can be addressed once this lands. I have left a TODO.
@@ -449,7 +452,9 @@ Agent::Agent(Environment* env) : parent_env_(env), | |||
client_(nullptr), | |||
platform_(nullptr), | |||
enabled_(false), | |||
next_context_number_(1) {} | |||
next_context_number_(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.
Question: any reason do keep using initialization lists when we support C++11 (i.e. "Class Member Initialization")?
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.
Generally LGTM.
src/inspector_agent.cc
Outdated
MaybeLocal<Value> maybe_parsed = | ||
v8::JSON::Parse(context, string.ToLocalChecked()); | ||
if (maybe_parsed.IsEmpty()) { | ||
return; |
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.
Hint: what @addaleax is saying is that the return
here won't make the exception magically go away. To replicate the return
in JS a v8::TryCatch
is needed.
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.
Addressed.
src/inspector_agent.cc
Outdated
} | ||
} | ||
|
||
void Agent::EnableAsyncHook(Isolate* isolate) { |
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 would be good to add a HandleScope
here and in the method below because currently they leak handles into the caller's HandleScope
. It's harmless right now but could become an issue in future refactorings.
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.
Done.
src/inspector_agent.cc
Outdated
FatalError( | ||
"node::inspector::Agent::DisableAsyncHook", | ||
"Cannot disable 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.
These two methods could be dedup'd, e.g., by passing the Persistent<Function>
to use as a parameter (and the name in the error message as a 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.
Done.
src/inspector_agent.cc
Outdated
// can execute. The Debugger.setAsyncCallStackDepth message arrives too early | ||
// and we must intercept this in C++. | ||
|
||
v8::Isolate* isolate = parent_env_->isolate(); |
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 method looks like it needs a HandleScope
.
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.
Done.
src/inspector_agent.cc
Outdated
Local<Value> parsed = maybe_parsed.ToLocalChecked(); | ||
if (!parsed->IsObject()) { | ||
return; | ||
} |
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 shorten this to:
Local<Value> scratch;
Local<Object> parsed;
if (!v8::JSON::Parse(context, string).ToLocal(&scratch) ||
!scratch->IsObject() ||
!scratch->ToObject(context).ToLocal(&parsed)) {
return;
}
(General observation, applies to not just this block of 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.
Thanks. Done.
It might be possible to condense the code a bit further along these lines, but I left it at a point which seemed to be a good balance between terseness and readability.
src/inspector_agent.cc
Outdated
} else { | ||
pending_disable_async_hook_ = 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.
Could this be flattened like this?
if (pending_enable_async_hook_) {
// ...
} else if (!disable_async_hook_function_.IsEmpty()) {
// ...
} else {
// ...
}
If not, a code comment explaining why is probably in order.
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.
Done.
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.
Thank you @ofrobots for taking care of this and making my initial implementation of async stack traces more useful!
verifyAsyncHookDisabled('invalid message should not enable async hooks'); | ||
|
||
session.post('Debugger.setAsyncCallStackDepth', { maxDepth: 'five' }, () => { | ||
verifyAsyncHookDisabled('invalid maxDepth (sting) should not enable async' + |
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.
typo: sting
- should be string
?
|
||
// Verify that inspector-async-hook is no longer registered, | ||
// thus emitInit() ignores invalid arguments | ||
// See test/async-hooks/test-emit-init.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.
It makes me wonder - are DevTools clients disabling stack trace inspection before they disconnect? What happens when a 3rd-party DevTools client is implemented "incorrectly" and does not call setAsyncCallStackDepth
before closing the session - will we keep async stack traces enabled in such case, possibly forever if no other DevTool clients ever connects? Are we ok 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.
This is a good point. I think the alternative PR #16308 addresses this.
First glance through looks fine other than the suggestions @bnoordhuis has already made. It's good to see this change. One question: would it make any sense at all to have a command line flag to enable this by default at startup? |
src/inspector_agent.cc
Outdated
Local<Context> context = parent_env_->context(); | ||
|
||
MaybeLocal<String> string = | ||
String::NewFromTwoByte(isolate, message.characters16(), |
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.
StringView does not convert between encodings. I believe, right now all messages are 16 bit. Can you add CHECK (!message.is8Bit()) just to be sure?
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.
Let me raise some points: it's easy to get out-of-sync state with interception, since Debugger.disable and session disconnect disable async stacks as well. If at any time in future DevTools will support reload button for Node.js, we'll store session state as JSON and then use domain's restore method to restore state - setAsync.. won't be called in this case as well.
Alternative approaches:
- we can provide callback on V8InspectorClient and call it any time when async call stack depth is changed,
- we can expose JavaScript asyncTask* bindings which will contain fast return without entering C++ if async stacks are disabled.
I prefer second approach since it allows us not to grow inspector client API.
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.
LGTM modulo style nits.
HandleScope handle_scope(isolate); | ||
Local<Context> context = parent_env_->context(); | ||
|
||
v8::TryCatch try_catch(isolate); // catch and ignore exceptions. |
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.
s/catch/Catch/ and two spaces before the //
. (Although I don't think the comment adds much.)
|
||
session.post('Debugger.setAsyncCallStackDepth', { maxDepth: NaN }, () => { | ||
verifyAsyncHookDisabled('invalid maxDepth (NaN) should not enable async' + | ||
' hooks'); |
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.
Tiny nit: can you line this up here and below?
I am not opposed to that, however, I am not entirely clear on the exact use-case that addresses that isn't already addressed by |
With this change, we do async stack tracking only when explicitly
requested by the inspector client. This avoids unnecessary overhead
for clients that might not be interested in async stack traces.
Fixes: #16180
In practical terms, this means that we still get async stack tracking right from startup when
--inspect-brk
is used for debuggers that enable async stacks (DevTools does this by default, and I think WebStorm and others as well).When using
--inspect
, we no longer enable async stack tracking by default. The inspector client can request async stack tracking at the time of the connection. Any async resources already created will not have stack tracking. I believe this is the right behavior (see #16180). Users needing async stack tracking right from startup can use--inspect-brk
.Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
inspector
CI: https://ci.nodejs.org/job/node-test-pull-request/10785/