//! Energy to solution: the landscape emulator against tuned classical methods //! (`wai_quantum::quantum_landscape::solution`). //! //! ```text //! landscape_solution list --tier full|small //! The instance list of a tier, as JSON: what is pre-registered. //! landscape_solution program ID //! The program of one instance, by the name the list gives it. //! landscape_solution run --tier full|small --out DIR [--measure] [--window SECONDS] [--threads N] [--host LABEL] //! Tune and grade the tier, and write DIR/results.json: counts only, the same //! bytes on every machine. With --measure, then time every cell on one thread //! and read this host's energy meter around it, and write DIR/measured.json. //! landscape_solution render --results FILE [--measured FILE] //! The table as a document, from the result files. //! ``` //! //! The joules of `--measure` are this computer's, running software: one process //! on macOS, the whole processor packages on Linux, and stated unmetered //! anywhere the host has no meter this reads. They are never a device's energy. use serde_json::{Value, json}; use std::time::{Duration, Instant}; use wai_quantum::engine::energy; use wai_quantum::quantum_landscape::solution::{self, Meter, Tier, Unmetered}; /// The operating system's count of this process's CPU energy. struct Process { before: Option, } /// A thread's energy is added to its process when the thread leaves a core, so /// the thread sleeps before each read. const SETTLE: Duration = Duration::from_millis(2); impl Meter for Process { fn describe(&self) -> Value { json!({ "kind": "measured", "meter": "the operating system's per-process CPU energy counter (proc_pid_rusage: ri_energy_nj, with what system services billed to the process)", "class": "OnChipCounter", "scope": "whole process: this process and nothing else", "covers": "the CPU energy of this process's own threads over each window, as the processor's own accounting gives it, with no calibration behind it", "excludes": "memory, the host's idle draw, every other process, and everything outside the processor (supplies, cooling, storage, display)", "note": "read while the process runs; the thread sleeps 2 ms before each read so that its energy is posted", }) } fn start(&mut self) { std::thread::sleep(SETTLE); self.before = energy::own(); } fn stop(&mut self) -> Result { std::thread::sleep(SETTLE); let (Some(before), Some(after)) = (self.before.take(), energy::own()) else { return Err("this process's energy counter did not read: it is not live on this host".into()); }; let joules = after.joules() - before.joules(); if joules > 0.0 { Ok(joules) } else { Err("this process's energy counter did not advance over a window: it is not live on this host".into()) } } } /// The processor packages' energy counters. struct Package { packages: energy::Packages, before: Option, } impl Meter for Package { fn describe(&self) -> Value { json!({ "kind": "measured", "meter": "the processor packages' own energy counters, as the kernel's power-capping interface publishes them (/sys/class/powercap)", "class": "OnChipCounter", "scope": "whole package: every process on every processor package of this host, this table's among them", "covers": "the packages' energy over each window, their idle draw and every other process's work included; none of it is attributed to this table's process", "excludes": "memory and devices outside the package, supplies, cooling, storage", "update_ns": self.packages.probe.update_ns, }) } fn start(&mut self) { self.before = self.packages.read(); } fn stop(&mut self) -> Result { let (Some(before), Some(after)) = (self.before.take(), self.packages.read()) else { return Err("a package's energy counter did not read".into()); }; let reading = self.packages.between(&before, &after, 0).ok_or("a package's energy counter gave no difference over a window")?; if reading.energy_uj > 0 { Ok(reading.energy_uj as f64 / 1e6) } else { Err("the packages' energy counters did not advance over a window".into()) } } } fn meter() -> Box { if cfg!(target_os = "macos") { return match energy::own() { Some(_) => Box::new(Process { before: None }), None => Box::new(Unmetered("this process's energy counter does not read on this host".into())), }; } if cfg!(target_os = "linux") { return match energy::Packages::open() { Ok(packages) => Box::new(Package { packages, before: None }), Err(why) => Box::new(Unmetered(why)), }; } Box::new(Unmetered("no energy counter is read on this operating system".into())) } fn usage() -> ! { eprintln!("usage: landscape_solution list --tier full|small"); eprintln!(" landscape_solution program ID"); eprintln!(" landscape_solution run --tier full|small --out DIR [--measure] [--window SECONDS] [--threads N] [--host LABEL]"); eprintln!(" landscape_solution render --results FILE [--measured FILE]"); std::process::exit(2) } fn value(args: &[String], flag: &str) -> Option { args.iter().position(|a| a == flag).map(|k| args.get(k + 1).cloned().unwrap_or_else(|| usage())) } fn pretty(v: &Value) -> String { let mut s = serde_json::to_string_pretty(v).expect("a value is written"); s.push('\n'); s } fn read(path: &str) -> Value { let text = std::fs::read_to_string(path).unwrap_or_else(|e| { eprintln!("{path}: {e}"); std::process::exit(1) }); serde_json::from_str(&text).unwrap_or_else(|e| { eprintln!("{path}: {e}"); std::process::exit(1) }) } fn main() { let args: Vec = std::env::args().skip(1).collect(); let tier = || value(&args, "--tier").and_then(|t| Tier::from_name(&t)).unwrap_or_else(|| usage()); match args.first().map(String::as_str) { Some("list") => print!("{}", pretty(&solution::list(tier()))), Some("program") => match args.get(1).and_then(|id| solution::program_by_id(id)) { Some(text) => print!("{text}"), None => { eprintln!("no instance has that name; `list` gives every name"); std::process::exit(1) } }, Some("run") => { let tier = tier(); let out = std::path::PathBuf::from(value(&args, "--out").unwrap_or_else(|| usage())); let threads = value(&args, "--threads").map_or(0, |t| t.parse().unwrap_or_else(|_| usage())); std::fs::create_dir_all(&out).expect("the output folder is made"); let began = Instant::now(); let evaluated = solution::evaluate(tier, threads, &|line| eprintln!("{:8.1} s {line}", began.elapsed().as_secs_f64())); let results = evaluated.to_json(); std::fs::write(out.join("results.json"), pretty(&results)).expect("results.json is written"); eprintln!("{:8.1} s results.json written", began.elapsed().as_secs_f64()); if args.iter().any(|a| a == "--measure") { let window = value(&args, "--window").map_or(0.25, |w| w.parse().unwrap_or_else(|_| usage())); let cores = std::thread::available_parallelism().map_or(1, |p| p.get()); let label = value(&args, "--host").unwrap_or_else(|| format!("{} {}, {cores} logical cores", std::env::consts::OS, std::env::consts::ARCH)); let host = json!({ "label": label, "os": std::env::consts::OS, "arch": std::env::consts::ARCH, "logical_cores": cores, "threads_used": 1 }); let mut meter = meter(); let measured = solution::measure(&evaluated, meter.as_mut(), &|| began.elapsed().as_secs_f64(), window, &host, &|line| eprintln!("{:8.1} s {line}", began.elapsed().as_secs_f64())); std::fs::write(out.join("measured.json"), pretty(&measured)).expect("measured.json is written"); eprintln!("{:8.1} s measured.json written: energy {}", began.elapsed().as_secs_f64(), if measured["energy_unmetered"].is_null() { "measured" } else { "unmetered" }); } } Some("render") => { let results = read(&value(&args, "--results").unwrap_or_else(|| usage())); let measured = value(&args, "--measured").map(|p| read(&p)); print!("{}", solution::render(&results, measured.as_ref())); } _ => usage(), } }