Include the awaiter stack trace in the service protocol

- [x] Include the (non-empty) awaiter stack trace in every `getStack` RPC.
- [x] Append the causal stack trace to the final frame of the awaiter stack trace.
- [x] Unit test for awaiter stack trace.

BUG=
R=rmacnak@google.com

Review-Url: https://codereview.chromium.org/2782703002 .
This commit is contained in:
John McCutchan 2017-03-29 06:56:46 -07:00
parent 4de94b2d7b
commit d8555fb5a8
8 changed files with 206 additions and 14 deletions

View file

@ -4,7 +4,7 @@
part of models;
enum FrameKind { regular, asyncCausal, asyncSuspensionMarker }
enum FrameKind { regular, asyncCausal, asyncSuspensionMarker, asyncActivation }
abstract class Frame {
FrameKind get kind;

View file

@ -4494,6 +4494,8 @@ class Frame extends ServiceObject implements M.Frame {
return M.FrameKind.asyncCausal;
case 'AsyncSuspensionMarker':
return M.FrameKind.asyncSuspensionMarker;
case 'AsyncActivation':
return M.FrameKind.asyncActivation;
default:
throw new UnsupportedError('Unknown FrameKind: $frameKind');
}

View file

@ -0,0 +1,73 @@
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// VMOptions=--error_on_bad_type --error_on_bad_override --verbose_debug
import 'dart:developer';
import 'package:observatory/models.dart' as M;
import 'package:observatory/service_io.dart';
import 'package:unittest/unittest.dart';
import 'service_test_common.dart';
import 'test_helper.dart';
const LINE_C = 19;
const LINE_A = 24;
const LINE_B = 30;
foobar() async {
debugger();
print('foobar'); // LINE_C.
}
helper() async {
debugger();
print('helper'); // LINE_A.
await foobar();
}
testMain() {
debugger();
helper(); // LINE_B.
}
var tests = [
hasStoppedAtBreakpoint,
stoppedAtLine(LINE_B),
(Isolate isolate) async {
ServiceMap stack = await isolate.getStack();
// No awaiter frames because we are in a completely synchronous stack.
expect(stack['awaiterFrames'], isNull);
},
resumeIsolate,
hasStoppedAtBreakpoint,
stoppedAtLine(LINE_A),
(Isolate isolate) async {
ServiceMap stack = await isolate.getStack();
// No awaiter frames because there is no awaiter.
expect(stack['awaiterFrames'], isNull);
},
resumeIsolate,
hasStoppedAtBreakpoint,
stoppedAtLine(LINE_C),
(Isolate isolate) async {
// Verify awaiter stack trace is the current frame + the awaiter.
ServiceMap stack = await isolate.getStack();
expect(stack['awaiterFrames'], isNotNull);
List awaiterFrames = stack['awaiterFrames'];
expect(awaiterFrames.length, greaterThanOrEqualTo(4));
// Awaiter frame.
expect(await awaiterFrames[0].toUserString(),
stringContainsInOrder(['foobar', '.dart:19']));
// Awaiter frame.
expect(await awaiterFrames[1].toUserString(),
stringContainsInOrder(['helper', '.dart:25']));
// Suspension point.
expect(awaiterFrames[2].kind, equals(M.FrameKind.asyncSuspensionMarker));
// Causal frame.
expect(await awaiterFrames[3].toUserString(),
stringContainsInOrder(['testMain', '.dart:30']));
},
];
main(args) =>
runIsolateTestsSynchronous(args, tests, testeeConcurrent: testMain);

View file

@ -377,7 +377,8 @@ RawError* Debugger::PauseRequest(ServiceEvent::EventKind kind) {
if (trace->Length() > 0) {
event.set_top_frame(trace->FrameAt(0));
}
CacheStackTraces(trace, CollectAsyncCausalStackTrace());
CacheStackTraces(trace, CollectAsyncCausalStackTrace(),
CollectAwaiterReturnStackTrace());
resume_action_ = kContinue;
Pause(&event);
HandleSteppingRequest(trace);
@ -713,7 +714,7 @@ intptr_t ActivationFrame::ContextLevel() {
RawObject* ActivationFrame::GetAsyncContextVariable(const String& name) {
if (!function_.IsAsyncClosure()) {
if (!function_.IsAsyncClosure() && !function_.IsAsyncGenClosure()) {
return Object::null();
}
GetVarDescriptors();
@ -799,6 +800,11 @@ RawObject* ActivationFrame::GetAsyncAwaiter() {
}
RawObject* ActivationFrame::GetCausalStack() {
return GetAsyncContextVariable(Symbols::AsyncStackTraceVar());
}
bool ActivationFrame::HandlesException(const Instance& exc_obj) {
intptr_t try_index = TryIndex();
if (try_index < 0) {
@ -1309,6 +1315,8 @@ void ActivationFrame::PrintToJSONObject(JSONObject* jsobj, bool full) {
PrintToJSONObjectAsyncCausal(jsobj, full);
} else if (kind_ == kAsyncSuspensionMarker) {
PrintToJSONObjectAsyncSuspensionMarker(jsobj, full);
} else if (kind_ == kAsyncActivation) {
PrintToJSONObjectAsyncActivation(jsobj, full);
} else {
UNIMPLEMENTED();
}
@ -1343,7 +1351,8 @@ void ActivationFrame::PrintToJSONObjectRegular(JSONObject* jsobj, bool full) {
if ((var_name.raw() != Symbols::AsyncOperation().raw()) &&
(var_name.raw() != Symbols::AsyncCompleter().raw()) &&
(var_name.raw() != Symbols::ControllerStream().raw()) &&
(var_name.raw() != Symbols::AwaitJumpVar().raw())) {
(var_name.raw() != Symbols::AwaitJumpVar().raw()) &&
(var_name.raw() != Symbols::AsyncStackTraceVar().raw())) {
JSONObject jsvar(&jsvars);
jsvar.AddProperty("type", "BoundVariable");
var_name = String::ScrubName(var_name);
@ -1387,6 +1396,24 @@ void ActivationFrame::PrintToJSONObjectAsyncSuspensionMarker(JSONObject* jsobj,
}
void ActivationFrame::PrintToJSONObjectAsyncActivation(JSONObject* jsobj,
bool full) {
jsobj->AddProperty("type", "Frame");
jsobj->AddProperty("kind", KindToCString(kind_));
const Script& script = Script::Handle(SourceScript());
const TokenPosition pos = TokenPos().SourcePosition();
jsobj->AddLocation(script, pos);
jsobj->AddProperty("function", function(), !full);
jsobj->AddProperty("code", code());
if (full) {
// TODO(cutch): The old "full" script usage no longer fits
// in the world where we pass the script as part of the
// location.
jsobj->AddProperty("script", script, !full);
}
}
static bool IsFunctionVisible(const Function& function) {
return FLAG_show_invisible_frames || function.is_visible();
}
@ -1540,6 +1567,7 @@ Debugger::Debugger()
obj_cache_(NULL),
stack_trace_(NULL),
async_causal_stack_trace_(NULL),
awaiter_stack_trace_(NULL),
stepping_fp_(0),
skip_next_step_(false),
needs_breakpoint_cleanup_(false),
@ -1937,10 +1965,13 @@ DebuggerStackTrace* Debugger::CollectAwaiterReturnStackTrace() {
StackFrameIterator iterator(StackFrameIterator::kDontValidateFrames);
Code& code = Code::Handle(zone);
Smi& offset = Smi::Handle(zone);
Function& function = Function::Handle(zone);
Code& inlined_code = Code::Handle(zone);
Closure& async_activation = Closure::Handle(zone);
Object& next_async_activation = Object::Handle(zone);
Array& deopt_frame = Array::Handle(zone);
class StackTrace& async_stack_trace = StackTrace::Handle(zone);
for (StackFrame* frame = iterator.NextFrame(); frame != NULL;
frame = iterator.NextFrame()) {
@ -2011,10 +2042,51 @@ DebuggerStackTrace* Debugger::CollectAwaiterReturnStackTrace() {
// Append the awaiter return call stack.
while (!async_activation.IsNull()) {
ActivationFrame* activation = new ActivationFrame(async_activation);
async_activation ^= activation->GetAsyncAwaiter();
ActivationFrame* activation = new (zone) ActivationFrame(async_activation);
activation->ExtractTokenPositionFromAsyncClosure();
stack_trace->AddActivation(activation);
next_async_activation = activation->GetAsyncAwaiter();
if (next_async_activation.IsNull()) {
// No more awaiters. Extract the causal stack trace (if it exists).
async_stack_trace ^= activation->GetCausalStack();
break;
}
async_activation = Closure::RawCast(next_async_activation.raw());
}
// Now we append the asynchronous causal stack trace. These are not active
// frames but a historical record of how this asynchronous function was
// activated.
while (!async_stack_trace.IsNull()) {
for (intptr_t i = 0; i < async_stack_trace.Length(); i++) {
if (async_stack_trace.CodeAtFrame(i) == Code::null()) {
// Incomplete OutOfMemory/StackOverflow trace OR array padding.
break;
}
if (async_stack_trace.CodeAtFrame(i) ==
StubCode::AsynchronousGapMarker_entry()->code()) {
stack_trace->AddMarker(ActivationFrame::kAsyncSuspensionMarker);
// The frame immediately below the asynchronous gap marker is the
// identical to the frame above the marker. Skip the frame to enhance
// the readability of the trace.
i++;
} else {
code = Code::RawCast(async_stack_trace.CodeAtFrame(i));
offset = Smi::RawCast(async_stack_trace.PcOffsetAtFrame(i));
uword pc = code.PayloadStart() + offset.Value();
if (code.is_optimized()) {
for (InlinedFunctionsIterator it(code, pc); !it.Done();
it.Advance()) {
inlined_code = it.code();
stack_trace->AddAsyncCausalFrame(it.pc(), inlined_code);
}
} else {
stack_trace->AddAsyncCausalFrame(pc, code);
}
}
}
// Follow the link.
async_stack_trace = async_stack_trace.async_link();
}
return stack_trace;
@ -2055,6 +2127,17 @@ DebuggerStackTrace* Debugger::CurrentAsyncCausalStackTrace() {
}
DebuggerStackTrace* Debugger::AwaiterStackTrace() {
return (awaiter_stack_trace_ != NULL) ? awaiter_stack_trace_
: CollectAwaiterReturnStackTrace();
}
DebuggerStackTrace* Debugger::CurrentAwaiterStackTrace() {
return CollectAwaiterReturnStackTrace();
}
DebuggerStackTrace* Debugger::StackTraceFrom(const class StackTrace& ex_trace) {
DebuggerStackTrace* stack_trace = new DebuggerStackTrace(8);
Function& function = Function::Handle();
@ -2171,7 +2254,8 @@ void Debugger::PauseException(const Instance& exc) {
if (stack_trace->Length() > 0) {
event.set_top_frame(stack_trace->FrameAt(0));
}
CacheStackTraces(stack_trace, CollectAsyncCausalStackTrace());
CacheStackTraces(stack_trace, CollectAsyncCausalStackTrace(),
CollectAwaiterReturnStackTrace());
Pause(&event);
HandleSteppingRequest(stack_trace_); // we may get a rewind request
ClearCachedStackTraces();
@ -3175,17 +3259,21 @@ void Debugger::HandleSteppingRequest(DebuggerStackTrace* stack_trace,
void Debugger::CacheStackTraces(DebuggerStackTrace* stack_trace,
DebuggerStackTrace* async_causal_stack_trace) {
DebuggerStackTrace* async_causal_stack_trace,
DebuggerStackTrace* awaiter_stack_trace) {
ASSERT(stack_trace_ == NULL);
stack_trace_ = stack_trace;
ASSERT(async_causal_stack_trace_ == NULL);
async_causal_stack_trace_ = async_causal_stack_trace;
ASSERT(awaiter_stack_trace_ == NULL);
awaiter_stack_trace_ = awaiter_stack_trace;
}
void Debugger::ClearCachedStackTraces() {
stack_trace_ = NULL;
async_causal_stack_trace_ = NULL;
awaiter_stack_trace_ = NULL;
}
@ -3526,7 +3614,8 @@ RawError* Debugger::PauseStepping() {
}
CacheStackTraces(CollectStackTrace(), CollectAsyncCausalStackTrace());
CacheStackTraces(CollectStackTrace(), CollectAsyncCausalStackTrace(),
CollectAwaiterReturnStackTrace());
// If this step callback is part of stepping over an await statement,
// we saved the synthetic async breakpoint in PauseBreakpoint. We report
// that we are paused at that breakpoint and then delete it after continuing.
@ -3567,7 +3656,8 @@ RawError* Debugger::PauseBreakpoint() {
if (bpt_hit->is_synthetic_async()) {
DebuggerStackTrace* stack_trace = CollectStackTrace();
ASSERT(stack_trace->Length() > 0);
CacheStackTraces(stack_trace, CollectAsyncCausalStackTrace());
CacheStackTraces(stack_trace, CollectAsyncCausalStackTrace(),
CollectAwaiterReturnStackTrace());
// Hit a synthetic async breakpoint.
if (FLAG_verbose_debug) {
@ -3602,7 +3692,8 @@ RawError* Debugger::PauseBreakpoint() {
top_frame->pc());
}
CacheStackTraces(stack_trace, CollectAsyncCausalStackTrace());
CacheStackTraces(stack_trace, CollectAsyncCausalStackTrace(),
CollectAwaiterReturnStackTrace());
SignalPausedEvent(top_frame, bpt_hit);
// When we single step from a user breakpoint, our next stepping
// point will be at the exact same pc. Skip it.
@ -3671,7 +3762,8 @@ void Debugger::PauseDeveloper(const String& msg) {
DebuggerStackTrace* stack_trace = CollectStackTrace();
ASSERT(stack_trace->Length() > 0);
CacheStackTraces(stack_trace, CollectAsyncCausalStackTrace());
CacheStackTraces(stack_trace, CollectAsyncCausalStackTrace(),
CollectAwaiterReturnStackTrace());
// TODO(johnmccutchan): Send |msg| to Observatory.
// We are in the native call to Developer_debugger. the developer

View file

@ -336,6 +336,7 @@ class ActivationFrame : public ZoneAllocated {
void PrintToJSONObject(JSONObject* jsobj, bool full = false);
RawObject* GetAsyncAwaiter();
RawObject* GetCausalStack();
bool HandlesException(const Instance& exc_obj);
@ -343,7 +344,7 @@ class ActivationFrame : public ZoneAllocated {
void PrintToJSONObjectRegular(JSONObject* jsobj, bool full);
void PrintToJSONObjectAsyncCausal(JSONObject* jsobj, bool full);
void PrintToJSONObjectAsyncSuspensionMarker(JSONObject* jsobj, bool full);
void PrintToJSONObjectAsyncActivation(JSONObject* jsobj, bool full);
void PrintContextMismatchError(intptr_t ctx_slot,
intptr_t frame_ctx_level,
intptr_t var_ctx_level);
@ -368,6 +369,8 @@ class ActivationFrame : public ZoneAllocated {
return "AsyncCausal";
case kAsyncSuspensionMarker:
return "AsyncSuspensionMarker";
case kAsyncActivation:
return "AsyncActivation";
default:
UNREACHABLE();
return "";
@ -541,6 +544,9 @@ class Debugger {
DebuggerStackTrace* AsyncCausalStackTrace();
DebuggerStackTrace* CurrentAsyncCausalStackTrace();
DebuggerStackTrace* AwaiterStackTrace();
DebuggerStackTrace* CurrentAwaiterStackTrace();
// Returns a debugger stack trace corresponding to a dart.core.StackTrace.
// Frames corresponding to invisible functions are omitted. It is not valid
// to query local variables in the returned stack.
@ -693,7 +699,8 @@ class Debugger {
bool skip_next_step = false);
void CacheStackTraces(DebuggerStackTrace* stack_trace,
DebuggerStackTrace* async_causal_stack_trace);
DebuggerStackTrace* async_causal_stack_trace,
DebuggerStackTrace* awaiter_stack_trace);
void ClearCachedStackTraces();
@ -739,6 +746,7 @@ class Debugger {
// Current stack trace. Valid only while IsPaused().
DebuggerStackTrace* stack_trace_;
DebuggerStackTrace* async_causal_stack_trace_;
DebuggerStackTrace* awaiter_stack_trace_;
// When stepping through code, only pause the program if the top
// frame corresponds to this fp value, or if the top frame is

View file

@ -276,6 +276,10 @@ static bool IsFilteredIdentifier(const String& str) {
// Keep :await_jump_var for asynchronous debugging.
return false;
}
if (str.raw() == Symbols::AsyncStackTraceVar().raw()) {
// Keep :async_stack_trace for asynchronous debugging.
return false;
}
return str.CharAt(0) == ':';
}

View file

@ -1345,6 +1345,7 @@ static bool GetStack(Thread* thread, JSONStream* js) {
DebuggerStackTrace* stack = isolate->debugger()->StackTrace();
DebuggerStackTrace* async_causal_stack =
isolate->debugger()->AsyncCausalStackTrace();
DebuggerStackTrace* awaiter_stack = isolate->debugger()->AwaiterStackTrace();
// Do we want the complete script object and complete local variable objects?
// This is true for dump requests.
const bool full = BoolParameter::Parse(js->LookupParam("_full"), false);
@ -1373,6 +1374,17 @@ static bool GetStack(Thread* thread, JSONStream* js) {
}
}
if (awaiter_stack != NULL) {
JSONArray jsarr(&jsobj, "awaiterFrames");
intptr_t num_frames = awaiter_stack->Length();
for (intptr_t i = 0; i < num_frames; i++) {
ActivationFrame* frame = awaiter_stack->FrameAt(i);
JSONObject jsobj(&jsarr);
frame->PrintToJSONObject(&jsobj, full);
jsobj.AddProperty("index", i);
}
}
{
MessageHandler::AcquiredQueues aq(isolate->message_handler());
jsobj.AddProperty("messages", aq.queue());

View file

@ -2259,6 +2259,7 @@ enum FrameKind {
Regular,
AsyncCausal,
AsyncSuspensionMarker,
AsyncActivation
}
```