energy-bench 0.3.0

Methods for benchmarking the energy consumption of programs.
Documentation
use std::{ops::Index, path::PathBuf, thread, time::Duration};

use file_backed_value::FileBackedValue;
use gethostname::gethostname;
use indexmap::IndexMap;

use crate::EnergyAccumulator;

pub struct IdlePower(IndexMap<String, f32>);

impl IdlePower {
    pub fn init(path: Option<&PathBuf>, idle_seconds: usize, probes: &mut Box<dyn EnergyAccumulator>) -> Self {
        let filename = gethostname()
            .to_ascii_lowercase()
            .to_str()
            .map_or(format!("idle-{idle_seconds}s.json"), |user| {
                format!("idle-{idle_seconds}s-{user}.json")
            });

        let mut file = if let Some(path) = path {
            FileBackedValue::new_at(&filename, path)
        } else {
            FileBackedValue::new(&filename)
        };

        log::info!("Idle file (will be) stored at {:?}", file.path());
        file.set_dirty_time(Duration::from_hours(24 * 6)); // 6 days

        let idle = match file.get_or_insert_with(|| measure_idle(probes, idle_seconds)) {
            Ok(idle) => idle.clone(),
            Err(e) => {
                log::error!("Error reading idle file: {:?}", e);
                measure_idle(probes, idle_seconds)
            },
        };

        Self(idle)
    }
}

impl Index<&str> for IdlePower {
    type Output = f32;

    fn index(&self, key: &str) -> &Self::Output {
        &self.0[key]
    }
}

fn measure_idle(probes: &mut Box<dyn EnergyAccumulator>, seconds: usize) -> IndexMap<String, f32> {
    if seconds < 10 {
        log::warn!("Forcing mimimum idle time from {}s to 10s", seconds);
    }
    let seconds = seconds.max(10);

    let before = seconds.min(30);
    log::info!("Waiting for system to stabilise for {}s", before);
    thread::sleep(Duration::from_secs(before as u64));

    log::info!("Measuring idle for {}s", seconds);
    let mut samples: IndexMap<String, Vec<f32>> = IndexMap::new();
    const SECOND: Duration = Duration::from_secs(1);
    for _ in 0..seconds {
        probes.reset();
        thread::sleep(SECOND);
        for (k, v) in probes.elapsed() {
            samples.entry(k).or_default().push(v);
        }
    }

    let mut idle = IndexMap::new();
    for (k, mut values) in samples {
        values.sort_by(|a, b| a.total_cmp(b));
        let mid = values.len() / 2;
        let median = if values.len() % 2 == 0 {
            (values[mid - 1] + values[mid]) / 2.0
        } else {
            values[mid]
        };
        idle.insert(k, median);
    }

    log::info!("Idle power draw: {:#?}", idle);
    idle
}