Commit f537d778 authored by Benedikt Meurer's avatar Benedikt Meurer Committed by Commit Bot

[async] First prototype of zero-cost async stack traces.

This introduces a new flag --async-stack-traces, which enables zero-cost
async stack traces. This enriches the non-standard Error.stack property
with async stack frames computed from walking up the promise chains and
collecting all the await suspension points along the way. In Error.stack
these async frames are marked with "async" to make it possible to
distinguish them from regular frames, for example:

```
Error: Some error message
    at bar (<anonymous>)
    at async foo (<anonymous>)
```

It's zero-cost because no additional information is collected during the
execution of the program, but only the information already present in the
promise chains is used to reconstruct an approximation of the async stack
in case of an exception. But this approximation is limited to suspension
points at await's in async functions. This depends on a recent ECMAScript
specification change, flagged behind --harmony-await-optimization and
implied the --async-stack-traces flag. Without this change there's no
way to get from the outer promise of an async function to the rest of
the promise chain, since the link is broken by the indirection introduced
by await.

For async functions the special outer promise, named .promise in the
Parser desugaring, is now forcible allocated to stack slot 0 during
scope resolution, to make it accessible to the stack frame construction
logic. Note that this first prototype doesn't yet work fully support
async generators and might have other limitations.

Bug: v8:7522
Ref: nodejs/node#11865
Change-Id: I0cc8e3cdfe45dab56d3d506be2d25907409b01a9
Design-Document: http://bit.ly/v8-zero-cost-async-stack-traces
Reviewed-on: https://chromium-review.googlesource.com/c/1256762
Commit-Queue: Benedikt Meurer <bmeurer@chromium.org>
Reviewed-by: 's avatarAdam Klein <adamk@chromium.org>
Reviewed-by: 's avatarYang Guo <yangguo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#56363}
parent 25aa6c51
......@@ -308,6 +308,7 @@ void DeclarationScope::SetDefaults() {
has_arguments_parameter_ = false;
scope_uses_super_property_ = false;
has_rest_ = false;
has_promise_ = false;
sloppy_block_function_map_ = nullptr;
receiver_ = nullptr;
new_target_ = nullptr;
......@@ -785,6 +786,7 @@ Variable* DeclarationScope::DeclarePromiseVar(const AstRawString* name) {
DCHECK_NULL(promise_var());
Variable* result = EnsureRareData()->promise = NewTemporary(name);
result->set_is_used();
has_promise_ = true;
return result;
}
......@@ -1491,6 +1493,7 @@ void DeclarationScope::ResetAfterPreparsing(AstValueFactory* ast_value_factory,
sloppy_block_function_map_ = nullptr;
rare_data_ = nullptr;
has_rest_ = false;
has_promise_ = false;
DCHECK_NE(zone_, ast_value_factory->zone());
zone_->ReleaseMemory();
......@@ -2172,6 +2175,15 @@ void DeclarationScope::AllocateReceiver() {
AllocateParameter(receiver(), -1);
}
void DeclarationScope::AllocatePromise() {
if (!has_promise_) return;
DCHECK_NOT_NULL(promise_var());
DCHECK_EQ(this, promise_var()->scope());
AllocateStackSlot(promise_var());
DCHECK_EQ(VariableLocation::LOCAL, promise_var()->location());
DCHECK_EQ(kPromiseVarIndex, promise_var()->index());
}
void Scope::AllocateNonParameterLocal(Variable* var) {
DCHECK(var->scope() == this);
if (var->IsUnallocated() && MustAllocate(var)) {
......@@ -2240,6 +2252,11 @@ void Scope::AllocateVariablesRecursively() {
return;
}
// Make sure to allocate the .promise first, so that it get's
// the required stack slot 0 in case it's needed. See
// http://bit.ly/v8-zero-cost-async-stack-traces for details.
if (is_function_scope()) AsDeclarationScope()->AllocatePromise();
// Allocate variables for inner scopes.
for (Scope* scope = inner_scope_; scope != nullptr; scope = scope->sibling_) {
scope->AllocateVariablesRecursively();
......
......@@ -771,6 +771,9 @@ class V8_EXPORT_PRIVATE DeclarationScope : public Scope {
return GetRareVariable(RareVariable::kGeneratorObject);
}
// The variable holding the promise returned from async functions.
// Only valid for function scopes in async functions (i.e. not
// for async generators).
Variable* promise_var() const {
DCHECK(is_function_scope());
DCHECK(IsAsyncFunction(function_kind_));
......@@ -778,6 +781,11 @@ class V8_EXPORT_PRIVATE DeclarationScope : public Scope {
return GetRareVariable(RareVariable::kPromise);
}
// For async functions, the .promise variable is always allocated
// to a fixed stack slot, such that the stack trace construction
// logic can access it.
static constexpr int kPromiseVarIndex = 0;
// Parameters. The left-most parameter has index 0.
// Only valid for function and module scopes.
Variable* parameter(int index) const {
......@@ -905,6 +913,7 @@ class V8_EXPORT_PRIVATE DeclarationScope : public Scope {
void AllocateLocals();
void AllocateParameterLocals();
void AllocateReceiver();
void AllocatePromise();
void ResetAfterPreparsing(AstValueFactory* ast_value_factory, bool aborted);
......@@ -961,6 +970,8 @@ class V8_EXPORT_PRIVATE DeclarationScope : public Scope {
bool force_eager_compilation_ : 1;
// This function scope has a rest parameter.
bool has_rest_ : 1;
// This function scope has a .promise variable.
bool has_promise_ : 1;
// This scope has a parameter called "arguments".
bool has_arguments_parameter_ : 1;
// This scope uses "super" property ('super.foo').
......
......@@ -4362,6 +4362,7 @@ void Bootstrapper::ExportFromRuntime(Isolate* isolate,
Builtins::kCallSitePrototypeGetScriptNameOrSourceURL},
{"getThis", Builtins::kCallSitePrototypeGetThis},
{"getTypeName", Builtins::kCallSitePrototypeGetTypeName},
{"isAsync", Builtins::kCallSitePrototypeIsAsync},
{"isConstructor", Builtins::kCallSitePrototypeIsConstructor},
{"isEval", Builtins::kCallSitePrototypeIsEval},
{"isNative", Builtins::kCallSitePrototypeIsNative},
......
......@@ -137,6 +137,14 @@ BUILTIN(CallSitePrototypeGetTypeName) {
return *it.Frame()->GetTypeName();
}
BUILTIN(CallSitePrototypeIsAsync) {
HandleScope scope(isolate);
CHECK_CALLSITE(recv, "isAsync");
FrameArrayIterator it(isolate, GetFrameArray(isolate, recv),
GetFrameIndex(isolate, recv));
return isolate->heap()->ToBoolean(it.Frame()->IsAsync());
}
BUILTIN(CallSitePrototypeIsConstructor) {
HandleScope scope(isolate);
CHECK_CALLSITE(recv, "isConstructor");
......
......@@ -468,6 +468,7 @@ namespace internal {
CPP(CallSitePrototypeGetScriptNameOrSourceURL) \
CPP(CallSitePrototypeGetThis) \
CPP(CallSitePrototypeGetTypeName) \
CPP(CallSitePrototypeIsAsync) \
CPP(CallSitePrototypeIsConstructor) \
CPP(CallSitePrototypeIsEval) \
CPP(CallSitePrototypeIsNative) \
......
......@@ -265,6 +265,8 @@ bool Builtins::IsLazy(int index) {
case kArrayReduceRightPreLoopEagerDeoptContinuation:
case kArraySomeLoopEagerDeoptContinuation:
case kArraySomeLoopLazyDeoptContinuation:
case kAsyncFunctionAwaitResolveClosure: // https://crbug.com/v8/7522
case kAsyncFunctionAwaitRejectClosure: // https://crbug.com/v8/7522
case kAsyncGeneratorAwaitCaught: // https://crbug.com/v8/6786.
case kAsyncGeneratorAwaitUncaught: // https://crbug.com/v8/6786.
// CEntry variants must be immovable, whereas lazy deserialization allocates
......
......@@ -1042,6 +1042,9 @@ DEFINE_BOOL(trace_sim_messages, false,
"Trace simulator debug messages. Implied by --trace-sim.")
// isolate.cc
DEFINE_BOOL(async_stack_traces, false,
"include async stack traces in Error.stack")
DEFINE_IMPLICATION(async_stack_traces, harmony_await_optimization)
DEFINE_BOOL(stack_trace_on_illegal, false,
"print stack trace when an illegal exception is thrown")
DEFINE_BOOL(abort_on_uncaught_exception, false,
......
This diff is collapsed.
......@@ -305,6 +305,7 @@ void JSStackFrame::FromFrameArray(Isolate* isolate, Handle<FrameArray> array,
const int flags = array->Flags(frame_ix)->value();
is_constructor_ = (flags & FrameArray::kIsConstructor) != 0;
is_strict_ = (flags & FrameArray::kIsStrict) != 0;
is_async_ = (flags & FrameArray::kIsAsync) != 0;
}
JSStackFrame::JSStackFrame(Isolate* isolate, Handle<Object> receiver,
......@@ -315,6 +316,7 @@ JSStackFrame::JSStackFrame(Isolate* isolate, Handle<Object> receiver,
function_(function),
code_(code),
offset_(offset),
is_async_(false),
is_constructor_(false),
is_strict_(false) {}
......@@ -604,9 +606,13 @@ MaybeHandle<String> JSStackFrame::ToString() {
Handle<Object> function_name = GetFunctionName();
const bool is_toplevel = IsToplevel();
const bool is_async = IsAsync();
const bool is_constructor = IsConstructor();
const bool is_method_call = !(is_toplevel || is_constructor);
if (is_async) {
builder.AppendCString("async ");
}
if (is_method_call) {
AppendMethodCall(isolate_, this, &builder);
} else if (is_constructor) {
......
......@@ -71,6 +71,7 @@ class StackFrameBase {
virtual bool IsNative() = 0;
virtual bool IsToplevel() = 0;
virtual bool IsEval();
virtual bool IsAsync() const = 0;
virtual bool IsConstructor() = 0;
virtual bool IsStrict() const = 0;
......@@ -108,6 +109,7 @@ class JSStackFrame : public StackFrameBase {
bool IsNative() override;
bool IsToplevel() override;
bool IsAsync() const override { return is_async_; }
bool IsConstructor() override { return is_constructor_; }
bool IsStrict() const override { return is_strict_; }
......@@ -125,8 +127,9 @@ class JSStackFrame : public StackFrameBase {
Handle<AbstractCode> code_;
int offset_;
bool is_constructor_;
bool is_strict_;
bool is_async_ : 1;
bool is_constructor_ : 1;
bool is_strict_ : 1;
friend class FrameArrayIterator;
};
......@@ -150,6 +153,7 @@ class WasmStackFrame : public StackFrameBase {
bool IsNative() override { return false; }
bool IsToplevel() override { return false; }
bool IsAsync() const override { return false; }
bool IsConstructor() override { return false; }
bool IsStrict() const override { return false; }
bool IsInterpreted() const { return code_ == nullptr; }
......
......@@ -50,7 +50,8 @@ class FrameArray : public FixedArray {
kIsAsmJsWasmFrame = 1 << 2,
kIsStrict = 1 << 3,
kIsConstructor = 1 << 4,
kAsmJsAtNumberConversion = 1 << 5
kAsmJsAtNumberConversion = 1 << 5,
kIsAsync = 1 << 6
};
static Handle<FrameArray> AppendJSFrame(Handle<FrameArray> in,
......
......@@ -385,27 +385,27 @@ bytecode array length: 140
bytecodes: [
/* 16 E> */ B(StackCheck),
B(CallJSRuntime), U8(%async_function_promise_create), R(0), U8(0),
B(Star), R(3),
B(Star), R(0),
B(Mov), R(context), R(6),
B(Mov), R(context), R(7),
/* 36 S> */ B(LdaZero),
B(Star), R(1),
B(Star), R(2),
/* 41 S> */ B(LdaSmi), I8(10),
/* 41 E> */ B(TestLessThan), R(1), U8(0),
/* 41 E> */ B(TestLessThan), R(2), U8(0),
B(JumpIfFalse), U8(15),
/* 23 E> */ B(StackCheck),
/* 62 S> */ B(Mov), R(1), R(0),
/* 49 S> */ B(Ldar), R(0),
/* 62 S> */ B(Mov), R(2), R(1),
/* 49 S> */ B(Ldar), R(1),
B(Inc), U8(1),
B(Star), R(1),
B(Star), R(2),
B(JumpLoop), U8(17), I8(0),
B(LdaUndefined),
B(Star), R(9),
B(Mov), R(3), R(8),
B(Mov), R(0), R(8),
/* 49 E> */ B(InvokeIntrinsic), U8(Runtime::k_ResolvePromise), R(8), U8(2),
B(LdaZero),
B(Star), R(4),
B(Mov), R(3), R(5),
B(Mov), R(0), R(5),
B(Jump), U8(55),
B(Jump), U8(39),
B(Star), R(8),
......@@ -419,12 +419,12 @@ bytecodes: [
B(Star), R(10),
B(LdaFalse),
B(Star), R(11),
B(Mov), R(3), R(9),
B(Mov), R(0), R(9),
B(InvokeIntrinsic), U8(Runtime::k_RejectPromise), R(9), U8(3),
B(PopContext), R(8),
B(LdaZero),
B(Star), R(4),
B(Mov), R(3), R(5),
B(Mov), R(0), R(5),
B(Jump), U8(16),
B(LdaSmi), I8(-1),
B(Star), R(5),
......@@ -438,7 +438,7 @@ bytecodes: [
B(Star), R(6),
B(LdaFalse),
B(Star), R(8),
B(Mov), R(3), R(7),
B(Mov), R(0), R(7),
B(CallJSRuntime), U8(%async_function_promise_release), R(7), U8(2),
B(Ldar), R(6),
B(SetPendingMessage),
......@@ -473,47 +473,47 @@ frame size: 11
parameter count: 1
bytecode array length: 191
bytecodes: [
B(SwitchOnGeneratorState), R(1), U8(0), U8(1),
B(SwitchOnGeneratorState), R(2), U8(0), U8(1),
B(Mov), R(closure), R(3),
B(Mov), R(this), R(4),
B(InvokeIntrinsic), U8(Runtime::k_CreateJSGeneratorObject), R(3), U8(2),
B(Star), R(1),
B(Star), R(2),
/* 16 E> */ B(StackCheck),
B(CallJSRuntime), U8(%async_function_promise_create), R(0), U8(0),
B(Star), R(2),
B(Star), R(0),
B(Mov), R(context), R(5),
B(Mov), R(context), R(6),
/* 36 S> */ B(LdaZero),
B(Star), R(0),
B(Star), R(1),
/* 41 S> */ B(LdaSmi), I8(10),
/* 41 E> */ B(TestLessThan), R(0), U8(0),
/* 41 E> */ B(TestLessThan), R(1), U8(0),
B(JumpIfFalse), U8(50),
/* 23 E> */ B(StackCheck),
/* 52 S> */ B(Mov), R(1), R(7),
B(Mov), R(0), R(8),
B(Mov), R(2), R(9),
/* 52 S> */ B(Mov), R(2), R(7),
B(Mov), R(1), R(8),
B(Mov), R(0), R(9),
B(CallJSRuntime), U8(%async_function_await_uncaught), R(7), U8(3),
/* 52 E> */ B(SuspendGenerator), R(1), R(0), U8(7), U8(0),
B(ResumeGenerator), R(1), R(0), U8(7),
/* 52 E> */ B(SuspendGenerator), R(2), R(0), U8(7), U8(0),
B(ResumeGenerator), R(2), R(0), U8(7),
B(Star), R(7),
B(InvokeIntrinsic), U8(Runtime::k_GeneratorGetResumeMode), R(1), U8(1),
B(InvokeIntrinsic), U8(Runtime::k_GeneratorGetResumeMode), R(2), U8(1),
B(Star), R(8),
B(LdaZero),
B(TestReferenceEqual), R(8),
B(JumpIfTrue), U8(5),
B(Ldar), R(7),
B(ReThrow),
/* 49 S> */ B(Ldar), R(0),
/* 49 S> */ B(Ldar), R(1),
B(Inc), U8(1),
B(Star), R(0),
B(Star), R(1),
B(JumpLoop), U8(52), I8(0),
B(LdaUndefined),
B(Star), R(8),
B(Mov), R(2), R(7),
B(Mov), R(0), R(7),
/* 49 E> */ B(InvokeIntrinsic), U8(Runtime::k_ResolvePromise), R(7), U8(2),
B(LdaZero),
B(Star), R(3),
B(Mov), R(2), R(4),
B(Mov), R(0), R(4),
B(Jump), U8(55),
B(Jump), U8(39),
B(Star), R(7),
......@@ -527,12 +527,12 @@ bytecodes: [
B(Star), R(9),
B(LdaFalse),
B(Star), R(10),
B(Mov), R(2), R(8),
B(Mov), R(0), R(8),
B(InvokeIntrinsic), U8(Runtime::k_RejectPromise), R(8), U8(3),
B(PopContext), R(7),
B(LdaZero),
B(Star), R(3),
B(Mov), R(2), R(4),
B(Mov), R(0), R(4),
B(Jump), U8(16),
B(LdaSmi), I8(-1),
B(Star), R(4),
......@@ -546,7 +546,7 @@ bytecodes: [
B(Star), R(5),
B(LdaTrue),
B(Star), R(7),
B(Mov), R(2), R(6),
B(Mov), R(0), R(6),
B(CallJSRuntime), U8(%async_function_promise_release), R(6), U8(2),
B(Ldar), R(5),
B(SetPendingMessage),
......
// Copyright 2018 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --async-stack-traces
// Check that Error.prepareStackTrace doesn't expose strict
// mode closures, even in the presence of async frames.
Error.prepareStackTrace = (e, frames) => {
assertEquals(two, frames[0].getFunction());
assertEquals(two.name, frames[0].getFunctionName());
assertEquals(undefined, frames[1].getFunction());
assertEquals(one.name, frames[1].getFunctionName());
return frames;
};
async function one(x) {
"use strict";
return await two(x);
}
async function two(x) {
try {
x = await x;
throw new Error();
} catch (e) {
return e.stack;
}
}
one(1).catch(e => setTimeout(_ => {throw e}, 0));
// Copyright 2018 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --async-stack-traces
// Check that Error.prepareStackTrace doesn't expose strict
// mode closures, even in the presence of async frames.
Error.prepareStackTrace = (e, frames) => {
assertEquals(undefined, frames[0].getFunction());
assertEquals(two.name, frames[0].getFunctionName());
assertEquals(undefined, frames[1].getFunction());
assertEquals(one.name, frames[1].getFunctionName());
return frames;
};
async function one(x) {
return await two(x);
}
async function two(x) {
"use strict";
try {
x = await x;
throw new Error();
} catch (e) {
return e.stack;
}
}
one(1).catch(e => setTimeout(_ => {throw e}, 0));
// Copyright 2018 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --async-stack-traces
// Check that Error.prepareStackTrace properly marks async frames.
Error.prepareStackTrace = (e, frames) => {
assertEquals(two, frames[0].getFunction());
assertEquals(two.name, frames[0].getFunctionName());
assertFalse(frames[0].isAsync());
assertEquals(two, frames[1].getFunction());
assertEquals(one.name, frames[1].getFunctionName());
assertTrue(frames[1].isAsync());
return frames;
};
async function one(x) {
return await two(x);
}
async function two(x) {
try {
x = await x;
throw new Error();
} catch (e) {
return e.stack;
}
}
one(1).catch(e => setTimeout(_ => {throw e}, 0));
// Copyright 2018 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax --async-stack-traces
// Basic test with an explicit throw.
(function() {
async function one(x) {
await two(x);
}
async function two(x) {
await x;
throw new Error();
}
async function test(f) {
try {
await f(1);
assertUnreachable();
} catch (e) {
assertInstanceof(e, Error);
assertMatches(/Error.+at two.+at async one.+at async test/ms, e.stack);
}
}
assertPromiseResult((async () => {
await test(one);
await test(one);
%OptimizeFunctionOnNextCall(two);
await test(one);
%OptimizeFunctionOnNextCall(one);
await test(one);
})());
})();
// Basic test with an implicit throw (via ToNumber on Symbol).
(function() {
async function one(x) {
return await two(x);
}
async function two(x) {
await x;
return +x; // This will raise a TypeError.
}
async function test(f) {
try {
await f(Symbol());
assertUnreachable();
} catch (e) {
assertInstanceof(e, TypeError);
assertMatches(/TypeError.+at two.+at async one.+at async test/ms, e.stack);
}
}
assertPromiseResult((async() => {
await test(one);
await test(one);
%OptimizeFunctionOnNextCall(two);
await test(one);
%OptimizeFunctionOnNextCall(one);
await test(one);
})());
})();
// Basic test with throw in inlined function.
(function() {
function throwError() {
throw new Error();
}
async function one(x) {
return await two(x);
}
async function two(x) {
await x;
return throwError();
}
async function test(f) {
try {
await f(1);
assertUnreachable();
} catch (e) {
assertInstanceof(e, Error);
assertMatches(/Error.+at two.+at async one.+at async test/ms, e.stack);
}
}
assertPromiseResult((async() => {
await test(one);
await test(one);
%OptimizeFunctionOnNextCall(two);
await test(one);
%OptimizeFunctionOnNextCall(one);
await test(one);
})());
})();
// Basic test with async function inlined into sync function.
(function() {
function callOne(x) {
return one(x);
}
function callTwo(x) {
return two(x);
}
async function one(x) {
return await callTwo(x);
}
async function two(x) {
await x;
throw new Error();
}
async function test(f) {
try {
await f(1);
assertUnreachable();
} catch (e) {
assertInstanceof(e, Error);
assertMatches(/Error.+at two.+at async one.+at async test/ms, e.stack);
}
}
assertPromiseResult((async() => {
await test(callOne);
await test(callOne);
%OptimizeFunctionOnNextCall(callTwo);
await test(callOne);
%OptimizeFunctionOnNextCall(callOne);
await test(callOne);
})());
})();
// Basic test with async functions and promises chained via
// Promise.prototype.then(), which should still work following
// the generic chain upwards.
(function() {
async function one(x) {
return await two(x).then(x => x);
}
async function two(x) {
await x.then(x => x);
throw new Error();
}
async function test(f) {
try {
await f(Promise.resolve(1));
assertUnreachable();
} catch (e) {
assertInstanceof(e, Error);
assertMatches(/Error.+at two.+at async one.+at async test/ms, e.stack);
}
}
assertPromiseResult((async() => {
await test(one);
await test(one);
%OptimizeFunctionOnNextCall(two);
await test(one);
%OptimizeFunctionOnNextCall(one);
await test(one);
})());
})();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment