Enable async stack traces (#22517)

### What does this PR do?

Enables async stack traces

### How did you verify your code works?

Added tests

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
SUZUKI Sosuke
2025-09-12 09:53:06 +09:00
committed by GitHub
parent 88a0002f7e
commit 9479bb8a5b
13 changed files with 113 additions and 12 deletions

View File

@@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use")
option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading")
if(NOT WEBKIT_VERSION)
set(WEBKIT_VERSION 0ddf6f47af0a9782a354f61e06d7f83d097d9f84)
set(WEBKIT_VERSION 2d2e8dd5b020cc165e2bc1d284461b4504d624e5)
endif()
string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX)

View File

@@ -78,6 +78,7 @@ pub fn runWithBody(ctx: *ErrorReportRequest, body: []const u8, r: AnyResponse) !
.line_start_byte = 0,
},
.code_type = .None,
.is_async = false,
.remapped = false,
});
}

View File

@@ -76,6 +76,9 @@ void CallSite::finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, JSCStac
if (!stackFrame.codeBlock()) {
m_flags |= static_cast<unsigned int>(Flags::IsNative);
}
if (stackFrame.isAsync()) {
m_flags |= static_cast<unsigned int>(Flags::IsAsync);
}
}
template<typename Visitor>

View File

@@ -27,6 +27,7 @@ public:
IsNative = 8,
IsWasm = 16,
IsFunction = 32,
IsAsync = 64,
};
private:
@@ -79,6 +80,7 @@ public:
bool isConstructor() const { return m_flags & static_cast<unsigned int>(Flags::IsConstructor); }
bool isStrict() const { return m_flags & static_cast<unsigned int>(Flags::IsStrict); }
bool isNative() const { return m_flags & static_cast<unsigned int>(Flags::IsNative); }
bool isAsync() const { return m_flags & static_cast<unsigned int>(Flags::IsAsync); }
void setLineNumber(OrdinalNumber lineNumber) { m_lineNumber = lineNumber; }
void setColumnNumber(OrdinalNumber columnNumber) { m_columnNumber = columnNumber; }

View File

@@ -220,12 +220,12 @@ JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncIsConstructor, (JSGlobalObject * globa
return JSC::JSValue::encode(JSC::jsBoolean(isConstructor));
}
// TODO:
JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncIsAsync, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
ENTER_PROTO_FUNC();
return JSC::JSValue::encode(JSC::jsBoolean(false));
bool isAsync = callSite->isAsync();
return JSC::JSValue::encode(JSC::jsBoolean(isAsync));
}
// TODO:

View File

@@ -292,6 +292,7 @@ JSCStackFrame::JSCStackFrame(JSC::VM& vm, JSC::StackVisitor& visitor)
, m_sourceURL()
, m_functionName()
, m_isWasmFrame(false)
, m_isAsync(false)
, m_sourcePositionsState(SourcePositionsState::NotCalculated)
{
m_callee = visitor->callee().asCell();
@@ -340,6 +341,7 @@ JSCStackFrame::JSCStackFrame(JSC::VM& vm, const JSC::StackFrame& frame)
, m_sourceURL()
, m_functionName()
, m_isWasmFrame(false)
, m_isAsync(frame.isAsyncFrame())
, m_sourcePositionsState(SourcePositionsState::NotCalculated)
{
m_callee = frame.callee();

View File

@@ -65,6 +65,7 @@ private:
bool m_isWasmFrame = false;
bool m_isFunctionOrEval = false;
bool m_isAsync = false;
enum class SourcePositionsState {
NotCalculated,
@@ -89,6 +90,7 @@ public:
JSC::JSString* typeName();
bool isFunctionOrEval() const { return m_isFunctionOrEval; }
bool isAsync() const { return m_isAsync; }
bool hasBytecodeIndex() const { return (m_bytecodeIndex.offset() != UINT_MAX) && !m_isWasmFrame; }
JSC::BytecodeIndex bytecodeIndex() const

View File

@@ -300,6 +300,7 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c
JSC::Options::evalMode() = evalMode;
JSC::Options::heapGrowthSteepnessFactor() = 1.0;
JSC::Options::heapGrowthMaxIncrease() = 2.0;
JSC::Options::useAsyncStackTrace() = true;
JSC::dangerouslyOverrideJSCBytecodeCacheVersion(getWebKitBytecodeCacheVersion());
#ifdef BUN_DEBUG
@@ -628,6 +629,9 @@ WTF::String Bun::formatStackTrace(
sb.append(" at "_s);
if (!functionName.isEmpty()) {
if (frame.isAsyncFrame()) {
sb.append("async "_s);
}
sb.append(functionName);
sb.append(" ("_s);
}

View File

@@ -6,6 +6,7 @@ pub const ZigStackFrame = extern struct {
source_url: String,
position: ZigStackFramePosition,
code_type: ZigStackFrameCode,
is_async: bool,
/// This informs formatters whether to display as a blob URL or not
remapped: bool = false,
@@ -119,6 +120,7 @@ pub const ZigStackFrame = extern struct {
function_name: String,
code_type: ZigStackFrameCode,
enable_color: bool,
is_async: bool,
pub fn format(this: NameFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
const name = this.function_name;
@@ -141,14 +143,29 @@ pub const ZigStackFrame = extern struct {
.Function => {
if (!name.isEmpty()) {
if (this.enable_color) {
try std.fmt.format(writer, comptime Output.prettyFmt("<r><b><i>{}<r>", true), .{name});
if (this.is_async) {
try std.fmt.format(writer, comptime Output.prettyFmt("<r><b><i>async {}<r>", true), .{name});
} else {
try std.fmt.format(writer, comptime Output.prettyFmt("<r><b><i>{}<r>", true), .{name});
}
} else {
try std.fmt.format(writer, "{}", .{name});
if (this.is_async) {
try std.fmt.format(writer, "async {}", .{name});
} else {
try std.fmt.format(writer, "{}", .{name});
}
}
} else {
if (this.enable_color) {
try std.fmt.format(writer, comptime Output.prettyFmt("<r><d>", true) ++ "<anonymous>" ++ Output.prettyFmt("<r>", true), .{});
if (this.is_async) {
try std.fmt.format(writer, comptime Output.prettyFmt("<r><d>", true) ++ "async <anonymous>" ++ Output.prettyFmt("<r>", true), .{});
} else {
try std.fmt.format(writer, comptime Output.prettyFmt("<r><d>", true) ++ "<anonymous>" ++ Output.prettyFmt("<r>", true), .{});
}
} else {
if (this.is_async) {
try writer.writeAll("async ");
}
try writer.writeAll("<anonymous>");
}
}
@@ -178,10 +195,11 @@ pub const ZigStackFrame = extern struct {
.code_type = .None,
.source_url = .empty,
.position = .invalid,
.is_async = false,
};
pub fn nameFormatter(this: *const ZigStackFrame, comptime enable_color: bool) NameFormatter {
return NameFormatter{ .function_name = this.function_name, .code_type = this.code_type, .enable_color = enable_color };
return NameFormatter{ .function_name = this.function_name, .code_type = this.code_type, .enable_color = enable_color, .is_async = this.is_async };
}
pub fn sourceURLFormatter(this: *const ZigStackFrame, root_path: string, origin: ?*const ZigURL, exclude_line_column: bool, comptime enable_color: bool) SourceURLFormatter {

View File

@@ -4427,6 +4427,8 @@ static void populateStackFrameMetadata(JSC::VM& vm, JSC::JSGlobalObject* globalO
if (!functionName.isEmpty()) {
frame->function_name = Bun::toStringRef(functionName);
}
frame->is_async = stackFrame->isAsyncFrame();
}
static void populateStackFramePosition(const JSC::StackFrame* stackFrame, BunString* source_lines,
@@ -4552,6 +4554,7 @@ public:
bool isConstructor = false;
bool isGlobalCode = false;
bool isAsync = false;
};
WTF::StringView stack;
@@ -4677,20 +4680,25 @@ public:
StringView functionName = line.substring(0, openingParentheses - 1);
if (functionName == "<anonymous>"_s) {
functionName = StringView();
}
if (functionName == "global code"_s) {
functionName = StringView();
frame.isGlobalCode = true;
}
if (functionName.startsWith("async "_s)) {
frame.isAsync = true;
functionName = functionName.substring(6);
}
if (functionName.startsWith("new "_s)) {
frame.isConstructor = true;
functionName = functionName.substring(4);
}
if (functionName == "<anonymous>"_s) {
functionName = StringView();
}
frame.functionName = functionName;
return true;
@@ -4889,6 +4897,7 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global,
current.position.column_zero_based = frame.columnNumber.zeroBasedInt();
current.remapped = true;
current.is_async = frame.isAsync;
if (frame.isConstructor) {
current.code_type = ZigStackFrameCodeConstructor;

View File

@@ -181,6 +181,7 @@ typedef struct ZigStackFrame {
BunString source_url;
ZigStackFramePosition position;
ZigStackFrameCode code_type;
bool is_async;
bool remapped;
} ZigStackFrame;

View File

@@ -1,7 +1,7 @@
import { $ } from "bun";
import { expect, test } from "bun:test";
import "harness";
import { bunEnv, bunExe } from "harness";
import { bunEnv, bunExe, normalizeBunSnapshot } from "harness";
import { join } from "node:path";
test("name property is used for function calls in Error.stack", () => {
@@ -121,3 +121,32 @@ test("throwing inside an error suppresses the error and continues printing prope
`);
expect(exitCode).toBe(1);
});
test("Async functions frame should be included in stack trace", async () => {
async function foo() {
return await bar();
}
async function bar() {
return await baz();
}
async function baz() {
await 1;
return await qux();
}
async function qux() {
return new Error("error from qux");
}
const error = await foo();
console.log(error.stack);
expect(normalizeBunSnapshot(error.stack!)).toMatchInlineSnapshot(`
"Error: error from qux
at qux (file:NN:NN)
at baz (file:NN:NN)
at async bar (file:NN:NN)
at async foo (file:NN:NN)
at async <anonymous> (file:NN:NN)"
`);
});

View File

@@ -724,3 +724,33 @@ test("CallFrame.p.getScriptNameOrSourceURL inside eval", () => {
expect(prepare).toHaveBeenCalledTimes(1);
});
test("CallFrame.p.isAsync", async () => {
let prevPrepareStackTrace = Error.prepareStackTrace;
const prepare = mock((e, s) => {
expect(s[0].isAsync()).toBeFalse();
expect(s[1].isAsync()).toBeTrue();
expect(s[2].isAsync()).toBeTrue();
expect(s[3].isAsync()).toBeTrue();
});
Error.prepareStackTrace = prepare;
async function foo() {
await bar();
}
async function bar() {
await baz();
}
async function baz() {
await 1;
throw new Error("error from baz");
}
try {
await foo();
} catch (e) {
e.stack;
}
Error.prepareStackTrace = prevPrepareStackTrace;
expect(prepare).toHaveBeenCalledTimes(1);
});