Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/codegen/generate-js2native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const sourceFiles = readdirRecursiveWithExclusionsAndExtensionsSync(
const rustIdentifierPaths: Record<string, string> = {
"bun.rs": "bun.rs",
"ipc.rs": "runtime/ipc_host.rs",
"BunCPUProfiler.rs": "jsc/BunCPUProfiler.rs",
"Counters.rs": "jsc/Counters.rs",
"FrameworkRouter.rs": "runtime/bake/FrameworkRouter.rs",
"JSBundler.rs": "runtime/api/JSBundler.rs",
Expand Down
19 changes: 1 addition & 18 deletions src/js/node/v8.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,23 +331,6 @@ function serialize(arg1) {
return framed;
}

function getDefaultHeapSnapshotPath() {
const date = new Date();

const worker_threads = require("node:worker_threads");
const thread_id = worker_threads.threadId;

const yyyy = date.getFullYear();
const mm = date.getMonth().toString().padStart(2, "0");
const dd = date.getDate().toString().padStart(2, "0");
const hh = date.getHours().toString().padStart(2, "0");
const MM = date.getMinutes().toString().padStart(2, "0");
const ss = date.getSeconds().toString().padStart(2, "0");

// 'Heap-${yyyymmdd}-${hhmmss}-${pid}-${thread_id}.heapsnapshot'
return `Heap-${yyyy}${mm}${dd}-${hh}${MM}${ss}-${process.pid}-${thread_id}.heapsnapshot`;
}

let fs;

function writeHeapSnapshot(path, _options) {
Expand All @@ -360,7 +343,7 @@ function writeHeapSnapshot(path, _options) {
throw $ERR_INVALID_ARG_VALUE("path", path, "must be a non-empty string");
}
} else {
path = getDefaultHeapSnapshotPath();
path = $newRustFunction("BunCPUProfiler.rs", "jsHeapSnapshotFilename", 0)();
}

if (!fs) {
Expand Down
143 changes: 113 additions & 30 deletions src/jsc/BunCPUProfiler.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use core::ffi::c_int;
use core::sync::atomic::{AtomicU32, Ordering};
use std::io::Write as _;

use crate::VM;
use crate::{CallFrame, GregorianDateTime, JSGlobalObject, JSValue, JsResult, VM};
use bun_core::String as BunString;
use bun_paths::{AutoAbsPathChecked, PathBuffer};
use bun_sys::{self, Errno, Fd, FdDirExt as _};
Expand All @@ -22,6 +23,39 @@ pub struct CPUProfilerConfig {
pub md_format: bool,
pub json_format: bool,
pub interval: u32,
/// Numbers of the default-named files, 0 when `--cpu-prof-name` names the file.
json_sequence: u32,
md_sequence: u32,
}

impl CPUProfilerConfig {
/// Draws the numbers of the default-named files now, as node does when its profiler starts.
pub fn new(
name: &'static [u8],
dir: &'static [u8],
md_format: bool,
json_format: bool,
interval: u32,
) -> Self {
let default_named = name.is_empty();
Self {
name,
dir,
md_format,
json_format,
interval,
json_sequence: if default_named && json_format {
next_sequence()
} else {
0
},
md_sequence: if default_named && md_format {
next_sequence()
} else {
0
},
}
}
}

// C++ function declarations
Expand Down Expand Up @@ -157,7 +191,7 @@ fn build_output_path(
}
}
} else {
generate_default_filename(&mut filename_buf, is_md_format)?
generate_default_filename(&mut filename_buf, config, is_md_format)?
};

if !config.dir.is_empty() {
Expand All @@ -172,62 +206,85 @@ fn build_output_path(
Ok(())
}

fn generate_default_filename(
buf: &mut PathBuffer,
fn generate_default_filename<'a>(
buf: &'a mut PathBuffer,
config: &CPUProfilerConfig,
md_format: bool,
) -> Result<&[u8], ProfilerError> {
let extension: &str = if md_format { ".md" } else { ".cpuprofile" };
) -> Result<&'a [u8], ProfilerError> {
let (extension, sequence) = if md_format {
(".md", config.md_sequence)
} else {
(".cpuprofile", config.json_sequence)
};
let mut cursor = std::io::Cursor::new(&mut buf[..]);
write_diagnostic_filename(&mut cursor, "CPU", extension)
write_diagnostic_filename(&mut cursor, "CPU", extension, 0, local_time_now(), sequence)
.map_err(|_| ProfilerError::FilenameTooLong)?;
let len = usize::try_from(cursor.position()).expect("int cast");
Ok(&buf[..len])
}

/// Node's `seq`: https://github.com/nodejs/node/blob/v26.3.0/src/util.cc#L59
static SEQUENCE: AtomicU32 = AtomicU32::new(0);

pub(crate) fn next_sequence() -> u32 {
// Relaxed: the numbers only have to be distinct.
SEQUENCE.fetch_add(1, Ordering::Relaxed) + 1
}

/// Node's DiagnosticFilename: `<prefix>.<yyyymmdd>.<hhmmss>.<pid>.<tid>.<seq><extension>`.
/// https://github.com/nodejs/node/blob/main/src/util.cc (MakeFilename)
/// https://github.com/nodejs/node/blob/v26.3.0/src/util.cc#L239-L281 (MakeFilename)
pub(crate) fn write_diagnostic_filename(
cursor: &mut dyn std::io::Write,
prefix: &str,
extension: &str,
thread_id: u32,
time: GregorianDateTime,
sequence: u32,
) -> std::io::Result<()> {
#[cfg(windows)]
let pid = bun_sys::windows::GetCurrentProcessId();
#[cfg(not(windows))]
// SAFETY: getpid() is always safe to call.
let pid = unsafe { libc::getpid() };

let (year, month, day, hour, minute, second) = local_time_now();

static SEQ: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
let seq = SEQ.fetch_add(1, core::sync::atomic::Ordering::Relaxed) + 1;
let GregorianDateTime {
year,
month,
day,
hour,
minute,
second,
..
} = time;

write!(
cursor,
"{prefix}.{year:04}{month:02}{day:02}.{hour:02}{minute:02}{second:02}.{pid}.0.{seq:03}{extension}"
"{prefix}.{year:04}{month:02}{day:02}.{hour:02}{minute:02}{second:02}.{pid}.{thread_id}.{sequence:03}{extension}"
)
}

/// The local time the OS reports, which is what node's `MakeFilename` reads.
#[cfg(not(windows))]
pub(crate) fn local_time_now() -> (i32, u32, u32, u32, u32, u32) {
pub(crate) fn local_time_now() -> GregorianDateTime {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs()) as libc::time_t;
let mut tm: libc::tm = bun_core::ffi::zeroed();
// SAFETY: localtime_r only writes into `tm` and is thread-safe.
unsafe { libc::localtime_r(&raw const secs, &raw mut tm) };
(
tm.tm_year + 1900,
(tm.tm_mon + 1) as u32,
tm.tm_mday as u32,
tm.tm_hour as u32,
tm.tm_min as u32,
tm.tm_sec as u32,
)
GregorianDateTime {
year: tm.tm_year + 1900,
month: tm.tm_mon + 1,
day: tm.tm_mday,
hour: tm.tm_hour,
minute: tm.tm_min,
second: tm.tm_sec,
weekday: tm.tm_wday,
}
}

#[cfg(windows)]
pub(crate) fn local_time_now() -> (i32, u32, u32, u32, u32, u32) {
pub(crate) fn local_time_now() -> GregorianDateTime {
#[repr(C)]
#[derive(Default)]
struct SystemTime {
Expand All @@ -246,12 +303,38 @@ pub(crate) fn local_time_now() -> (i32, u32, u32, u32, u32, u32) {
let mut st = SystemTime::default();
// SAFETY: GetLocalTime only writes the out-param (kernel32 SYSTEMTIME layout).
unsafe { GetLocalTime(&mut st) };
(
i32::from(st.year),
u32::from(st.month),
u32::from(st.day),
u32::from(st.hour),
u32::from(st.minute),
u32::from(st.second),
GregorianDateTime {
year: i32::from(st.year),
month: i32::from(st.month),
day: i32::from(st.day),
hour: i32::from(st.hour),
minute: i32::from(st.minute),
second: i32::from(st.second),
weekday: i32::from(st.day_of_week),
}
}

/// The default name of `v8.writeHeapSnapshot()`: https://github.com/nodejs/node/blob/v26.3.0/src/heap_utils.cc#L461-L462
pub fn js_heap_snapshot_filename(global: &JSGlobalObject, _frame: &CallFrame) -> JsResult<JSValue> {
let thread_id = global
.bun_vm()
.worker_ref()
.map_or(0, |worker| worker.thread_id());
// The DateCache is the zone `Date` uses. Unlike libc, it follows `process.env.TZ`.
let time = global.ms_to_gregorian_date_time(bun_core::time::milli_timestamp() as f64);

// Every field is a 32-bit integer, so the name is at most 119 bytes.
let mut name = [0u8; 128];
let mut cursor = std::io::Cursor::new(&mut name[..]);
write_diagnostic_filename(
&mut cursor,
"Heap",
".heapsnapshot",
thread_id,
time,
next_sequence(),
)
.expect("a heap snapshot name fits in 128 bytes");
let len = usize::try_from(cursor.position()).expect("int cast");
crate::bun_string_jsc::create_utf8_for_js(global, &name[..len])
}
42 changes: 37 additions & 5 deletions src/jsc/BunHeapProfiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ pub struct HeapProfilerConfig {
pub name: &'static [u8],
pub dir: &'static [u8],
pub text_format: bool,
/// Number of the default-named profile, 0 when `--heap-prof-name` names the file.
sequence: u32,
}

impl HeapProfilerConfig {
/// Draws the number of a default-named profile now, like `CPUProfilerConfig::new`.
pub fn new(name: &'static [u8], dir: &'static [u8], text_format: bool) -> Self {
Self {
name,
dir,
text_format,
sequence: if name.is_empty() {
crate::bun_cpu_profiler::next_sequence()
} else {
0
},
}
}
}

// C++ function declarations
Expand Down Expand Up @@ -108,7 +126,7 @@ fn build_output_path(
let filename: &[u8] = if !config.name.is_empty() {
config.name
} else {
generate_default_filename(&mut filename_buf, config.text_format)?
generate_default_filename(&mut filename_buf, config)?
};

// Join directory and filename; `join` resolves absolute segments where
Expand All @@ -120,11 +138,25 @@ fn build_output_path(
Ok(())
}

fn generate_default_filename(buf: &mut PathBuffer, text_format: bool) -> Result<&[u8], Error> {
let extension: &str = if text_format { ".md" } else { ".heapprofile" };
fn generate_default_filename<'a>(
buf: &'a mut PathBuffer,
config: &HeapProfilerConfig,
) -> Result<&'a [u8], Error> {
let extension: &str = if config.text_format {
".md"
} else {
".heapprofile"
};
let mut cursor = std::io::Cursor::new(&mut buf[..]);
crate::bun_cpu_profiler::write_diagnostic_filename(&mut cursor, "Heap", extension)
.map_err(|_| crate::CrateError::Sys(bun_errno::SystemErrno::ENOSPC))?;
crate::bun_cpu_profiler::write_diagnostic_filename(
&mut cursor,
"Heap",
extension,
0,
crate::bun_cpu_profiler::local_time_now(),
config.sequence,
)
.map_err(|_| crate::CrateError::Sys(bun_errno::SystemErrno::ENOSPC))?;
let written = usize::try_from(cursor.position()).expect("int cast");
Ok(&buf.as_slice()[..written])
}
5 changes: 5 additions & 0 deletions src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,11 @@ impl WebWorker {
self.execution_context_id
}

/// The `worker.threadId` node exposes (context ids start at 1 on the main thread).
pub(crate) fn thread_id(&self) -> u32 {
self.execution_context_id.saturating_sub(1)
}

/// The C++ `WorkerMessagingProxy`, handed to `Zig__GlobalObject__create` so
/// the worker's global is born knowing its options (env, argv, workerData).
#[inline]
Expand Down
16 changes: 8 additions & 8 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1357,13 +1357,13 @@ impl Run<'_> {
let name: &'static [u8] = unsafe { &*std::ptr::from_ref::<[u8]>(opts.name.as_ref()) };
// SAFETY: same process-lifetime erasure as `name` above.
let dir: &'static [u8] = unsafe { &*std::ptr::from_ref::<[u8]>(opts.dir.as_ref()) };
vm.cpu_profiler_config = Some(bun_jsc::bun_cpu_profiler::CPUProfilerConfig {
vm.cpu_profiler_config = Some(bun_jsc::bun_cpu_profiler::CPUProfilerConfig::new(
name,
dir,
md_format: opts.md_format,
json_format: opts.json_format,
interval: opts.interval,
});
opts.md_format,
opts.json_format,
opts.interval,
));
bun_jsc::bun_cpu_profiler::set_sampling_interval(opts.interval);
// SAFETY: `vm.jsc_vm` set in `init`.
bun_jsc::bun_cpu_profiler::start_cpu_profiler(unsafe { &mut *vm.jsc_vm });
Expand All @@ -1377,11 +1377,11 @@ impl Run<'_> {
let name: &'static [u8] = unsafe { &*std::ptr::from_ref::<[u8]>(opts.name.as_ref()) };
// SAFETY: same process-lifetime erasure as `name` above.
let dir: &'static [u8] = unsafe { &*std::ptr::from_ref::<[u8]>(opts.dir.as_ref()) };
vm.heap_profiler_config = Some(bun_jsc::bun_heap_profiler::HeapProfilerConfig {
vm.heap_profiler_config = Some(bun_jsc::bun_heap_profiler::HeapProfilerConfig::new(
name,
dir,
text_format: opts.text_format,
});
opts.text_format,
));
bun_analytics::features::heap_snapshot.fetch_add(1, Ordering::Relaxed);
}

Expand Down
1 change: 1 addition & 0 deletions src/runtime/dispatch_js2native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub(crate) use bun_install_jsc::npm_jsc::package_manifest_bindings_generate as i
pub(crate) use bun_install_jsc::ini_jsc::ini_testing_load_npmrc_from_js as ini_ini_ini_testing_ap_is_load_npmrc_from_js;
pub(crate) use bun_install_jsc::ini_jsc::ini_testing_parse as ini_ini_ini_testing_ap_is_parse;

pub(crate) use bun_jsc::bun_cpu_profiler::js_heap_snapshot_filename as jsc_bun_cpu_profiler_js_heap_snapshot_filename;
pub(crate) use bun_jsc::bytecode_order_recorder::names_for_testing as jsc_bytecode_order_recorder_names_for_testing;
pub(crate) use bun_jsc::counters::create_counters_object as jsc_counters_create_counters_object;
pub(crate) use bun_jsc::event_loop::get_active_tasks as jsc_event_loop_get_active_tasks;
Expand Down
Loading
Loading