Files
bun.sh/src/bun.js/Debugger.zig
pfg 05d0475c6c Update to zig 0.15.2 (#24204)
Fixes ENG-21287

Build times, from `bun run build && echo '//' >> src/main.zig && time
bun run build`

|Platform|0.14.1|0.15.2|Speedup|
|-|-|-|-|
|macos debug asan|126.90s|106.27s|1.19x|
|macos debug noasan|60.62s|50.85s|1.19x|
|linux debug asan|292.77s|241.45s|1.21x|
|linux debug noasan|146.58s|130.94s|1.12x|
|linux debug use_llvm=false|n/a|78.27s|1.87x|
|windows debug asan|177.13s|142.55s|1.24x|

Runtime performance:

- next build memory usage may have gone up by 5%. Otherwise seems the
same. Some code with writers may have gotten slower, especially one
instance of a counting writer and a few instances of unbuffered writers
that now have vtable overhead.
- File size reduced by 800kb (from 100.2mb to 99.4mb)

Improvements:

- `@export` hack is no longer needed for watch
- native x86_64 backend for linux builds faster. to use it, set use_llvm
false and no_link_obj false. also set `ASAN_OPTIONS=detect_leaks=0`
otherwise it will spam the output with tens of thousands of lines of
debug info errors. may need to use the zig lldb fork for debugging.
- zig test-obj, which we will be able to use for zig unit tests

Still an issue:

- false 'dependency loop' errors remain in watch mode
- watch mode crashes observed

Follow-up:

- [ ] search `comptime Writer: type` and `comptime W: type` and remove
- [ ] remove format_mode in our zig fork
- [ ] remove deprecated.zig autoFormatLabelFallback
- [ ] remove deprecated.zig autoFormatLabel
- [ ] remove deprecated.BufferedWriter and BufferedReader
- [ ] remove override_no_export_cpp_apis as it is no longer needed
- [ ] css Parser(W) -> Parser, and remove all the comptime writer: type
params
- [ ] remove deprecated writer fully

Files that add lines:

```
649     src/deprecated.zig
167     scripts/pack-codegen-for-zig-team.ts
54      scripts/cleartrace-impl.js
46      scripts/cleartrace.ts
43      src/windows.zig
18      src/fs.zig
17      src/bun.js/ConsoleObject.zig
16      src/output.zig
12      src/bun.js/test/debug.zig
12      src/bun.js/node/node_fs.zig
8       src/env_loader.zig
7       src/css/printer.zig
7       src/cli/init_command.zig
7       src/bun.js/node.zig
6       src/string/escapeRegExp.zig
6       src/install/PnpmMatcher.zig
5       src/bun.js/webcore/Blob.zig
4       src/crash_handler.zig
4       src/bun.zig
3       src/install/lockfile/bun.lock.zig
3       src/cli/update_interactive_command.zig
3       src/cli/pack_command.zig
3       build.zig
2       src/Progress.zig
2       src/install/lockfile/lockfile_json_stringify_for_debugging.zig
2       src/css/small_list.zig
2       src/bun.js/webcore/prompt.zig
1       test/internal/ban-words.test.ts
1       test/internal/ban-limits.json
1       src/watcher/WatcherTrace.zig
1       src/transpiler.zig
1       src/shell/builtin/cp.zig
1       src/js_printer.zig
1       src/io/PipeReader.zig
1       src/install/bin.zig
1       src/css/selectors/selector.zig
1       src/cli/run_command.zig
1       src/bun.js/RuntimeTranspilerStore.zig
1       src/bun.js/bindings/JSRef.zig
1       src/bake/DevServer.zig
```

Files that remove lines:

```
-1      src/test/recover.zig
-1      src/sql/postgres/SocketMonitor.zig
-1      src/sql/mysql/MySQLRequestQueue.zig
-1      src/sourcemap/CodeCoverage.zig
-1      src/css/values/color_js.zig
-1      src/compile_target.zig
-1      src/bundler/linker_context/convertStmtsForChunk.zig
-1      src/bundler/bundle_v2.zig
-1      src/bun.js/webcore/blob/read_file.zig
-1      src/ast/base.zig
-2      src/sql/postgres/protocol/ArrayList.zig
-2      src/shell/builtin/mkdir.zig
-2      src/install/PackageManager/patchPackage.zig
-2      src/install/PackageManager/PackageManagerDirectories.zig
-2      src/fmt.zig
-2      src/css/declaration.zig
-2      src/css/css_parser.zig
-2      src/collections/baby_list.zig
-2      src/bun.js/bindings/ZigStackFrame.zig
-2      src/ast/E.zig
-3      src/StandaloneModuleGraph.zig
-3      src/deps/picohttp.zig
-3      src/deps/libuv.zig
-3      src/btjs.zig
-4      src/threading/Futex.zig
-4      src/shell/builtin/touch.zig
-4      src/meta.zig
-4      src/install/lockfile.zig
-4      src/css/selectors/parser.zig
-5      src/shell/interpreter.zig
-5      src/css/error.zig
-5      src/bun.js/web_worker.zig
-5      src/bun.js.zig
-6      src/cli/test_command.zig
-6      src/bun.js/VirtualMachine.zig
-6      src/bun.js/uuid.zig
-6      src/bun.js/bindings/JSValue.zig
-9      src/bun.js/test/pretty_format.zig
-9      src/bun.js/api/BunObject.zig
-14     src/install/install_binding.zig
-14     src/fd.zig
-14     src/bun.js/node/path.zig
-14     scripts/pack-codegen-for-zig-team.sh
-17     src/bun.js/test/diff_format.zig
```

`git diff --numstat origin/main...HEAD | awk '{ print ($1-$2)"\t"$3 }' |
sort -rn`

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Co-authored-by: Meghan Denny <meghan@bun.com>
Co-authored-by: tayor.fish <contact@taylor.fish>
2025-11-10 14:38:26 -08:00

450 lines
16 KiB
Zig

path_or_port: ?[]const u8 = null,
from_environment_variable: []const u8 = "",
script_execution_context_id: u32 = 0,
next_debugger_id: u64 = 1,
poll_ref: bun.Async.KeepAlive = .{},
wait_for_connection: Wait = .off,
// wait_for_connection: bool = false,
set_breakpoint_on_first_line: bool = false,
mode: enum {
/// Bun acts as the server. https://debug.bun.sh/ uses this
listen,
/// Bun connects to this path. The VSCode extension uses this.
connect,
} = .listen,
test_reporter_agent: TestReporterAgent = .{},
lifecycle_reporter_agent: LifecycleAgent = .{},
frontend_dev_server_agent: BunFrontendDevServerAgent = .{},
http_server_agent: HTTPServerAgent = .{},
must_block_until_connected: bool = false,
pub const Wait = enum { off, shortly, forever };
pub const log = Output.scoped(.debugger, .visible);
extern "c" fn Bun__createJSDebugger(*JSGlobalObject) u32;
extern "c" fn Bun__ensureDebugger(u32, bool) void;
extern "c" fn Bun__startJSDebuggerThread(*JSGlobalObject, u32, *bun.String, c_int, bool) void;
var futex_atomic: std.atomic.Value(u32) = .init(0);
pub fn waitForDebuggerIfNecessary(this: *VirtualMachine) void {
const debugger = &(this.debugger orelse return);
if (!debugger.must_block_until_connected) {
return;
}
defer debugger.must_block_until_connected = false;
Debugger.log("spin", .{});
while (futex_atomic.load(.monotonic) > 0) {
bun.Futex.waitForever(&futex_atomic, 1);
}
if (comptime Environment.enable_logs)
Debugger.log("waitForDebugger: {f}", .{Output.ElapsedFormatter{
.colors = Output.enable_ansi_colors_stderr,
.duration_ns = @truncate(@as(u128, @intCast(std.time.nanoTimestamp() - bun.cli.start_time))),
}});
Bun__ensureDebugger(debugger.script_execution_context_id, debugger.wait_for_connection != .off);
// Sleep up to 30ms for automatic inspection.
const wait_for_connection_delay_ms = 30;
var deadline: bun.timespec = if (debugger.wait_for_connection == .shortly) bun.timespec.now().addMs(wait_for_connection_delay_ms) else undefined;
if (comptime Environment.isWindows) {
// TODO: remove this when tickWithTimeout actually works properly on Windows.
if (debugger.wait_for_connection == .shortly) {
uv.uv_update_time(this.uvLoop());
var timer = bun.handleOom(bun.default_allocator.create(uv.Timer));
timer.* = std.mem.zeroes(uv.Timer);
timer.init(this.uvLoop());
const onDebuggerTimer = struct {
fn call(handle: *uv.Timer) callconv(.c) void {
const vm = VirtualMachine.get();
vm.debugger.?.poll_ref.unref(vm);
uv.uv_close(@ptrCast(handle), deinitTimer);
}
fn deinitTimer(handle: *anyopaque) callconv(.c) void {
bun.default_allocator.destroy(@as(*uv.Timer, @ptrCast(@alignCast(handle))));
}
}.call;
timer.start(wait_for_connection_delay_ms, 0, &onDebuggerTimer);
timer.ref();
}
}
while (debugger.wait_for_connection != .off) {
this.eventLoop().tick();
switch (debugger.wait_for_connection) {
.forever => {
this.eventLoop().autoTickActive();
if (comptime Environment.enable_logs)
log("waited: {D}", .{@as(i64, @truncate(std.time.nanoTimestamp() - bun.cli.start_time))});
},
.shortly => {
// Handle .incrementRefConcurrently
if (comptime Environment.isPosix) {
const pending_unref = this.pending_unref_counter;
if (pending_unref > 0) {
this.pending_unref_counter = 0;
this.uwsLoop().unrefCount(pending_unref);
}
}
this.uwsLoop().tickWithTimeout(&deadline);
if (comptime Environment.enable_logs)
log("waited: {D}", .{@as(i64, @truncate(std.time.nanoTimestamp() - bun.cli.start_time))});
const elapsed = bun.timespec.now();
if (elapsed.order(&deadline) != .lt) {
debugger.poll_ref.unref(this);
log("Timed out waiting for the debugger", .{});
break;
}
},
.off => {
break;
},
}
}
}
pub var has_created_debugger: bool = false;
pub fn create(this: *VirtualMachine, globalObject: *JSGlobalObject) !void {
log("create", .{});
jsc.markBinding(@src());
if (!has_created_debugger) {
has_created_debugger = true;
std.mem.doNotOptimizeAway(&TestReporterAgent.Bun__TestReporterAgentDisable);
std.mem.doNotOptimizeAway(&LifecycleAgent.Bun__LifecycleAgentDisable);
std.mem.doNotOptimizeAway(&TestReporterAgent.Bun__TestReporterAgentEnable);
std.mem.doNotOptimizeAway(&LifecycleAgent.Bun__LifecycleAgentEnable);
var debugger = &this.debugger.?;
debugger.script_execution_context_id = Bun__createJSDebugger(globalObject);
if (!this.has_started_debugger) {
this.has_started_debugger = true;
var thread = try std.Thread.spawn(.{}, startJSDebuggerThread, .{this});
thread.detach();
}
this.eventLoop().ensureWaker();
if (debugger.wait_for_connection != .off) {
debugger.poll_ref.ref(this);
debugger.must_block_until_connected = true;
}
}
}
pub fn startJSDebuggerThread(other_vm: *VirtualMachine) void {
var arena = bun.MimallocArena.init();
Output.Source.configureNamedThread("Debugger");
log("startJSDebuggerThread", .{});
jsc.markBinding(@src());
// Create a thread-local env_loader to avoid allocator threading violations
const thread_allocator = arena.allocator();
const env_map = thread_allocator.create(DotEnv.Map) catch @panic("Failed to create debugger env map");
env_map.* = DotEnv.Map.init(thread_allocator);
const env_loader = thread_allocator.create(DotEnv.Loader) catch @panic("Failed to create debugger env loader");
env_loader.* = DotEnv.Loader.init(env_map, thread_allocator);
var vm = VirtualMachine.init(.{
.allocator = thread_allocator,
.args = std.mem.zeroes(bun.schema.api.TransformOptions),
.store_fd = false,
.env_loader = env_loader,
}) catch @panic("Failed to create Debugger VM");
vm.allocator = arena.allocator();
vm.arena = &arena;
vm.transpiler.configureDefines() catch @panic("Failed to configure defines");
vm.is_main_thread = false;
vm.eventLoop().ensureWaker();
const callback = jsc.OpaqueWrap(VirtualMachine, start);
vm.global.vm().holdAPILock(other_vm, callback);
}
pub export fn Debugger__didConnect() void {
var this = VirtualMachine.get();
if (this.debugger.?.wait_for_connection != .off) {
this.debugger.?.wait_for_connection = .off;
this.debugger.?.poll_ref.unref(this);
this.eventLoop().wakeup();
}
}
fn start(other_vm: *VirtualMachine) void {
jsc.markBinding(@src());
var this = VirtualMachine.get();
const debugger = other_vm.debugger.?;
const loop = this.eventLoop();
if (debugger.from_environment_variable.len > 0) {
var url = bun.String.cloneUTF8(debugger.from_environment_variable);
loop.enter();
defer loop.exit();
Bun__startJSDebuggerThread(this.global, debugger.script_execution_context_id, &url, 1, debugger.mode == .connect);
}
if (debugger.path_or_port) |path_or_port| {
var url = bun.String.cloneUTF8(path_or_port);
loop.enter();
defer loop.exit();
Bun__startJSDebuggerThread(this.global, debugger.script_execution_context_id, &url, 0, debugger.mode == .connect);
}
this.global.handleRejectedPromises();
if (this.log.msgs.items.len > 0) {
this.log.print(Output.errorWriter()) catch {};
Output.prettyErrorln("\n", .{});
Output.flush();
}
log("wake", .{});
futex_atomic.store(0, .monotonic);
bun.Futex.wake(&futex_atomic, 1);
other_vm.eventLoop().wakeup();
this.eventLoop().tick();
other_vm.eventLoop().wakeup();
while (true) {
while (this.isEventLoopAlive()) {
this.tick();
this.eventLoop().autoTickActive();
}
this.eventLoop().tickPossiblyForever();
}
}
pub const AsyncTaskTracker = struct {
id: u64,
pub fn init(vm: *VirtualMachine) AsyncTaskTracker {
return .{ .id = vm.nextAsyncTaskID() };
}
pub fn didSchedule(this: AsyncTaskTracker, globalObject: *JSGlobalObject) void {
if (this.id == 0) return;
didScheduleAsyncCall(globalObject, AsyncCallType.EventListener, this.id, true);
}
pub fn didCancel(this: AsyncTaskTracker, globalObject: *JSGlobalObject) void {
if (this.id == 0) return;
didCancelAsyncCall(globalObject, AsyncCallType.EventListener, this.id);
}
pub fn willDispatch(this: AsyncTaskTracker, globalObject: *JSGlobalObject) void {
if (this.id == 0) {
return;
}
willDispatchAsyncCall(globalObject, AsyncCallType.EventListener, this.id);
}
pub fn didDispatch(this: AsyncTaskTracker, globalObject: *JSGlobalObject) void {
if (this.id == 0) {
return;
}
didDispatchAsyncCall(globalObject, AsyncCallType.EventListener, this.id);
}
};
pub const AsyncCallType = enum(u8) {
DOMTimer = 1,
EventListener = 2,
PostMessage = 3,
RequestAnimationFrame = 4,
Microtask = 5,
};
extern fn Debugger__didScheduleAsyncCall(*JSGlobalObject, AsyncCallType, u64, bool) void;
extern fn Debugger__didCancelAsyncCall(*JSGlobalObject, AsyncCallType, u64) void;
extern fn Debugger__didDispatchAsyncCall(*JSGlobalObject, AsyncCallType, u64) void;
extern fn Debugger__willDispatchAsyncCall(*JSGlobalObject, AsyncCallType, u64) void;
pub fn didScheduleAsyncCall(globalObject: *JSGlobalObject, call: AsyncCallType, id: u64, single_shot: bool) void {
jsc.markBinding(@src());
Debugger__didScheduleAsyncCall(globalObject, call, id, single_shot);
}
pub fn didCancelAsyncCall(globalObject: *JSGlobalObject, call: AsyncCallType, id: u64) void {
jsc.markBinding(@src());
Debugger__didCancelAsyncCall(globalObject, call, id);
}
pub fn didDispatchAsyncCall(globalObject: *JSGlobalObject, call: AsyncCallType, id: u64) void {
jsc.markBinding(@src());
Debugger__didDispatchAsyncCall(globalObject, call, id);
}
pub fn willDispatchAsyncCall(globalObject: *JSGlobalObject, call: AsyncCallType, id: u64) void {
jsc.markBinding(@src());
Debugger__willDispatchAsyncCall(globalObject, call, id);
}
pub const TestReporterAgent = struct {
handle: ?*Handle = null,
const debug = Output.scoped(.TestReporterAgent, .visible);
/// this enum is kept in sync with c++ InspectorTestReporterAgent.cpp `enum class BunTestStatus`
pub const TestStatus = enum(u8) {
pass,
fail,
timeout,
skip,
todo,
skipped_because_label,
};
pub const TestType = enum(u8) {
@"test" = 0,
describe = 1,
};
pub const Handle = opaque {
extern "c" fn Bun__TestReporterAgentReportTestFound(agent: *Handle, callFrame: *jsc.CallFrame, testId: c_int, name: *bun.String, item_type: TestType, parentId: c_int) void;
extern "c" fn Bun__TestReporterAgentReportTestStart(agent: *Handle, testId: c_int) void;
extern "c" fn Bun__TestReporterAgentReportTestEnd(agent: *Handle, testId: c_int, bunTestStatus: TestStatus, elapsed: f64) void;
pub fn reportTestFound(this: *Handle, callFrame: *jsc.CallFrame, testId: i32, name: *bun.String, item_type: TestType, parentId: i32) void {
Bun__TestReporterAgentReportTestFound(this, callFrame, testId, name, item_type, parentId);
}
pub fn reportTestStart(this: *Handle, testId: c_int) void {
Bun__TestReporterAgentReportTestStart(this, testId);
}
pub fn reportTestEnd(this: *Handle, testId: c_int, bunTestStatus: TestStatus, elapsed: f64) void {
Bun__TestReporterAgentReportTestEnd(this, testId, bunTestStatus, elapsed);
}
};
pub export fn Bun__TestReporterAgentEnable(agent: *Handle) void {
if (VirtualMachine.get().debugger) |*debugger| {
debug("enable", .{});
debugger.test_reporter_agent.handle = agent;
}
}
pub export fn Bun__TestReporterAgentDisable(_: *Handle) void {
if (VirtualMachine.get().debugger) |*debugger| {
debug("disable", .{});
debugger.test_reporter_agent.handle = null;
}
}
/// Caller must ensure that it is enabled first.
///
/// Since we may have to call .deinit on the name string.
pub fn reportTestFound(this: TestReporterAgent, callFrame: *jsc.CallFrame, test_id: i32, name: *bun.String, item_type: TestType, parentId: i32) void {
debug("reportTestFound", .{});
this.handle.?.reportTestFound(callFrame, test_id, name, item_type, parentId);
}
/// Caller must ensure that it is enabled first.
pub fn reportTestStart(this: TestReporterAgent, test_id: i32) void {
debug("reportTestStart", .{});
this.handle.?.reportTestStart(test_id);
}
/// Caller must ensure that it is enabled first.
pub fn reportTestEnd(this: TestReporterAgent, test_id: i32, bunTestStatus: TestStatus, elapsed: f64) void {
debug("reportTestEnd", .{});
this.handle.?.reportTestEnd(test_id, bunTestStatus, elapsed);
}
pub fn isEnabled(this: TestReporterAgent) bool {
return this.handle != null;
}
};
pub const LifecycleAgent = struct {
handle: ?*Handle = null,
const debug = Output.scoped(.LifecycleAgent, .visible);
pub const Handle = opaque {
extern "c" fn Bun__LifecycleAgentReportReload(agent: *Handle) void;
extern "c" fn Bun__LifecycleAgentReportError(agent: *Handle, exception: *ZigException) void;
extern "c" fn Bun__LifecycleAgentPreventExit(agent: *Handle) void;
extern "c" fn Bun__LifecycleAgentStopPreventingExit(agent: *Handle) void;
pub fn preventExit(this: *Handle) void {
Bun__LifecycleAgentPreventExit(this);
}
pub fn stopPreventingExit(this: *Handle) void {
Bun__LifecycleAgentStopPreventingExit(this);
}
pub fn reportReload(this: *Handle) void {
debug("reportReload", .{});
Bun__LifecycleAgentReportReload(this);
}
pub fn reportError(this: *Handle, exception: *ZigException) void {
debug("reportError", .{});
Bun__LifecycleAgentReportError(this, exception);
}
};
pub export fn Bun__LifecycleAgentEnable(agent: *Handle) void {
if (VirtualMachine.get().debugger) |*debugger| {
debug("enable", .{});
debugger.lifecycle_reporter_agent.handle = agent;
}
}
pub export fn Bun__LifecycleAgentDisable(agent: *Handle) void {
_ = agent; // autofix
if (VirtualMachine.get().debugger) |*debugger| {
debug("disable", .{});
debugger.lifecycle_reporter_agent.handle = null;
}
}
pub fn reportReload(this: *LifecycleAgent) void {
if (this.handle) |handle| {
handle.reportReload();
}
}
pub fn reportError(this: *LifecycleAgent, exception: *ZigException) void {
if (this.handle) |handle| {
handle.reportError(exception);
}
}
pub fn isEnabled(this: *const LifecycleAgent) bool {
return this.handle != null;
}
};
pub const DebuggerId = bun.GenericIndex(i32, Debugger);
pub const BunFrontendDevServerAgent = @import("./api/server/InspectorBunFrontendDevServerAgent.zig").BunFrontendDevServerAgent;
pub const HTTPServerAgent = @import("./bindings/HTTPServerAgent.zig");
const DotEnv = @import("../env_loader.zig");
const std = @import("std");
const bun = @import("bun");
const Environment = bun.Environment;
const Output = bun.Output;
const uv = bun.windows.libuv;
const jsc = bun.jsc;
const Debugger = jsc.Debugger;
const JSGlobalObject = jsc.JSGlobalObject;
const VirtualMachine = jsc.VirtualMachine;
const ZigException = jsc.ZigException;