From 9479bb8a5bbbad21d0d3c9314e7d56e2cc844c2b Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Fri, 12 Sep 2025 09:53:06 +0900 Subject: [PATCH] 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> --- cmake/tools/SetupWebKit.cmake | 2 +- src/bake/DevServer/ErrorReportRequest.zig | 1 + src/bun.js/bindings/CallSite.cpp | 3 ++ src/bun.js/bindings/CallSite.h | 2 ++ src/bun.js/bindings/CallSitePrototype.cpp | 4 +-- src/bun.js/bindings/ErrorStackTrace.cpp | 2 ++ src/bun.js/bindings/ErrorStackTrace.h | 2 ++ src/bun.js/bindings/ZigGlobalObject.cpp | 4 +++ src/bun.js/bindings/ZigStackFrame.zig | 26 ++++++++++++++--- src/bun.js/bindings/bindings.cpp | 17 ++++++++--- src/bun.js/bindings/headers-handwritten.h | 1 + test/js/bun/test/stack.test.ts | 31 ++++++++++++++++++++- test/js/node/v8/capture-stack-trace.test.js | 30 ++++++++++++++++++++ 13 files changed, 113 insertions(+), 12 deletions(-) diff --git a/cmake/tools/SetupWebKit.cmake b/cmake/tools/SetupWebKit.cmake index ca700a2ce4..54d4f15bbc 100644 --- a/cmake/tools/SetupWebKit.cmake +++ b/cmake/tools/SetupWebKit.cmake @@ -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) diff --git a/src/bake/DevServer/ErrorReportRequest.zig b/src/bake/DevServer/ErrorReportRequest.zig index c622557e70..2d0241f0d5 100644 --- a/src/bake/DevServer/ErrorReportRequest.zig +++ b/src/bake/DevServer/ErrorReportRequest.zig @@ -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, }); } diff --git a/src/bun.js/bindings/CallSite.cpp b/src/bun.js/bindings/CallSite.cpp index 1dc975265d..158049649e 100644 --- a/src/bun.js/bindings/CallSite.cpp +++ b/src/bun.js/bindings/CallSite.cpp @@ -76,6 +76,9 @@ void CallSite::finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, JSCStac if (!stackFrame.codeBlock()) { m_flags |= static_cast(Flags::IsNative); } + if (stackFrame.isAsync()) { + m_flags |= static_cast(Flags::IsAsync); + } } template diff --git a/src/bun.js/bindings/CallSite.h b/src/bun.js/bindings/CallSite.h index 31236d80b3..4d63a3b167 100644 --- a/src/bun.js/bindings/CallSite.h +++ b/src/bun.js/bindings/CallSite.h @@ -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(Flags::IsConstructor); } bool isStrict() const { return m_flags & static_cast(Flags::IsStrict); } bool isNative() const { return m_flags & static_cast(Flags::IsNative); } + bool isAsync() const { return m_flags & static_cast(Flags::IsAsync); } void setLineNumber(OrdinalNumber lineNumber) { m_lineNumber = lineNumber; } void setColumnNumber(OrdinalNumber columnNumber) { m_columnNumber = columnNumber; } diff --git a/src/bun.js/bindings/CallSitePrototype.cpp b/src/bun.js/bindings/CallSitePrototype.cpp index 35340632a0..53f5405c04 100644 --- a/src/bun.js/bindings/CallSitePrototype.cpp +++ b/src/bun.js/bindings/CallSitePrototype.cpp @@ -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: diff --git a/src/bun.js/bindings/ErrorStackTrace.cpp b/src/bun.js/bindings/ErrorStackTrace.cpp index a2d0b46ec7..75ac0f70e9 100644 --- a/src/bun.js/bindings/ErrorStackTrace.cpp +++ b/src/bun.js/bindings/ErrorStackTrace.cpp @@ -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(); diff --git a/src/bun.js/bindings/ErrorStackTrace.h b/src/bun.js/bindings/ErrorStackTrace.h index 3df2c966c0..e50d15eefa 100644 --- a/src/bun.js/bindings/ErrorStackTrace.h +++ b/src/bun.js/bindings/ErrorStackTrace.h @@ -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 diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index fc584871ca..98b998abad 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -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); } diff --git a/src/bun.js/bindings/ZigStackFrame.zig b/src/bun.js/bindings/ZigStackFrame.zig index 8b9a153d13..89bb594cf0 100644 --- a/src/bun.js/bindings/ZigStackFrame.zig +++ b/src/bun.js/bindings/ZigStackFrame.zig @@ -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("{}", true), .{name}); + if (this.is_async) { + try std.fmt.format(writer, comptime Output.prettyFmt("async {}", true), .{name}); + } else { + try std.fmt.format(writer, comptime Output.prettyFmt("{}", 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("", true) ++ "" ++ Output.prettyFmt("", true), .{}); + if (this.is_async) { + try std.fmt.format(writer, comptime Output.prettyFmt("", true) ++ "async " ++ Output.prettyFmt("", true), .{}); + } else { + try std.fmt.format(writer, comptime Output.prettyFmt("", true) ++ "" ++ Output.prettyFmt("", true), .{}); + } } else { + if (this.is_async) { + try writer.writeAll("async "); + } try writer.writeAll(""); } } @@ -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 { diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 6d6b513211..b908be7fef 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -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 == ""_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 == ""_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; diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 8d15002abd..560d3bd568 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -181,6 +181,7 @@ typedef struct ZigStackFrame { BunString source_url; ZigStackFramePosition position; ZigStackFrameCode code_type; + bool is_async; bool remapped; } ZigStackFrame; diff --git a/test/js/bun/test/stack.test.ts b/test/js/bun/test/stack.test.ts index 782215e123..94573ea43d 100644 --- a/test/js/bun/test/stack.test.ts +++ b/test/js/bun/test/stack.test.ts @@ -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 (file:NN:NN)" + `); +}); diff --git a/test/js/node/v8/capture-stack-trace.test.js b/test/js/node/v8/capture-stack-trace.test.js index 814aee3ab3..6e5d2008f2 100644 --- a/test/js/node/v8/capture-stack-trace.test.js +++ b/test/js/node/v8/capture-stack-trace.test.js @@ -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); +});