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

inspector: track async stacks when necessary #16260

Closed
wants to merge 3 commits into from

Conversation

ofrobots
Copy link
Contributor

@ofrobots ofrobots commented Oct 17, 2017

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), or vcbuild test (Windows) passes
  • commit message follows commit guidelines
Affected core subsystem(s)

inspector

CI: https://ci.nodejs.org/job/node-test-pull-request/10785/

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. labels Oct 17, 2017
@ofrobots
Copy link
Contributor Author

ofrobots commented Oct 17, 2017

CI: https://ci.nodejs.org/job/node-test-pull-request/10783/ See OP for CI link.

R= @nodejs/v8-inspector
/cc @nodejs/diagnostics

return;
}

Local<Value> method = maybe_method.ToLocalChecked();
Copy link
Member

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?

Copy link
Contributor Author

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

Maybe<double> maybe_depth = depth_value->NumberValue(context);
if (maybe_depth.IsNothing()) {
return;
Copy link
Member

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()

Copy link
Contributor Author

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; }
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

MaybeLocal<String> string =
String::NewFromTwoByte(isolate, message.characters16(),
v8::NewStringType::kNormal, message.length());
if (string.IsEmpty()) {
Copy link
Member

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?

Copy link
Contributor Author

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.

@addaleax addaleax added async_hooks Issues and PRs related to the async hooks subsystem. inspector Issues and PRs related to the V8 inspector protocol labels Oct 17, 2017
@@ -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),
Copy link
Contributor

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")?

Copy link
Member

@TimothyGu TimothyGu left a comment

Choose a reason for hiding this comment

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

Generally LGTM.

MaybeLocal<Value> maybe_parsed =
v8::JSON::Parse(context, string.ToLocalChecked());
if (maybe_parsed.IsEmpty()) {
return;
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed.

}
}

void Agent::EnableAsyncHook(Isolate* isolate) {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

FatalError(
"node::inspector::Agent::DisableAsyncHook",
"Cannot disable Inspector's AsyncHook, please report this.");
}
Copy link
Member

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.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

// can execute. The Debugger.setAsyncCallStackDepth message arrives too early
// and we must intercept this in C++.

v8::Isolate* isolate = parent_env_->isolate();
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Local<Value> parsed = maybe_parsed.ToLocalChecked();
if (!parsed->IsObject()) {
return;
}
Copy link
Member

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.)

Copy link
Contributor Author

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.

} else {
pending_disable_async_hook_ = true;
}
}
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor

@bajtos bajtos left a 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' +
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@jasnell
Copy link
Member

jasnell commented Oct 18, 2017

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?

Local<Context> context = parent_env_->context();

MaybeLocal<String> string =
String::NewFromTwoByte(isolate, message.characters16(),
Copy link
Contributor

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?

Copy link
Member

@alexkozy alexkozy left a 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.

Copy link
Member

@bnoordhuis bnoordhuis left a 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.
Copy link
Member

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');
Copy link
Member

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?

@ofrobots
Copy link
Contributor Author

@ak239 raised some good points. I've implemented an alternative fix to the problem that depends on an upstream back port: #16308. Abandoning in favor of that PR.

@ofrobots ofrobots closed this Oct 18, 2017
@ofrobots
Copy link
Contributor Author

ofrobots commented Oct 18, 2017

@jasnell

One question: would it make any sense at all to have a command line flag to enable this by default at startup?

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 --inspect-brk or through the inspector module. If there are valid uses-cases, I think the flag can be added at any point in a semver-minor. Right now I want to make sure we fix the problematic behavior from #16180 before we head into LTS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
async_hooks Issues and PRs related to the async hooks subsystem. c++ Issues and PRs that require attention from people who are familiar with C++. inspector Issues and PRs related to the V8 inspector protocol lib / src Issues and PRs related to general changes in the lib or src directory.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

async stack tracking should not be enabled by default
10 participants