Compare commits

...

8 Commits

Author SHA1 Message Date
Claude Bot
19c62ef05f fix: add missing exception check in profiler report lambda
The report lambda in functionRunProfiler declared a throwScope but
did not release it before returning. This caused ASAN builds to fail
with "Unchecked JS exception" error. Use RELEASE_AND_RETURN macro
to properly release the scope.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 21:26:16 +00:00
vadim-anthropic
e8767fde97 Merge branch 'main' into claude/node-inspector-profiler 2026-01-11 16:59:34 -08:00
Jarred Sumner
884f71a75d Merge branch 'main' into claude/node-inspector-profiler 2026-01-10 19:21:23 -08:00
Vadim Spivak
9b4a0b65ff Return error for unsupported coverage APIs
Coverage APIs (getBestEffortCoverage, startPreciseCoverage, etc.) now
return an error instead of silently returning empty objects. This is
consistent with how other unsupported methods are handled.
2026-01-09 20:53:17 -08:00
Jarred Sumner
acc4642694 Merge branch 'main' into claude/node-inspector-profiler 2026-01-09 20:17:21 -08:00
Vadim Spivak
338a8a7c61 Address PR feedback
- Use Bun:: namespace functions instead of Bun__ extern "C" wrappers
- Use createNotEnoughArgumentsError() for missing arguments
- Use Bun::V::validateInteger() for proper integer validation
- Remove unused extern "C" wrappers (setSamplingInterval, isCPUProfilerRunning)
- Reset sampling interval to default in profile() when not specified
- Delete flaky timestamp test (low value, wrong assumptions)
2026-01-09 18:42:19 -08:00
autofix-ci[bot]
a40703c2bd [autofix.ci] apply automated fixes 2026-01-10 01:20:39 +00:00
Vadim Spivak
aba74246ed feat(node:inspector): implement Profiler API
Implement the Chrome DevTools Protocol Profiler domain for node:inspector:
- Profiler.enable/disable - Enable or disable the profiler
- Profiler.start/stop - Start and stop CPU profiling
- Profiler.setSamplingInterval - Configure sampling interval in microseconds

Returns CPU profiles in Chrome DevTools JSON format, compatible with
VS Code, Chrome DevTools, and speedscope.

Also fixes bun:jsc profile() to use pause() instead of shutdown(),
allowing the profiler to be restarted within the same process.
2026-01-09 16:50:52 -08:00
10 changed files with 677 additions and 29 deletions

View File

@@ -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",
@@ -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" },
@@ -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"),
@@ -285,6 +288,7 @@ pub const HardcodedModule = enum {
nodeEntry("http2"),
nodeEntry("https"),
nodeEntry("inspector"),
nodeEntry("inspector/promises"),
nodeEntry("module"),
nodeEntry("net"),
nodeEntry("os"),
@@ -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" } },
@@ -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);

View File

@@ -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;
void setSamplingInterval(int intervalMicroseconds)
{
s_samplingInterval = intervalMicroseconds;
}
bool isCPUProfilerRunning()
{
return s_isProfilerRunning;
}
void startCPUProfiler(JSC::VM& vm)
{
@@ -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 {
@@ -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();
// Build Chrome CPU Profiler format
// Map from stack frame signature to node ID

View File

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

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()) {
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));
}
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());
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, {});
JSObject* result = constructEmptyObject(globalObject, globalObject->objectPrototype(), 3);
@@ -678,12 +689,13 @@ JSC_DEFINE_HOST_FUNCTION(functionRunProfiler, (JSGlobalObject * globalObject, Ca
result->putDirect(vm, Identifier::fromString(vm, "bytecodes"_s), jsString(vm, byteCodes.toString()));
result->putDirect(vm, Identifier::fromString(vm, "stackTraces"_s), stackTraces);
return result;
RELEASE_AND_RETURN(throwScope, result);
};
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();
}

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