feat(node:inspector): implement Profiler API (#25939)

This commit is contained in:
vadim-anthropic
2026-01-16 10:12:28 -08:00
committed by GitHub
parent bcbb4fc35d
commit 8da29af1ae
10 changed files with 683 additions and 31 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View 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()));
}

View 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);

View File

@@ -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);

View 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,
};

View File

@@ -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`);
}
} }
} }

View File

@@ -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);
});
}); });

View 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);
});
});