From 9fca05b83adbcaf86f0392e7a2ccdd3517aa0a0a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:12:39 +0000 Subject: [PATCH 1/3] console: write timeEnd/timeLog to stdout instead of stderr Node.js routes console.timeEnd() and console.timeLog() output through console.log, which writes to stdout. Bun was writing to stderr, so redirecting stdout did not capture the timing output. Switch the elapsed-time print, the label print, and the extra-argument formatter in timeLog to their stdout equivalents. Fixes #12031 --- src/jsc/ConsoleObject.rs | 14 +++--- test/js/web/console/console-timeLog.test.ts | 47 ++++++++++++++++++--- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 1335268276bb..576d12d1c6f2 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -6003,12 +6003,12 @@ pub extern "C" fn Bun__ConsoleObject__timeEnd( }; let Some(value) = prev else { return }; // get the duration in microseconds, then display it in milliseconds - Output::print_elapsed( + Output::print_elapsed_stdout( (value.read() / bun_core::time::NS_PER_US) as f64 / bun_core::time::US_PER_MS as f64, ); match len { - 0 => Output::print_errorln(format_args!("")), - _ => Output::print_errorln(format_args!(" {}", bstr::BStr::new(slice))), + 0 => Output::println(format_args!("")), + _ => Output::println(format_args!(" {}", bstr::BStr::new(slice))), } Output::flush(); @@ -6035,12 +6035,12 @@ pub extern "C" fn Bun__ConsoleObject__timeLog( return; }; // get the duration in microseconds, then display it in milliseconds - Output::print_elapsed( + Output::print_elapsed_stdout( (value.read() / bun_core::time::NS_PER_US) as f64 / bun_core::time::US_PER_MS as f64, ); match len { 0 => {} - _ => Output::print_error(format_args!(" {}", bstr::BStr::new(slice))), + _ => Output::print(format_args!(" {}", bstr::BStr::new(slice))), } Output::flush(); @@ -6058,14 +6058,14 @@ pub extern "C" fn Bun__ConsoleObject__timeLog( // this VM; JS-thread-only. Kept as a raw deref (not `vm_console_mut`) so the // resulting `writer` borrow does not pin a long-lived `&mut ConsoleObject` // across the `fmt.format(...)` calls below, which can re-enter JS. - let mut writer = unsafe { (*console).error_writer() }; + let mut writer = unsafe { (*console).writer() }; // SAFETY: caller passes a valid (args, args_len) pair. for &arg in unsafe { bun_core::ffi::slice(args, args_len) } { let Ok(tag) = formatter::Tag::get(arg, global) else { return; }; let _ = bun_io::Write::write_all(&mut writer, b" "); - if Output::enable_ansi_colors_stderr() { + if Output::enable_ansi_colors_stdout() { let _ = fmt.format::(tag, &mut writer, arg, global); } else { let _ = fmt.format::(tag, &mut writer, arg, global); diff --git a/test/js/web/console/console-timeLog.test.ts b/test/js/web/console/console-timeLog.test.ts index bf7ddc485e5b..42f0dade60bf 100644 --- a/test/js/web/console/console-timeLog.test.ts +++ b/test/js/web/console/console-timeLog.test.ts @@ -11,8 +11,8 @@ it.concurrent("console.timeEnd with empty label emits exactly one trailing newli stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stdout).toBe(""); - expect(stderr).toMatch(/^\[[\d.]+[mnµ]?s\]\n$/); + expect(stderr).toBe(""); + expect(stdout).toMatch(/^\[[\d.]+[mnµ]?s\]\n$/); expect(exitCode).toBe(0); }); @@ -24,25 +24,58 @@ it.concurrent("console.timeEnd with non-empty label emits exactly one trailing n stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stdout).toBe(""); - expect(stderr).toMatch(/^\[[\d.]+[mnµ]?s\] abc\n$/); + expect(stderr).toBe(""); + expect(stdout).toMatch(/^\[[\d.]+[mnµ]?s\] abc\n$/); + expect(exitCode).toBe(0); +}); + +// https://github.com/oven-sh/bun/issues/12031 +it.concurrent("console.timeEnd writes to stdout, not stderr", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", `console.time(); console.timeEnd();`], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toMatch(/^\[[\d.]+[mnµ]?s\] default\n$/); + expect(exitCode).toBe(0); +}); + +// https://github.com/oven-sh/bun/issues/12031 +it.concurrent("console.timeLog writes to stdout, not stderr", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", `console.time(); console.timeLog(); console.timeLog("default", "extra", "args");`], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + const lines = stdout.split("\n"); + expect(lines[0]).toMatch(/^\[[\d.]+[mnµ]?s\] default$/); + expect(lines[1]).toMatch(/^\[[\d.]+[mnµ]?s\] default extra args$/); + expect(lines[2]).toBe(""); + expect(lines.length).toBe(3); expect(exitCode).toBe(0); }); it("should log to console correctly", async () => { - const { stderr, exited } = spawn({ + const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), join(import.meta.dir, "console-timeLog.js")], stdin: null, stdout: "pipe", stderr: "pipe", env: bunEnv, }); - expect(await exited).toBe(0); - const outText = await stderr.text(); + const [outText, errText, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); + expect(errText).toBe(""); const expectedText = (await file(join(import.meta.dir, "console-timeLog.expected.txt")).text()).replaceAll( "\r\n", "\n", ); expect(outText.replace(/^\[.+?s\] /gm, "")).toBe(expectedText.replace(/^\[.+?s\] /gm, "")); + expect(exitCode).toBe(0); }); From f6ed44538332c0e76f376fe46622514c29a74a65 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:23:00 +0000 Subject: [PATCH 2/3] ci: retrigger From 76c588256ada5f8d66cae4bcf4d8ddaf9be6fafc Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:32:41 +0000 Subject: [PATCH 3/3] test: mark console-timeLog integration test concurrent --- test/js/web/console/console-timeLog.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/web/console/console-timeLog.test.ts b/test/js/web/console/console-timeLog.test.ts index 42f0dade60bf..f606af3ed898 100644 --- a/test/js/web/console/console-timeLog.test.ts +++ b/test/js/web/console/console-timeLog.test.ts @@ -61,7 +61,7 @@ it.concurrent("console.timeLog writes to stdout, not stderr", async () => { expect(exitCode).toBe(0); }); -it("should log to console correctly", async () => { +it.concurrent("should log to console correctly", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), join(import.meta.dir, "console-timeLog.js")], stdin: null,