amphetamine 0.1.1

Reclaim memory and win scheduler contention on Apple Silicon, safely.
//! Machine state read straight from mach and sysctl.

use anyhow::{Result, anyhow};
use std::ffi::c_void;
use std::mem::{size_of, zeroed};
use std::process::Command;

const HOST_VM_INFO64: libc::c_int = 4;

unsafe extern "C" {
    fn host_statistics64(
        host: libc::mach_port_t,
        flavor: libc::c_int,
        info: *mut libc::c_int,
        count: *mut libc::c_uint,
    ) -> libc::c_int;
    fn mach_host_self() -> libc::mach_port_t;
}

/// `struct xsw_usage` from `<sys/sysctl.h>`.
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct XswUsage {
    total: u64,
    avail: u64,
    used: u64,
    pagesize: u32,
    encrypted: i32,
}

#[derive(Debug, Clone, Default)]
pub struct Memory {
    pub total: u64,
    pub free: u64,
    pub active: u64,
    pub inactive: u64,
    pub wired: u64,
    pub compressed: u64,
    pub purgeable: u64,
    /// Pages faulted back in from compression or swap since boot. A large and
    /// growing value is the signature of a machine that has been thrashing.
    pub decompressions: u64,
}

impl Memory {
    /// What macOS' own Memory Pressure graph approximates: everything that is
    /// not immediately reclaimable.
    pub fn footprint(&self) -> u64 {
        self.active + self.wired + self.compressed
    }

    /// Memory the kernel can hand back without anything exiting: page cache
    /// that has not been touched lately, plus explicitly discardable pages.
    pub fn reclaimable(&self) -> u64 {
        self.inactive + self.purgeable
    }

    pub fn pressure_pct(&self) -> f64 {
        if self.total == 0 {
            return 0.0;
        }
        self.footprint() as f64 / self.total as f64 * 100.0
    }
}

#[derive(Debug, Clone, Default)]
pub struct Swap {
    pub total: u64,
    pub used: u64,
}

impl Swap {
    pub fn used_pct(&self) -> f64 {
        if self.total == 0 {
            return 0.0;
        }
        self.used as f64 / self.total as f64 * 100.0
    }
}

/// Apple Silicon's only user-facing performance selector. There is no clock
/// control below this — the variants are the whole knob.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PowerMode {
    Low,
    Automatic,
    High,
    Unknown,
}

impl std::fmt::Display for PowerMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Self::Low => "Low Power",
            Self::Automatic => "Automatic",
            Self::High => "High Power",
            Self::Unknown => "unknown",
        })
    }
}

#[derive(Debug, Clone)]
pub struct Power {
    pub mode: PowerMode,
    /// True when the OS has recorded a thermal or CPU-power limit. This is the
    /// only condition under which clock speed is actually being held back.
    pub throttled: bool,
    pub thermal_note: String,
}

#[derive(Debug, Clone)]
pub struct Snapshot {
    pub memory: Memory,
    pub swap: Swap,
    pub power: Power,
}

pub fn snapshot() -> Result<Snapshot> {
    Ok(Snapshot {
        memory: memory()?,
        swap: swap()?,
        power: power(),
    })
}

fn sysctl_by_name<T: Copy + Default>(name: &str) -> Result<T> {
    let cname = std::ffi::CString::new(name)?;
    let mut out = T::default();
    let mut len = size_of::<T>();
    let rc = unsafe {
        libc::sysctlbyname(
            cname.as_ptr(),
            &mut out as *mut T as *mut c_void,
            &mut len,
            std::ptr::null_mut(),
            0,
        )
    };
    if rc != 0 {
        return Err(anyhow!(
            "sysctl {name}: {}",
            std::io::Error::last_os_error()
        ));
    }
    Ok(out)
}

pub fn memory() -> Result<Memory> {
    let total: u64 = sysctl_by_name("hw.memsize")?;
    let page: u64 = sysctl_by_name::<u32>("hw.pagesize")? as u64;

    let mut vm: libc::vm_statistics64 = unsafe { zeroed() };
    let mut count = (size_of::<libc::vm_statistics64>() / size_of::<libc::c_int>()) as libc::c_uint;
    let rc = unsafe {
        host_statistics64(
            mach_host_self(),
            HOST_VM_INFO64,
            &mut vm as *mut _ as *mut libc::c_int,
            &mut count,
        )
    };
    if rc != 0 {
        return Err(anyhow!("host_statistics64 failed: {rc}"));
    }

    Ok(Memory {
        total,
        // Speculative pages are read-ahead the kernel will drop on demand, so
        // they count as free rather than as consumption.
        free: (vm.free_count as u64 + vm.speculative_count as u64) * page,
        active: vm.active_count as u64 * page,
        inactive: vm.inactive_count as u64 * page,
        wired: vm.wire_count as u64 * page,
        compressed: vm.compressor_page_count as u64 * page,
        purgeable: vm.purgeable_count as u64 * page,
        decompressions: vm.decompressions as u64,
    })
}

pub fn swap() -> Result<Swap> {
    let u: XswUsage = sysctl_by_name("vm.swapusage")?;
    Ok(Swap {
        total: u.total,
        used: u.used,
    })
}

/// `pmset` is the supported interface for these values and there is no stable
/// C API for the power mode, so this shells out — read-only, no privileges.
pub fn power() -> Power {
    let mode = Command::new("pmset")
        .arg("-g")
        .output()
        .ok()
        .and_then(|o| {
            String::from_utf8_lossy(&o.stdout)
                .lines()
                .find_map(|l| {
                    l.split_whitespace()
                        .nth(1)
                        .filter(|_| l.contains("powermode"))
                })
                .map(str::to_owned)
        })
        .map_or(PowerMode::Unknown, |v| match v.trim() {
            "0" => PowerMode::Automatic,
            "1" => PowerMode::Low,
            "2" => PowerMode::High,
            _ => PowerMode::Unknown,
        });

    let therm = Command::new("pmset")
        .args(["-g", "therm"])
        .output()
        .map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
        .unwrap_or_default();

    // pmset reports "No thermal warning level has been recorded" when clean;
    // any recorded level means something is actively limiting the CPU.
    let throttled = therm.contains("CPU_Speed_Limit")
        || (therm.contains("warning level") && !therm.contains("No thermal warning"));

    Power {
        mode,
        throttled,
        thermal_note: if throttled {
            therm
                .lines()
                .find(|l| l.contains("Limit"))
                .unwrap_or("thermal limit recorded")
                .trim()
                .to_owned()
        } else {
            "no thermal or CPU-power limit recorded".to_owned()
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn memory_reads_are_coherent() {
        let m = memory().expect("host_statistics64 should work on macOS");
        assert!(m.total > 0);
        // Every bucket must fit inside physical RAM; a unit or page-size error
        // would blow past this immediately.
        assert!(m.footprint() <= m.total, "footprint exceeds physical RAM");
        assert!(m.free <= m.total);
        assert!((0.0..=100.0).contains(&m.pressure_pct()));
    }

    #[test]
    fn swap_accounting_adds_up() {
        let s = swap().expect("vm.swapusage should be readable");
        assert!(s.used <= s.total);
        assert!((0.0..=100.0).contains(&s.used_pct()));
    }
}