//! The readers a sink's decode table is measured with: energy-measurement §9's profiles.
//!
//! Each reader is a cumulative energy [`Counter`] in nanojoules with the profile it reads
//! under. None takes a build dependency: the platform's own libraries are bound at run
//! time, so a reader that is not on this platform, or not readable by this process, is
//! reported with its status ([`CounterStatus`]) rather than failing the build.
//!
//! - `macos-task` ([`TaskEnergy`]): `proc_pid_rusage` at `RUSAGE_INFO_V6`, for this process
//!   and every child process it registers ([`Children`]): `ri_energy_nj + ri_billed_energy −
//!   ri_serviced_energy`. A child is read while it runs, and once more after it exits and
//!   before it is reaped ([`Children::run`]), since a reaped child's energy enters no counter.
//! - `macos-gpu` ([`IoReportGpu`]): IOReport, group `Energy Model`, channel `GPU Energy`, as
//!   the difference of a sample from the one taken when the reader opened.
//! - `linux-rapl` ([`Rapl`]): the powercap package zones' `energy_uj`, summed, across each
//!   zone's wrap.
//! - `linux-nvml` ([`Nvml`]): `nvmlDeviceGetTotalEnergyConsumption` of device 0, surveyed for
//!   plausibility against the device's own power reading and enforced limit.
//!
//! [`survey`] reads every reader this platform has at the table's sampling interval and gives
//! each its status under energy-measurement §9.1.

use std::ffi::{c_char, c_int, c_void, CString};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

/// energy-measurement §9.1's statuses, and `implausible` (§9.3).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CounterStatus {
    Live,
    Frozen,
    NeedsPrivilege,
    NoCounter,
    NotImplemented,
    Implausible,
}

impl CounterStatus {
    pub fn label(self) -> &'static str {
        match self {
            CounterStatus::Live => "live",
            CounterStatus::Frozen => "frozen",
            CounterStatus::NeedsPrivilege => "needs-privilege",
            CounterStatus::NoCounter => "no-counter",
            CounterStatus::NotImplemented => "not-implemented",
            CounterStatus::Implausible => "implausible",
        }
    }
}

/// A cumulative energy counter, in nanojoules.
pub trait Counter: Send {
    /// The profile it reads under (energy-measurement §9).
    fn profile(&self) -> &'static str;
    /// What it reads, for the table's survey.
    fn detail(&self) -> String;
    /// The counter now; `None` when a read fails.
    fn read_nj(&mut self) -> Option<u64>;
    /// Where it wraps, in nanojoules (`None`: it does not wrap in a run's span).
    fn wrap_nj(&self) -> Option<u64> {
        None
    }
}

/// One interval of a bracket (sink-energy §4.3): the counter's advance in nanojoules across
/// its wrap, and the interval's length in microseconds.
pub fn advance(c0: u64, c1: u64, wrap: Option<u64>) -> u64 {
    if c1 >= c0 {
        c1 - c0
    } else {
        wrap.map(|w| w.saturating_sub(c0).saturating_add(c1)).unwrap_or(0)
    }
}

unsafe extern "C" {
    fn dlopen(path: *const c_char, mode: c_int) -> *mut c_void;
    fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
}

const RTLD_NOW: c_int = 2;

fn open_lib(path: &str) -> Option<*mut c_void> {
    let p = CString::new(path).ok()?;
    let h = unsafe { dlopen(p.as_ptr(), RTLD_NOW) };
    (!h.is_null()).then_some(h)
}

fn sym(h: *mut c_void, name: &str) -> Option<*mut c_void> {
    let n = CString::new(name).ok()?;
    let s = unsafe { dlsym(h, n.as_ptr()) };
    (!s.is_null()).then_some(s)
}

// ---------------------------------------------------------------------------------------
// macos-task
// ---------------------------------------------------------------------------------------

/// The processes a [`TaskEnergy`] reader sums besides its own: the children running now
/// with their last reading, and the readings of those that have exited.
#[derive(Default)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub struct Ledger {
    live: Vec<(i32, u64)>,
    finished_nj: u64,
}

/// A ledger shared between the reader and the workers that run children.
#[derive(Clone, Default)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub struct Children(Arc<Mutex<Ledger>>);

#[cfg(target_os = "macos")]
mod task {
    use super::*;

    unsafe extern "C" {
        fn proc_pid_rusage(pid: c_int, flavor: c_int, buffer: *mut c_void) -> c_int;
        fn getpid() -> c_int;
        fn waitid(idtype: c_int, id: u32, infop: *mut c_void, options: c_int) -> c_int;
    }
    const RUSAGE_INFO_V6: c_int = 6;
    const P_PID: c_int = 1;
    const WEXITED: c_int = 0x4;
    const WNOWAIT: c_int = 0x20;
    // Byte offsets in `struct rusage_info_v6`.
    const BILLED: usize = 264;
    const SERVICED: usize = 272;
    const ENERGY: usize = 336;

    /// `ri_energy_nj + ri_billed_energy − ri_serviced_energy` of `pid`, in nanojoules.
    pub fn task_nj(pid: i32) -> Option<u64> {
        let mut buf = [0u64; 128];
        let r = unsafe { proc_pid_rusage(pid, RUSAGE_INFO_V6, buf.as_mut_ptr().cast()) };
        if r != 0 {
            return None;
        }
        let at = |off: usize| buf[off / 8];
        Some((at(ENERGY) + at(BILLED)).saturating_sub(at(SERVICED)))
    }

    pub fn own_pid() -> i32 {
        unsafe { getpid() }
    }

    /// Wait for `pid` to exit without reaping it.
    pub fn wait_unreaped(pid: i32) -> bool {
        let mut info = [0u8; 256];
        unsafe { waitid(P_PID, pid as u32, info.as_mut_ptr().cast(), WEXITED | WNOWAIT) == 0 }
    }
}

impl Children {
    /// Run `cmd` as a child the ledger counts: registered as it starts, read once more
    /// after it exits and before it is reaped. Its exit status, or `None` if it could not
    /// run or its exit could not be read.
    #[cfg(target_os = "macos")]
    pub fn run(&self, cmd: &mut std::process::Command) -> Option<std::process::ExitStatus> {
        let mut child = cmd.spawn().ok()?;
        let pid = child.id() as i32;
        self.0.lock().unwrap().live.push((pid, 0));
        let exited = task::wait_unreaped(pid);
        let last = task::task_nj(pid);
        {
            let mut l = self.0.lock().unwrap();
            let held = l.live.iter().position(|x| x.0 == pid).map(|i| l.live.remove(i).1).unwrap_or(0);
            l.finished_nj += last.unwrap_or(held).max(held);
        }
        let status = child.wait().ok();
        status.filter(|_| exited)
    }

    #[cfg(not(target_os = "macos"))]
    pub fn run(&self, cmd: &mut std::process::Command) -> Option<std::process::ExitStatus> {
        cmd.status().ok()
    }
}

/// `macos-task`: this process's energy and its registered children's.
pub struct TaskEnergy {
    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
    children: Children,
}

impl TaskEnergy {
    /// `None` off macOS, or where the counter cannot be read.
    pub fn open(children: Children) -> Option<TaskEnergy> {
        #[cfg(target_os = "macos")]
        {
            task::task_nj(task::own_pid())?;
            Some(TaskEnergy { children })
        }
        #[cfg(not(target_os = "macos"))]
        {
            let _ = children;
            None
        }
    }
}

impl Counter for TaskEnergy {
    fn profile(&self) -> &'static str {
        "macos-task"
    }
    fn detail(&self) -> String {
        "proc_pid_rusage RUSAGE_INFO_V6: ri_energy_nj + ri_billed_energy - ri_serviced_energy, this process and its children".into()
    }
    fn read_nj(&mut self) -> Option<u64> {
        #[cfg(target_os = "macos")]
        {
            let own = task::task_nj(task::own_pid())?;
            let mut l = self.children.0.lock().unwrap();
            for c in l.live.iter_mut() {
                if let Some(v) = task::task_nj(c.0) {
                    c.1 = c.1.max(v);
                }
            }
            Some(own + l.finished_nj + l.live.iter().map(|c| c.1).sum::<u64>())
        }
        #[cfg(not(target_os = "macos"))]
        {
            None
        }
    }
}

// ---------------------------------------------------------------------------------------
// macos-gpu
// ---------------------------------------------------------------------------------------

type Cf = *const c_void;

/// `macos-gpu`: IOReport's `GPU Energy` channel.
pub struct IoReportGpu {
    f: IoFns,
    sub: Cf,
    chans: Cf,
    first: Cf,
    key: Cf,
}

// The reader is used from one thread at a time; the CF objects it holds are its own.
unsafe impl Send for IoReportGpu {}

#[derive(Clone, Copy)]
#[allow(clippy::type_complexity)]
struct IoFns {
    cfstr: unsafe extern "C" fn(Cf, *const c_char, u32) -> Cf,
    cfstr_get: unsafe extern "C" fn(Cf, *mut c_char, isize, u32) -> bool,
    dict_get: unsafe extern "C" fn(Cf, Cf) -> Cf,
    dict_count: unsafe extern "C" fn(Cf) -> isize,
    dict_mut_copy: unsafe extern "C" fn(Cf, isize, Cf) -> Cf,
    arr_count: unsafe extern "C" fn(Cf) -> isize,
    arr_get: unsafe extern "C" fn(Cf, isize) -> Cf,
    release: unsafe extern "C" fn(Cf),
    copy_group: unsafe extern "C" fn(Cf, Cf, u64, u64, u64) -> Cf,
    subscribe: unsafe extern "C" fn(Cf, Cf, *mut Cf, u64, Cf) -> Cf,
    samples: unsafe extern "C" fn(Cf, Cf, Cf) -> Cf,
    delta: unsafe extern "C" fn(Cf, Cf, Cf) -> Cf,
    name: unsafe extern "C" fn(Cf) -> Cf,
    unit: unsafe extern "C" fn(Cf) -> Cf,
    value: unsafe extern "C" fn(Cf, *mut i32) -> i64,
}

const UTF8: u32 = 0x0800_0100;

impl IoFns {
    // Each pointer becomes the field's own function type, which the struct states.
    #[allow(clippy::missing_transmute_annotations)]
    fn load() -> Option<IoFns> {
        let cf = open_lib("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")?;
        let io = open_lib("/usr/lib/libIOReport.dylib")?;
        unsafe {
            macro_rules! f {
                ($h:expr, $n:expr) => {
                    std::mem::transmute::<*mut c_void, _>(sym($h, $n)?)
                };
            }
            Some(IoFns {
                cfstr: f!(cf, "CFStringCreateWithCString"),
                cfstr_get: f!(cf, "CFStringGetCString"),
                dict_get: f!(cf, "CFDictionaryGetValue"),
                dict_count: f!(cf, "CFDictionaryGetCount"),
                dict_mut_copy: f!(cf, "CFDictionaryCreateMutableCopy"),
                arr_count: f!(cf, "CFArrayGetCount"),
                arr_get: f!(cf, "CFArrayGetValueAtIndex"),
                release: f!(cf, "CFRelease"),
                copy_group: f!(io, "IOReportCopyChannelsInGroup"),
                subscribe: f!(io, "IOReportCreateSubscription"),
                samples: f!(io, "IOReportCreateSamples"),
                delta: f!(io, "IOReportCreateSamplesDelta"),
                name: f!(io, "IOReportChannelGetChannelName"),
                unit: f!(io, "IOReportChannelGetUnitLabel"),
                value: f!(io, "IOReportSimpleGetIntegerValue"),
            })
        }
    }

    fn string(&self, s: Cf) -> String {
        if s.is_null() {
            return String::new();
        }
        let mut b = [0 as c_char; 256];
        if unsafe { (self.cfstr_get)(s, b.as_mut_ptr(), 256, UTF8) } {
            unsafe { std::ffi::CStr::from_ptr(b.as_ptr()) }.to_string_lossy().into_owned()
        } else {
            String::new()
        }
    }
}

impl IoReportGpu {
    /// `None` off macOS, or where IOReport has no `Energy Model` group.
    pub fn open() -> Option<IoReportGpu> {
        if !cfg!(target_os = "macos") {
            return None;
        }
        let f = IoFns::load()?;
        unsafe {
            let group = CString::new("Energy Model").ok()?;
            let g = (f.cfstr)(std::ptr::null(), group.as_ptr(), UTF8);
            let ch = (f.copy_group)(g, std::ptr::null(), 0, 0, 0);
            if ch.is_null() {
                return None;
            }
            let chans = (f.dict_mut_copy)(std::ptr::null(), (f.dict_count)(ch), ch);
            let mut subd: Cf = std::ptr::null();
            let sub = (f.subscribe)(std::ptr::null(), chans, &mut subd, 0, std::ptr::null());
            if sub.is_null() {
                return None;
            }
            let first = (f.samples)(sub, chans, std::ptr::null());
            let k = CString::new("IOReportChannels").ok()?;
            let key = (f.cfstr)(std::ptr::null(), k.as_ptr(), UTF8);
            let mut r = IoReportGpu { f, sub, chans, first, key };
            r.read_nj()?;
            Some(r)
        }
    }
}

impl Counter for IoReportGpu {
    fn profile(&self) -> &'static str {
        "macos-gpu"
    }
    fn detail(&self) -> String {
        "IOReport Energy Model / GPU Energy".into()
    }
    fn read_nj(&mut self) -> Option<u64> {
        let f = self.f;
        unsafe {
            let now = (f.samples)(self.sub, self.chans, std::ptr::null());
            if now.is_null() {
                return None;
            }
            let d = (f.delta)(self.first, now, std::ptr::null());
            (f.release)(now);
            if d.is_null() {
                return None;
            }
            let arr = (f.dict_get)(d, self.key);
            let mut out = None;
            for i in 0..(f.arr_count)(arr) {
                let c = (f.arr_get)(arr, i);
                if f.string((f.name)(c)) == "GPU Energy" {
                    let v = (f.value)(c, std::ptr::null_mut()).max(0) as u64;
                    out = match f.string((f.unit)(c)).as_str() {
                        "nJ" => Some(v),
                        "uJ" => Some(v * 1_000),
                        "mJ" => Some(v * 1_000_000),
                        _ => None,
                    };
                }
            }
            (f.release)(d);
            out
        }
    }
}

// ---------------------------------------------------------------------------------------
// linux-rapl
// ---------------------------------------------------------------------------------------

/// `linux-rapl`: the powercap package zones, summed.
pub struct Rapl {
    zones: Vec<(String, u64)>,
}

/// Why the package zones cannot be read: absent, or present and not readable.
pub fn rapl_status() -> Result<Vec<String>, CounterStatus> {
    let Ok(dir) = std::fs::read_dir("/sys/class/powercap") else { return Err(CounterStatus::NoCounter) };
    let mut zones: Vec<String> = dir
        .filter_map(Result::ok)
        .map(|e| e.path())
        .filter(|p| {
            p.file_name().and_then(|n| n.to_str()).is_some_and(|n| n.starts_with("intel-rapl:") && n.matches(':').count() == 1)
                && std::fs::read_to_string(p.join("name")).is_ok_and(|n| n.trim().starts_with("package-"))
        })
        .map(|p| p.display().to_string())
        .collect();
    zones.sort();
    if zones.is_empty() {
        return Err(CounterStatus::NoCounter);
    }
    for z in &zones {
        if std::fs::read_to_string(format!("{z}/energy_uj")).is_err() {
            return Err(CounterStatus::NeedsPrivilege);
        }
    }
    Ok(zones)
}

impl Rapl {
    pub fn open() -> Option<Rapl> {
        let zones = rapl_status().ok()?;
        let read = |p: String| std::fs::read_to_string(p).ok()?.trim().parse::<u64>().ok();
        let zones = zones.into_iter().map(|z| Some((z.clone(), read(format!("{z}/max_energy_range_uj"))?.saturating_add(1)))).collect::<Option<Vec<_>>>()?;
        Some(Rapl { zones })
    }
    fn zone_uj(z: &str) -> Option<u64> {
        std::fs::read_to_string(format!("{z}/energy_uj")).ok()?.trim().parse().ok()
    }
}

/// A sum of wrapping zones, kept as one cumulative counter.
pub struct RaplCounter {
    rapl: Rapl,
    last: Vec<u64>,
    total_nj: u64,
}

impl RaplCounter {
    pub fn open() -> Option<RaplCounter> {
        let rapl = Rapl::open()?;
        let last = rapl.zones.iter().map(|z| Rapl::zone_uj(&z.0)).collect::<Option<Vec<_>>>()?;
        Some(RaplCounter { rapl, last, total_nj: 0 })
    }
}

impl Counter for RaplCounter {
    fn profile(&self) -> &'static str {
        "linux-rapl"
    }
    fn detail(&self) -> String {
        format!("powercap energy_uj: {}", self.rapl.zones.iter().map(|z| z.0.as_str()).collect::<Vec<_>>().join(", "))
    }
    fn read_nj(&mut self) -> Option<u64> {
        for (i, z) in self.rapl.zones.iter().enumerate() {
            let now = Rapl::zone_uj(&z.0)?;
            let d = advance(self.last[i], now, Some(z.1));
            // A reset reads as nearly a whole range in one interval: no reading (§9.3).
            if d > z.1 / 2 {
                return None;
            }
            self.total_nj += d * 1_000;
            self.last[i] = now;
        }
        Some(self.total_nj)
    }
}

// ---------------------------------------------------------------------------------------
// linux-nvml
// ---------------------------------------------------------------------------------------

/// `linux-nvml`: device 0's total energy.
pub struct Nvml {
    energy: unsafe extern "C" fn(*mut c_void, *mut u64) -> c_int,
    power: unsafe extern "C" fn(*mut c_void, *mut u32) -> c_int,
    limit: unsafe extern "C" fn(*mut c_void, *mut u32) -> c_int,
    dev: *mut c_void,
}

unsafe impl Send for Nvml {}

impl Nvml {
    // Each pointer becomes the field's own function type, which the struct states.
    #[allow(clippy::missing_transmute_annotations)]
    pub fn open() -> Option<Nvml> {
        let h = open_lib("libnvidia-ml.so.1")?;
        unsafe {
            let init: unsafe extern "C" fn() -> c_int = std::mem::transmute(sym(h, "nvmlInit_v2")?);
            let by_index: unsafe extern "C" fn(u32, *mut *mut c_void) -> c_int = std::mem::transmute(sym(h, "nvmlDeviceGetHandleByIndex_v2")?);
            if init() != 0 {
                return None;
            }
            let mut dev = std::ptr::null_mut();
            if by_index(0, &mut dev) != 0 {
                return None;
            }
            Some(Nvml {
                energy: std::mem::transmute(sym(h, "nvmlDeviceGetTotalEnergyConsumption")?),
                power: std::mem::transmute(sym(h, "nvmlDeviceGetPowerUsage")?),
                limit: std::mem::transmute(sym(h, "nvmlDeviceGetEnforcedPowerLimit")?),
                dev,
            })
        }
    }

    /// The device's own power reading, in milliwatts.
    pub fn power_mw(&self) -> Option<u32> {
        let mut p = 0u32;
        (unsafe { (self.power)(self.dev, &mut p) } == 0).then_some(p)
    }

    /// The enforced power limit, in milliwatts.
    pub fn limit_mw(&self) -> Option<u32> {
        let mut p = 0u32;
        (unsafe { (self.limit)(self.dev, &mut p) } == 0).then_some(p)
    }

    /// §9.3's plausibility over `intervals` reads at `every`: the counter's rate in no
    /// interval above 1.5 times the enforced limit, nor twice the mean power reading plus
    /// 5 W. Returns the verdict and the rates and readings it saw, in milliwatts.
    pub fn plausible(&mut self, intervals: u32, every: Duration) -> (bool, Vec<(u64, u64)>) {
        let limit = self.limit_mw().unwrap_or(0) as u64;
        let mut seen = Vec::new();
        let Some(mut c0) = self.read_nj() else { return (false, seen) };
        let mut t0 = Instant::now();
        for _ in 0..intervals {
            std::thread::sleep(every);
            let Some(c1) = self.read_nj() else { return (false, seen) };
            let dt = t0.elapsed().as_micros().max(1) as u64;
            seen.push(((c1.saturating_sub(c0)) / dt, self.power_mw().unwrap_or(0) as u64)); // nJ/µs = mW
            (c0, t0) = (c1, Instant::now());
        }
        let mean = seen.iter().map(|s| s.1).sum::<u64>() / seen.len().max(1) as u64;
        let ok = !seen.is_empty() && seen.iter().all(|&(rate, _)| 2 * rate <= 3 * limit && rate <= 2 * mean + 5_000);
        (ok, seen)
    }
}

impl Counter for Nvml {
    fn profile(&self) -> &'static str {
        "linux-nvml"
    }
    fn detail(&self) -> String {
        "nvmlDeviceGetTotalEnergyConsumption, device 0".into()
    }
    fn read_nj(&mut self) -> Option<u64> {
        let mut mj = 0u64;
        (unsafe { (self.energy)(self.dev, &mut mj) } == 0).then_some(mj.saturating_mul(1_000_000))
    }
}

// ---------------------------------------------------------------------------------------
// The survey
// ---------------------------------------------------------------------------------------

/// A survey entry: profile, status, detail.
#[derive(Clone, Debug)]
pub struct Surveyed {
    pub profile: &'static str,
    pub status: CounterStatus,
    pub detail: String,
}

/// Read `c` at `every` for `intervals` and give its status under energy-measurement §3.1:
/// live when it advanced in at least five intervals, in two consecutive ones, and in at
/// least four of every five.
pub fn liveness(c: &mut dyn Counter, intervals: u32, every: Duration) -> CounterStatus {
    let Some(mut c0) = c.read_nj() else { return CounterStatus::Frozen };
    let (mut live, mut run, mut longest) = (0u32, 0u32, 0u32);
    for _ in 0..intervals {
        std::thread::sleep(every);
        let Some(c1) = c.read_nj() else { return CounterStatus::Frozen };
        if advance(c0, c1, c.wrap_nj()) > 0 {
            live += 1;
            run += 1;
            longest = longest.max(run);
        } else {
            run = 0;
        }
        c0 = c1;
    }
    if live >= 5 && longest >= 2 && 5 * live >= 4 * intervals { CounterStatus::Live } else { CounterStatus::Frozen }
}

/// Every reader this platform has, each with its status, and the live ones opened.
pub fn survey(children: &Children, every: Duration, intervals: u32) -> (Vec<Surveyed>, Vec<Box<dyn Counter>>) {
    let mut out = Vec::new();
    let mut live: Vec<Box<dyn Counter>> = Vec::new();
    let mut take = |c: Option<Box<dyn Counter>>, profile: &'static str, missing: CounterStatus, missing_detail: &str| match c {
        Some(mut c) => {
            let status = liveness(c.as_mut(), intervals, every);
            out.push(Surveyed { profile, status, detail: c.detail() });
            if status == CounterStatus::Live {
                live.push(c);
            }
        }
        None => out.push(Surveyed { profile, status: missing, detail: missing_detail.into() }),
    };
    if cfg!(target_os = "macos") {
        take(TaskEnergy::open(children.clone()).map(|c| Box::new(c) as Box<dyn Counter>), "macos-task", CounterStatus::NotImplemented, "proc_pid_rusage RUSAGE_INFO_V6");
        take(IoReportGpu::open().map(|c| Box::new(c) as Box<dyn Counter>), "macos-gpu", CounterStatus::NoCounter, "IOReport Energy Model / GPU Energy");
    } else if cfg!(target_os = "linux") {
        match rapl_status() {
            Ok(_) => take(RaplCounter::open().map(|c| Box::new(c) as Box<dyn Counter>), "linux-rapl", CounterStatus::NoCounter, "powercap package zones"),
            Err(s) => out.push(Surveyed { profile: "linux-rapl", status: s, detail: "powercap package zones".into() }),
        }
        match Nvml::open() {
            None => out.push(Surveyed { profile: "linux-nvml", status: CounterStatus::NoCounter, detail: "libnvidia-ml: no device".into() }),
            Some(mut n) => {
                let (ok, seen) = n.plausible(5, Duration::from_secs(1));
                let detail = format!(
                    "{}; counter rate and power reading per 1 s interval (mW): {}",
                    n.detail(),
                    seen.iter().map(|(r, p)| format!("{r}/{p}")).collect::<Vec<_>>().join(" ")
                );
                // A plausible counter is read at its own interval (1 s), which no table at
                // another interval reads: it is surveyed, and gives no rows here.
                let status = if ok { CounterStatus::NotImplemented } else { CounterStatus::Implausible };
                out.push(Surveyed { profile: "linux-nvml", status, detail });
            }
        }
    }
    out.sort_by(|a, b| (a.profile, &a.detail).cmp(&(b.profile, &b.detail)));
    (out, live)
}

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

    /// A counter whose reads are scripted.
    struct Scripted(Vec<Option<u64>>, usize);
    impl Counter for Scripted {
        fn profile(&self) -> &'static str {
            "macos-task"
        }
        fn detail(&self) -> String {
            "scripted".into()
        }
        fn read_nj(&mut self) -> Option<u64> {
            let v = self.0.get(self.1).copied().flatten();
            self.1 += 1;
            v
        }
    }

    /// energy-measurement §3.1 on a counter: an advance in every interval is live; a counter
    /// that holds its value, or moves in one lump, or fails to read, is not.
    #[test]
    fn a_counter_is_live_only_when_it_advances_as_a_live_rail_does() {
        let d = Duration::from_millis(0);
        let rising: Vec<Option<u64>> = (0..11).map(|i| Some(i * 1000)).collect();
        assert_eq!(liveness(&mut Scripted(rising, 0), 10, d), CounterStatus::Live);
        assert_eq!(liveness(&mut Scripted(vec![Some(7); 11], 0), 10, d), CounterStatus::Frozen);
        let lump: Vec<Option<u64>> = (0..11).map(|i| Some(if i < 6 { 0 } else { 5_000 })).collect();
        assert_eq!(liveness(&mut Scripted(lump, 0), 10, d), CounterStatus::Frozen, "one lump is one advance");
        // seven of ten advance: below four of every five.
        let seven: Vec<Option<u64>> = [0, 1, 2, 3, 3, 4, 5, 5, 6, 6, 7].into_iter().map(Some).collect();
        assert_eq!(liveness(&mut Scripted(seven, 0), 10, d), CounterStatus::Frozen);
        let failing = vec![Some(0), Some(1), None];
        assert_eq!(liveness(&mut Scripted(failing, 0), 10, d), CounterStatus::Frozen, "a read that fails is no reading");
    }

    /// An advance across a counter's wrap, and none where a counter with no wrap goes back.
    #[test]
    fn an_advance_crosses_the_wrap() {
        assert_eq!(advance(10, 25, Some(100)), 15);
        assert_eq!(advance(90, 5, Some(100)), 15);
        assert_eq!(advance(90, 5, None), 0);
    }

    /// The per-process reader reads this process, and its figure rises while it works.
    #[cfg(target_os = "macos")]
    #[test]
    fn the_task_counter_reads_this_process() {
        let mut t = TaskEnergy::open(Children::default()).expect("proc_pid_rusage reads this process");
        let a = t.read_nj().unwrap();
        let mut x = 0u64;
        let start = Instant::now();
        while start.elapsed() < Duration::from_millis(50) {
            x = std::hint::black_box(x.wrapping_mul(31).wrapping_add(7));
        }
        std::thread::sleep(Duration::from_millis(20)); // leave the core so the work folds in
        let b = t.read_nj().unwrap();
        assert!(b > a, "50 ms of work read {a} then {b} nJ");
    }
}
