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
36 changes: 34 additions & 2 deletions profile-bee/bin/profile-bee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ async fn main() -> std::result::Result<(), anyhow::Error> {
if spawn.is_none() {
if let Some(target_pid) = pid {
warn_nodejs_without_perf_map(target_pid);
warn_bun_without_jitdump(target_pid);
}
}

Expand Down Expand Up @@ -903,6 +904,35 @@ fn warn_nodejs_without_perf_map(pid: u32) {
eprintln!("Or use profile-bee's auto-injection: probee -- node <script>\n");
}

/// Warn if profiling an already-running Bun process that lacks a JITDump
/// file. Without `BUN_JSC_useJITDump=1`, JavaScriptCore JIT-compiled
/// JavaScript functions show as `[unknown]` in flamegraphs.
fn warn_bun_without_jitdump(pid: u32) {
let exe_link = format!("/proc/{}/exe", pid);
let exe_path = match std::fs::read_link(&exe_link) {
Ok(p) => p,
Err(_) => return,
};
let basename = exe_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !profile_bee::spawn::is_bun_program(basename) {
return;
}

// Check for JITDump file
if profile_bee::jitdump::find_jitdump_for_pid(pid).is_some() {
return; // JITDump file present, JIT symbols will be resolved
}

eprintln!(
"\n\x1b[33mWarning: Profiling Bun process (PID {}) without JIT symbol support.\x1b[0m",
pid
);
eprintln!("JavaScript function names will appear as [unknown] in the flamegraph.");
eprintln!("To enable JIT symbol resolution, restart Bun with:");
eprintln!(" BUN_JSC_useJITDump=1 bun <script>");
eprintln!("Or use profile-bee's auto-injection: probee -- bun <script>\n");
}

/// Handle `probee symbolize <file.raw> [-o output.svg] [-o output.folded]`.
///
/// Re-symbolizes a raw capture file produced by `-o profile.raw`, resolving
Expand Down Expand Up @@ -1631,10 +1661,11 @@ async fn run_combined_mode(
let (mut ebpf_profiler, ring_buf, tgid_request_tx) =
setup_ebpf_and_dwarf(&mut config, &perf_tx, pid, opt.dwarf.unwrap_or(false))?;

// Warn if targeting an existing Node.js process without perf-map
// Warn if targeting an existing Node.js/Bun process without JIT symbol support
if spawn.is_none() {
if let Some(target_pid) = pid {
warn_nodejs_without_perf_map(target_pid);
warn_bun_without_jitdump(target_pid);
}
}

Expand Down Expand Up @@ -1739,10 +1770,11 @@ async fn run_tui_mode(opt: Opt) -> std::result::Result<(), anyhow::Error> {
let (mut ebpf_profiler, ring_buf, tgid_request_tx) =
setup_ebpf_and_dwarf(&mut config, &perf_tx, pid, opt.dwarf.unwrap_or(false))?;

// Warn if targeting an existing Node.js process without perf-map
// Warn if targeting an existing Node.js/Bun process without JIT symbol support
if spawn.is_none() {
if let Some(target_pid) = pid {
warn_nodejs_without_perf_map(target_pid);
warn_bun_without_jitdump(target_pid);
}
}

Expand Down
68 changes: 68 additions & 0 deletions profile-bee/src/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use aya::Ebpf;
use profile_bee_common::{StackInfo, EVENT_TRACE_ALWAYS, PROCESS_EVENT_EXEC, PROCESS_EVENT_EXIT};

use crate::ebpf::{apply_dwarf_refresh, FramePointersPod, StackInfoPod, V8ProcInfoPod};
use crate::jitdump::{self, JitSymbolTable};
use crate::pipeline::{DwarfThreadMsg, PerfWork};
use crate::process_metadata::ProcessMetadataCache;
use crate::trace_handler::TraceHandler;
Expand Down Expand Up @@ -311,6 +312,61 @@ impl ProfilingEventLoop {
}
}

/// Try to load a JITDump symbol table for a newly-seen PID.
///
/// Checks for `/tmp/jit-<pid>.dump` and, if found, parses it and
/// registers the symbol table in the trace handler. This enables
/// resolution of JIT-compiled function names from runtimes like
/// Bun (JavaScriptCore), Java HotSpot, and LuaJIT.
fn try_load_jitdump_for_pid(&mut self, tgid: u32) {
if self.trace_handler.has_jit_table(tgid) {
return;
}
if let Some(path) = jitdump::find_jitdump_for_pid(tgid) {
match JitSymbolTable::load_from_file(&path) {
Ok(table) if !table.is_empty() => {
self.trace_handler.register_jit_table(tgid, table);
}
Ok(_) => tracing::debug!("JITDump for pid {} is empty", tgid),
Err(e) => tracing::debug!("JITDump read failed for pid {}: {}", tgid, e),
}
}
}

/// Reload JITDump symbol tables for all known PIDs.
///
/// Used by streaming modes (TUI/serve) to pick up newly JIT-compiled
/// functions. For PIDs without an existing table, tries a fresh load.
/// For PIDs with a table, does an incremental reload from where the
/// last read left off.
pub fn reload_jitdump_tables(&mut self) {
for &tgid in &self.known_tgids.clone() {
if self.trace_handler.has_jit_table(tgid) {
// Incremental reload
if let Some(path) = jitdump::find_jitdump_for_pid(tgid) {
if let Some(table) = self.trace_handler.jit_table_mut(tgid) {
match table.reload_from_file(&path) {
Ok(n) if n > 0 => {
tracing::debug!(
"reloaded {} new JITDump symbols for pid {}",
n,
tgid
);
}
Ok(_) => {}
Err(e) => {
tracing::debug!("JITDump reload failed for pid {}: {}", tgid, e)
}
}
}
}
} else {
// Try fresh load
self.try_load_jitdump_for_pid(tgid);
}
}
}

/// Drain events from the PerfWork channel, optionally symbolize stacks
/// on the fly, and return `(local_counting, stopped)`.
///
Expand Down Expand Up @@ -383,10 +439,14 @@ impl ProfilingEventLoop {
// V8 introspection (eBPF FP context extraction +
// userspace heap reader for JS symbol resolution).
self.try_setup_v8_for_pid(stack.tgid);
// Try loading JITDump symbols for runtimes like
// Bun (JSC), Java HotSpot, and LuaJIT.
self.try_load_jitdump_for_pid(stack.tgid);
}
} else if stack.tgid != 0 && self.known_tgids.insert(stack.tgid) {
// Even without DWARF thread, detect V8 processes
self.try_setup_v8_for_pid(stack.tgid);
self.try_load_jitdump_for_pid(stack.tgid);
}
if local_counting {
let trace = self.trace_count.entry(stack).or_insert(0);
Expand Down Expand Up @@ -481,6 +541,14 @@ impl ProfilingEventLoop {
}

tracing::debug!("drain_events: processed {} events", queue_processed);

// Reload JITDump symbol tables at the end of the collection window.
// JIT runtimes (Bun, Java, etc.) write symbols incrementally as code
// is compiled — the initial load on first PID sight may have found an
// empty or nonexistent file. This final pass picks up all symbols
// written during the profiling window.
self.reload_jitdump_tables();

(local_counting, stopped)
}

Expand Down
Loading
Loading