hotpath 0.15.0

Simple async Rust profiler with memory and data-flow insights - quickly find and debug performance bottlenecks.
Documentation
//! macOS Mach kernel FFI for thread metrics collection

use super::ThreadMetrics;
use std::mem;

// Mach kernel types and constants (using C naming conventions)
#[allow(non_camel_case_types)]
mod types {
    pub(crate) type kern_return_t = libc::c_int;
    pub(crate) type mach_port_t = libc::c_uint;
    pub(crate) type thread_act_t = mach_port_t;
    pub(crate) type thread_act_array_t = *mut thread_act_t;
    pub(crate) type mach_msg_type_number_t = libc::c_uint;
    pub(crate) type integer_t = libc::c_int;
}

use types::*;

const KERN_SUCCESS: kern_return_t = 0;
const THREAD_BASIC_INFO: libc::c_int = 3;

// Mach thread run states (from <mach/thread_info.h>)
const TH_STATE_RUNNING: i32 = 1;
const TH_STATE_STOPPED: i32 = 2;
const TH_STATE_WAITING: i32 = 3;
const TH_STATE_UNINTERRUPTIBLE: i32 = 4;
const TH_STATE_HALTED: i32 = 5;

fn run_state_to_status(state: i32) -> (String, String) {
    match state {
        TH_STATE_RUNNING => ("Running ".to_string(), "1".to_string()),
        TH_STATE_STOPPED => ("Stopped".to_string(), "2".to_string()),
        TH_STATE_WAITING => ("Sleeping".to_string(), "3".to_string()),
        TH_STATE_UNINTERRUPTIBLE => ("Blocked ".to_string(), "4".to_string()),
        TH_STATE_HALTED => ("Halted  ".to_string(), "5".to_string()),
        _ => ("Unknown".to_string(), state.to_string()),
    }
}

#[allow(non_camel_case_types)]
#[repr(C)]
#[derive(Debug, Copy, Clone)]
struct time_value_t {
    seconds: integer_t,
    microseconds: integer_t,
}

#[allow(non_camel_case_types)]
#[repr(C)]
#[derive(Debug, Copy, Clone)]
struct thread_basic_info {
    user_time: time_value_t,
    system_time: time_value_t,
    cpu_usage: integer_t,
    policy: integer_t,
    run_state: integer_t,
    flags: integer_t,
    suspend_count: integer_t,
    sleep_time: integer_t,
}

extern "C" {
    fn mach_task_self() -> mach_port_t;

    fn task_threads(
        target_task: mach_port_t,
        act_list: *mut thread_act_array_t,
        act_list_cnt: *mut mach_msg_type_number_t,
    ) -> kern_return_t;

    fn thread_info(
        target_act: thread_act_t,
        flavor: libc::c_int,
        thread_info_out: *mut integer_t,
        thread_info_outCnt: *mut mach_msg_type_number_t,
    ) -> kern_return_t;

    fn mach_vm_deallocate(target: mach_port_t, address: u64, size: u64) -> kern_return_t;

    fn pthread_from_mach_thread_np(thread: mach_port_t) -> libc::pthread_t;
}

/// Collect per-thread CPU usage metrics for the current process on macOS
pub(crate) fn collect_thread_metrics() -> Result<Vec<ThreadMetrics>, String> {
    unsafe {
        let task = mach_task_self();
        let mut thread_list: thread_act_array_t = std::ptr::null_mut();
        let mut thread_count: mach_msg_type_number_t = 0;

        // Get list of all threads in the current task
        let kr = task_threads(task, &mut thread_list, &mut thread_count);
        if kr != KERN_SUCCESS {
            return Err(format!("task_threads failed with code: {}", kr));
        }

        let mut metrics = Vec::new();

        for i in 0..thread_count {
            let thread = *thread_list.offset(i as isize);

            match get_thread_info(thread, i as u64) {
                Ok(metric) => metrics.push(metric),
                Err(_) => {
                    // Thread may have exited between listing and querying - this is normal
                }
            }
        }

        let vm_size = (thread_count as usize * mem::size_of::<mach_port_t>()) as u64;
        mach_vm_deallocate(task, thread_list as u64, vm_size);

        Ok(metrics)
    }
}

unsafe fn get_thread_info(thread: thread_act_t, index: u64) -> Result<ThreadMetrics, String> {
    let mut thread_info_data: thread_basic_info = mem::zeroed();
    let mut thread_info_count = (mem::size_of::<thread_basic_info>() / mem::size_of::<integer_t>())
        as mach_msg_type_number_t;

    let kr = thread_info(
        thread,
        THREAD_BASIC_INFO,
        &mut thread_info_data as *mut _ as *mut integer_t,
        &mut thread_info_count,
    );

    if kr != KERN_SUCCESS {
        return Err(format!("thread_info failed with code: {}", kr));
    }

    let os_tid = thread as u64;
    let name = get_thread_name(thread).unwrap_or_else(|| format!("thread_{}", index));
    let (status, status_code) = run_state_to_status(thread_info_data.run_state);
    let cpu_user = thread_info_data.user_time.seconds as f64
        + (thread_info_data.user_time.microseconds as f64 / 1_000_000.0);
    let cpu_sys = thread_info_data.system_time.seconds as f64
        + (thread_info_data.system_time.microseconds as f64 / 1_000_000.0);

    Ok(super::ThreadMetrics::new(
        os_tid,
        name,
        status,
        status_code,
        cpu_user,
        cpu_sys,
    ))
}

unsafe fn get_thread_name(thread: thread_act_t) -> Option<String> {
    let pthread = pthread_from_mach_thread_np(thread);

    if pthread == 0 {
        return None;
    }

    // Try to get pthread name via libc
    let mut name_buf = [0i8; 256];
    if libc::pthread_getname_np(pthread, name_buf.as_mut_ptr(), name_buf.len()) == 0 {
        // Find the null terminator
        let len = name_buf.iter().position(|&c| c == 0).unwrap_or(0);
        if len > 0 {
            let name_bytes: Vec<u8> = name_buf[..len].iter().map(|&c| c as u8).collect();
            if let Ok(name) = String::from_utf8(name_bytes) {
                if !name.is_empty() {
                    return Some(name);
                }
            }
        }
    }

    None
}

/// Get the RSS (Resident Set Size) of the current process in bytes
pub(crate) fn get_rss_bytes() -> Option<u64> {
    // Use rusage to get RSS - this is the most reliable cross-platform approach
    unsafe {
        let mut rusage: libc::rusage = std::mem::zeroed();
        if libc::getrusage(libc::RUSAGE_SELF, &mut rusage) == 0 {
            // On macOS, ru_maxrss is in bytes
            Some(rusage.ru_maxrss as u64)
        } else {
            None
        }
    }
}

#[cfg(all(test, target_os = "macos"))]
mod tests {
    use super::*;
    use std::time::Duration;

    #[test]
    fn macos_thread_metrics_smoke_test() {
        let metrics = collect_thread_metrics().expect("collect_thread_metrics should succeed");
        assert!(!metrics.is_empty());

        for m in &metrics {
            assert_ne!(m.os_tid, 0, "os_tid should not be zero");

            assert!(
                m.cpu_user >= 0.0,
                "cpu_user should be non-negative, got {}",
                m.cpu_user
            );
            assert!(
                m.cpu_sys >= 0.0,
                "cpu_sys should be non-negative, got {}",
                m.cpu_sys
            );
            assert!(
                m.cpu_total >= 0.0,
                "cpu_total should be non-negative, got {}",
                m.cpu_total
            );
        }

        std::thread::sleep(Duration::from_millis(10));

        let metrics2 =
            collect_thread_metrics().expect("second collect_thread_metrics should succeed");

        if !metrics.is_empty() && !metrics2.is_empty() {
            use std::collections::HashMap;

            let mut first_map = HashMap::new();
            for m in &metrics {
                first_map.insert(m.os_tid, m.cpu_total);
            }

            for m in &metrics2 {
                if let Some(first_total) = first_map.get(&m.os_tid) {
                    let delta = m.cpu_total - first_total;
                    assert!(
                        delta > -0.1,
                        "cpu_total went backwards too much for tid {}: {} -> {} (Δ={})",
                        m.os_tid,
                        first_total,
                        m.cpu_total,
                        delta
                    );
                }
            }
        }
    }
}