mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Compare commits
1 Commits
deps/updat
...
cursor/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b9bdebffd |
@@ -638,6 +638,11 @@ pub fn enterUWSLoop(this: *VirtualMachine) void {
|
||||
}
|
||||
|
||||
pub fn onBeforeExit(this: *VirtualMachine) void {
|
||||
// Emit BeforeExit trace event
|
||||
if (TraceEvents.instance != null) {
|
||||
TraceEvents.emitInstant("BeforeExit");
|
||||
}
|
||||
|
||||
this.exit_handler.dispatchOnBeforeExit();
|
||||
var dispatch = false;
|
||||
while (true) {
|
||||
@@ -696,6 +701,16 @@ pub fn setEntryPointEvalResultCJS(this: *VirtualMachine, value: JSValue) callcon
|
||||
}
|
||||
|
||||
pub fn onExit(this: *VirtualMachine) void {
|
||||
// Emit RunCleanup trace event
|
||||
if (TraceEvents.instance != null) {
|
||||
TraceEvents.emitInstant("RunCleanup");
|
||||
}
|
||||
|
||||
// Emit AtExit trace event
|
||||
if (TraceEvents.instance != null) {
|
||||
TraceEvents.emitInstant("AtExit");
|
||||
}
|
||||
|
||||
this.exit_handler.dispatchOnExit();
|
||||
this.is_shutting_down = true;
|
||||
|
||||
@@ -710,6 +725,11 @@ pub fn onExit(this: *VirtualMachine) void {
|
||||
hook.execute();
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize trace events to close the JSON file properly
|
||||
if (TraceEvents.instance != null) {
|
||||
TraceEvents.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
extern fn Zig__GlobalObject__destructOnExit(*JSGlobalObject) void;
|
||||
@@ -908,6 +928,11 @@ pub fn initWithModuleGraph(
|
||||
vm.configureDebugger(opts.debugger);
|
||||
vm.body_value_hive_allocator = Body.Value.HiveAllocator.init(bun.typedAllocator(JSC.WebCore.Body.Value));
|
||||
|
||||
// Emit the Environment trace event for Node.js compatibility
|
||||
if (TraceEvents.instance != null) {
|
||||
TraceEvents.emitInstant("Environment");
|
||||
}
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
@@ -3566,3 +3591,4 @@ const DotEnv = bun.DotEnv;
|
||||
const HotReloader = JSC.hot_reloader.HotReloader;
|
||||
const Body = webcore.Body;
|
||||
const Counters = @import("./Counters.zig");
|
||||
const TraceEvents = @import("./node/node_trace_events.zig").TraceEvents;
|
||||
|
||||
@@ -12,6 +12,12 @@ const api = bun.api;
|
||||
const StatWatcherScheduler = @import("../node/node_fs_stat_watcher.zig").StatWatcherScheduler;
|
||||
const Timer = @This();
|
||||
const DNSResolver = @import("./bun/dns_resolver.zig").DNSResolver;
|
||||
const SystemTimer = bun.Timer;
|
||||
const timespec = bun.timespec;
|
||||
const log = bun.Output.scoped(.timer, false);
|
||||
const min_timer_id = 1;
|
||||
const Strong = JSC.Strong;
|
||||
const TraceEvents = @import("../node/node_trace_events.zig").TraceEvents;
|
||||
|
||||
/// TimeoutMap is map of i32 to nullable Timeout structs
|
||||
/// i32 is exposed to JavaScript and can be used with clearTimeout, clearInterval, etc.
|
||||
@@ -281,6 +287,14 @@ pub const All = struct {
|
||||
}
|
||||
|
||||
pub fn drainTimers(this: *All, vm: *VirtualMachine) void {
|
||||
// Emit RunTimers trace event
|
||||
if (TraceEvents.instance != null) {
|
||||
TraceEvents.emitInstant("RunTimers");
|
||||
}
|
||||
|
||||
this.lock.lock();
|
||||
defer this.lock.unlock();
|
||||
|
||||
// Set in next().
|
||||
var now: timespec = undefined;
|
||||
// Split into a separate variable to avoid increasing the size of the timespec type.
|
||||
@@ -1505,8 +1519,6 @@ pub const EventLoopTimer = struct {
|
||||
pub fn deinit(_: *EventLoopTimer) void {}
|
||||
};
|
||||
|
||||
const timespec = bun.timespec;
|
||||
|
||||
/// A timer created by WTF code and invoked by Bun's event loop
|
||||
pub const WTFTimer = @import("../WTFTimer.zig");
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ static StringView extractCookieName(const StringView& cookie)
|
||||
{
|
||||
auto nameEnd = cookie.find('=');
|
||||
if (nameEnd == notFound)
|
||||
return String();
|
||||
return StringView();
|
||||
return cookie.substring(0, nameEnd);
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,11 @@ fn tickWithCount(this: *EventLoop, virtual_machine: *VirtualMachine) u32 {
|
||||
}
|
||||
|
||||
pub fn tickImmediateTasks(this: *EventLoop, virtual_machine: *VirtualMachine) void {
|
||||
// Emit CheckImmediate trace event
|
||||
if (TraceEvents.instance != null) {
|
||||
TraceEvents.emitInstant("CheckImmediate");
|
||||
}
|
||||
|
||||
var to_run_now = this.immediate_tasks;
|
||||
|
||||
this.immediate_tasks = this.next_immediate_tasks;
|
||||
@@ -201,6 +206,11 @@ pub fn tickImmediateTasks(this: *EventLoop, virtual_machine: *VirtualMachine) vo
|
||||
exception_thrown = task.runImmediateTask(virtual_machine);
|
||||
}
|
||||
|
||||
// Emit RunAndClearNativeImmediates trace event after running all immediate tasks
|
||||
if (to_run_now.items.len > 0 and TraceEvents.instance != null) {
|
||||
TraceEvents.emitInstant("RunAndClearNativeImmediates");
|
||||
}
|
||||
|
||||
// make sure microtasks are drained if the last task had an exception
|
||||
if (exception_thrown) {
|
||||
this.maybeDrainMicrotasks();
|
||||
@@ -644,3 +654,6 @@ const Environment = bun.Environment;
|
||||
const Waker = bun.Async.Waker;
|
||||
const uws = bun.uws;
|
||||
const Async = bun.Async;
|
||||
const ManagedChannelMap = JSC.ManagedChannelMap;
|
||||
const getAllocator = bun.getAllocator;
|
||||
const TraceEvents = @import("./node/node_trace_events.zig").TraceEvents;
|
||||
|
||||
176
src/bun.js/node/node_trace_events.zig
Normal file
176
src/bun.js/node/node_trace_events.zig
Normal file
@@ -0,0 +1,176 @@
|
||||
const std = @import("std");
|
||||
const bun = @import("bun");
|
||||
const JSC = bun.JSC;
|
||||
const os = std.os;
|
||||
const Output = bun.Output;
|
||||
const strings = bun.strings;
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
const JSString = JSC.JSString;
|
||||
const JSValue = JSC.JSValue;
|
||||
|
||||
extern fn uv_os_getpid() c_int;
|
||||
|
||||
/// Node.js trace events implementation
|
||||
/// This is used when --trace-event-categories is passed
|
||||
pub const TraceEvents = struct {
|
||||
const Self = @This();
|
||||
|
||||
/// Singleton instance
|
||||
pub var instance: ?*Self = null;
|
||||
|
||||
file: ?std.fs.File = null,
|
||||
categories: []const u8 = "",
|
||||
first_event: bool = true,
|
||||
enabled: bool = false,
|
||||
recursion_guard: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
|
||||
pid: u32,
|
||||
start_time: u64,
|
||||
events: std.ArrayList(Event) = undefined,
|
||||
allocator: std.mem.Allocator,
|
||||
is_writing: bool = false, // Guard against recursion
|
||||
|
||||
const Event = struct {
|
||||
name: []const u8,
|
||||
cat: []const u8,
|
||||
ph: u8, // phase - B (begin), E (end), X (complete), I (instant)
|
||||
ts: u64, // timestamp in microseconds
|
||||
pid: u32,
|
||||
tid: u32,
|
||||
dur: ?u64 = null, // duration in microseconds (for X events)
|
||||
args: ?JSC.JSValue = null,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, categories: []const u8, cwd: []const u8) !void {
|
||||
if (instance != null) return;
|
||||
|
||||
const self = try allocator.create(TraceEvents);
|
||||
self.* = .{
|
||||
.categories = categories,
|
||||
.pid = @intCast(uv_os_getpid()),
|
||||
.start_time = bun.getRoughTickCountMs(),
|
||||
.events = std.ArrayList(Event).init(allocator),
|
||||
.allocator = allocator,
|
||||
.enabled = true, // Enable trace events
|
||||
};
|
||||
|
||||
// Create trace file path
|
||||
var buf: bun.PathBuffer = undefined;
|
||||
const path = try std.fmt.bufPrint(&buf, "{s}/node_trace.1.log", .{cwd});
|
||||
self.file = try std.fs.cwd().createFile(path, .{ .truncate = true });
|
||||
|
||||
// Write opening bracket
|
||||
_ = try self.file.?.write("{\"traceEvents\":[\n");
|
||||
|
||||
instance = self;
|
||||
|
||||
// Emit the required metadata
|
||||
const header =
|
||||
\\{"name":"__metadata","ph":"M","pid":
|
||||
;
|
||||
|
||||
var meta_buf: [1024]u8 = undefined;
|
||||
const metadata = try std.fmt.bufPrint(&meta_buf, "{s}{d},\"tid\":0,\"ts\":0,\"args\":{{\"name\":\"bun\"}}}},\n", .{ header, self.pid });
|
||||
|
||||
_ = try self.file.?.write(metadata);
|
||||
}
|
||||
|
||||
pub fn deinit() void {
|
||||
if (instance) |self| {
|
||||
self.finalize() catch {};
|
||||
self.allocator.destroy(self);
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn writeMetadataEvent(self: *TraceEvents) !void {
|
||||
// Write the initial JSON structure
|
||||
const header =
|
||||
\\{"traceEvents":[
|
||||
\\{"name":"process_name","ph":"M","pid":
|
||||
;
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
const metadata = try std.fmt.bufPrint(&buf, "{s}{d},\"tid\":0,\"ts\":0,\"args\":{{\"name\":\"bun\"}}}},\n", .{ header, self.pid });
|
||||
|
||||
self.file.?.writeAll(metadata) catch {};
|
||||
}
|
||||
|
||||
/// Emit a trace event (thread-safe)
|
||||
pub fn emit(name: []const u8, phase: u8) void {
|
||||
const self = instance orelse return;
|
||||
|
||||
// Prevent recursion using atomic compare-and-swap
|
||||
if (self.recursion_guard.swap(true, .seq_cst)) {
|
||||
// Already emitting, skip this event
|
||||
return;
|
||||
}
|
||||
defer _ = self.recursion_guard.swap(false, .seq_cst);
|
||||
|
||||
const now = bun.getRoughTickCountMs();
|
||||
const ts = (now - self.start_time) * 1000; // Convert to microseconds
|
||||
|
||||
// Buffer the event in memory instead of writing immediately
|
||||
const event = Event{
|
||||
.name = name,
|
||||
.cat = "node.environment",
|
||||
.ph = phase,
|
||||
.ts = ts,
|
||||
.pid = self.pid,
|
||||
.tid = 0, // Main thread
|
||||
};
|
||||
|
||||
// Append to events array (we'll write them all during finalization)
|
||||
self.events.append(event) catch {
|
||||
// If we can't allocate, just skip this event
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
/// Helper to emit instant events (most common case)
|
||||
pub fn emitInstant(name: []const u8) void {
|
||||
emit(name, 'I');
|
||||
}
|
||||
|
||||
pub fn emitBegin(name: []const u8) void {
|
||||
emit(name, 'B');
|
||||
}
|
||||
|
||||
pub fn emitEnd(name: []const u8) void {
|
||||
emit(name, 'E');
|
||||
}
|
||||
|
||||
fn writeEvent(self: *TraceEvents, event: *const Event) !void {
|
||||
if (self.file == null) return;
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
const json = try std.fmt.bufPrint(&buf,
|
||||
\\{{"name":"{s}","cat":"{s}","ph":"{c}","ts":{d},"pid":{d},"tid":{d}}},
|
||||
\\
|
||||
, .{ event.name, event.cat, event.ph, event.ts, event.pid, event.tid });
|
||||
|
||||
_ = try self.file.?.write(json);
|
||||
}
|
||||
|
||||
pub fn finalize(self: *TraceEvents) !void {
|
||||
if (self.file) |file| {
|
||||
// Write all buffered events
|
||||
for (self.events.items) |*event| {
|
||||
try self.writeEvent(event);
|
||||
}
|
||||
|
||||
// Write the closing bracket
|
||||
_ = try file.write("{\"name\":\"__metadata\",\"ph\":\"M\",\"pid\":0,\"tid\":0,\"ts\":0,\"args\":{\"thread_name\":\"__metadata\"}}]}\n");
|
||||
file.close();
|
||||
self.file = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shouldEmit(category: []const u8) bool {
|
||||
const self = instance orelse return false;
|
||||
if (!self.enabled) return false;
|
||||
|
||||
// Check if the category matches the enabled categories
|
||||
// For now, we'll emit all "node.environment" events when enabled
|
||||
return strings.eqlComptime(category, "node.environment");
|
||||
}
|
||||
};
|
||||
@@ -21,6 +21,7 @@ const DNSResolver = @import("bun.js/api/bun/dns_resolver.zig").DNSResolver;
|
||||
|
||||
const OpaqueWrap = JSC.OpaqueWrap;
|
||||
const VirtualMachine = JSC.VirtualMachine;
|
||||
const TraceEvents = @import("./bun.js/node/node_trace_events.zig").TraceEvents;
|
||||
|
||||
var run: Run = undefined;
|
||||
pub const Run = struct {
|
||||
@@ -112,6 +113,13 @@ pub const Run = struct {
|
||||
vm.is_main_thread = true;
|
||||
JSC.VirtualMachine.is_main_thread_vm = true;
|
||||
|
||||
// Initialize trace events if enabled
|
||||
if (ctx.runtime_options.trace_event_categories.len > 0) {
|
||||
TraceEvents.init(vm.allocator, ctx.runtime_options.trace_event_categories, ctx.args.absolute_working_dir orelse ".") catch |err| {
|
||||
Output.prettyErrorln("Failed to initialize trace events: {s}", .{@errorName(err)});
|
||||
};
|
||||
}
|
||||
|
||||
doPreconnect(ctx.runtime_options.preconnect);
|
||||
|
||||
const callback = OpaqueWrap(Run, Run.start);
|
||||
@@ -257,6 +265,13 @@ pub const Run = struct {
|
||||
vm.is_main_thread = true;
|
||||
JSC.VirtualMachine.is_main_thread_vm = true;
|
||||
|
||||
// Initialize trace events if enabled
|
||||
if (ctx.runtime_options.trace_event_categories.len > 0) {
|
||||
TraceEvents.init(vm.allocator, ctx.runtime_options.trace_event_categories, ctx.args.absolute_working_dir orelse ".") catch |err| {
|
||||
Output.prettyErrorln("Failed to initialize trace events: {s}", .{@errorName(err)});
|
||||
};
|
||||
}
|
||||
|
||||
// Allow setting a custom timezone
|
||||
if (vm.transpiler.env.get("TZ")) |tz| {
|
||||
if (tz.len > 0) {
|
||||
|
||||
@@ -237,6 +237,7 @@ pub const Arguments = struct {
|
||||
clap.parseParam("--zero-fill-buffers Boolean to force Buffer.allocUnsafe(size) to be zero-filled.") catch unreachable,
|
||||
clap.parseParam("--redis-preconnect Preconnect to $REDIS_URL at startup") catch unreachable,
|
||||
clap.parseParam("--no-addons Throw an error if process.dlopen is called, and disable export condition \"node-addons\"") catch unreachable,
|
||||
clap.parseParam("--trace-event-categories <STR> Enable trace events for the specified categories (Node.js compatibility)") catch unreachable,
|
||||
};
|
||||
|
||||
const auto_or_run_params = [_]ParamType{
|
||||
@@ -851,6 +852,10 @@ pub const Arguments = struct {
|
||||
if (args.flag("--zero-fill-buffers")) {
|
||||
Bun__Node__ZeroFillBuffers = true;
|
||||
}
|
||||
|
||||
if (args.option("--trace-event-categories")) |categories| {
|
||||
ctx.runtime_options.trace_event_categories = categories;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.port != null and opts.origin == null) {
|
||||
@@ -1547,6 +1552,7 @@ pub const Command = struct {
|
||||
/// compatibility.
|
||||
expose_gc: bool = false,
|
||||
preserve_symlinks_main: bool = false,
|
||||
trace_event_categories: []const u8 = "",
|
||||
};
|
||||
|
||||
var global_cli_ctx: Context = undefined;
|
||||
|
||||
59
test/js/node/test/parallel/test-trace-events-environment.js
Normal file
59
test/js/node/test/parallel/test-trace-events-environment.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// Flags: --no-warnings
|
||||
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const cp = require('child_process');
|
||||
const fs = require('fs');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
|
||||
// This tests the emission of node.environment trace events
|
||||
|
||||
const names = new Set([
|
||||
'Environment',
|
||||
'RunAndClearNativeImmediates',
|
||||
'CheckImmediate',
|
||||
'RunTimers',
|
||||
'BeforeExit',
|
||||
'RunCleanup',
|
||||
'AtExit',
|
||||
]);
|
||||
|
||||
if (process.argv[2] === 'child') {
|
||||
/* eslint-disable no-unused-expressions */
|
||||
// This is just so that the child has something to do.
|
||||
1 + 1;
|
||||
// These ensure that the RunTimers, CheckImmediate, and
|
||||
// RunAndClearNativeImmediates appear in the list.
|
||||
setImmediate(() => { 1 + 1; });
|
||||
setTimeout(() => { 1 + 1; }, 1);
|
||||
/* eslint-enable no-unused-expressions */
|
||||
} else {
|
||||
tmpdir.refresh();
|
||||
|
||||
const proc = cp.fork(__filename,
|
||||
[ 'child' ], {
|
||||
cwd: tmpdir.path,
|
||||
execArgv: [
|
||||
'--trace-event-categories',
|
||||
'node.environment',
|
||||
]
|
||||
});
|
||||
|
||||
proc.once('exit', common.mustCall(async () => {
|
||||
const file = tmpdir.resolve('node_trace.1.log');
|
||||
const checkSet = new Set();
|
||||
|
||||
assert(fs.existsSync(file));
|
||||
const data = await fs.promises.readFile(file);
|
||||
JSON.parse(data.toString()).traceEvents
|
||||
.filter((trace) => trace.cat !== '__metadata')
|
||||
.forEach((trace) => {
|
||||
assert.strictEqual(trace.pid, proc.pid);
|
||||
assert(names.has(trace.name));
|
||||
checkSet.add(trace.name);
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(names, checkSet);
|
||||
}));
|
||||
}
|
||||
5
test_trace_minimal.js
Normal file
5
test_trace_minimal.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// Minimal test - just check if trace file is created
|
||||
console.log("Starting minimal trace test");
|
||||
process.on("exit", () => {
|
||||
console.log("Process exiting");
|
||||
});
|
||||
25
test_trace_simple.js
Normal file
25
test_trace_simple.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// Simple test to see if trace events are being written
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const tmpdir = require("os").tmpdir();
|
||||
|
||||
// Change to a temp directory
|
||||
process.chdir(tmpdir);
|
||||
|
||||
// Simple program that should emit a few trace events
|
||||
setTimeout(() => {
|
||||
console.log("Timer fired");
|
||||
process.exit(0);
|
||||
}, 10);
|
||||
|
||||
process.on("exit", () => {
|
||||
console.log("Exit event");
|
||||
const file = path.join(tmpdir, "node_trace.1.log");
|
||||
console.log("Looking for trace file at:", file);
|
||||
console.log("File exists:", fs.existsSync(file));
|
||||
if (fs.existsSync(file)) {
|
||||
const content = fs.readFileSync(file, "utf8");
|
||||
console.log("Trace file content:");
|
||||
console.log(content);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user