use super::{
CpuFrequencySampleV2, HardwareCounterSampleV2, HardwareFieldSourceV2, SchedulerSampleV2,
SourcedEvidenceV2, SpanCounterReading, ThreadCpuV2, TopologyEvidenceV2,
HARDWARE_EVIDENCE_V2_VERSION,
};
use crate::collector::{CollectorAvailability, CollectorCapability, CollectorId};
use crate::schema_v2::{Evidence, EvidenceGap};
use std::ffi::{c_char, c_int, c_void};
pub(super) const COUNTER_SOURCE: HardwareFieldSourceV2 = HardwareFieldSourceV2::MacOsApi;
pub(super) const MEMORY_STALL_GAP: EvidenceGap = EvidenceGap::Unsupported;
const TASK_EVENTS_INFO: c_int = 2;
const TASK_EVENTS_INFO_COUNT: u32 = 8;
const THREAD_BASIC_INFO: c_int = 3;
const THREAD_BASIC_INFO_COUNT: u32 = 10;
const KERN_SUCCESS: c_int = 0;
type MachPort = u32;
extern "C" {
fn mach_task_self() -> MachPort;
fn task_info(task: MachPort, flavor: c_int, info: *mut c_int, count: *mut u32) -> c_int;
fn task_threads(task: MachPort, threads: *mut *mut MachPort, count: *mut u32) -> c_int;
fn thread_info(thread: MachPort, flavor: c_int, info: *mut c_int, count: *mut u32) -> c_int;
fn vm_deallocate(task: MachPort, address: usize, size: usize) -> c_int;
fn mach_port_deallocate(task: MachPort, name: MachPort) -> c_int;
fn sysctlbyname(
name: *const c_char,
oldp: *mut c_void,
oldlenp: *mut usize,
newp: *const c_void,
newlen: usize,
) -> c_int;
}
fn task_context_switches() -> Option<u64> {
let mut info: [c_int; TASK_EVENTS_INFO_COUNT as usize] = [0; TASK_EVENTS_INFO_COUNT as usize];
let mut count = TASK_EVENTS_INFO_COUNT;
let result = unsafe {
task_info(
mach_task_self(),
TASK_EVENTS_INFO,
info.as_mut_ptr(),
&mut count,
)
};
(result == KERN_SUCCESS).then(|| info[7] as u64)
}
fn thread_cpu_ns(port: MachPort) -> Option<u64> {
let mut info: [c_int; THREAD_BASIC_INFO_COUNT as usize] = [0; THREAD_BASIC_INFO_COUNT as usize];
let mut count = THREAD_BASIC_INFO_COUNT;
let result = unsafe { thread_info(port, THREAD_BASIC_INFO, info.as_mut_ptr(), &mut count) };
if result != KERN_SUCCESS {
return None;
}
let user_ns = (info[0] as u64) * 1_000_000_000 + (info[1] as u64) * 1_000;
let system_ns = (info[2] as u64) * 1_000_000_000 + (info[3] as u64) * 1_000;
Some(user_ns.saturating_add(system_ns))
}
fn sysctl_u64(name: &'static [u8]) -> Option<u64> {
let mut value = 0_u64;
let mut size = std::mem::size_of::<u64>();
let result = unsafe {
sysctlbyname(
name.as_ptr().cast(),
std::ptr::addr_of_mut!(value).cast(),
&mut size,
std::ptr::null(),
0,
)
};
(result == 0).then_some(value)
}
fn unsupported(detail: &'static str, collector: CollectorId) -> CollectorCapability {
CollectorCapability::unavailable(collector, CollectorAvailability::Unsupported, detail)
}
pub(super) struct CounterState;
pub(super) struct SchedulerState;
pub(super) struct PlatformCollectors {
pub counter_capability: CollectorCapability,
pub scheduler_capability: CollectorCapability,
pub utilization_capability: CollectorCapability,
pub topology_capability: CollectorCapability,
pub counters: CounterState,
pub scheduler: SchedulerState,
}
pub(super) fn platform_collectors() -> PlatformCollectors {
PlatformCollectors {
counter_capability: unsupported(
"macOS exposes no public per-thread PMU counters; kpc is a private framework",
CollectorId::HardwareCounters,
),
scheduler_capability: if task_context_switches().is_some() {
let mut capability = CollectorCapability::available(CollectorId::SchedulerActivity);
capability.detail = Some(
"total context switches via task_info(TASK_EVENTS_INFO); voluntary split, migrations, and runqueue delay are not exposed by macOS"
.to_owned(),
);
capability
} else {
CollectorCapability::unavailable(
CollectorId::SchedulerActivity,
CollectorAvailability::Unavailable,
"task_info(TASK_EVENTS_INFO) failed for the current task",
)
},
utilization_capability: CollectorCapability::available(CollectorId::ThreadUtilization),
topology_capability: CollectorCapability::available(CollectorId::CpuTopology),
counters: CounterState,
scheduler: SchedulerState,
}
}
fn pmu_gap() -> SourcedEvidenceV2<u64> {
SourcedEvidenceV2::gapped(HardwareFieldSourceV2::MacOsApi, EvidenceGap::Unsupported)
}
pub(super) fn sample_counters(
_state: &mut CounterState,
elapsed_ns: u64,
) -> HardwareCounterSampleV2 {
HardwareCounterSampleV2 {
version: HARDWARE_EVIDENCE_V2_VERSION,
elapsed_ns,
cycles: pmu_gap(),
instructions: pmu_gap(),
cache_references: pmu_gap(),
cache_misses: pmu_gap(),
branch_instructions: pmu_gap(),
branch_misses: pmu_gap(),
stalled_cycles_frontend: pmu_gap(),
stalled_cycles_backend: pmu_gap(),
stalled_cycles_memory: pmu_gap(),
}
}
pub(super) fn empty_scheduler_sample() -> SchedulerSampleV2 {
sample_scheduler(&mut SchedulerState)
}
pub(super) fn sample_scheduler(_state: &mut SchedulerState) -> SchedulerSampleV2 {
let mach_gap =
|| SourcedEvidenceV2::gapped(HardwareFieldSourceV2::MacOsApi, EvidenceGap::Unsupported);
SchedulerSampleV2 {
version: HARDWARE_EVIDENCE_V2_VERSION,
voluntary_context_switches: mach_gap(),
involuntary_context_switches: mach_gap(),
total_context_switches: match task_context_switches() {
Some(switches) => {
SourcedEvidenceV2::recorded(switches, HardwareFieldSourceV2::MacOsApi)
}
None => {
SourcedEvidenceV2::gapped(HardwareFieldSourceV2::MacOsApi, EvidenceGap::Unavailable)
}
},
cpu_migrations: mach_gap(),
runqueue_delay_ns: mach_gap(),
timeslices: mach_gap(),
}
}
pub(super) fn sample_thread_utilization() -> (Vec<ThreadCpuV2>, u64) {
let mut threads = Vec::new();
let mut dropped = 0_u64;
let mut ports: *mut MachPort = std::ptr::null_mut();
let mut count = 0_u32;
let result = unsafe { task_threads(mach_task_self(), &mut ports, &mut count) };
if result != KERN_SUCCESS || ports.is_null() {
return (threads, dropped);
}
for index in 0..count as usize {
let port = unsafe { *ports.add(index) };
match thread_cpu_ns(port) {
Some(cpu_time_ns) => threads.push(ThreadCpuV2 {
version: HARDWARE_EVIDENCE_V2_VERSION,
thread_id: u64::from(port),
cpu_time_ns,
}),
None => dropped = dropped.saturating_add(1),
}
unsafe {
mach_port_deallocate(mach_task_self(), port);
}
}
unsafe {
vm_deallocate(
mach_task_self(),
ports as usize,
count as usize * std::mem::size_of::<MachPort>(),
);
}
threads.sort_unstable_by_key(|thread| thread.thread_id);
(threads, dropped)
}
pub(super) fn sample_frequency(_elapsed_ns: u64) -> Option<CpuFrequencySampleV2> {
None
}
pub(super) fn frequency_availability() -> Evidence<HardwareFieldSourceV2> {
Evidence::unavailable(EvidenceGap::Unsupported)
}
pub(super) fn capture_topology() -> TopologyEvidenceV2 {
let logical_cpus = sysctl_u64(b"hw.ncpu\0")
.and_then(|value| u32::try_from(value).ok())
.or_else(|| {
std::thread::available_parallelism()
.ok()
.map(|count| count.get() as u32)
})
.unwrap_or(1);
let sysctl_u32 = |name: &'static [u8]| {
sysctl_u64(name)
.and_then(|value| u32::try_from(value).ok())
.filter(|value| *value > 0)
};
let sourced = |value: Option<u32>| match value {
Some(value) => SourcedEvidenceV2::recorded(value, HardwareFieldSourceV2::MacOsApi),
None => {
SourcedEvidenceV2::gapped(HardwareFieldSourceV2::MacOsApi, EvidenceGap::Unsupported)
}
};
TopologyEvidenceV2 {
version: HARDWARE_EVIDENCE_V2_VERSION,
logical_cpus,
physical_cores: sourced(sysctl_u32(b"hw.physicalcpu\0")),
packages: sourced(sysctl_u32(b"hw.packages\0")),
numa_nodes: sourced(None),
affinity_cpus: sourced(None),
cpu_quota_milli: SourcedEvidenceV2::gapped(
HardwareFieldSourceV2::MacOsApi,
EvidenceGap::Unsupported,
),
}
}
pub(crate) fn span_counter_reading() -> Option<SpanCounterReading> {
None
}