diff --git a/samply-mac-preload/Cargo.lock b/samply-mac-preload/Cargo.lock index 3f14c0dd..989e57d5 100644 --- a/samply-mac-preload/Cargo.lock +++ b/samply-mac-preload/Cargo.lock @@ -2,15 +2,47 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "libc" version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b" +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "samply-mac-preload" version = "0.1.0" dependencies = [ "libc", + "spin", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "spin" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0959fd6f767df20b231736396e4f602171e00d95205676286e79d4a4eb67bef" +dependencies = [ + "lock_api", ] diff --git a/samply-mac-preload/Cargo.toml b/samply-mac-preload/Cargo.toml index 09edac50..65eef3d5 100644 --- a/samply-mac-preload/Cargo.toml +++ b/samply-mac-preload/Cargo.toml @@ -5,6 +5,9 @@ authors = ["Markus Stange "] edition = "2021" license = "MIT OR Apache-2.0" +[workspace] +# This crate is not part of the samply workspace. + [lib] crate_type = ["cdylib"] @@ -17,4 +20,5 @@ panic = 'abort' [dependencies] libc = { version = "0.2.70", default-features = false } +spin = "0.9.6" diff --git a/samply-mac-preload/README.md b/samply-mac-preload/README.md index 94ca2028..5bf0e9f2 100644 --- a/samply-mac-preload/README.md +++ b/samply-mac-preload/README.md @@ -5,12 +5,4 @@ rustup target add x86_64-apple-darwin rustup target add aarch64-apple-darwin ``` -``` -export SDKROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -MACOSX_DEPLOYMENT_TARGET=10.7 cargo build --release --target=x86_64-apple-darwin -mv target/x86_64-apple-darwin/release/libsamply_mac_preload.dylib binaries/libsamply_mac_preload_x86_64.dylib -MACOSX_DEPLOYMENT_TARGET=11.0 cargo build --release --target=aarch64-apple-darwin -mv target/aarch64-apple-darwin/release/libsamply_mac_preload.dylib binaries/libsamply_mac_preload_arm64.dylib -lipo binaries/libsamply_mac_preload_* -create -output binaries/libsamply_mac_preload.dylib -gzip -cvf binaries/libsamply_mac_preload.dylib > ../samply/resources/libsamply_mac_preload.dylib.gz -``` +Run `build.sh` from inside this directory to update the files inside `binaries/` and to copy the updated dylib into `../samply/resources/`. diff --git a/samply-mac-preload/binaries/libsamply_mac_preload.dylib b/samply-mac-preload/binaries/libsamply_mac_preload.dylib index beb723f5..9bbd4ccf 100755 Binary files a/samply-mac-preload/binaries/libsamply_mac_preload.dylib and b/samply-mac-preload/binaries/libsamply_mac_preload.dylib differ diff --git a/samply-mac-preload/binaries/libsamply_mac_preload_arm64.dylib b/samply-mac-preload/binaries/libsamply_mac_preload_arm64.dylib index b9e6b119..46817c28 100755 Binary files a/samply-mac-preload/binaries/libsamply_mac_preload_arm64.dylib and b/samply-mac-preload/binaries/libsamply_mac_preload_arm64.dylib differ diff --git a/samply-mac-preload/binaries/libsamply_mac_preload_x86_64.dylib b/samply-mac-preload/binaries/libsamply_mac_preload_x86_64.dylib index 437c9f0c..b61eabc5 100755 Binary files a/samply-mac-preload/binaries/libsamply_mac_preload_x86_64.dylib and b/samply-mac-preload/binaries/libsamply_mac_preload_x86_64.dylib differ diff --git a/samply-mac-preload/build.sh b/samply-mac-preload/build.sh new file mode 100755 index 00000000..c9a8b751 --- /dev/null +++ b/samply-mac-preload/build.sh @@ -0,0 +1,7 @@ +export SDKROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk +MACOSX_DEPLOYMENT_TARGET=10.7 cargo build --release --target=x86_64-apple-darwin +mv target/x86_64-apple-darwin/release/libsamply_mac_preload.dylib binaries/libsamply_mac_preload_x86_64.dylib +MACOSX_DEPLOYMENT_TARGET=11.0 cargo build --release --target=aarch64-apple-darwin +mv target/aarch64-apple-darwin/release/libsamply_mac_preload.dylib binaries/libsamply_mac_preload_arm64.dylib +lipo binaries/libsamply_mac_preload_* -create -output binaries/libsamply_mac_preload.dylib +gzip -cvf binaries/libsamply_mac_preload.dylib > ../samply/resources/libsamply_mac_preload.dylib.gz diff --git a/samply-mac-preload/src/lib.rs b/samply-mac-preload/src/lib.rs index 53dbbafc..fd98314f 100644 --- a/samply-mac-preload/src/lib.rs +++ b/samply-mac-preload/src/lib.rs @@ -1,17 +1,28 @@ #![no_main] #![no_std] -#[cfg(not(test))] -#[panic_handler] -fn panic(_panic: &core::panic::PanicInfo<'_>) -> ! { - unsafe { libc::abort() } -} +use libc::{c_char, c_int, mode_t, FILE}; + +use core::ffi::CStr; mod mach_ipc; mod mach_sys; use mach_ipc::{channel, mach_task_self, OsIpcChannel, OsIpcSender}; +extern "C" { + fn open(path: *const c_char, flags: c_int, mode: mode_t) -> c_int; + fn fopen(filename: *const c_char, mode: *const c_char) -> *mut FILE; +} + +static CHANNEL_SENDER: spin::Mutex> = spin::Mutex::new(None); + +#[cfg(not(test))] +#[panic_handler] +fn panic(_panic: &core::panic::PanicInfo<'_>) -> ! { + unsafe { libc::abort() } +} + // Run our code as early as possible, by pretending to be a global constructor. // This code was taken from https://github.com/neon-bindings/neon/blob/2277e943a619579c144c1da543874f4a7ec39879/src/lib.rs#L40-L44 #[used] @@ -48,9 +59,90 @@ fn set_up_samply_connection() -> Option<()> { message_bytes[7..11].copy_from_slice(&pid.to_le_bytes()); tx1.send(&message_bytes, [OsIpcChannel::Sender(tx0), c]) .ok()?; + *CHANNEL_SENDER.lock() = Some(tx1); // Wait for the parent to tell us to proceed, in case it wants to do any more setup with our task. - let mut recv_buf = [0; 32]; + let mut recv_buf = [0; 256]; let result = rx0.recv(&mut recv_buf).ok()?; assert_eq!(b"Proceed", &result); Some(()) } + +// Override the `open` function, in order to be able to observe the file +// paths of opened files. +// +// We use this to detect jitdump files. +#[no_mangle] +extern "C" fn samply_hooked_open(path: *const c_char, flags: c_int, mode: mode_t) -> c_int { + // unsafe { + // libc::printf(b"open(%s, %d, %u)\n\0".as_ptr() as *const i8, path, flags, mode as c_uint); + // } + + if let Ok(path) = unsafe { CStr::from_ptr(path) }.to_str() { + detect_and_send_jitdump_path(path); + } + + // Call the original. Do this at the end, so that this is compiled as a tail call. + // + // WARNING: What we are doing here is even sketchier than it seems. The `open` function + // is variadic: It can be called with or without the mode parameter. I have not found + // the right way to forward those variadic args properly. So by using a tail call, we + // can hope that the compiled code leaves the arguments completely untouched and just + // jumps to the called function, and everything should work out fine in terms of the + // call ABI. + unsafe { open(path, flags, mode) } +} + +// Override fopen for the same reason. +#[no_mangle] +extern "C" fn samply_hooked_fopen(path: *const c_char, mode: *const c_char) -> *mut FILE { + // unsafe { + // libc::printf(b"fopen(%s, %s\n\0".as_ptr() as *const i8, path, mode); + // } + + if let Ok(path) = unsafe { CStr::from_ptr(path) }.to_str() { + detect_and_send_jitdump_path(path); + } + + // Call the original. + unsafe { fopen(path, mode) } +} + +fn detect_and_send_jitdump_path(path: &str) { + if path.len() > 256 - 12 || !path.ends_with(".dump") || !path.contains("/jit-") { + return; + } + + let channel_sender = CHANNEL_SENDER.lock(); + let Some(sender) = channel_sender.as_ref() else { return }; + let pid = unsafe { libc::getpid() }; + let mut message_bytes = [0; 256]; + message_bytes[0..7].copy_from_slice(b"Jitdump"); + message_bytes[7..11].copy_from_slice(&pid.to_le_bytes()); + message_bytes[11] = path.len() as u8; + message_bytes[12..][..path.len()].copy_from_slice(path.as_bytes()); + let _ = sender.send(&message_bytes, []); +} + +#[allow(non_camel_case_types)] +pub struct InterposeEntry { + _new: *const (), + _old: *const (), +} + +#[used] +#[allow(dead_code)] +#[allow(non_upper_case_globals)] +#[link_section = "__DATA,__interpose"] +pub static mut _interpose_open: InterposeEntry = InterposeEntry { + _new: samply_hooked_open as *const (), + _old: open as *const (), +}; + +#[used] +#[allow(dead_code)] +#[allow(non_upper_case_globals)] +#[link_section = "__DATA,__interpose"] +pub static mut _interpose_fopen: InterposeEntry = InterposeEntry { + _new: samply_hooked_fopen as *const (), + _old: fopen as *const (), +}; diff --git a/samply-mac-preload/src/mach_ipc.rs b/samply-mac-preload/src/mach_ipc.rs index 8d940f53..acc3b6d2 100644 --- a/samply-mac-preload/src/mach_ipc.rs +++ b/samply-mac-preload/src/mach_ipc.rs @@ -233,7 +233,7 @@ impl OsIpcReceiver { message as *mut _, flags, 0, - (*message).header.msgh_size, + buf.len() as u32, port, timeout, MACH_PORT_NULL, diff --git a/samply/resources/libsamply_mac_preload.dylib.gz b/samply/resources/libsamply_mac_preload.dylib.gz index b70f656c..03f7aadb 100644 Binary files a/samply/resources/libsamply_mac_preload.dylib.gz and b/samply/resources/libsamply_mac_preload.dylib.gz differ diff --git a/samply/src/mac/process_launcher.rs b/samply/src/mac/process_launcher.rs index d34bacd5..36abce68 100644 --- a/samply/src/mac/process_launcher.rs +++ b/samply/src/mac/process_launcher.rs @@ -2,6 +2,8 @@ use std::ffi::OsStr; use std::fs::File; use std::io::Write; use std::mem; +use std::os::unix::prelude::OsStrExt; +use std::path::PathBuf; use std::process::{Child, Command}; use std::time::Duration; @@ -86,30 +88,49 @@ impl TaskAccepter { )) } - pub fn try_accept(&mut self, timeout: Duration) -> Result { + pub fn next_message(&mut self, timeout: Duration) -> Result { // Wait until the child is ready let (res, mut channels, _) = self .server .accept(BlockingMode::BlockingWithTimeout(timeout))?; - assert_eq!(res.len(), 11); - assert!(&res[0..7] == b"My task"); - let mut pid_bytes: [u8; 4] = Default::default(); - pid_bytes.copy_from_slice(&res[7..11]); - let pid = u32::from_le_bytes(pid_bytes); - let task_channel = channels.pop().unwrap(); - let sender_channel = channels.pop().unwrap(); - let sender_channel = sender_channel.into_sender(); - - let task = task_channel.into_port(); - - Ok(AcceptedTask { - task, - pid, - sender_channel, - }) + let received_stuff = match res.split_at(7) { + (b"My task", pid_bytes) => { + assert!(pid_bytes.len() == 4); + let pid = + u32::from_le_bytes([pid_bytes[0], pid_bytes[1], pid_bytes[2], pid_bytes[3]]); + let task_channel = channels.pop().unwrap(); + let sender_channel = channels.pop().unwrap(); + let sender_channel = sender_channel.into_sender(); + + let task = task_channel.into_port(); + + ReceivedStuff::AcceptedTask(AcceptedTask { + task, + pid, + sender_channel, + }) + } + (b"Jitdump", jitdump_info) => { + let pid_bytes = &jitdump_info[0..4]; + let pid = + u32::from_le_bytes([pid_bytes[0], pid_bytes[1], pid_bytes[2], pid_bytes[3]]); + let len = jitdump_info[4] as usize; + let path = &jitdump_info[5..][..len]; + ReceivedStuff::JitdumpPath(pid, OsStr::from_bytes(path).into()) + } + (other, _) => { + panic!("Unexpected message: {:?}", other); + } + }; + Ok(received_stuff) } } +pub enum ReceivedStuff { + AcceptedTask(AcceptedTask), + JitdumpPath(u32, PathBuf), +} + pub struct AcceptedTask { task: mach_port_t, pid: u32, diff --git a/samply/src/mac/profiler.rs b/samply/src/mac/profiler.rs index 9c93732e..c08f4802 100644 --- a/samply/src/mac/profiler.rs +++ b/samply/src/mac/profiler.rs @@ -1,6 +1,8 @@ use crossbeam_channel::unbounded; use serde_json::to_writer; +use std::collections::hash_map::Entry; +use std::collections::HashMap; use std::ffi::OsString; use std::fs::File; use std::io::BufWriter; @@ -12,7 +14,7 @@ use std::thread; use std::time::{Duration, Instant}; use super::error::SamplingError; -use super::process_launcher::{MachError, TaskAccepter}; +use super::process_launcher::{MachError, ReceivedStuff, TaskAccepter}; use super::sampler::{Sampler, TaskInit}; use crate::server::{start_server_main, ServerProps}; @@ -58,28 +60,57 @@ pub fn start_recording( TaskAccepter::create_and_launch_root_task(&command_name, command_args)?; let (accepter_sender, accepter_receiver) = unbounded(); - let accepter_thread = thread::spawn(move || loop { - if let Ok(()) = accepter_receiver.try_recv() { - break; - } - let timeout = Duration::from_secs_f64(1.0); - match task_accepter.try_accept(timeout) { - Ok(mut accepted_task) => { - let send_result = task_sender.send(TaskInit { - start_time: Instant::now(), - task: accepted_task.take_task(), - pid: accepted_task.get_id(), - }); - if send_result.is_err() { - // The sampler has already shut down. This task arrived too late. - } - accepted_task.start_execution(); - } - Err(MachError::RcvTimedOut) => { - // TODO: give status back via task_sender + let accepter_thread = thread::spawn(move || { + // Loop while accepting messages from the spawned process tree. + + // A map of pids to channel senders, to notify existing tasks of Jitdump + // paths. Having the mapping here lets us deliver the path to the right + // task even in cases where a process execs into a new task with the same pid. + let mut jitdump_path_senders_per_pid = HashMap::new(); + + loop { + if let Ok(()) = accepter_receiver.try_recv() { + break; } - Err(err) => { - eprintln!("Encountered error while waiting for task port: {err:?}"); + let timeout = Duration::from_secs_f64(1.0); + match task_accepter.next_message(timeout) { + Ok(ReceivedStuff::AcceptedTask(mut accepted_task)) => { + let pid = accepted_task.get_id(); + let (jitdump_path_sender, jitdump_path_receiver) = unbounded(); + let send_result = task_sender.send(TaskInit { + start_time: Instant::now(), + task: accepted_task.take_task(), + pid, + jitdump_path_receiver, + }); + jitdump_path_senders_per_pid.insert(pid, jitdump_path_sender); + if send_result.is_err() { + // The sampler has already shut down. This task arrived too late. + } + accepted_task.start_execution(); + } + Ok(ReceivedStuff::JitdumpPath(pid, path)) => { + match jitdump_path_senders_per_pid.entry(pid) { + Entry::Occupied(mut entry) => { + let send_result = entry.get_mut().send(path); + if send_result.is_err() { + // The task is probably already dead. The path arrived too late. + entry.remove(); + } + } + Entry::Vacant(_entry) => { + eprintln!( + "Received a Jitdump path for pid {pid} which I don't have a task for." + ); + } + } + } + Err(MachError::RcvTimedOut) => { + // TODO: give status back via task_sender + } + Err(err) => { + eprintln!("Encountered error while waiting for task port: {err:?}"); + } } } }); diff --git a/samply/src/mac/sampler.rs b/samply/src/mac/sampler.rs index 40550abb..1a6b8e80 100644 --- a/samply/src/mac/sampler.rs +++ b/samply/src/mac/sampler.rs @@ -5,7 +5,7 @@ use fxprof_processed_profile::{ use mach::port::mach_port_t; use std::mem; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::thread; use std::time::SystemTime; use std::time::{Duration, Instant}; @@ -18,6 +18,7 @@ pub struct TaskInit { pub start_time: Instant, pub task: mach_port_t, pub pid: u32, + pub jitdump_path_receiver: Receiver, } pub struct Sampler { @@ -75,6 +76,7 @@ impl Sampler { let root_task = TaskProfiler::new( root_task_init.task, root_task_init.pid, + root_task_init.jitdump_path_receiver, timestamp_maker.make_ts(root_task_init.start_time), &self.command_name, &mut profile, @@ -97,6 +99,7 @@ impl Sampler { let new_task = match TaskProfiler::new( task_init.task, task_init.pid, + task_init.jitdump_path_receiver, timestamp_maker.make_ts(task_init.start_time), &self.command_name, &mut profile, @@ -123,6 +126,7 @@ impl Sampler { let sample_timestamp = timestamp_maker.make_ts(sample_instant); if let Some(task) = &mut live_root_task { + task.check_jitdump(); let still_alive = task.sample(sample_timestamp, &mut unwinder_cache, &mut profile)?; if !still_alive { @@ -134,6 +138,7 @@ impl Sampler { let mut other_tasks = Vec::with_capacity(live_other_tasks.capacity()); mem::swap(&mut live_other_tasks, &mut other_tasks); for mut task in other_tasks.into_iter() { + task.check_jitdump(); let still_alive = task.sample(sample_timestamp, &mut unwinder_cache, &mut profile)?; if still_alive { @@ -155,6 +160,7 @@ impl Sampler { let new_task = TaskProfiler::new( task_init.task, task_init.pid, + task_init.jitdump_path_receiver, timestamp_maker.make_ts(task_init.start_time), &self.command_name, &mut profile, @@ -163,7 +169,7 @@ impl Sampler { .expect("couldn't create TaskProfiler"); live_other_tasks.push(new_task); } else { - println!("All tasks terminated."); + eprintln!("All tasks terminated."); break; } } diff --git a/samply/src/mac/task_profiler.rs b/samply/src/mac/task_profiler.rs index 3d502641..0c03b5e3 100644 --- a/samply/src/mac/task_profiler.rs +++ b/samply/src/mac/task_profiler.rs @@ -1,3 +1,4 @@ +use crossbeam_channel::Receiver; use framehop::{ CacheNative, MayAllocateDuringUnwind, Module, ModuleUnwindData, TextByteData, Unwinder, UnwinderNative, @@ -6,6 +7,7 @@ use fxprof_processed_profile::debugid::DebugId; use fxprof_processed_profile::{ CategoryPairHandle, LibraryInfo, ProcessHandle, Profile, Timestamp, }; +use linux_perf_data::jitdump::JitDumpReader; use mach::mach_types::thread_act_port_array_t; use mach::mach_types::thread_act_t; use mach::message::mach_msg_type_number_t; @@ -19,10 +21,10 @@ use samply_symbols::{object, DebugIdExt}; use wholesym::samply_symbols; use std::collections::hash_map::Entry; -use std::collections::{HashMap, HashSet}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::mem; use std::ops::Deref; -use std::path::Path; +use std::path::{Path, PathBuf}; use super::error::SamplingError; use super::kernel_error::{IntoResult, KernelError}; @@ -86,12 +88,16 @@ pub struct TaskProfiler { ignored_errors: Vec, unwinder: UnwinderNative, default_category: CategoryPairHandle, + jitdump_path_receiver: Receiver, + pending_jitdump_paths: VecDeque, + jitdumps: VecDeque>, } impl TaskProfiler { pub fn new( task: mach_port_t, pid: u32, + jitdump_path_receiver: Receiver, start_time: Timestamp, command_name: &str, profile: &mut Profile, @@ -122,6 +128,9 @@ impl TaskProfiler { ignored_errors: Vec::new(), unwinder: UnwinderNative::new(), default_category, + jitdump_path_receiver, + pending_jitdump_paths: Default::default(), + jitdumps: Default::default(), }) } @@ -322,6 +331,25 @@ impl TaskProfiler { self.unwinder.add_module(module); } + pub fn check_jitdump(&mut self) { + while let Ok(jitdump_path) = self.jitdump_path_receiver.try_recv() { + self.pending_jitdump_paths.push_back(jitdump_path); + } + + self.pending_jitdump_paths.retain_mut(|path| { + fn jitdump_for_path(path: &Path) -> Option> { + JitDumpReader::new(std::fs::File::open(path).ok()?).ok() + } + let Some(reader) = jitdump_for_path(path) else { return true }; + self.jitdumps.push_back(reader); + false // "Do not retain", i.e. remove from pending_jitdump_paths + }); + + for _jitdump in &mut self.jitdumps { + // TODO + } + } + pub fn notify_dead(&mut self, end_time: Timestamp, profile: &mut Profile) { for (_, mut thread) in self.live_threads.drain() { thread.notify_dead(end_time, profile);