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);
26 changes: 21 additions & 5 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 All @@ -706,8 +718,11 @@ JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, Ca

JSNativeStdFunction* resolve = JSNativeStdFunction::create(
vm, globalObject, 0, "resolve"_s,
[report](JSGlobalObject* globalObject, CallFrame* callFrame) {
return JSValue::encode(JSPromise::resolvedPromise(globalObject, report(globalObject->vm(), globalObject)));
[report](JSGlobalObject* globalObject, CallFrame* callFrame) -> JSC::EncodedJSValue {
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
JSValue result = report(globalObject->vm(), globalObject);
RETURN_IF_EXCEPTION(scope, {});
RELEASE_AND_RETURN(scope, JSValue::encode(JSPromise::resolvedPromise(globalObject, result)));
});
JSNativeStdFunction* reject = JSNativeStdFunction::create(
vm, globalObject, 0, "reject"_s,
Expand All @@ -723,7 +738,8 @@ JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, Ca
return JSValue::encode(afterOngoingPromiseCapability);
}

return JSValue::encode(report(vm, globalObject));
JSValue result = report(vm, globalObject);
RELEASE_AND_RETURN(throwScope, JSValue::encode(result));
}

JSC_DECLARE_HOST_FUNCTION(functionGenerateHeapSnapshotForDebugging);
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,
};
Loading