Skip to content
Merged
13 changes: 13 additions & 0 deletions src/Watcher.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const Watcher = @This();

const DebugLogScope = bun.Output.Scoped(.watcher, .visible);
const log = DebugLogScope.log;
const WatcherTrace = @import("./watcher/WatcherTrace.zig");

// This will always be [max_count]WatchEvent,
// We avoid statically allocating because it increases the binary size.
Expand Down Expand Up @@ -95,9 +96,18 @@ pub fn init(comptime T: type, ctx: *T, fs: *bun.fs.FileSystem, allocator: std.me

try Platform.init(&watcher.platform, fs.top_level_dir);

// Initialize trace file if BUN_WATCHER_TRACE env var is set
WatcherTrace.init();

return watcher;
}

/// Write trace events to the trace file if enabled.
/// This runs on the watcher thread, so no locking is needed.
pub fn writeTraceEvents(this: *Watcher, events: []WatchEvent, changed_files: []?[:0]u8) void {
WatcherTrace.writeEvents(this, events, changed_files);
}

pub fn start(this: *Watcher) !void {
bun.assert(this.watchloop_handle == null);
this.thread = try std.Thread.spawn(.{}, threadMain, .{this});
Expand Down Expand Up @@ -244,6 +254,9 @@ fn threadMain(this: *Watcher) !void {
}
this.watchlist.deinit(this.allocator);

// Close trace file if open
WatcherTrace.deinit();

const allocator = this.allocator;
allocator.destroy(this);
}
Expand Down
1 change: 1 addition & 0 deletions src/watcher/INotifyWatcher.zig
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ fn processINotifyEventBatch(this: *bun.Watcher, event_count: usize, temp_name_li
defer this.mutex.unlock();
if (this.running) {
// all_events.len == 0 is checked above, so last_event_index + 1 is safe
this.writeTraceEvents(all_events[0 .. last_event_index + 1], this.changed_filepaths[0..name_off]);
this.onFileUpdate(this.ctx, all_events[0 .. last_event_index + 1], this.changed_filepaths[0..name_off], this.watchlist);
}

Expand Down
1 change: 1 addition & 0 deletions src/watcher/KEventWatcher.zig
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ pub fn watchLoopCycle(this: *Watcher) bun.sys.Maybe(void) {
this.mutex.lock();
defer this.mutex.unlock();
if (this.running) {
this.writeTraceEvents(watchevents, this.changed_filepaths[0..watchevents.len]);
this.onFileUpdate(this.ctx, watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist);
}

Expand Down
108 changes: 108 additions & 0 deletions src/watcher/WatcherTrace.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const std = @import("std");
const bun = @import("../bun.zig");
const Watcher = @import("../Watcher.zig");

/// Optional trace file for debugging watcher events
var trace_file: ?bun.FileDescriptor = null;

/// Initialize trace file if BUN_WATCHER_TRACE env var is set.
/// Only checks once on first call.
pub fn init() void {
if (trace_file != null) return;

if (bun.getenvZ("BUN_WATCHER_TRACE")) |trace_path| {
if (trace_path.len > 0) {
const flags = bun.O.WRONLY | bun.O.CREAT | bun.O.APPEND;
const mode = 0o644;
switch (bun.sys.openA(trace_path, flags, mode)) {
.result => |fd| {
trace_file = fd;
},
.err => {
// Silently ignore errors opening trace file
},
}
}
}
}

/// Write trace events to the trace file if enabled.
/// This is called from the watcher thread, so no locking is needed.
pub fn writeEvents(watcher: *Watcher, events: []Watcher.WatchEvent, changed_files: []?[:0]u8) void {
const fd = trace_file orelse return;

var buf: [16384]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this slop and use a buffered writer with bun.sys.File

const writer = stream.writer();

// Get current timestamp
const timestamp = std.time.milliTimestamp();

for (events) |event| {
stream.reset();

const watchlist_slice = watcher.watchlist.slice();
const file_paths = watchlist_slice.items(.file_path);
const file_path = if (event.index < file_paths.len) file_paths[event.index] else "(unknown)";

// Write JSON manually for each event
writer.writeAll("{\"timestamp\":") catch return;
writer.print("{d}", .{timestamp}) catch return;
writer.writeAll(",\"index\":") catch return;
writer.print("{d}", .{event.index}) catch return;
writer.writeAll(",\"path\":\"") catch return;

// Escape quotes and backslashes in path
for (file_path) |c| {
if (c == '"' or c == '\\') {
writer.writeByte('\\') catch return;
}
writer.writeByte(c) catch return;
}
writer.writeAll("\"") catch return;

// Write individual operation flags
writer.writeAll(",\"delete\":") catch return;
writer.writeAll(if (event.op.delete) "true" else "false") catch return;
writer.writeAll(",\"write\":") catch return;
writer.writeAll(if (event.op.write) "true" else "false") catch return;
writer.writeAll(",\"rename\":") catch return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a bunch of booleans it should just be an array of events that happened

writer.writeAll(if (event.op.rename) "true" else "false") catch return;
writer.writeAll(",\"metadata\":") catch return;
writer.writeAll(if (event.op.metadata) "true" else "false") catch return;
writer.writeAll(",\"move_to\":") catch return;
writer.writeAll(if (event.op.move_to) "true" else "false") catch return;

// Add changed file names if any
const names = event.names(changed_files);
writer.writeAll(",\"changed_files\":[") catch return;
var first = true;
for (names) |name_opt| {
if (name_opt) |name| {
if (!first) writer.writeAll(",") catch return;
first = false;
writer.writeAll("\"") catch return;
// Escape quotes and backslashes in filename
for (name) |c| {
if (c == '"' or c == '\\') {
writer.writeByte('\\') catch return;
}
writer.writeByte(c) catch return;
}
writer.writeAll("\"") catch return;
}
}
writer.writeAll("]}\n") catch return;

const written = stream.getWritten();
_ = bun.sys.write(fd, written);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a BufferedWriter instead and defer flush at the end of the function scope

}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

/// Close the trace file if open
pub fn deinit() void {
if (trace_file) |fd| {
fd.close();
trace_file = null;
}
}
1 change: 1 addition & 0 deletions src/watcher/WindowsWatcher.zig
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ fn processWatchEventBatch(this: *bun.Watcher, event_count: usize) bun.sys.Maybe(

log("calling onFileUpdate (all_events.len = {d})", .{all_events.len});

this.writeTraceEvents(all_events, this.changed_filepaths[0 .. last_event_index + 1]);
this.onFileUpdate(this.ctx, all_events, this.changed_filepaths[0 .. last_event_index + 1], this.watchlist);

return .success;
Expand Down
Loading
Loading