Skip to content

inspector: fix Runtime.consoleAPICalled timestamp unit and emit count/time events over DevTools WS - #35736

Open
robobun wants to merge 6 commits into
mainfrom
farm/873c4d68/fix-inspector-cdp-console-timestamp-and-count
Open

inspector: fix Runtime.consoleAPICalled timestamp unit and emit count/time events over DevTools WS#35736
robobun wants to merge 6 commits into
mainfrom
farm/873c4d68/fix-inspector-cdp-console-timestamp-and-count

Conversation

@robobun

@robobun robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Two fixes for Runtime.consoleAPICalled on the DevTools-protocol WebSocket server (inspector.open() / --inspect):

Repro

import inspector from "node:inspector";
import { spawn } from "node:child_process";
if (process.env.__CHILD) {
  inspector.open(0, "127.0.0.1");
  process.stdout.write("URL " + inspector.url() + "\n");
  globalThis.emit = () => {
    console.log("hello-log"); console.count("cnt"); console.count("cnt");
    console.time("tm"); console.timeEnd("tm"); return "emitted";
  };
  setInterval(() => {}, 1000);
} else {
  const child = spawn(process.execPath, [import.meta.filename],
    { env: { ...process.env, __CHILD: "1" }, stdio: ["ignore", "pipe", "inherit"] });
  let out = ""; child.stdout.on("data", d => (out += d));
  const url = await new Promise(r => { const iv = setInterval(() => {
    const m = /URL (\S+)/.exec(out); if (m) { clearInterval(iv); r(m[1]); } }, 20); });
  const ws = new WebSocket(url); const events = []; const done = new Map();
  ws.onmessage = e => { const m = JSON.parse(String(e.data));
    m.id !== undefined ? done.set(m.id, m) : events.push(m); };
  await new Promise(r => (ws.onopen = r));
  const send = (id, method, params = {}) => new Promise(res => {
    ws.send(JSON.stringify({ id, method, params }));
    const iv = setInterval(() => { if (done.has(id)) { clearInterval(iv); res(done.get(id)); } }, 5); });
  await send(1, "Runtime.enable"); await send(2, "Runtime.evaluate", { expression: "emit()" });
  await new Promise(r => setTimeout(r, 500));
  const cons = events.filter(m => m.method === "Runtime.consoleAPICalled");
  console.log(cons.map(m => m.params.type), cons[0]?.params?.timestamp);
  // bun before: [ "log" ] 1784974692.1001534
  // bun after:  [ "log", "debug", "debug", "timeEnd" ] 1784974692100.1534
  // node:       [ "log", "count", "count", "timeEnd" ] 1784974692970.869
  child.kill();
}

Cause

  • Timestamp in seconds: JSC's ConsoleMessage::addToFrontend writes m_timestamp.secondsSinceEpoch().value() into Console.messageAdded. The CDP adapter (src/js/internal/inspector/cdp.ts) forwarded it verbatim into Runtime.consoleAPICalled.timestamp / Runtime.exceptionThrown.timestamp, but CDP's Runtime.Timestamp is defined as milliseconds since epoch. A frontend treating the value as ms renders Jan 1970.
  • count / time* missing: ConsoleObject::messageWithTypeAndLevel and profile/profileEnd forward to globalObject->inspectorController().consoleClient(), but count, countReset, time, timeLog and timeEnd called only into Bun's own printer. InspectorConsoleAgent never saw those calls, so no Console.messageAdded was emitted and the adapter had nothing to translate.

Fix

  • cdp.ts: rescale message.timestamp from seconds to milliseconds when building Runtime.consoleAPICalled / Runtime.exceptionThrown.
  • ConsoleObject.cpp: forward count / countReset / time / timeLog / timeEnd to the inspector's console client, matching the existing messageWithTypeAndLevel pattern. JSC reports count as {type:"log", level:"debug"} (CDP type debug) and timeLog/timeEnd as {type:"timing"} (CDP type timeEnd); Node reports count / timeEnd. DevTools renders all of these.

Verification

test/js/node/inspector/inspector.test.ts gains a test that opens the inspector, connects as a CDP client, issues console.log/count/count/time/timeLog/timeEnd, and asserts both the millisecond-scale timestamp (bracketed by Date.now()) and the emitted event types / texts. Without the fix the timestamp assertion receives ~1.78e9 and the type list is ["log", "log"].


[review] gate passed · iteration 3 · 3 files touched

fails on main (without fix)
ASAN without fix: BUILD FAILED (no junit output)
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/inspector/inspector.test.ts
ninja: Entering directory `/workspace/bun/build/debug'
[1/25] gen cpp.rs (cppbind)
[2/25] gen JS modules (bundle-modules)
Preprocess modules (19040ms)
Bundle modules (96ms)
Postprocesss modules (311ms)
Bundle Functions (4213ms)
Generate Code (24ms)

[23.70s] Bundled "src/js" for development
  2761 kb
  193 internal modules
  13 native modules
  90 internal functions across 19 files
[2/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

[3/6] cxx obj/unified/UnifiedSource-src_jsc_bindings-3.cpp.o
[4/6] link bun-debug
[5/6] bun-debug --revision
FAILED: bun-debug.smoke-test-passed 
/workspace/bun/build/release/bun /workspace/bun/scripts/build/stream.ts check --console sh -c '( env BUN_DEBUG_QUIET_LOGS=1 setarch x86_64 -R /workspace/bun/build/debug/bun-debug --revision || env BUN_DEBUG_QUIET_LOGS=1 /workspace/bun/build/debug/bun-debug --revision ) && touch bun-debug.smoke-test-p
... (truncated)

release without fix: 22 FAILED
bun test v1.3.14 (0d9b296a)

test/js/node/inspector/inspector.test.ts:
(pass) inspector.url() [2.73ms]
(pass) inspector.console [0.90ms]
22 | test("inspector.console", () => {
23 |   expect(inspector.console).toBeObject();
24 | });
25 | 
26 | test("inspector.close() is a no-op when the inspector is not open", () => {
27 |   expect(() => inspector.close()).not.toThrow();
                                           ^
error: expect(received).not.toThrow()

Error name: "NotImplementedError"
Error message: "node:inspector is not yet implemented in Bun. Track the status & thumbs up the issue: https://github.com/oven-sh/bun/issues/2445"

      at <anonymous> (/workspace/bun/test/js/node/inspector/inspector.test.ts:27:39)
(fail) inspector.close() is a no-op when the inspector is not open [1.67ms]
33 |     inspector.waitForDebugger();
34 |   } catch (caught) {
35 |     error = caught;
36 |   }
37 |   expect(error).toBeDefined();
38 |   expect(error.code).toBe("ERR_INSPECTOR_NOT_ACTIVE");
                          ^
error: expect(received).toBe(expected)

Expected: "ERR_INSPECTOR_NOT_ACTIVE"
Received: "ERR_NOT_IMPLEMENTED"

      at <anonymous> (/workspace/bun/test/js/node/ins
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/inspector/inspector.test.ts
bun test v1.4.0 (2ae31c0ba)

test/js/node/inspector/inspector.test.ts:
(pass) inspector.url() [14.08ms]
(pass) inspector.console [16.33ms]
(pass) inspector.close() is a no-op when the inspector is not open [9.30ms]
(pass) inspector.waitForDebugger() throws ERR_INSPECTOR_NOT_ACTIVE when the inspector is not active [9.71ms]
(pass) inspector.open() serves the DevTools protocol and /json discovery endpoints [6960.80ms]
(pass) Runtime.consoleAPICalled over the DevTools WebSocket reports a millisecond timestamp and emits events for console.count/time* [3176.23ms]
(pass) inspector.close() followed by inspector.open() starts a new server [2530.44ms]
(pass) inspector.open() can be retried after a failed start [2743.32ms]
(pass) inspector.open() with wait=true does not hang the process after a bind failure [2763.23ms]
(pass) inspector.waitForDebugger() blocks until a client resumes the process [3261.83ms]
(pass) inspector.waitForDebugger() blocks again on the second call after a frontend disconnects [3522
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1380ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/22] gen cpp.rs (cppbind)
[2/22] gen JS modules (bundle-modules)
Preprocess modules (18228ms)
Bundle modules (339ms)
Postprocesss modules (1267ms)
Bundle Functions (2821ms)
Generate Code (42ms)

[22.72s] Bundled "src/js" for production
  2570 kb
  193 internal modules
  13 native modules
  90 internal functions across 19 files
[2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_install v0.0.0 (/workspace/bun/src/install)
�[1m�[92m   Compiling�[0m bun_jsc v0.0.0 (/workspace/bun/src/jsc)
�[1m�[92m   Compiling�[0m bun_ast_jsc v0.0.0 (/workspace/bun/src/ast_jsc)
�[1m�[92m   Compiling�[0m bun_js_parser_jsc v0.0.0 (/workspace/bun/src/js_parser_jsc)
�[1m�[92m   Compiling�[0m bun_patch_jsc v0.0.0 (/workspace/bun/src/patch_jsc)
�[1m�[92m   Compiling�[0m bun_css_jsc v0.0.0 (/workspace/bun/src/css_jsc)
�[1m�[92m   Compiling�[0m bun_semver_jsc v0.0.0 (/workspace
... (truncated)
diff hotspot
src/js/internal/inspector/cdp.ts         |  24 +++++---
 src/jsc/bindings/ConsoleObject.cpp       |  25 ++++++++
 test/js/node/inspector/inspector.test.ts | 101 ++++++++++++++++++++++++++++++-
 3 files changed, 139 insertions(+), 11 deletions(-)

gate history · 3 passed · 2 rejected · iteration 3

evidence per changed file
file                                      reads  edits  tests
src/js/internal/inspector/cdp.ts              7      5      0
src/jsc/bindings/ConsoleObject.cpp            1      1      0
test/js/node/inspector/inspector.test.ts      6     10      0

…/time events over the DevTools WebSocket

Two fixes for the DevTools-protocol server that inspector.open() / --inspect
exposes:

- Runtime.consoleAPICalled.timestamp (and Runtime.exceptionThrown.timestamp)
  was forwarded verbatim from JSC's Console.messageAdded, which reports
  WallTime::secondsSinceEpoch(). CDP's Runtime.Timestamp is milliseconds
  since epoch, so the adapter now multiplies by 1000. A frontend that
  treated the old value as milliseconds rendered Jan-1970 timestamps.

- console.count/countReset/time/timeLog/timeEnd never reached the inspector
  console agent: ConsoleObject.cpp forwarded messageWithTypeAndLevel and
  profile/profileEnd to inspectorController().consoleClient() but not the
  counter/timer entry points, so a connected DevTools saw no event at all
  for those calls. They are now forwarded the same way, which surfaces them
  as Runtime.consoleAPICalled (count as CDP type "debug", timeLog/timeEnd
  as "timeEnd").
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4a998706-28b8-4b9f-aa51-635323bcd6cc

📥 Commits

Reviewing files that changed from the base of the PR and between df6c7ee and 2ae31c0.

📒 Files selected for processing (3)
  • src/js/internal/inspector/cdp.ts
  • src/jsc/bindings/ConsoleObject.cpp
  • test/js/node/inspector/inspector.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 6:17 PM PT - Jul 25th, 2026

@robobun, your commit 2ae31c0 is still building in Build #81363, but has 1 failures so far (All Failures):

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. node: inspector/CDP + bun inspect v26 compat (+70 tests) #35396 - Implements the same console.count/countReset/time/timeLog/timeEnd inspector forwarding via inspectorController().consoleClient() and addresses timestamp handling in the CDP adapter

🤖 Generated with Claude Code

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate of #35396. That PR adds client->count/time* calls inside a new jsFunction_inspectorConsoleCall for the inspector.console object (the V8-style inspector-only console that never writes to stdout). This PR fixes the global console.count / console.time* path in ConsoleObject.cpp, which is what a connected DevTools actually sees for ordinary user code, and it rescales Console.messageAdded.timestamp from seconds to milliseconds in #translateConsoleMessage; #35396 touches neither of those spots.

Both PRs edit src/js/internal/inspector/cdp.ts, so whichever lands second will need a small rebase there.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the ConsoleObject.cpp forwards are byte-for-byte the existing messageWithTypeAndLevel/profile pattern (same inspectable() guard, same arguments.copyRef() for timeLog), and the seconds→ms rescale in cdp.ts is applied to both emit sites. The inline nit about countReset coverage is worth folding in but non-blocking.

Extended reasoning...

Overview

Two fixes for the DevTools-protocol inspector server: (1) src/js/internal/inspector/cdp.ts rescales JSC's seconds-since-epoch Console.messageAdded.timestamp to CDP's milliseconds for both Runtime.consoleAPICalled and Runtime.exceptionThrown; (2) src/jsc/bindings/ConsoleObject.cpp forwards count/countReset/time/timeLog/timeEnd to inspectorController().consoleClient() so InspectorConsoleAgent emits events for them. A new subprocess test in test/js/node/inspector/inspector.test.ts opens the inspector, connects as a CDP client, and asserts both the ms-scale timestamp (bracketed by Date.now()) and the emitted event types/texts.

Security risks

None. The change only adds inspector-frontend forwarding behind the existing globalObject->inspectable() guard and does arithmetic on a timestamp. No user-controlled input parsing, no auth/crypto, no new network surface.

Level of scrutiny

Low-to-moderate. The C++ hunks are purely additive and copy the exact pattern already used in the same file for messageWithTypeAndLevel, profile, and profileEnd — same inspectable() gate, same consoleClient() fetch, and timeLog uses arguments.copyRef() before the local arguments.ptr() read just like messageWithTypeAndLevel does. InspectorConsoleAgent::count/time* maintain their own counter/timer state independent of Bun's stdout printer, which is the intended split (same as WebKit); both are invoked on every call so they stay in sync. No new lifetime, refcount, or exception-scope concerns — the inspector client calls don't run user JS. The cdp.ts change replaces message.timestamp ?? Date.now() with a typeof === "number" check plus * 1000, applied consistently at both emit sites; the Date.now() fallback is pre-existing.

Other factors

The test follows the file's established fixture pattern (subprocess, injectedScriptChildEnv, concurrent pipe drain, awaits a __done__ sentinel rather than sleeping, exact-array assertions on types/texts). The one finding is a coverage nit — countReset isn't exercised — which is valid per REVIEW.md's "every sibling entry point" rule but doesn't indicate a defect; the forward is mechanically identical to the four covered siblings. No prior reviews on the PR to reconcile with.

Comment thread test/js/node/inspector/inspector.test.ts
Comment thread src/js/internal/inspector/cdp.ts
Comment thread src/js/internal/inspector/cdp.ts Outdated
…g string reaches DevTools

JSC's InspectorConsoleAgent::logTiming puts the "label: N.NNNms" string in
Console.messageAdded.text and only the caller's extra arguments in
parameters. The adapter preferred parameters when present, so the timing was
dropped from the Runtime.consoleAPICalled args for timeLog. Prepend the text
as args[0] in that case, matching Node.
Comment thread src/js/internal/inspector/cdp.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — both earlier review notes (untested countReset forward; timeLog dropping the elapsed-time string) are addressed in 79e136a and aa8db45.

Checked: the five ConsoleObject.cpp forwards mirror the existing messageWithTypeAndLevel/profile pattern including arguments.copyRef() for timeLog; the * 1000 rescale replaces both prior message.timestamp ?? Date.now() sites so Runtime.exceptionThrown is covered too; the new args prepend spreads into a fresh array rather than mutating message.parameters. The new test brackets the timestamp with Date.now() readings, awaits the __done__ sentinel instead of sleeping, and the countTexts ["cnt: 1", "cnt: 2", "cnt: 1"] assertion makes the countReset hunk load-bearing.

Extended reasoning...

Overview

Three files: src/jsc/bindings/ConsoleObject.cpp adds inspector-client forwarding to count/countReset/time/timeLog/timeEnd (five mechanically identical hunks copying the pattern already used by messageWithTypeAndLevel and profile/profileEnd in the same file); src/js/internal/inspector/cdp.ts rescales JSC's seconds-since-epoch Console.messageAdded.timestamp to CDP's milliseconds and prepends the elapsed-time text to args for type:"timing" messages that carry extra parameters; test/js/node/inspector/inspector.test.ts gains one subprocess test that connects as a CDP client and asserts the timestamp range, event types, count texts, and timing args.

Security risks

None. This is DevTools-protocol translation that is only reachable when globalObject->inspectable() is true (a debugger is attached). No parsing of untrusted external input beyond what the adapter already handled; no auth/crypto/permissions.

Level of scrutiny

Low-to-moderate. The C++ change is purely additive and pattern-matched from adjacent code in the same file — the inspectable() guard means it is a no-op in normal execution, and arguments.copyRef() for timeLog follows the same non-consuming-Ref idiom as messageWithTypeAndLevel. The adapter change is a unit conversion plus a small conditional; the pre-existing Date.now() fallback is preserved (now only taken when timestamp is not a number). The PR description includes before/after evidence on both ASAN-debug and release builds showing the test fails on main for the right reasons (seconds-scale timestamp; missing event types).

Other factors

Two prior review rounds from me on this PR were both addressed: the test now exercises countReset (third count reads "cnt: 1", so reverting that hunk would surface as "cnt: 3"), and the adapter now prepends the timing string so timeLog events carry the elapsed time as args[0] matching Node. The two comment-cop flags on the adapter comments were reasonably rebutted — they are short protocol-translation notes consistent with the rest of cdp.ts, not workaround justifications. The test follows the file's established fixture/spawn/JSON-summary pattern, uses port: 0, drains stdout/stderr/exited concurrently, and awaits the __done__ sentinel event rather than sleeping. All threads on the PR are marked resolved and no human reviewer has outstanding comments.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — both earlier findings addressed (countReset now load-bearing via cnt: 1 after reset; timeLog now prepends the elapsed-time text as args[0]).

Checked the final revision: the || rewrite of the type-map lookup is behavior-equivalent (all CONSOLE_TYPE_MAP values are non-empty strings); arguments.copyRef() in timeLog matches the existing messageWithTypeAndLevel pattern and leaves the original Ref valid for the Bun__ConsoleObject__timeLog call below; the fixture awaits the __done__ sentinel rather than sleeping.

Extended reasoning...

Overview

Two fixes to the DevTools-protocol path: (1) cdp.ts rescales JSC's seconds-since-epoch Console.messageAdded.timestamp to CDP's milliseconds and prepends the text timing string to args for type:"timing" messages that carry parameters; (2) ConsoleObject.cpp forwards count/countReset/time/timeLog/timeEnd to inspectorController().consoleClient(), mirroring the existing messageWithTypeAndLevel and profile/profileEnd blocks in the same file. A new test in inspector.test.ts opens the inspector, connects as a CDP client, and asserts the millisecond-bracketed timestamp plus the exact event types/texts for all five newly-forwarded calls.

Security risks

None. This is inspector/DevTools protocol translation — no auth, no external input parsing beyond JSC's own inspector messages, no new network surface.

Level of scrutiny

Low-to-medium. The C++ change is five identical 4-line hunks that copy the neighbouring messageWithTypeAndLevel pattern verbatim (inspectable() guard → consoleClient() null-check → forward). The TS change is a * 1000 and a conditional array-prepend inside a translation function that this file already owns. No lifetime, allocation, or threading changes.

Other factors

  • I raised two issues on earlier revisions (missing countReset coverage; timeLog args dropping the elapsed-time string). Both were fixed in 79e136a and aa8db45 and the threads are resolved. The comment-cop bot flags on the two protocol-translation comments were reasonably rebutted (the file's existing style annotates every non-obvious JSC↔CDP mapping) and resolved.
  • The test is load-bearing for every ConsoleObject.cpp hunk: reverting countReset yields cnt: 3 instead of cnt: 1; reverting time breaks both timeEnd assertions; reverting the timestamp rescale fails the beforeMs/afterMs bracket by ~3 orders of magnitude. The PR description shows both ASAN-debug and release runs passing with the fix and failing without.
  • Checked that the (jscType && CONSOLE_TYPE_MAP[jscType]) || … rewrite is equivalent to the old ternary — all map values are truthy strings so the || fallthrough behaves identically. Checked that arguments.copyRef() doesn't consume the rvalue ref, so arguments.ptr() on the next line remains valid (same as messageWithTypeAndLevel at the top of the file).

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — both earlier review notes (countReset coverage, timeLog dropping the timing string) are addressed and all threads are resolved.

What was reviewed:

  • ConsoleObject.cpp forwards for count/countReset/time/timeLog/timeEnd copy the exact inspectable() → consoleClient() pattern already used by messageWithTypeAndLevel and profile/profileEnd in the same file; timeLog uses arguments.copyRef() so Bun's own printer below still sees the args.
  • cdp.ts: seconds→ms rescale, timing-text prepend, and the CONSOLE_TYPE_MAP ternary→|| rewrite (all map values are non-empty strings, so behavior is identical).
  • New test covers all five forwards, brackets the timestamp with Date.now(), and awaits the __done__ sentinel rather than sleeping.
Extended reasoning...

Overview

Two fixes to the DevTools-protocol path: (1) src/jsc/bindings/ConsoleObject.cpp forwards count/countReset/time/timeLog/timeEnd to inspectorController().consoleClient(), mirroring the existing messageWithTypeAndLevel/profile pattern in the same file; (2) src/js/internal/inspector/cdp.ts rescales JSC's seconds-since-epoch Console.messageAdded.timestamp to CDP's milliseconds and prepends the elapsed-time text to args for type:"timing" messages so console.timeLog events carry the duration. A new test in test/js/node/inspector/inspector.test.ts exercises the full round-trip over the WebSocket.

Security risks

None. The inspector forwarding is gated on globalObject->inspectable() (unchanged gate), and the adapter change is a pure unit conversion / array prepend on data already flowing through it. No new surface, no auth/crypto/permissions.

Level of scrutiny

Low-to-moderate. The C++ change is five copies of a 4-line block that already exists twice in the same file (messageWithTypeAndLevel, profile/profileEnd) — I checked that timeLog's arguments.copyRef() matches messageWithTypeAndLevel's handling so the subsequent arguments.ptr() read remains valid. The cdp.ts change is a small refactor plus two protocol-translation fixes; I verified the CONSOLE_TYPE_MAP[jscType] ? … : …… || … rewrite is behavior-preserving because every map value is a non-empty string, and that the pre-existing Date.now() fallback is unchanged (JSC always sends a numeric timestamp, and the adapter runs on the debugger thread where user-land Date tampering doesn't reach).

Other factors

Both of my earlier findings on this PR were folded in: 79e136a added countReset coverage (third cnt: 1 proves the reset reached the agent), and aa8db45 added the timing-text prepend so timeLog emits ["tm: …ms", "mid"] like Node. The comment-cop bot's two flags were answered (protocol-translation notes matching the file's existing style) and resolved. The gate evidence shows the new test fails on main (timestamp ~1.78e9 and missing timing string) and passes on the PR under both ASAN-debug and release. Test follows harness conventions: tempDir, injectedScriptChildEnv, concurrent pipe drain, and awaits an observable __done__ event rather than a sleep.

…orDebugger subprocess tests the same 30s budget as their neighbours

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — both prior review notes (countReset coverage, timeLog args prepend) are addressed and all threads resolved.

What was reviewed:

  • ConsoleObject.cpp: the five new inspectable() → consoleClient()->X() forwards mirror the existing messageWithTypeAndLevel/profile pattern; timeLog uses arguments.copyRef() so the Ref survives for Bun's own printer below.
  • cdp.ts: the (jscType && CONSOLE_TYPE_MAP[jscType]) || … refactor is behaviorally identical to the old ternary (all map values are non-empty strings); the timing-args prepend builds a fresh array rather than mutating parameters.
  • Test: awaits the __done__ sentinel (no sleep), drains all pipes concurrently, and every ConsoleObject.cpp hunk including countReset is now load-bearing via the ["cnt: 1", "cnt: 2", "cnt: 1"] assertion.
Extended reasoning...

Overview

Three files: ConsoleObject.cpp gains five identical if (globalObject->inspectable()) { client->X(...) } blocks in count/countReset/time/timeLog/timeEnd, copied verbatim from the pre-existing pattern in messageWithTypeAndLevel and profile/profileEnd in the same file. cdp.ts rescales JSC's seconds-since-epoch Console.messageAdded.timestamp to CDP's milliseconds and prepends the elapsed-time text to args for type:"timing" messages so console.timeLog events carry their duration. The test file gains one new subprocess test and two 30s timeout alignments on neighbouring waitForDebugger tests.

Security risks

None. This is inspector/DevTools protocol translation for a debugging feature the user explicitly opts into via inspector.open() / --inspect. No auth, crypto, permissions, or untrusted-input parsing is touched. The Date.now() fallback and array operations in cdp.ts match the file's existing (non-primordial) style — the adapter runs on the debugger thread against JSON from JSC's own backend, and none of those calls are new to this PR.

Level of scrutiny

Low-to-moderate. The C++ change is purely additive and mechanically identical across five sites to an established pattern two functions away; the only ownership subtlety (Ref<ScriptArguments>&& in timeLog) is handled with copyRef() exactly as messageWithTypeAndLevel does. The TypeScript change is a unit conversion plus a small conditional prepend, both with explanatory comments consistent with the file's translation-note style. The gate evidence shows the new test failing on main (seconds-scale timestamp, missing events) and passing on the PR under both ASAN-debug and release.

Other factors

Both of my earlier 🟡 findings were folded in: 79e136a added countReset coverage so all five forwards are load-bearing, and aa8db45 added the timing-text prepend so timeLog events carry the elapsed time as args[0] matching Node. The comment-cop bot flags were reasonably rebutted (these are protocol-translation notes in a file full of them, not workaround justifications). The type computation refactor from ternary to || is equivalent because every CONSOLE_TYPE_MAP value is a non-empty string. The new test follows harness conventions (tempDir, bunExe, injectedScriptChildEnv, concurrent pipe drain, awaits the __done__ sentinel rather than sleeping, brackets the timestamp with Date.now() readings). A duplicate-PR bot flagged #35396, but the author correctly explained they touch different code paths (global console vs inspector.console).

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Build 81363 is red on four build-bun job timeouts (darwin-aarch64, linux-x64-musl, linux-aarch64-musl, freebsd-aarch64). Every build in the 81300-81400 range on other branches shows the same pattern (1-6 build-bun timeouts each), so this is fleet-wide build capacity rather than this diff.

The one lane that completed (windows-aarch64) built and all eight test shards passed. The gate verified fail-before/pass-after on both ASAN-debug and release. Ready for a maintainer when the build fleet recovers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants