stackpulse 0.10.0

Linux perf_event stack sampling with native unwinding, symbolization, and compact spooling
Documentation
use std::{fs, io};

pub(super) fn online_cpu_ids() -> io::Result<Vec<u32>> {
    if let Some(ids) = fs::read_to_string("/sys/devices/system/cpu/online")
        .ok()
        .and_then(|list| parse_cpu_list(list.trim()))
    {
        return Ok(ids);
    }

    let stat = fs::read_to_string("/proc/stat")?;
    parse_proc_stat_cpu_ids(&stat).ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidData,
            "no online CPU IDs found in sysfs or /proc/stat",
        )
    })
}

pub(super) fn parse_cpu_list(list: &str) -> Option<Vec<u32>> {
    let cpus = list
        .split(',')
        .map(str::trim)
        .filter(|part| !part.is_empty())
        .try_fold(Vec::new(), |mut cpus, part| {
            if let Some((start, end)) = part.split_once('-') {
                let start = start.parse::<u32>().ok()?;
                let end = end.parse::<u32>().ok()?;
                if start > end {
                    return None;
                }
                cpus.extend(start..=end);
            } else {
                cpus.push(part.parse::<u32>().ok()?);
            }
            Some(cpus)
        })?;
    (!cpus.is_empty()).then_some(cpus)
}

fn parse_proc_stat_cpu_ids(stat: &str) -> Option<Vec<u32>> {
    let mut ids: Vec<_> = stat
        .lines()
        .filter_map(|line| line.split_ascii_whitespace().next())
        .filter_map(|label| label.strip_prefix("cpu"))
        .filter(|suffix| !suffix.is_empty() && suffix.bytes().all(|byte| byte.is_ascii_digit()))
        .filter_map(|suffix| suffix.parse().ok())
        .collect();
    ids.sort_unstable();
    ids.dedup();
    (!ids.is_empty()).then_some(ids)
}

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

    #[test]
    fn parse_cpu_list_rejects_empty_and_invalid_lists() {
        assert_eq!(parse_cpu_list(""), None);
        assert_eq!(parse_cpu_list(" , "), None);
        assert_eq!(parse_cpu_list("0,nope"), None);
        assert_eq!(parse_cpu_list("4-3"), None);
    }

    #[test]
    fn proc_stat_cpu_ids_preserve_sparse_kernel_labels() {
        let stat = "cpu  10 20 30 40\ncpu2 1 2 3 4\ncpu17 5 6 7 8\ncpu2 9 9 9 9\n\
                    cpufreq 0 0 0 0\nintr 123\n";

        assert_eq!(parse_proc_stat_cpu_ids(stat), Some(vec![2, 17]));
        assert_eq!(parse_proc_stat_cpu_ids("cpu 1 2 3 4\nintr 5\n"), None);
    }
}