mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
feat(node:inspector): implement Profiler API (#25939)
This commit is contained in:
@@ -65,6 +65,7 @@ pub const HardcodedModule = enum {
|
|||||||
@"node:trace_events",
|
@"node:trace_events",
|
||||||
@"node:repl",
|
@"node:repl",
|
||||||
@"node:inspector",
|
@"node:inspector",
|
||||||
|
@"node:inspector/promises",
|
||||||
@"node:http2",
|
@"node:http2",
|
||||||
@"node:diagnostics_channel",
|
@"node:diagnostics_channel",
|
||||||
@"node:dgram",
|
@"node:dgram",
|
||||||
@@ -121,6 +122,7 @@ pub const HardcodedModule = enum {
|
|||||||
.{ "node:http2", .@"node:http2" },
|
.{ "node:http2", .@"node:http2" },
|
||||||
.{ "node:https", .@"node:https" },
|
.{ "node:https", .@"node:https" },
|
||||||
.{ "node:inspector", .@"node:inspector" },
|
.{ "node:inspector", .@"node:inspector" },
|
||||||
|
.{ "node:inspector/promises", .@"node:inspector/promises" },
|
||||||
.{ "node:module", .@"node:module" },
|
.{ "node:module", .@"node:module" },
|
||||||
.{ "node:net", .@"node:net" },
|
.{ "node:net", .@"node:net" },
|
||||||
.{ "node:readline", .@"node:readline" },
|
.{ "node:readline", .@"node:readline" },
|
||||||
@@ -230,6 +232,7 @@ pub const HardcodedModule = enum {
|
|||||||
nodeEntry("node:http2"),
|
nodeEntry("node:http2"),
|
||||||
nodeEntry("node:https"),
|
nodeEntry("node:https"),
|
||||||
nodeEntry("node:inspector"),
|
nodeEntry("node:inspector"),
|
||||||
|
nodeEntry("node:inspector/promises"),
|
||||||
nodeEntry("node:module"),
|
nodeEntry("node:module"),
|
||||||
nodeEntry("node:net"),
|
nodeEntry("node:net"),
|
||||||
nodeEntry("node:os"),
|
nodeEntry("node:os"),
|
||||||
@@ -285,6 +288,7 @@ pub const HardcodedModule = enum {
|
|||||||
nodeEntry("http2"),
|
nodeEntry("http2"),
|
||||||
nodeEntry("https"),
|
nodeEntry("https"),
|
||||||
nodeEntry("inspector"),
|
nodeEntry("inspector"),
|
||||||
|
nodeEntry("inspector/promises"),
|
||||||
nodeEntry("module"),
|
nodeEntry("module"),
|
||||||
nodeEntry("net"),
|
nodeEntry("net"),
|
||||||
nodeEntry("os"),
|
nodeEntry("os"),
|
||||||
@@ -366,10 +370,6 @@ pub const HardcodedModule = enum {
|
|||||||
.{ "bun:internal-for-testing", .{ .path = "bun:internal-for-testing" } },
|
.{ "bun:internal-for-testing", .{ .path = "bun:internal-for-testing" } },
|
||||||
.{ "ffi", .{ .path = "bun:ffi" } },
|
.{ "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
|
// Thirdparty packages we override
|
||||||
.{ "@vercel/fetch", .{ .path = "@vercel/fetch" } },
|
.{ "@vercel/fetch", .{ .path = "@vercel/fetch" } },
|
||||||
.{ "isomorphic-fetch", .{ .path = "isomorphic-fetch" } },
|
.{ "isomorphic-fetch", .{ .path = "isomorphic-fetch" } },
|
||||||
@@ -394,12 +394,7 @@ pub const HardcodedModule = enum {
|
|||||||
.{ "vitest", .{ .path = "bun:test" } },
|
.{ "vitest", .{ .path = "bun:test" } },
|
||||||
};
|
};
|
||||||
|
|
||||||
const node_extra_alias_kvs = [_]struct { string, Alias }{
|
const node_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs);
|
||||||
nodeEntry("node:inspector/promises"),
|
|
||||||
nodeEntry("inspector/promises"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const node_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ node_extra_alias_kvs);
|
|
||||||
pub const bun_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ bun_extra_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);
|
const bun_test_aliases = bun.ComptimeStringMap(Alias, common_alias_kvs ++ bun_extra_alias_kvs ++ bun_test_extra_alias_kvs);
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ namespace Bun {
|
|||||||
|
|
||||||
// Store the profiling start time in microseconds since Unix epoch
|
// Store the profiling start time in microseconds since Unix epoch
|
||||||
static double s_profilingStartTime = 0.0;
|
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;
|
||||||
|
|
||||||
|
void setSamplingInterval(int intervalMicroseconds)
|
||||||
|
{
|
||||||
|
s_samplingInterval = intervalMicroseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isCPUProfilerRunning()
|
||||||
|
{
|
||||||
|
return s_isProfilerRunning;
|
||||||
|
}
|
||||||
|
|
||||||
void startCPUProfiler(JSC::VM& vm)
|
void startCPUProfiler(JSC::VM& vm)
|
||||||
{
|
{
|
||||||
@@ -35,12 +48,10 @@ void startCPUProfiler(JSC::VM& vm)
|
|||||||
stopwatch->start();
|
stopwatch->start();
|
||||||
|
|
||||||
JSC::SamplingProfiler& samplingProfiler = vm.ensureSamplingProfiler(WTF::move(stopwatch));
|
JSC::SamplingProfiler& samplingProfiler = vm.ensureSamplingProfiler(WTF::move(stopwatch));
|
||||||
|
samplingProfiler.setTimingInterval(WTF::Seconds::fromMicroseconds(s_samplingInterval));
|
||||||
// Set sampling interval to 1ms (1000 microseconds) to match Node.js
|
|
||||||
samplingProfiler.setTimingInterval(WTF::Seconds::fromMicroseconds(1000));
|
|
||||||
|
|
||||||
samplingProfiler.noticeCurrentThreadAsJSCExecutionThread();
|
samplingProfiler.noticeCurrentThreadAsJSCExecutionThread();
|
||||||
samplingProfiler.start();
|
samplingProfiler.start();
|
||||||
|
s_isProfilerRunning = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProfileNode {
|
struct ProfileNode {
|
||||||
@@ -56,27 +67,29 @@ struct ProfileNode {
|
|||||||
|
|
||||||
WTF::String stopCPUProfilerAndGetJSON(JSC::VM& vm)
|
WTF::String stopCPUProfilerAndGetJSON(JSC::VM& vm)
|
||||||
{
|
{
|
||||||
|
s_isProfilerRunning = false;
|
||||||
|
|
||||||
JSC::SamplingProfiler* profiler = vm.samplingProfiler();
|
JSC::SamplingProfiler* profiler = vm.samplingProfiler();
|
||||||
if (!profiler)
|
if (!profiler)
|
||||||
return WTF::String();
|
return WTF::String();
|
||||||
|
|
||||||
// Shut down the profiler thread first - this is critical!
|
// JSLock is re-entrant, so always acquiring it handles both JS and shutdown contexts
|
||||||
profiler->shutdown();
|
|
||||||
|
|
||||||
// Need to hold the VM lock to safely access stack traces
|
|
||||||
JSC::JSLockHolder locker(vm);
|
JSC::JSLockHolder locker(vm);
|
||||||
|
|
||||||
// Defer GC while we're working with stack traces
|
// Defer GC while we're working with stack traces
|
||||||
JSC::DeferGC deferGC(vm);
|
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();
|
auto& lock = profiler->getLock();
|
||||||
WTF::Locker profilerLocker { lock };
|
WTF::Locker profilerLocker { lock };
|
||||||
|
profiler->pause();
|
||||||
|
|
||||||
// releaseStackTraces() calls processUnverifiedStackTraces() internally
|
// releaseStackTraces() calls processUnverifiedStackTraces() internally
|
||||||
auto stackTraces = profiler->releaseStackTraces();
|
auto stackTraces = profiler->releaseStackTraces();
|
||||||
|
profiler->clearData();
|
||||||
if (stackTraces.isEmpty())
|
|
||||||
return WTF::String();
|
|
||||||
|
|
||||||
// Build Chrome CPU Profiler format
|
// Build Chrome CPU Profiler format
|
||||||
// Map from stack frame signature to node ID
|
// Map from stack frame signature to node ID
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ class VM;
|
|||||||
|
|
||||||
namespace Bun {
|
namespace Bun {
|
||||||
|
|
||||||
|
void setSamplingInterval(int intervalMicroseconds);
|
||||||
|
bool isCPUProfilerRunning();
|
||||||
|
|
||||||
// Start the CPU profiler
|
// Start the CPU profiler
|
||||||
void startCPUProfiler(JSC::VM& vm);
|
void startCPUProfiler(JSC::VM& vm);
|
||||||
|
|
||||||
// Stop the CPU profiler and convert to Chrome CPU profiler JSON format
|
// 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);
|
WTF::String stopCPUProfilerAndGetJSON(JSC::VM& vm);
|
||||||
|
|
||||||
} // namespace Bun
|
} // namespace Bun
|
||||||
|
|||||||
49
src/bun.js/bindings/JSInspectorProfiler.cpp
Normal file
49
src/bun.js/bindings/JSInspectorProfiler.cpp
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
JSC_DECLARE_HOST_FUNCTION(jsFunction_isCPUProfilerRunning);
|
||||||
|
JSC_DEFINE_HOST_FUNCTION(jsFunction_isCPUProfilerRunning, (JSGlobalObject*, CallFrame*))
|
||||||
|
{
|
||||||
|
return JSValue::encode(jsBoolean(Bun::isCPUProfilerRunning()));
|
||||||
|
}
|
||||||
9
src/bun.js/bindings/JSInspectorProfiler.h
Normal file
9
src/bun.js/bindings/JSInspectorProfiler.h
Normal file
@@ -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);
|
||||||
@@ -655,6 +655,10 @@ JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, Ca
|
|||||||
if (sampleValue.isNumber()) {
|
if (sampleValue.isNumber()) {
|
||||||
unsigned sampleInterval = sampleValue.toUInt32(globalObject);
|
unsigned sampleInterval = sampleValue.toUInt32(globalObject);
|
||||||
samplingProfiler.setTimingInterval(Seconds::fromMicroseconds(sampleInterval));
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto report = [](JSC::VM& vm,
|
const auto report = [](JSC::VM& vm,
|
||||||
@@ -670,7 +674,14 @@ JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, Ca
|
|||||||
|
|
||||||
JSValue stackTraces = JSONParse(globalObject, samplingProfiler.stackTracesAsJSON()->toJSONString());
|
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();
|
||||||
|
}
|
||||||
RETURN_IF_EXCEPTION(throwScope, {});
|
RETURN_IF_EXCEPTION(throwScope, {});
|
||||||
|
|
||||||
JSObject* result = constructEmptyObject(globalObject, globalObject->objectPrototype(), 3);
|
JSObject* result = constructEmptyObject(globalObject, globalObject->objectPrototype(), 3);
|
||||||
@@ -682,8 +693,9 @@ JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, Ca
|
|||||||
};
|
};
|
||||||
const auto reportFailure = [](JSC::VM& vm) -> JSC::JSValue {
|
const auto reportFailure = [](JSC::VM& vm) -> JSC::JSValue {
|
||||||
if (auto* samplingProfiler = vm.samplingProfiler()) {
|
if (auto* samplingProfiler = vm.samplingProfiler()) {
|
||||||
|
auto& lock = samplingProfiler->getLock();
|
||||||
|
WTF::Locker locker { lock };
|
||||||
samplingProfiler->pause();
|
samplingProfiler->pause();
|
||||||
samplingProfiler->shutdown();
|
|
||||||
samplingProfiler->clearData();
|
samplingProfiler->clearData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,8 +718,11 @@ JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, Ca
|
|||||||
|
|
||||||
JSNativeStdFunction* resolve = JSNativeStdFunction::create(
|
JSNativeStdFunction* resolve = JSNativeStdFunction::create(
|
||||||
vm, globalObject, 0, "resolve"_s,
|
vm, globalObject, 0, "resolve"_s,
|
||||||
[report](JSGlobalObject* globalObject, CallFrame* callFrame) {
|
[report](JSGlobalObject* globalObject, CallFrame* callFrame) -> JSC::EncodedJSValue {
|
||||||
return JSValue::encode(JSPromise::resolvedPromise(globalObject, report(globalObject->vm(), globalObject)));
|
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(
|
JSNativeStdFunction* reject = JSNativeStdFunction::create(
|
||||||
vm, globalObject, 0, "reject"_s,
|
vm, globalObject, 0, "reject"_s,
|
||||||
@@ -723,7 +738,8 @@ JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, Ca
|
|||||||
return JSValue::encode(afterOngoingPromiseCapability);
|
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);
|
JSC_DECLARE_HOST_FUNCTION(functionGenerateHeapSnapshotForDebugging);
|
||||||
|
|||||||
28
src/js/node/inspector.promises.ts
Normal file
28
src/js/node/inspector.promises.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
console,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
url,
|
||||||
|
waitForDebugger,
|
||||||
|
Session,
|
||||||
|
};
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
// Hardcoded module "node:inspector" and "node:inspector/promises"
|
// Hardcoded module "node:inspector" and "node:inspector/promises"
|
||||||
// This is a stub! None of this is actually implemented yet.
|
// Profiler APIs are implemented; other inspector APIs are stubs.
|
||||||
const { hideFromStack, throwNotImplemented } = require("internal/shared");
|
const { hideFromStack, throwNotImplemented } = require("internal/shared");
|
||||||
const EventEmitter = require("node:events");
|
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() {
|
function open() {
|
||||||
throwNotImplemented("node:inspector", 2445);
|
throwNotImplemented("node:inspector", 2445);
|
||||||
}
|
}
|
||||||
@@ -22,9 +28,110 @@ function waitForDebugger() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Session extends EventEmitter {
|
class Session extends EventEmitter {
|
||||||
constructor() {
|
#connected = false;
|
||||||
super();
|
#profilerEnabled = false;
|
||||||
throwNotImplemented("node:inspector", 2445);
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Sync throw for errors when no callback
|
||||||
|
if (result instanceof Error) {
|
||||||
|
throw result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 new Error("Coverage APIs are not supported");
|
||||||
|
|
||||||
|
default:
|
||||||
|
return new Error(`Inspector method "${method}" is not supported`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,4 +183,34 @@ describe("bun:jsc", () => {
|
|||||||
const input = await promise;
|
const input = await promise;
|
||||||
expect({ ...input }).toStrictEqual({ "0": 2 });
|
expect({ ...input }).toStrictEqual({ "0": 2 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.todoIf(isBuildKite && isWindows)("profile can be called multiple times", () => {
|
||||||
|
// Fibonacci generates deep stacks and is CPU-intensive
|
||||||
|
function fib(n: number): number {
|
||||||
|
if (n <= 1) return n;
|
||||||
|
return fib(n - 1) + fib(n - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First profile call
|
||||||
|
const result1 = profile(() => fib(30));
|
||||||
|
expect(result1).toBeDefined();
|
||||||
|
expect(result1.functions).toBeDefined();
|
||||||
|
expect(result1.stackTraces).toBeDefined();
|
||||||
|
expect(result1.stackTraces.traces.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Second profile call - should work after first one completed
|
||||||
|
// This verifies that shutdown() -> pause() fix works
|
||||||
|
const result2 = profile(() => fib(30));
|
||||||
|
expect(result2).toBeDefined();
|
||||||
|
expect(result2.functions).toBeDefined();
|
||||||
|
expect(result2.stackTraces).toBeDefined();
|
||||||
|
expect(result2.stackTraces.traces.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Third profile call - verify profiler can be reused multiple times
|
||||||
|
const result3 = profile(() => fib(30));
|
||||||
|
expect(result3).toBeDefined();
|
||||||
|
expect(result3.functions).toBeDefined();
|
||||||
|
expect(result3.stackTraces).toBeDefined();
|
||||||
|
expect(result3.stackTraces.traces.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
402
test/js/node/inspector/inspector-profiler.test.ts
Normal file
402
test/js/node/inspector/inspector-profiler.test.ts
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import inspector from "node:inspector";
|
||||||
|
import inspectorPromises from "node:inspector/promises";
|
||||||
|
|
||||||
|
describe("node:inspector", () => {
|
||||||
|
describe("Session", () => {
|
||||||
|
let session: inspector.Session;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
session = new inspector.Session();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
try {
|
||||||
|
session.disconnect();
|
||||||
|
} catch {
|
||||||
|
// Ignore if already disconnected
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Session is a constructor", () => {
|
||||||
|
expect(inspector.Session).toBeInstanceOf(Function);
|
||||||
|
expect(session).toBeInstanceOf(inspector.Session);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Session extends EventEmitter", () => {
|
||||||
|
expect(typeof session.on).toBe("function");
|
||||||
|
expect(typeof session.emit).toBe("function");
|
||||||
|
expect(typeof session.removeListener).toBe("function");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connect() establishes connection", () => {
|
||||||
|
expect(() => session.connect()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connect() throws if already connected", () => {
|
||||||
|
session.connect();
|
||||||
|
expect(() => session.connect()).toThrow("already connected");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("connectToMainThread() works like connect()", () => {
|
||||||
|
expect(() => session.connectToMainThread()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("disconnect() closes connection cleanly", () => {
|
||||||
|
session.connect();
|
||||||
|
expect(() => session.disconnect()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("disconnect() is a no-op if not connected", () => {
|
||||||
|
expect(() => session.disconnect()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("post() throws if not connected", () => {
|
||||||
|
expect(() => session.post("Profiler.enable")).toThrow("not connected");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("post() with callback calls callback with error if not connected", async () => {
|
||||||
|
const { promise, resolve, reject } = Promise.withResolvers<Error>();
|
||||||
|
session.post("Profiler.enable", err => {
|
||||||
|
if (err) resolve(err);
|
||||||
|
else reject(new Error("Expected error"));
|
||||||
|
});
|
||||||
|
const error = await promise;
|
||||||
|
expect(error.message).toContain("not connected");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Profiler", () => {
|
||||||
|
let session: inspector.Session;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
session = new inspector.Session();
|
||||||
|
session.connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
try {
|
||||||
|
session.disconnect();
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Profiler.enable succeeds", () => {
|
||||||
|
const result = session.post("Profiler.enable");
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Profiler.disable succeeds", () => {
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
const result = session.post("Profiler.disable");
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Profiler.start without enable throws", () => {
|
||||||
|
expect(() => session.post("Profiler.start")).toThrow("not enabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Profiler.start after enable succeeds", () => {
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
const result = session.post("Profiler.start");
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Profiler.stop without start throws", () => {
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
expect(() => session.post("Profiler.stop")).toThrow("not started");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Profiler.stop returns valid profile", () => {
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
session.post("Profiler.start");
|
||||||
|
|
||||||
|
// Do some work to generate profile data
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < 10000; i++) {
|
||||||
|
sum += Math.sqrt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = session.post("Profiler.stop");
|
||||||
|
|
||||||
|
expect(result).toHaveProperty("profile");
|
||||||
|
const profile = result.profile;
|
||||||
|
|
||||||
|
// Validate profile structure
|
||||||
|
expect(profile).toHaveProperty("nodes");
|
||||||
|
expect(profile).toHaveProperty("startTime");
|
||||||
|
expect(profile).toHaveProperty("endTime");
|
||||||
|
expect(profile).toHaveProperty("samples");
|
||||||
|
expect(profile).toHaveProperty("timeDeltas");
|
||||||
|
|
||||||
|
expect(profile.nodes).toBeArray();
|
||||||
|
expect(profile.nodes.length).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
|
// First node should be (root)
|
||||||
|
const rootNode = profile.nodes[0];
|
||||||
|
expect(rootNode).toHaveProperty("id", 1);
|
||||||
|
expect(rootNode).toHaveProperty("callFrame");
|
||||||
|
expect(rootNode.callFrame).toHaveProperty("functionName", "(root)");
|
||||||
|
expect(rootNode.callFrame).toHaveProperty("scriptId", "0");
|
||||||
|
expect(rootNode.callFrame).toHaveProperty("url", "");
|
||||||
|
expect(rootNode.callFrame).toHaveProperty("lineNumber", -1);
|
||||||
|
expect(rootNode.callFrame).toHaveProperty("columnNumber", -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("complete enable->start->stop workflow", () => {
|
||||||
|
// Enable profiler
|
||||||
|
const enableResult = session.post("Profiler.enable");
|
||||||
|
expect(enableResult).toEqual({});
|
||||||
|
|
||||||
|
// Start profiling
|
||||||
|
const startResult = session.post("Profiler.start");
|
||||||
|
expect(startResult).toEqual({});
|
||||||
|
|
||||||
|
// Do some work
|
||||||
|
function fibonacci(n: number): number {
|
||||||
|
if (n <= 1) return n;
|
||||||
|
return fibonacci(n - 1) + fibonacci(n - 2);
|
||||||
|
}
|
||||||
|
fibonacci(20);
|
||||||
|
|
||||||
|
// Stop profiling
|
||||||
|
const stopResult = session.post("Profiler.stop");
|
||||||
|
expect(stopResult).toHaveProperty("profile");
|
||||||
|
|
||||||
|
// Disable profiler
|
||||||
|
const disableResult = session.post("Profiler.disable");
|
||||||
|
expect(disableResult).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("samples and timeDeltas have same length", () => {
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
session.post("Profiler.start");
|
||||||
|
|
||||||
|
// Do some work
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < 5000; i++) {
|
||||||
|
sum += Math.sqrt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = session.post("Profiler.stop");
|
||||||
|
const profile = result.profile;
|
||||||
|
|
||||||
|
expect(profile.samples.length).toBe(profile.timeDeltas.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("samples reference valid node IDs", () => {
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
session.post("Profiler.start");
|
||||||
|
|
||||||
|
// Do some work
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < 5000; i++) {
|
||||||
|
sum += Math.sqrt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = session.post("Profiler.stop");
|
||||||
|
const profile = result.profile;
|
||||||
|
|
||||||
|
const nodeIds = new Set(profile.nodes.map((n: any) => n.id));
|
||||||
|
for (const sample of profile.samples) {
|
||||||
|
expect(nodeIds.has(sample)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Profiler.setSamplingInterval works", () => {
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
const result = session.post("Profiler.setSamplingInterval", { interval: 500 });
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Profiler.setSamplingInterval throws if profiler is running", () => {
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
session.post("Profiler.start");
|
||||||
|
expect(() => session.post("Profiler.setSamplingInterval", { interval: 500 })).toThrow(
|
||||||
|
"Cannot change sampling interval while profiler is running",
|
||||||
|
);
|
||||||
|
session.post("Profiler.stop");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Profiler.setSamplingInterval requires positive interval", () => {
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
expect(() => session.post("Profiler.setSamplingInterval", { interval: 0 })).toThrow();
|
||||||
|
expect(() => session.post("Profiler.setSamplingInterval", { interval: -1 })).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("double Profiler.start is a no-op", () => {
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
session.post("Profiler.start");
|
||||||
|
const result = session.post("Profiler.start");
|
||||||
|
expect(result).toEqual({});
|
||||||
|
session.post("Profiler.stop");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("profiler can be restarted after stop", () => {
|
||||||
|
// First run
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
session.post("Profiler.start");
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < 1000; i++) sum += i;
|
||||||
|
const result1 = session.post("Profiler.stop");
|
||||||
|
expect(result1).toHaveProperty("profile");
|
||||||
|
|
||||||
|
// Second run
|
||||||
|
session.post("Profiler.start");
|
||||||
|
for (let i = 0; i < 1000; i++) sum += i;
|
||||||
|
const result2 = session.post("Profiler.stop");
|
||||||
|
expect(result2).toHaveProperty("profile");
|
||||||
|
|
||||||
|
// Both profiles should be valid
|
||||||
|
expect(result1.profile.nodes.length).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(result2.profile.nodes.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("disconnect() stops running profiler", () => {
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
session.post("Profiler.start");
|
||||||
|
session.disconnect();
|
||||||
|
|
||||||
|
// Create new session and verify profiler was stopped
|
||||||
|
const session2 = new inspector.Session();
|
||||||
|
session2.connect();
|
||||||
|
session2.post("Profiler.enable");
|
||||||
|
|
||||||
|
// This should work without error (profiler is not running)
|
||||||
|
const result = session2.post("Profiler.setSamplingInterval", { interval: 500 });
|
||||||
|
expect(result).toEqual({});
|
||||||
|
session2.disconnect();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("callback API", () => {
|
||||||
|
test("post() with callback receives result", async () => {
|
||||||
|
const session = new inspector.Session();
|
||||||
|
session.connect();
|
||||||
|
|
||||||
|
const { promise, resolve } = Promise.withResolvers<any>();
|
||||||
|
session.post("Profiler.enable", (err, result) => {
|
||||||
|
resolve({ err, result });
|
||||||
|
});
|
||||||
|
|
||||||
|
const { err, result } = await promise;
|
||||||
|
expect(err).toBeNull();
|
||||||
|
expect(result).toEqual({});
|
||||||
|
session.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("post() with callback receives error", async () => {
|
||||||
|
const session = new inspector.Session();
|
||||||
|
session.connect();
|
||||||
|
|
||||||
|
const { promise, resolve } = Promise.withResolvers<any>();
|
||||||
|
session.post("Profiler.start", (err, result) => {
|
||||||
|
resolve({ err, result });
|
||||||
|
});
|
||||||
|
|
||||||
|
const { err, result } = await promise;
|
||||||
|
expect(err).toBeInstanceOf(Error);
|
||||||
|
expect(err.message).toContain("not enabled");
|
||||||
|
session.disconnect();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("unsupported methods", () => {
|
||||||
|
test("unsupported method throws", () => {
|
||||||
|
const session = new inspector.Session();
|
||||||
|
session.connect();
|
||||||
|
expect(() => session.post("Runtime.evaluate")).toThrow("not supported");
|
||||||
|
session.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("coverage APIs throw not supported", () => {
|
||||||
|
const session = new inspector.Session();
|
||||||
|
session.connect();
|
||||||
|
session.post("Profiler.enable");
|
||||||
|
expect(() => session.post("Profiler.getBestEffortCoverage")).toThrow("not supported");
|
||||||
|
expect(() => session.post("Profiler.startPreciseCoverage")).toThrow("not supported");
|
||||||
|
expect(() => session.post("Profiler.stopPreciseCoverage")).toThrow("not supported");
|
||||||
|
expect(() => session.post("Profiler.takePreciseCoverage")).toThrow("not supported");
|
||||||
|
session.disconnect();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("exports", () => {
|
||||||
|
test("url() returns undefined", () => {
|
||||||
|
expect(inspector.url()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("console is exported", () => {
|
||||||
|
expect(inspector.console).toBeObject();
|
||||||
|
expect(inspector.console.log).toBe(globalThis.console.log);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("open() throws not implemented", () => {
|
||||||
|
expect(() => inspector.open()).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("close() throws not implemented", () => {
|
||||||
|
expect(() => inspector.close()).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("waitForDebugger() throws not implemented", () => {
|
||||||
|
expect(() => inspector.waitForDebugger()).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("node:inspector/promises", () => {
|
||||||
|
test("Session is exported", () => {
|
||||||
|
expect(inspectorPromises.Session).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("post() returns a Promise", async () => {
|
||||||
|
const session = new inspectorPromises.Session();
|
||||||
|
session.connect();
|
||||||
|
|
||||||
|
const result = session.post("Profiler.enable");
|
||||||
|
expect(result).toBeInstanceOf(Promise);
|
||||||
|
|
||||||
|
await expect(result).resolves.toEqual({});
|
||||||
|
session.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("post() rejects on error", async () => {
|
||||||
|
const session = new inspectorPromises.Session();
|
||||||
|
session.connect();
|
||||||
|
|
||||||
|
await expect(session.post("Profiler.start")).rejects.toThrow("not enabled");
|
||||||
|
session.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("complete profiling workflow with promises", async () => {
|
||||||
|
const session = new inspectorPromises.Session();
|
||||||
|
session.connect();
|
||||||
|
|
||||||
|
await session.post("Profiler.enable");
|
||||||
|
await session.post("Profiler.start");
|
||||||
|
|
||||||
|
// Do some work
|
||||||
|
function work(n: number): number {
|
||||||
|
if (n <= 1) return n;
|
||||||
|
return work(n - 1) + work(n - 2);
|
||||||
|
}
|
||||||
|
work(15);
|
||||||
|
|
||||||
|
const result = await session.post("Profiler.stop");
|
||||||
|
expect(result).toHaveProperty("profile");
|
||||||
|
expect(result.profile.nodes).toBeArray();
|
||||||
|
|
||||||
|
await session.post("Profiler.disable");
|
||||||
|
session.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("other exports are the same as node:inspector", () => {
|
||||||
|
expect(inspectorPromises.url).toBe(inspector.url);
|
||||||
|
expect(inspectorPromises.console).toBe(inspector.console);
|
||||||
|
expect(inspectorPromises.open).toBe(inspector.open);
|
||||||
|
expect(inspectorPromises.close).toBe(inspector.close);
|
||||||
|
expect(inspectorPromises.waitForDebugger).toBe(inspector.waitForDebugger);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user