diff --git a/.lldbinit b/.lldbinit index a2357365bb..b2012a63b9 100644 --- a/.lldbinit +++ b/.lldbinit @@ -1,4 +1,16 @@ -# command script import vendor/zig/tools/lldb_pretty_printers.py -command script import vendor/WebKit/Tools/lldb/lldb_webkit.py +# Tell LLDB what to do when the debugged process receives SIGPWR: pass it through to the process +# (-p), but do not stop the process (-s) or notify the user (-n). +# +# JSC's garbage collector sends this signal (as configured by Bun WebKit in +# Thread::initializePlatformThreading() in ThreadingPOSIX.cpp) to the JS thread to suspend or resume +# it. So stopping the process would just create noise when debugging any long-running script. +process handle -p true -s false -n false SIGPWR -# type summary add --summary-string "${var} | inner=${var[0-30]}, source=${var[33-64]}, tag=${var[31-32]}" "unsigned long" +command script import misctools/lldb/lldb_pretty_printers.py +type category enable zig.lang +type category enable zig.std + +command script import misctools/lldb/lldb_webkit.py + +command script delete btjs +command alias btjs p {printf("gathering btjs trace...\n");printf("%s\n", (char*)dumpBtjsTrace())} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index cd232a98a4..157d1859f7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -38,7 +37,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -60,7 +58,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -76,7 +73,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -92,7 +88,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -108,7 +103,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -125,7 +119,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -147,7 +140,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -169,7 +161,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -188,7 +179,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -203,7 +193,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -221,7 +210,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -236,7 +224,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -253,7 +240,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -275,7 +261,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -297,7 +282,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -313,7 +297,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -329,7 +312,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -345,7 +327,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -361,7 +342,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -378,7 +358,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -400,7 +379,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -421,7 +399,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, // bun test [*] { @@ -437,7 +414,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -452,7 +428,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -468,7 +443,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], "serverReadyAction": { "pattern": "https://debug.bun.sh/#localhost:([0-9]+)/", "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", @@ -488,7 +462,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "lldb", @@ -503,7 +476,6 @@ }, "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, // Windows: bun test [file] { @@ -1129,7 +1101,6 @@ ], "console": "internalConsole", // Don't pause when the GC runs while the debugger is open. - "postRunCommands": ["command source '${workspaceFolder}/misctools/lldb/lldb_commands'"], }, { "type": "bun", diff --git a/misctools/lldb/lldb_commands b/misctools/lldb/lldb_commands deleted file mode 100644 index fde59a77a9..0000000000 --- a/misctools/lldb/lldb_commands +++ /dev/null @@ -1,13 +0,0 @@ -# Tell LLDB what to do when the debugged process receives SIGPWR: pass it through to the process -# (-p), but do not stop the process (-s) or notify the user (-n). -# -# JSC's garbage collector sends this signal (as configured by Bun WebKit in -# Thread::initializePlatformThreading() in ThreadingPOSIX.cpp) to the JS thread to suspend or resume -# it. So stopping the process would just create noise when debugging any long-running script. -process handle -p true -s false -n false SIGPWR - -command script import misctools/lldb/lldb_pretty_printers.py -type category enable zig.lang -type category enable zig.std - -command script import misctools/lldb/lldb_webkit.py diff --git a/misctools/lldb/lldb_webkit.py b/misctools/lldb/lldb_webkit.py index a6b11771ee..57239dc9f4 100644 --- a/misctools/lldb/lldb_webkit.py +++ b/misctools/lldb/lldb_webkit.py @@ -329,7 +329,7 @@ def btjs(debugger, command, result, internal_dict): addressFormat = '#0{width}x'.format(width=target.GetAddressByteSize() * 2 + 2) process = target.GetProcess() thread = process.GetSelectedThread() - jscModule = target.module["JavaScriptCore"] + jscModule = target.module["JavaScriptCore"] or target.module["bun"] or target.module["bun-debug"] if jscModule.FindSymbol("JSC::CallFrame::describeFrame").GetSize() or jscModule.FindSymbol("_ZN3JSC9CallFrame13describeFrameEv").GetSize(): annotateJSFrames = True diff --git a/src/btjs.zig b/src/btjs.zig new file mode 100644 index 0000000000..cd4e1dd73a --- /dev/null +++ b/src/btjs.zig @@ -0,0 +1,248 @@ +const std = @import("std"); +const bun = @import("root").bun; + +extern const jsc_llint_begin: u8; +extern const jsc_llint_end: u8; +/// allocated using bun.default_allocator. when called from lldb, it is never freed. +pub export fn dumpBtjsTrace() [*:0]const u8 { + if (@import("builtin").mode != .Debug) return "dumpBtjsTrace is disabled in release builds"; + + var result_writer = std.ArrayList(u8).init(bun.default_allocator); + const w = result_writer.writer(); + + const debug_info = std.debug.getSelfDebugInfo() catch |err| { + w.print("Unable to dump stack trace: Unable to open debug info: {s}\x00", .{@errorName(err)}) catch { + result_writer.deinit(); + return "".ptr; + }; + return @ptrCast((result_writer.toOwnedSlice() catch { + result_writer.deinit(); + return "".ptr; + }).ptr); + }; + + // std.log.info("jsc_llint_begin: {x}", .{@intFromPtr(&jsc_llint_begin)}); + // std.log.info("jsc_llint_end: {x}", .{@intFromPtr(&jsc_llint_end)}); + + const tty_config = std.io.tty.detectConfig(std.io.getStdOut()); + + var context: std.debug.ThreadContext = undefined; + const has_context = std.debug.getContext(&context); + + var it: std.debug.StackIterator = (if (has_context) blk: { + break :blk std.debug.StackIterator.initWithContext(null, debug_info, &context) catch null; + } else null) orelse std.debug.StackIterator.init(null, null); + defer it.deinit(); + + while (it.next()) |return_address| { + printLastUnwindError(&it, debug_info, w, tty_config); + + // On arm64 macOS, the address of the last frame is 0x0 rather than 0x1 as on x86_64 macOS, + // therefore, we do a check for `return_address == 0` before subtracting 1 from it to avoid + // an overflow. We do not need to signal `StackIterator` as it will correctly detect this + // condition on the subsequent iteration and return `null` thus terminating the loop. + // same behaviour for x86-windows-msvc + const address = return_address -| 1; + printSourceAtAddress(debug_info, w, address, tty_config, it.fp) catch {}; + } else { + printLastUnwindError(&it, debug_info, w, tty_config); + } + + // remove nulls + for (result_writer.items) |*itm| if (itm.* == 0) { + itm.* = ' '; + }; + // add null terminator + result_writer.append(0) catch { + result_writer.deinit(); + return "".ptr; + }; + return @ptrCast((result_writer.toOwnedSlice() catch { + result_writer.deinit(); + return "".ptr; + }).ptr); +} + +pub fn printSourceAtAddress(debug_info: *std.debug.SelfInfo, out_stream: anytype, address: usize, tty_config: std.io.tty.Config, fp: usize) !void { + const module = debug_info.getModuleForAddress(address) catch |err| switch (err) { + error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config), + else => return err, + }; + + const symbol_info = module.getSymbolAtAddress(debug_info.allocator, address) catch |err| switch (err) { + error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config), + else => return err, + }; + defer if (symbol_info.source_location) |sl| debug_info.allocator.free(sl.file_name); + + const probably_llint = address > @intFromPtr(&jsc_llint_begin) and address < @intFromPtr(&jsc_llint_end); + var allow_llint = true; + if (std.mem.startsWith(u8, symbol_info.name, "__")) { + allow_llint = false; // disallow llint for __ZN3JSC11Interpreter20executeModuleProgramEPNS_14JSModuleRecordEPNS_23ModuleProgramExecutableEPNS_14JSGlobalObjectEPNS_19JSModuleEnvironmentENS_7JSValueES9_ + } + if (std.mem.startsWith(u8, symbol_info.name, "_llint_call_javascript")) { + allow_llint = false; // disallow llint for _llint_call_javascript + } + const do_llint = probably_llint and allow_llint; + + const frame: *const bun.JSC.CallFrame = @ptrFromInt(fp); + if (do_llint) { + const srcloc = frame.getCallerSrcLoc(bun.JSC.Bun__getVM().global); + try tty_config.setColor(out_stream, .bold); + try out_stream.print("{s}:{d}:{d}: ", .{ srcloc.str, srcloc.line, srcloc.column }); + try tty_config.setColor(out_stream, .reset); + } + + try printLineInfo( + out_stream, + symbol_info.source_location, + address, + symbol_info.name, + symbol_info.compile_unit_name, + tty_config, + printLineFromFileAnyOs, + do_llint, + ); + if (do_llint) { + const desc = frame.describeFrame(); + try out_stream.print(" {s}\n ", .{desc}); + try tty_config.setColor(out_stream, .green); + try out_stream.writeAll("^"); + try tty_config.setColor(out_stream, .reset); + try out_stream.writeAll("\n"); + } +} + +fn printUnknownSource(debug_info: *std.debug.SelfInfo, out_stream: anytype, address: usize, tty_config: std.io.tty.Config) !void { + const module_name = debug_info.getModuleNameForAddress(address); + return printLineInfo( + out_stream, + null, + address, + "???", + module_name orelse "???", + tty_config, + printLineFromFileAnyOs, + false, + ); +} +fn printLineInfo( + out_stream: anytype, + source_location: ?std.debug.SourceLocation, + address: usize, + symbol_name: []const u8, + compile_unit_name: []const u8, + tty_config: std.io.tty.Config, + comptime printLineFromFile: anytype, + do_llint: bool, +) !void { + nosuspend { + try tty_config.setColor(out_stream, .bold); + + if (source_location) |*sl| { + try out_stream.print("{s}:{d}:{d}", .{ sl.file_name, sl.line, sl.column }); + } else if (!do_llint) { + try out_stream.writeAll("???:?:?"); + } + + try tty_config.setColor(out_stream, .reset); + if (!do_llint or source_location != null) try out_stream.writeAll(": "); + try tty_config.setColor(out_stream, .dim); + try out_stream.print("0x{x} in {s} ({s})", .{ address, symbol_name, compile_unit_name }); + try tty_config.setColor(out_stream, .reset); + try out_stream.writeAll("\n"); + + // Show the matching source code line if possible + if (source_location) |sl| { + if (printLineFromFile(out_stream, sl)) { + if (sl.column > 0) { + // The caret already takes one char + const space_needed = @as(usize, @intCast(sl.column - 1)); + + try out_stream.writeByteNTimes(' ', space_needed); + try tty_config.setColor(out_stream, .green); + try out_stream.writeAll("^"); + try tty_config.setColor(out_stream, .reset); + } + try out_stream.writeAll("\n"); + } else |err| switch (err) { + error.EndOfFile, error.FileNotFound => {}, + error.BadPathName => {}, + error.AccessDenied => {}, + else => return err, + } + } + } +} + +fn printLineFromFileAnyOs(out_stream: anytype, source_location: std.debug.SourceLocation) !void { + // Need this to always block even in async I/O mode, because this could potentially + // be called from e.g. the event loop code crashing. + var f = try std.fs.cwd().openFile(source_location.file_name, .{}); + defer f.close(); + // TODO fstat and make sure that the file has the correct size + + var buf: [std.mem.page_size]u8 = undefined; + var amt_read = try f.read(buf[0..]); + const line_start = seek: { + var current_line_start: usize = 0; + var next_line: usize = 1; + while (next_line != source_location.line) { + const slice = buf[current_line_start..amt_read]; + if (std.mem.indexOfScalar(u8, slice, '\n')) |pos| { + next_line += 1; + if (pos == slice.len - 1) { + amt_read = try f.read(buf[0..]); + current_line_start = 0; + } else current_line_start += pos + 1; + } else if (amt_read < buf.len) { + return error.EndOfFile; + } else { + amt_read = try f.read(buf[0..]); + current_line_start = 0; + } + } + break :seek current_line_start; + }; + const slice = buf[line_start..amt_read]; + if (std.mem.indexOfScalar(u8, slice, '\n')) |pos| { + const line = slice[0 .. pos + 1]; + std.mem.replaceScalar(u8, line, '\t', ' '); + return out_stream.writeAll(line); + } else { // Line is the last inside the buffer, and requires another read to find delimiter. Alternatively the file ends. + std.mem.replaceScalar(u8, slice, '\t', ' '); + try out_stream.writeAll(slice); + while (amt_read == buf.len) { + amt_read = try f.read(buf[0..]); + if (std.mem.indexOfScalar(u8, buf[0..amt_read], '\n')) |pos| { + const line = buf[0 .. pos + 1]; + std.mem.replaceScalar(u8, line, '\t', ' '); + return out_stream.writeAll(line); + } else { + const line = buf[0..amt_read]; + std.mem.replaceScalar(u8, line, '\t', ' '); + try out_stream.writeAll(line); + } + } + // Make sure printing last line of file inserts extra newline + try out_stream.writeByte('\n'); + } +} + +fn printLastUnwindError(it: *std.debug.StackIterator, debug_info: *std.debug.SelfInfo, out_stream: anytype, tty_config: std.io.tty.Config) void { + if (!std.debug.have_ucontext) return; + if (it.getLastError()) |unwind_error| { + printUnwindError(debug_info, out_stream, unwind_error.address, unwind_error.err, tty_config) catch {}; + } +} + +fn printUnwindError(debug_info: *std.debug.SelfInfo, out_stream: anytype, address: usize, err: std.debug.UnwindError, tty_config: std.io.tty.Config) !void { + const module_name = debug_info.getModuleNameForAddress(address) orelse "???"; + try tty_config.setColor(out_stream, .dim); + if (err == error.MissingDebugInfo) { + try out_stream.print("Unwind information for `{s}:0x{x}` was not available, trace may be incomplete\n\n", .{ module_name, address }); + } else { + try out_stream.print("Unwind error at address `{s}:0x{x}` ({}), trace may be incomplete\n\n", .{ module_name, address, err }); + } + try tty_config.setColor(out_stream, .reset); +} diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 6c2ed8272c..37401db957 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -4823,6 +4823,7 @@ const InternalTestingAPIs = struct { comptime { _ = Crypto.JSPasswordObject.JSPasswordObject__create; + _ = @import("../../btjs.zig").dumpBtjsTrace; BunObject.exportAll(); } diff --git a/src/bun.js/bindings/CallFrame.zig b/src/bun.js/bindings/CallFrame.zig index 199277cec1..8350c66f5a 100644 --- a/src/bun.js/bindings/CallFrame.zig +++ b/src/bun.js/bindings/CallFrame.zig @@ -190,4 +190,9 @@ pub const CallFrame = opaque { .column = column, }; } + + extern fn Bun__CallFrame__describeFrame(*const CallFrame) [*:0]const u8; + pub fn describeFrame(self: *const CallFrame) [:0]const u8 { + return std.mem.span(Bun__CallFrame__describeFrame(self)); + } }; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index b9ee5d1195..28361ac008 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -6443,3 +6443,10 @@ CPP_DECL bool Bun__util__isInsideNodeModules(JSC::JSGlobalObject* globalObject, return inNodeModules; } + +#if BUN_DEBUG +CPP_DECL const char* Bun__CallFrame__describeFrame(JSC::CallFrame* callFrame) +{ + return callFrame->describeFrame(); +} +#endif diff --git a/src/symbols.def b/src/symbols.def index 4225732ee9..ed8dce0cff 100644 --- a/src/symbols.def +++ b/src/symbols.def @@ -572,6 +572,7 @@ EXPORTS node_api_create_property_key_latin1 node_api_create_property_key_utf16 node_api_create_property_key_utf8 + dumpBtjsTrace ?TryGetCurrent@Isolate@v8@@SAPEAV12@XZ ?GetCurrent@Isolate@v8@@SAPEAV12@XZ ?GetCurrentContext@Isolate@v8@@QEAA?AV?$Local@VContext@v8@@@2@XZ diff --git a/src/symbols.dyn b/src/symbols.dyn index 88f97b6e92..c2aa3aca63 100644 --- a/src/symbols.dyn +++ b/src/symbols.dyn @@ -224,4 +224,5 @@ __ZN2v812api_internal17FromJustIsNothingEv; _uv_os_getpid; _uv_os_getppid; + _dumpBtjsTrace; }; diff --git a/src/symbols.txt b/src/symbols.txt index 93bb5e644c..282f0bfd96 100644 --- a/src/symbols.txt +++ b/src/symbols.txt @@ -223,3 +223,4 @@ __ZNK2v85Value10IsFunctionEv __ZN2v812api_internal17FromJustIsNothingEv _uv_os_getpid _uv_os_getppid +_dumpBtjsTrace \ No newline at end of file