Skip to content
15 changes: 5 additions & 10 deletions src/bun.js/HardcodedModule.zig
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub const HardcodedModule = enum {
@"node:trace_events",
@"node:repl",
@"node:inspector",
@"node:inspector/promises",
@"node:http2",
@"node:diagnostics_channel",
@"node:dgram",
Expand Down Expand Up @@ -121,6 +122,7 @@ pub const HardcodedModule = enum {
.{ "node:http2", .@"node:http2" },
.{ "node:https", .@"node:https" },
.{ "node:inspector", .@"node:inspector" },
.{ "node:inspector/promises", .@"node:inspector/promises" },
.{ "node:module", .@"node:module" },
.{ "node:net", .@"node:net" },
.{ "node:readline", .@"node:readline" },
Expand Down Expand Up @@ -230,6 +232,7 @@ pub const HardcodedModule = enum {
nodeEntry("node:http2"),
nodeEntry("node:https"),
nodeEntry("node:inspector"),
nodeEntry("node:inspector/promises"),
nodeEntry("node:module"),
nodeEntry("node:net"),
nodeEntry("node:os"),
Expand Down Expand Up @@ -285,6 +288,7 @@ pub const HardcodedModule = enum {
nodeEntry("http2"),
nodeEntry("https"),
nodeEntry("inspector"),
nodeEntry("inspector/promises"),
nodeEntry("module"),
nodeEntry("net"),
nodeEntry("os"),
Expand Down Expand Up @@ -366,10 +370,6 @@ pub const HardcodedModule = enum {
.{ "bun:internal-for-testing", .{ .path = "bun:internal-for-testing" } },
.{ "ffi", .{ .path = "bun:ffi" } },

// inspector/promises is not implemented, it is an alias of inspector
.{ "node:inspector/promises", .{ .path = "node:inspector", .node_builtin = true } },
.{ "inspector/promises", .{ .path = "node:inspector", .node_builtin = true } },

// Thirdparty packages we override
.{ "@vercel/fetch", .{ .path = "@vercel/fetch" } },
.{ "isomorphic-fetch", .{ .path = "isomorphic-fetch" } },
Expand All @@ -394,12 +394,7 @@ pub const HardcodedModule = enum {
.{ "vitest", .{ .path = "bun:test" } },
};

const node_extra_alias_kvs = [_]struct { string, Alias }{
nodeEntry("node:inspector/promises"),
nodeEntry("inspector/promises"),
};

const node_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ node_extra_alias_kvs);
const node_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs);
pub const bun_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ bun_extra_alias_kvs);
const bun_test_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ bun_extra_alias_kvs ++ bun_test_extra_alias_kvs);

Expand Down
35 changes: 24 additions & 11 deletions src/bun.js/bindings/BunCPUProfiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ namespace Bun {

// Store the profiling start time in microseconds since Unix epoch
static double s_profilingStartTime = 0.0;
// Set sampling interval to 1ms (1000 microseconds) to match Node.js
static int s_samplingInterval = 1000;
static bool s_isProfilerRunning = false;
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved

void setSamplingInterval(int intervalMicroseconds)
{
s_samplingInterval = intervalMicroseconds;
}

bool isCPUProfilerRunning()
{
return s_isProfilerRunning;
}
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved

void startCPUProfiler(JSC::VM& vm)
{
Expand All @@ -35,12 +48,10 @@ void startCPUProfiler(JSC::VM& vm)
stopwatch->start();

JSC::SamplingProfiler& samplingProfiler = vm.ensureSamplingProfiler(WTF::move(stopwatch));

// Set sampling interval to 1ms (1000 microseconds) to match Node.js
samplingProfiler.setTimingInterval(WTF::Seconds::fromMicroseconds(1000));

samplingProfiler.setTimingInterval(WTF::Seconds::fromMicroseconds(s_samplingInterval));
samplingProfiler.noticeCurrentThreadAsJSCExecutionThread();
samplingProfiler.start();
s_isProfilerRunning = true;
}

struct ProfileNode {
Expand All @@ -56,27 +67,29 @@ struct ProfileNode {

WTF::String stopCPUProfilerAndGetJSON(JSC::VM& vm)
{
s_isProfilerRunning = false;

JSC::SamplingProfiler* profiler = vm.samplingProfiler();
if (!profiler)
return WTF::String();

// Shut down the profiler thread first - this is critical!
profiler->shutdown();

// Need to hold the VM lock to safely access stack traces
// JSLock is re-entrant, so always acquiring it handles both JS and shutdown contexts
JSC::JSLockHolder locker(vm);

// Defer GC while we're working with stack traces
JSC::DeferGC deferGC(vm);

// Pause the profiler while holding the lock - this is critical for thread safety.
// The sampling thread holds this lock while modifying traces, so holding it here
// ensures no concurrent modifications. We use pause() instead of shutdown() to
// allow the profiler to be restarted for the inspector API.
auto& lock = profiler->getLock();
WTF::Locker profilerLocker { lock };
profiler->pause();

// releaseStackTraces() calls processUnverifiedStackTraces() internally
auto stackTraces = profiler->releaseStackTraces();

if (stackTraces.isEmpty())
return WTF::String();
profiler->clearData();

Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved
// Build Chrome CPU Profiler format
// Map from stack frame signature to node ID
Expand Down
5 changes: 4 additions & 1 deletion src/bun.js/bindings/BunCPUProfiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ class VM;

namespace Bun {

void setSamplingInterval(int intervalMicroseconds);
bool isCPUProfilerRunning();

// Start the CPU profiler
void startCPUProfiler(JSC::VM& vm);

// Stop the CPU profiler and convert to Chrome CPU profiler JSON format
// Returns JSON string, or empty string on failure
// Returns JSON string, or empty string if profiler was never started
WTF::String stopCPUProfilerAndGetJSON(JSC::VM& vm);

} // namespace Bun
49 changes: 49 additions & 0 deletions src/bun.js/bindings/JSInspectorProfiler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include "root.h"
#include "helpers.h"
#include "BunCPUProfiler.h"
#include "NodeValidator.h"
#include <JavaScriptCore/JSGlobalObject.h>
#include <JavaScriptCore/VM.h>
#include <JavaScriptCore/Error.h>

using namespace JSC;

JSC_DECLARE_HOST_FUNCTION(jsFunction_startCPUProfiler);
JSC_DEFINE_HOST_FUNCTION(jsFunction_startCPUProfiler, (JSGlobalObject * globalObject, CallFrame*))
{
Bun::startCPUProfiler(globalObject->vm());
return JSValue::encode(jsUndefined());
}

JSC_DECLARE_HOST_FUNCTION(jsFunction_stopCPUProfiler);
JSC_DEFINE_HOST_FUNCTION(jsFunction_stopCPUProfiler, (JSGlobalObject * globalObject, CallFrame*))
{
auto& vm = globalObject->vm();
auto result = Bun::stopCPUProfilerAndGetJSON(vm);
return JSValue::encode(jsString(vm, result));
}
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved

JSC_DECLARE_HOST_FUNCTION(jsFunction_setCPUSamplingInterval);
JSC_DEFINE_HOST_FUNCTION(jsFunction_setCPUSamplingInterval, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

if (callFrame->argumentCount() < 1) {
throwVMError(globalObject, scope, createNotEnoughArgumentsError(globalObject));
return {};
}

int interval;
Bun::V::validateInteger(scope, globalObject, callFrame->uncheckedArgument(0), "interval"_s, jsNumber(1), jsUndefined(), &interval);
RETURN_IF_EXCEPTION(scope, {});

Bun::setSamplingInterval(interval);
return JSValue::encode(jsUndefined());
}
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved

JSC_DECLARE_HOST_FUNCTION(jsFunction_isCPUProfilerRunning);
JSC_DEFINE_HOST_FUNCTION(jsFunction_isCPUProfilerRunning, (JSGlobalObject*, CallFrame*))
{
return JSValue::encode(jsBoolean(Bun::isCPUProfilerRunning()));
}
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved
9 changes: 9 additions & 0 deletions src/bun.js/bindings/JSInspectorProfiler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

#include "root.h"
#include <JavaScriptCore/JSCJSValue.h>

JSC_DECLARE_HOST_FUNCTION(jsFunction_startCPUProfiler);
JSC_DECLARE_HOST_FUNCTION(jsFunction_stopCPUProfiler);
JSC_DECLARE_HOST_FUNCTION(jsFunction_setCPUSamplingInterval);
JSC_DECLARE_HOST_FUNCTION(jsFunction_isCPUProfilerRunning);
16 changes: 14 additions & 2 deletions src/bun.js/modules/BunJSCModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,10 @@ JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, Ca
if (sampleValue.isNumber()) {
unsigned sampleInterval = sampleValue.toUInt32(globalObject);
samplingProfiler.setTimingInterval(Seconds::fromMicroseconds(sampleInterval));
} else {
// Reset to default interval (1000 microseconds) to ensure each profile()
// call is independent of previous calls
samplingProfiler.setTimingInterval(Seconds::fromMicroseconds(1000));
}
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved

const auto report = [](JSC::VM& vm,
Expand All @@ -670,7 +674,14 @@ JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, Ca

JSValue stackTraces = JSONParse(globalObject, samplingProfiler.stackTracesAsJSON()->toJSONString());

samplingProfiler.shutdown();
// Use pause() instead of shutdown() to allow the profiler to be restarted
// shutdown() sets m_isShutDown=true which is never reset, making the profiler unusable
{
auto& lock = samplingProfiler.getLock();
WTF::Locker locker { lock };
samplingProfiler.pause();
samplingProfiler.clearData();
}
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved
RETURN_IF_EXCEPTION(throwScope, {});

JSObject* result = constructEmptyObject(globalObject, globalObject->objectPrototype(), 3);
Expand All @@ -682,8 +693,9 @@ JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, Ca
};
const auto reportFailure = [](JSC::VM& vm) -> JSC::JSValue {
if (auto* samplingProfiler = vm.samplingProfiler()) {
auto& lock = samplingProfiler->getLock();
WTF::Locker locker { lock };
samplingProfiler->pause();
samplingProfiler->shutdown();
samplingProfiler->clearData();
}

Expand Down
28 changes: 28 additions & 0 deletions src/js/node/inspector.promises.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Hardcoded module "node:inspector/promises"
const inspector = require("node:inspector");

const { Session: BaseSession, console, open, close, url, waitForDebugger } = inspector;

// Promise-based Session that wraps the callback-based Session
class Session extends BaseSession {
post(method: string, params?: object): Promise<any> {
return new Promise((resolve, reject) => {
super.post(method, params, (err: Error | null, result: any) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved
}

export default {
console,
open,
close,
url,
waitForDebugger,
Session,
};
113 changes: 110 additions & 3 deletions src/js/node/inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
const { hideFromStack, throwNotImplemented } = require("internal/shared");
const EventEmitter = require("node:events");

// Native profiler functions exposed via $newCppFunction
const startCPUProfiler = $newCppFunction("JSInspectorProfiler.cpp", "jsFunction_startCPUProfiler", 0);
const stopCPUProfiler = $newCppFunction("JSInspectorProfiler.cpp", "jsFunction_stopCPUProfiler", 0);
const setCPUSamplingInterval = $newCppFunction("JSInspectorProfiler.cpp", "jsFunction_setCPUSamplingInterval", 1);
const isCPUProfilerRunning = $newCppFunction("JSInspectorProfiler.cpp", "jsFunction_isCPUProfilerRunning", 0);

function open() {
throwNotImplemented("node:inspector", 2445);
}
Expand All @@ -22,9 +28,110 @@ function waitForDebugger() {
}

class Session extends EventEmitter {
constructor() {
super();
throwNotImplemented("node:inspector", 2445);
#connected = false;
#profilerEnabled = false;

connect() {
if (this.#connected) {
throw new Error("Session is already connected");
}
this.#connected = true;
}

connectToMainThread() {
this.connect();
}

disconnect() {
if (!this.#connected) return;
if (isCPUProfilerRunning()) stopCPUProfiler();
this.#profilerEnabled = false;
this.#connected = false;
}
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved

post(
method: string,
params?: object | ((err: Error | null, result?: any) => void),
callback?: (err: Error | null, result?: any) => void,
) {
// Handle overloaded signature: post(method, callback)
if (typeof params === "function") {
callback = params;
params = undefined;
}

if (!this.#connected) {
const error = new Error("Session is not connected");
if (callback) {
queueMicrotask(() => callback(error));
return;
}
throw error;
}

const result = this.#handleMethod(method, params as object | undefined);

if (callback) {
// Callback API - async
queueMicrotask(() => {
if (result instanceof Error) {
callback(result, undefined);
} else {
callback(null, result);
}
});
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved
} else {
// Sync throw for errors when no callback
if (result instanceof Error) {
throw result;
}
return result;
}
}
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved

#handleMethod(method: string, params?: object): any {
switch (method) {
case "Profiler.enable":
this.#profilerEnabled = true;
return {};

case "Profiler.disable":
if (isCPUProfilerRunning()) {
stopCPUProfiler();
}
this.#profilerEnabled = false;
return {};

case "Profiler.start":
if (!this.#profilerEnabled) return new Error("Profiler is not enabled. Call Profiler.enable first.");
if (!isCPUProfilerRunning()) startCPUProfiler();
return {};

case "Profiler.stop":
if (!isCPUProfilerRunning()) return new Error("Profiler is not started. Call Profiler.start first.");
try {
return { profile: JSON.parse(stopCPUProfiler()) };
} catch (e) {
return new Error(`Failed to parse profile JSON: ${e}`);
}
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved

case "Profiler.setSamplingInterval": {
if (isCPUProfilerRunning()) return new Error("Cannot change sampling interval while profiler is running");
const interval = (params as any)?.interval;
if (typeof interval !== "number" || interval <= 0) return new Error("interval must be a positive number");
setCPUSamplingInterval(interval);
return {};
}

case "Profiler.getBestEffortCoverage":
case "Profiler.startPreciseCoverage":
case "Profiler.stopPreciseCoverage":
case "Profiler.takePreciseCoverage":
return {};
Comment thread
vadim-anthropic marked this conversation as resolved.
Resolved

default:
return new Error(`Inspector method "${method}" is not supported`);
}
}
}

Expand Down
Loading