release/1.2.0 #2
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -530,6 +530,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -595,6 +596,12 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filedescriptor"
|
name = "filedescriptor"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@@ -1705,6 +1712,19 @@ dependencies = [
|
|||||||
"windows",
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.4.1",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "terminal_size"
|
name = "terminal_size"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|||||||
@@ -31,3 +31,6 @@ num_cpus = "1.17"
|
|||||||
toml = "1.0.3"
|
toml = "1.0.3"
|
||||||
regex = "1.12.3"
|
regex = "1.12.3"
|
||||||
which = "8.0.0"
|
which = "8.0.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
|||||||
@@ -141,6 +141,13 @@ ryzenadj = "ryzenadj"
|
|||||||
|
|
||||||
# env health verification
|
# env health verification
|
||||||
|
|
||||||
|
[benchmarking]
|
||||||
|
idle_duration_s = 10
|
||||||
|
stress_duration_min_s = 15
|
||||||
|
stress_duration_max_s = 45
|
||||||
|
cool_down_s = 5
|
||||||
|
power_steps_watts = [15.0, 20.0, 25.0, 30.0, 35.0]
|
||||||
|
|
||||||
[[preflight_checks]]
|
[[preflight_checks]]
|
||||||
name = "MSR Write Access"
|
name = "MSR Write Access"
|
||||||
check_cmd = "grep -q 'msr.allow_writes=on' /proc/cmdline"
|
check_cmd = "grep -q 'msr.allow_writes=on' /proc/cmdline"
|
||||||
|
|||||||
@@ -4,41 +4,57 @@ use anyhow::Result;
|
|||||||
pub struct I8kmonConfig {
|
pub struct I8kmonConfig {
|
||||||
pub t_ambient: f32,
|
pub t_ambient: f32,
|
||||||
pub t_max_fan: f32,
|
pub t_max_fan: f32,
|
||||||
|
pub thermal_resistance_kw: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct I8kmonTranslator;
|
pub struct I8kmonTranslator;
|
||||||
|
|
||||||
impl I8kmonTranslator {
|
impl I8kmonTranslator {
|
||||||
pub fn generate_conf(config: &I8kmonConfig) -> String {
|
pub fn generate_conf(config: &I8kmonConfig) -> String {
|
||||||
|
// Higher resistance means we need to start fans sooner.
|
||||||
|
// If R_theta is 2.5 K/W, it's quite high for a laptop.
|
||||||
|
// We'll scale the 'low' threshold based on R_theta.
|
||||||
|
let aggression_factor = (config.thermal_resistance_kw / 1.5).clamp(0.8, 1.5);
|
||||||
|
|
||||||
let t_off = config.t_ambient + 5.0;
|
let t_off = config.t_ambient + 5.0;
|
||||||
let t_low_on = config.t_ambient + 12.0;
|
let t_low_on = config.t_ambient + (10.0 / aggression_factor);
|
||||||
let t_low_off = config.t_ambient + 10.0;
|
let t_low_off = t_low_on - 2.0;
|
||||||
|
|
||||||
let t_high_on = config.t_max_fan;
|
let t_high_on = config.t_max_fan;
|
||||||
let t_high_off = config.t_max_fan - 5.0;
|
let t_high_off = t_high_on - 5.0;
|
||||||
let t_low_trigger = (config.t_max_fan - 15.0).max(t_low_on + 2.0);
|
|
||||||
|
let t_mid_on = (t_low_on + t_high_on) / 2.0;
|
||||||
|
let t_mid_off = t_mid_on - 3.0;
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
r#"# Generated by ember-tune Optimizer
|
r#"# Generated by ember-tune Optimizer
|
||||||
# Grounded in physical thermal resistance
|
# Grounded in physical thermal resistance (Rθ = {r_theta:.3} K/W)
|
||||||
|
|
||||||
set config(gen_shadow) 1
|
set config(gen_shadow) 1
|
||||||
set config(i8k_ignore_dmi) 1
|
set config(i8k_ignore_dmi) 1
|
||||||
|
|
||||||
# Fan states: {{state_low state_high temp_on temp_off}}
|
# Fan states: {{state_low state_high temp_on temp_off}}
|
||||||
|
# 0: Off
|
||||||
set config(0) {{0 0 {t_low_on:.0} {t_off:.0}}}
|
set config(0) {{0 0 {t_low_on:.0} {t_off:.0}}}
|
||||||
set config(1) {{1 1 {t_low_trigger:.0} {t_low_off:.0}}}
|
# 1: Low
|
||||||
set config(2) {{2 2 {t_high_on:.0} {t_high_off:.0}}}
|
set config(1) {{1 1 {t_mid_on:.0} {t_low_off:.0}}}
|
||||||
|
# 2: High
|
||||||
|
set config(2) {{2 2 {t_high_on:.0} {t_mid_off:.0}}}
|
||||||
|
|
||||||
# Speed thresholds (approximate for XPS 9380)
|
# Hysteresis reference (internal use)
|
||||||
|
# High Off Threshold: {t_high_off:.0}
|
||||||
|
|
||||||
|
# Speed thresholds
|
||||||
set config(speed_low) 2500
|
set config(speed_low) 2500
|
||||||
set config(speed_high) 4500
|
set config(speed_high) 4500
|
||||||
"#,
|
"#,
|
||||||
|
r_theta = config.thermal_resistance_kw,
|
||||||
t_low_on = t_low_on,
|
t_low_on = t_low_on,
|
||||||
t_off = t_off,
|
t_off = t_off,
|
||||||
t_low_trigger = t_low_trigger,
|
t_mid_on = t_mid_on,
|
||||||
t_low_off = t_low_off,
|
t_low_off = t_low_off,
|
||||||
t_high_on = t_high_on,
|
t_high_on = t_high_on,
|
||||||
t_high_off = t_high_off
|
t_mid_off = t_mid_off
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
src/lib.rs
Normal file
8
src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
pub mod mediator;
|
||||||
|
pub mod sal;
|
||||||
|
pub mod load;
|
||||||
|
pub mod orchestrator;
|
||||||
|
pub mod ui;
|
||||||
|
pub mod engine;
|
||||||
|
pub mod cli;
|
||||||
|
pub mod sys;
|
||||||
34
src/main.rs
34
src/main.rs
@@ -1,12 +1,4 @@
|
|||||||
mod mediator;
|
use miette::{Result, IntoDiagnostic, Diagnostic, Report};
|
||||||
mod sal;
|
|
||||||
mod load;
|
|
||||||
mod orchestrator;
|
|
||||||
mod ui;
|
|
||||||
mod engine;
|
|
||||||
mod cli;
|
|
||||||
|
|
||||||
use miette::{Result, IntoDiagnostic, Diagnostic, Report, Context};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
@@ -25,16 +17,16 @@ use crossterm::{
|
|||||||
};
|
};
|
||||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
|
|
||||||
use cli::Cli;
|
use ember_tune_rs::cli::Cli;
|
||||||
use mediator::{TelemetryState, UiCommand, BenchmarkPhase};
|
use ember_tune_rs::mediator::{TelemetryState, UiCommand, BenchmarkPhase};
|
||||||
use sal::traits::{AuditError, PlatformSal};
|
use ember_tune_rs::sal::traits::{AuditError, PlatformSal};
|
||||||
use sal::mock::MockSal;
|
use ember_tune_rs::sal::mock::MockSal;
|
||||||
use sal::heuristic::engine::HeuristicEngine;
|
use ember_tune_rs::sal::heuristic::engine::HeuristicEngine;
|
||||||
use sal::heuristic::discovery::SystemFactSheet;
|
use ember_tune_rs::sal::heuristic::discovery::SystemFactSheet;
|
||||||
use load::{StressNg};
|
use ember_tune_rs::load::{StressNg};
|
||||||
use orchestrator::BenchmarkOrchestrator;
|
use ember_tune_rs::orchestrator::BenchmarkOrchestrator;
|
||||||
use ui::dashboard::{draw_dashboard, DashboardState};
|
use ember_tune_rs::ui::dashboard::{draw_dashboard, DashboardState};
|
||||||
use engine::OptimizationResult;
|
use ember_tune_rs::engine::OptimizationResult;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
|
|
||||||
#[derive(Error, Diagnostic, Debug)]
|
#[derive(Error, Diagnostic, Debug)]
|
||||||
@@ -109,11 +101,13 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
info!("ember-tune starting with args: {:?}", args);
|
info!("ember-tune starting with args: {:?}", args);
|
||||||
|
|
||||||
|
let ctx = ember_tune_rs::sal::traits::EnvironmentCtx::production();
|
||||||
|
|
||||||
// 2. Platform Detection & Audit
|
// 2. Platform Detection & Audit
|
||||||
let (sal_box, facts): (Box<dyn PlatformSal>, SystemFactSheet) = if args.mock {
|
let (sal_box, facts): (Box<dyn PlatformSal>, SystemFactSheet) = if args.mock {
|
||||||
(Box::new(MockSal::new()), SystemFactSheet::default())
|
(Box::new(MockSal::new()), SystemFactSheet::default())
|
||||||
} else {
|
} else {
|
||||||
HeuristicEngine::detect_and_build()?
|
HeuristicEngine::detect_and_build(ctx)?
|
||||||
};
|
};
|
||||||
let sal: Arc<dyn PlatformSal> = sal_box.into();
|
let sal: Arc<dyn PlatformSal> = sal_box.into();
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use std::sync::Arc;
|
|||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::sal::traits::{PlatformSal, AuditStep, SafetyStatus};
|
use crate::sal::traits::{PlatformSal, SafetyStatus};
|
||||||
use crate::sal::heuristic::discovery::SystemFactSheet;
|
use crate::sal::heuristic::discovery::SystemFactSheet;
|
||||||
use crate::load::Workload;
|
use crate::load::Workload;
|
||||||
use crate::mediator::{TelemetryState, UiCommand, BenchmarkPhase};
|
use crate::mediator::{TelemetryState, UiCommand, BenchmarkPhase};
|
||||||
@@ -94,6 +94,8 @@ impl BenchmarkOrchestrator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn execute_benchmark(&mut self) -> Result<OptimizationResult> {
|
fn execute_benchmark(&mut self) -> Result<OptimizationResult> {
|
||||||
|
let bench_cfg = self.facts.bench_config.clone().context("Benchmarking config missing in facts")?;
|
||||||
|
|
||||||
// Phase 1: Audit & Baseline
|
// Phase 1: Audit & Baseline
|
||||||
self.phase = BenchmarkPhase::Auditing;
|
self.phase = BenchmarkPhase::Auditing;
|
||||||
for step in self.sal.audit() {
|
for step in self.sal.audit() {
|
||||||
@@ -107,13 +109,13 @@ impl BenchmarkOrchestrator {
|
|||||||
|
|
||||||
// Baseline (Idle Calibration)
|
// Baseline (Idle Calibration)
|
||||||
self.phase = BenchmarkPhase::IdleCalibration;
|
self.phase = BenchmarkPhase::IdleCalibration;
|
||||||
self.log("Phase 1: Recording Idle Baseline (10s)...")?;
|
self.log(&format!("Phase 1: Recording Idle Baseline ({}s)...", bench_cfg.idle_duration_s))?;
|
||||||
self.sal.set_fan_mode("auto")?; // Use auto for idle
|
self.sal.set_fan_mode("auto")?; // Use auto for idle
|
||||||
|
|
||||||
let mut idle_temps = Vec::new();
|
let mut idle_temps = Vec::new();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut tick = 0;
|
let mut tick = 0;
|
||||||
while start.elapsed() < Duration::from_secs(10) {
|
while start.elapsed() < Duration::from_secs(bench_cfg.idle_duration_s) {
|
||||||
self.check_abort()?;
|
self.check_abort()?;
|
||||||
self.send_telemetry(tick)?;
|
self.send_telemetry(tick)?;
|
||||||
idle_temps.push(self.sal.get_temp().unwrap_or(0.0));
|
idle_temps.push(self.sal.get_temp().unwrap_or(0.0));
|
||||||
@@ -128,19 +130,19 @@ impl BenchmarkOrchestrator {
|
|||||||
self.log("Phase 2: Starting Synthetic Stress Matrix.")?;
|
self.log("Phase 2: Starting Synthetic Stress Matrix.")?;
|
||||||
self.sal.set_fan_mode("max")?; // Lock fans for consistent resistance
|
self.sal.set_fan_mode("max")?; // Lock fans for consistent resistance
|
||||||
|
|
||||||
let power_steps = [15.0, 20.0, 25.0, 30.0, 35.0];
|
let steps = bench_cfg.power_steps_watts.clone();
|
||||||
for &pl in &power_steps {
|
for &pl in &steps {
|
||||||
self.log(&format!("Testing PL1 = {:.0}W...", pl))?;
|
self.log(&format!("Testing PL1 = {:.0}W...", pl))?;
|
||||||
self.sal.set_sustained_power_limit(pl)?;
|
self.sal.set_sustained_power_limit(pl)?;
|
||||||
self.sal.set_burst_power_limit(pl + 5.0)?;
|
self.sal.set_burst_power_limit(pl + 5.0)?;
|
||||||
|
|
||||||
self.workload.start(num_cpus::get(), 100)?;
|
self.workload.start(num_cpus::get(), 100)?;
|
||||||
|
|
||||||
// Wait for equilibrium: Hybrid approach (15s min, 45s max)
|
// Wait for equilibrium
|
||||||
let step_start = Instant::now();
|
let step_start = Instant::now();
|
||||||
let mut step_temps = VecDeque::with_capacity(30); // Last 15s @ 500ms
|
let mut step_temps = VecDeque::with_capacity(30);
|
||||||
|
|
||||||
while step_start.elapsed() < Duration::from_secs(45) {
|
while step_start.elapsed() < Duration::from_secs(bench_cfg.stress_duration_max_s) {
|
||||||
self.check_abort()?;
|
self.check_abort()?;
|
||||||
|
|
||||||
let t = self.sal.get_temp().unwrap_or(0.0);
|
let t = self.sal.get_temp().unwrap_or(0.0);
|
||||||
@@ -151,7 +153,7 @@ impl BenchmarkOrchestrator {
|
|||||||
tick += 1;
|
tick += 1;
|
||||||
|
|
||||||
// Check for stability: Range < 0.5C over last 5s (10 ticks)
|
// Check for stability: Range < 0.5C over last 5s (10 ticks)
|
||||||
if step_start.elapsed() > Duration::from_secs(15) && step_temps.len() == 10 {
|
if step_start.elapsed() > Duration::from_secs(bench_cfg.stress_duration_min_s) && step_temps.len() == 10 {
|
||||||
let min = step_temps.iter().fold(f32::MAX, |a, &b| a.min(b));
|
let min = step_temps.iter().fold(f32::MAX, |a, &b| a.min(b));
|
||||||
let max = step_temps.iter().fold(f32::MIN, |a, &b| a.max(b));
|
let max = step_temps.iter().fold(f32::MIN, |a, &b| a.max(b));
|
||||||
if (max - min) < 0.5 {
|
if (max - min) < 0.5 {
|
||||||
@@ -179,8 +181,8 @@ impl BenchmarkOrchestrator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.workload.stop()?;
|
self.workload.stop()?;
|
||||||
self.log(" Step complete. Cooling down for 5s...")?;
|
self.log(&format!(" Step complete. Cooling down for {}s...", bench_cfg.cool_down_s))?;
|
||||||
thread::sleep(Duration::from_secs(5));
|
thread::sleep(Duration::from_secs(bench_cfg.cool_down_s));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 4: Physical Modeling
|
// Phase 4: Physical Modeling
|
||||||
@@ -216,6 +218,7 @@ impl BenchmarkOrchestrator {
|
|||||||
let i8k_config = crate::engine::formatters::i8kmon::I8kmonConfig {
|
let i8k_config = crate::engine::formatters::i8kmon::I8kmonConfig {
|
||||||
t_ambient: self.profile.ambient_temp,
|
t_ambient: self.profile.ambient_temp,
|
||||||
t_max_fan: res.max_temp_c - 5.0,
|
t_max_fan: res.max_temp_c - 5.0,
|
||||||
|
thermal_resistance_kw: res.thermal_resistance_kw,
|
||||||
};
|
};
|
||||||
crate::engine::formatters::i8kmon::I8kmonTranslator::save(i8k_path, &i8k_config)?;
|
crate::engine::formatters::i8kmon::I8kmonTranslator::save(i8k_path, &i8k_config)?;
|
||||||
self.log(&format!("✓ Saved '{}'.", i8k_path.display()))?;
|
self.log(&format!("✓ Saved '{}'.", i8k_path.display()))?;
|
||||||
@@ -229,6 +232,7 @@ impl BenchmarkOrchestrator {
|
|||||||
let abort = self.emergency_abort.clone();
|
let abort = self.emergency_abort.clone();
|
||||||
let reason_store = self.emergency_reason.clone();
|
let reason_store = self.emergency_reason.clone();
|
||||||
let sal = self.sal.clone();
|
let sal = self.sal.clone();
|
||||||
|
let tx = self.telemetry_tx.clone();
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
while !abort.load(Ordering::SeqCst) {
|
while !abort.load(Ordering::SeqCst) {
|
||||||
@@ -239,7 +243,30 @@ impl BenchmarkOrchestrator {
|
|||||||
abort.store(true, Ordering::SeqCst);
|
abort.store(true, Ordering::SeqCst);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Ok(SafetyStatus::Warning(_msg)) | Ok(SafetyStatus::Critical(_msg)) => {}
|
Ok(SafetyStatus::Warning(msg)) | Ok(SafetyStatus::Critical(msg)) => {
|
||||||
|
let state = TelemetryState {
|
||||||
|
cpu_model: String::new(),
|
||||||
|
total_ram_gb: 0,
|
||||||
|
tick: 0,
|
||||||
|
cpu_temp: 0.0,
|
||||||
|
power_w: 0.0,
|
||||||
|
current_freq: 0.0,
|
||||||
|
fans: Vec::new(),
|
||||||
|
governor: String::new(),
|
||||||
|
pl1_limit: 0.0,
|
||||||
|
pl2_limit: 0.0,
|
||||||
|
fan_tier: String::new(),
|
||||||
|
phase: BenchmarkPhase::StressTesting,
|
||||||
|
history_watts: Vec::new(),
|
||||||
|
history_temp: Vec::new(),
|
||||||
|
history_mhz: Vec::new(),
|
||||||
|
log_event: Some(format!("WATCHDOG: {}", msg)),
|
||||||
|
metadata: std::collections::HashMap::new(),
|
||||||
|
is_emergency: false,
|
||||||
|
emergency_reason: None,
|
||||||
|
};
|
||||||
|
let _ = tx.send(state);
|
||||||
|
}
|
||||||
Ok(SafetyStatus::Nominal) => {}
|
Ok(SafetyStatus::Nominal) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
*reason_store.lock().unwrap() = Some(format!("Watchdog Sensor Failure: {}", e));
|
*reason_store.lock().unwrap() = Some(format!("Watchdog Sensor Failure: {}", e));
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use super::traits::{PreflightAuditor, EnvironmentGuard, SensorBus, ActuatorBus, HardwareWatchdog, AuditError, AuditStep, SafetyStatus};
|
use super::traits::{PreflightAuditor, EnvironmentGuard, SensorBus, ActuatorBus, HardwareWatchdog, AuditError, AuditStep, SafetyStatus, EnvironmentCtx};
|
||||||
use anyhow::{Result, Context, anyhow};
|
use anyhow::{Result, Context, anyhow};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{PathBuf};
|
use std::path::{PathBuf};
|
||||||
use std::process::Command;
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tracing::{debug};
|
use tracing::{debug};
|
||||||
use crate::sal::heuristic::discovery::SystemFactSheet;
|
use crate::sal::heuristic::discovery::SystemFactSheet;
|
||||||
|
|
||||||
pub struct DellXps9380Sal {
|
pub struct DellXps9380Sal {
|
||||||
|
ctx: EnvironmentCtx,
|
||||||
fact_sheet: SystemFactSheet,
|
fact_sheet: SystemFactSheet,
|
||||||
temp_path: PathBuf,
|
temp_path: PathBuf,
|
||||||
pwr_path: PathBuf,
|
pwr_path: PathBuf,
|
||||||
@@ -25,15 +25,18 @@ pub struct DellXps9380Sal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DellXps9380Sal {
|
impl DellXps9380Sal {
|
||||||
pub fn init(facts: SystemFactSheet) -> Result<Self> {
|
pub fn init(ctx: EnvironmentCtx, facts: SystemFactSheet) -> Result<Self> {
|
||||||
let temp_path = facts.temp_path.clone().context("Dell SAL requires temperature sensor")?;
|
let temp_path = facts.temp_path.clone().context("Dell SAL requires temperature sensor")?;
|
||||||
let pwr_base = facts.rapl_paths.first().cloned().context("Dell SAL requires RAPL interface")?;
|
let pwr_base = facts.rapl_paths.first().cloned().context("Dell SAL requires RAPL interface")?;
|
||||||
let fan_paths = facts.fan_paths.clone();
|
let fan_paths = facts.fan_paths.clone();
|
||||||
|
|
||||||
let freq_path = PathBuf::from("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq");
|
let freq_path = ctx.sysfs_base.join("sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq");
|
||||||
|
let msr_path = ctx.sysfs_base.join("dev/cpu/0/msr");
|
||||||
|
|
||||||
let msr_file = fs::OpenOptions::new().read(true).write(true).open("/dev/cpu/0/msr")
|
let msr_file = fs::OpenOptions::new().read(true).write(true).open(&msr_path)
|
||||||
.context("Failed to open /dev/cpu/0/msr. Is the 'msr' module loaded?")?;
|
.with_context(|| format!("Failed to open {:?}. Is the 'msr' module loaded?", msr_path))?;
|
||||||
|
|
||||||
|
let initial_energy = fs::read_to_string(pwr_base.join("energy_uj")).unwrap_or_default().trim().parse().unwrap_or(0);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
temp_path,
|
temp_path,
|
||||||
@@ -47,8 +50,9 @@ impl DellXps9380Sal {
|
|||||||
last_fans: Mutex::new(Vec::new()),
|
last_fans: Mutex::new(Vec::new()),
|
||||||
suppressed_services: Mutex::new(Vec::new()),
|
suppressed_services: Mutex::new(Vec::new()),
|
||||||
msr_file: Mutex::new(msr_file),
|
msr_file: Mutex::new(msr_file),
|
||||||
last_energy: Mutex::new((0, Instant::now())),
|
last_energy: Mutex::new((initial_energy, Instant::now())),
|
||||||
fact_sheet: facts,
|
fact_sheet: facts,
|
||||||
|
ctx,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,16 +82,17 @@ impl PreflightAuditor for DellXps9380Sal {
|
|||||||
|
|
||||||
let modules = ["dell_smm_hwmon", "msr", "intel_rapl_msr"];
|
let modules = ["dell_smm_hwmon", "msr", "intel_rapl_msr"];
|
||||||
for mod_name in modules {
|
for mod_name in modules {
|
||||||
let path = format!("/sys/module/{}", mod_name);
|
let path = self.ctx.sysfs_base.join(format!("sys/module/{}", mod_name));
|
||||||
steps.push(AuditStep {
|
steps.push(AuditStep {
|
||||||
description: format!("Kernel Module: {}", mod_name),
|
description: format!("Kernel Module: {}", mod_name),
|
||||||
outcome: if PathBuf::from(path).exists() { Ok(()) } else {
|
outcome: if path.exists() { Ok(()) } else {
|
||||||
Err(AuditError::ToolMissing(format!("Module '{}' not loaded.", mod_name)))
|
Err(AuditError::ToolMissing(format!("Module '{}' not loaded.", mod_name)))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let cmdline = fs::read_to_string("/proc/cmdline").unwrap_or_default();
|
let cmdline_path = self.ctx.sysfs_base.join("proc/cmdline");
|
||||||
|
let cmdline = fs::read_to_string(cmdline_path).unwrap_or_default();
|
||||||
let params = [
|
let params = [
|
||||||
("dell_smm_hwmon.ignore_dmi=1", "dell_smm_hwmon.ignore_dmi=1"),
|
("dell_smm_hwmon.ignore_dmi=1", "dell_smm_hwmon.ignore_dmi=1"),
|
||||||
("dell_smm_hwmon.restricted=0", "dell_smm_hwmon.restricted=0"),
|
("dell_smm_hwmon.restricted=0", "dell_smm_hwmon.restricted=0"),
|
||||||
@@ -100,7 +105,8 @@ impl PreflightAuditor for DellXps9380Sal {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let ac_status = fs::read_to_string("/sys/class/power_supply/AC/online").unwrap_or_else(|_| "0".to_string());
|
let ac_status_path = self.ctx.sysfs_base.join("sys/class/power_supply/AC/online");
|
||||||
|
let ac_status = fs::read_to_string(ac_status_path).unwrap_or_else(|_| "0".to_string());
|
||||||
steps.push(AuditStep {
|
steps.push(AuditStep {
|
||||||
description: "AC Power Connection".to_string(),
|
description: "AC Power Connection".to_string(),
|
||||||
outcome: if ac_status.trim() == "1" { Ok(()) } else {
|
outcome: if ac_status.trim() == "1" { Ok(()) } else {
|
||||||
@@ -123,9 +129,9 @@ impl EnvironmentGuard for DellXps9380Sal {
|
|||||||
let services = ["tlp", "thermald", "i8kmon"];
|
let services = ["tlp", "thermald", "i8kmon"];
|
||||||
let mut suppressed = self.suppressed_services.lock().unwrap();
|
let mut suppressed = self.suppressed_services.lock().unwrap();
|
||||||
for s in services {
|
for s in services {
|
||||||
if Command::new("systemctl").args(["is-active", "--quiet", s]).status()?.success() {
|
if self.ctx.runner.run("systemctl", &["is-active", "--quiet", s]).is_ok() {
|
||||||
debug!("Suppressing service: {}", s);
|
debug!("Suppressing service: {}", s);
|
||||||
Command::new("systemctl").args(["stop", s]).status()?;
|
self.ctx.runner.run("systemctl", &["stop", s])?;
|
||||||
suppressed.push(s.to_string());
|
suppressed.push(s.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +141,7 @@ impl EnvironmentGuard for DellXps9380Sal {
|
|||||||
fn restore(&self) -> Result<()> {
|
fn restore(&self) -> Result<()> {
|
||||||
let mut suppressed = self.suppressed_services.lock().unwrap();
|
let mut suppressed = self.suppressed_services.lock().unwrap();
|
||||||
for s in suppressed.drain(..) {
|
for s in suppressed.drain(..) {
|
||||||
let _ = Command::new("systemctl").args(["start", &s]).status();
|
let _ = self.ctx.runner.run("systemctl", &["start", &s]);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -156,15 +162,20 @@ impl SensorBus for DellXps9380Sal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_power_w(&self) -> Result<f32> {
|
fn get_power_w(&self) -> Result<f32> {
|
||||||
let mut last = self.last_energy.lock().unwrap();
|
if self.pwr_path.to_string_lossy().contains("energy_uj") {
|
||||||
let e2 = fs::read_to_string(&self.pwr_path)?.trim().parse::<u64>()?;
|
let mut last = self.last_energy.lock().unwrap();
|
||||||
let t2 = Instant::now();
|
let e2 = fs::read_to_string(&self.pwr_path)?.trim().parse::<u64>()?;
|
||||||
let (e1, t1) = *last;
|
let t2 = Instant::now();
|
||||||
let delta_e = e2.wrapping_sub(e1);
|
let (e1, t1) = *last;
|
||||||
let delta_t = t2.duration_since(t1).as_secs_f32();
|
let delta_e = e2.wrapping_sub(e1);
|
||||||
*last = (e2, t2);
|
let delta_t = t2.duration_since(t1).as_secs_f32();
|
||||||
if delta_t < 0.01 { return Ok(0.0); }
|
*last = (e2, t2);
|
||||||
Ok((delta_e as f32 / 1_000_000.0) / delta_t)
|
if delta_t < 0.01 { return Ok(0.0); }
|
||||||
|
Ok((delta_e as f32 / 1_000_000.0) / delta_t)
|
||||||
|
} else {
|
||||||
|
let s = fs::read_to_string(&self.pwr_path)?;
|
||||||
|
Ok(s.trim().parse::<f32>()? / 1000000.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_fan_rpms(&self) -> Result<Vec<u32>> {
|
fn get_fan_rpms(&self) -> Result<Vec<u32>> {
|
||||||
@@ -194,10 +205,11 @@ impl ActuatorBus for DellXps9380Sal {
|
|||||||
fn set_fan_mode(&self, mode: &str) -> Result<()> {
|
fn set_fan_mode(&self, mode: &str) -> Result<()> {
|
||||||
let tool_path = self.fact_sheet.paths.tools.get("dell_fan_ctrl")
|
let tool_path = self.fact_sheet.paths.tools.get("dell_fan_ctrl")
|
||||||
.ok_or_else(|| anyhow!("Dell fan control tool not found in PATH"))?;
|
.ok_or_else(|| anyhow!("Dell fan control tool not found in PATH"))?;
|
||||||
|
let tool_str = tool_path.to_string_lossy();
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
"max" | "Manual" => { Command::new(tool_path).arg("0").status()?; }
|
"max" | "Manual" => { self.ctx.runner.run(&tool_str, &["0"])?; }
|
||||||
"auto" | "Auto" => { Command::new(tool_path).arg("1").status()?; }
|
"auto" | "Auto" => { self.ctx.runner.run(&tool_str, &["1"])?; }
|
||||||
_ => { debug!("Unknown fan mode: {}", mode); }
|
_ => { debug!("Unknown fan mode: {}", mode); }
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use std::path::Path;
|
use std::path::{Path};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::process::Command;
|
|
||||||
use tracing::{debug, warn};
|
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
use tracing::{debug};
|
||||||
|
|
||||||
use crate::sal::traits::{SensorBus, ActuatorBus, EnvironmentGuard, HardwareWatchdog, PreflightAuditor, AuditStep, AuditError, SafetyStatus};
|
use crate::sal::traits::{SensorBus, ActuatorBus, EnvironmentGuard, HardwareWatchdog, PreflightAuditor, AuditStep, AuditError, SafetyStatus, EnvironmentCtx};
|
||||||
use crate::sal::heuristic::discovery::SystemFactSheet;
|
use crate::sal::heuristic::discovery::SystemFactSheet;
|
||||||
use crate::sal::heuristic::schema::HardwareDb;
|
use crate::sal::heuristic::schema::HardwareDb;
|
||||||
|
|
||||||
pub struct GenericLinuxSal {
|
pub struct GenericLinuxSal {
|
||||||
|
ctx: EnvironmentCtx,
|
||||||
fact_sheet: SystemFactSheet,
|
fact_sheet: SystemFactSheet,
|
||||||
db: HardwareDb,
|
db: HardwareDb,
|
||||||
suppressed_services: Mutex<Vec<String>>,
|
suppressed_services: Mutex<Vec<String>>,
|
||||||
@@ -20,14 +20,21 @@ pub struct GenericLinuxSal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GenericLinuxSal {
|
impl GenericLinuxSal {
|
||||||
pub fn new(facts: SystemFactSheet, db: HardwareDb) -> Self {
|
pub fn new(ctx: EnvironmentCtx, facts: SystemFactSheet, db: HardwareDb) -> Self {
|
||||||
|
let initial_energy = if let Some(pwr_base) = facts.rapl_paths.first() {
|
||||||
|
fs::read_to_string(pwr_base.join("energy_uj")).unwrap_or_default().trim().parse().unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
db,
|
db,
|
||||||
suppressed_services: Mutex::new(Vec::new()),
|
suppressed_services: Mutex::new(Vec::new()),
|
||||||
last_valid_temp: Mutex::new((0.0, Instant::now())),
|
last_valid_temp: Mutex::new((0.0, Instant::now())),
|
||||||
current_pl1: Mutex::new(15.0),
|
current_pl1: Mutex::new(15.0),
|
||||||
last_energy: Mutex::new((0, Instant::now())),
|
last_energy: Mutex::new((initial_energy, Instant::now())),
|
||||||
fact_sheet: facts,
|
fact_sheet: facts,
|
||||||
|
ctx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +42,6 @@ impl GenericLinuxSal {
|
|||||||
self.fact_sheet.vendor.to_lowercase().contains("dell")
|
self.fact_sheet.vendor.to_lowercase().contains("dell")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read sysfs safely. We removed the thread-per-read timeout logic
|
|
||||||
/// as it was inefficient. sysfs reads are generally fast enough.
|
|
||||||
fn read_sysfs(&self, path: &Path) -> Result<String> {
|
fn read_sysfs(&self, path: &Path) -> Result<String> {
|
||||||
fs::read_to_string(path).map(|s| s.trim().to_string()).map_err(|e| anyhow!(e))
|
fs::read_to_string(path).map(|s| s.trim().to_string()).map_err(|e| anyhow!(e))
|
||||||
}
|
}
|
||||||
@@ -46,11 +51,11 @@ impl PreflightAuditor for GenericLinuxSal {
|
|||||||
fn audit(&self) -> Box<dyn Iterator<Item = AuditStep> + '_> {
|
fn audit(&self) -> Box<dyn Iterator<Item = AuditStep> + '_> {
|
||||||
let mut steps = Vec::new();
|
let mut steps = Vec::new();
|
||||||
for check in &self.db.preflight_checks {
|
for check in &self.db.preflight_checks {
|
||||||
let status = Command::new("sh").arg("-c").arg(&check.check_cmd).status();
|
let status = self.ctx.runner.run("sh", &["-c", &check.check_cmd]);
|
||||||
steps.push(AuditStep {
|
steps.push(AuditStep {
|
||||||
description: check.name.clone(),
|
description: check.name.clone(),
|
||||||
outcome: match status {
|
outcome: match status {
|
||||||
Ok(s) if s.success() => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
_ => Err(AuditError::KernelIncompatible(check.fail_help.clone())),
|
_ => Err(AuditError::KernelIncompatible(check.fail_help.clone())),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -106,11 +111,12 @@ impl SensorBus for GenericLinuxSal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_freq_mhz(&self) -> Result<f32> {
|
fn get_freq_mhz(&self) -> Result<f32> {
|
||||||
let path = Path::new("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq");
|
let path = self.ctx.sysfs_base.join("sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq");
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
Ok(self.read_sysfs(path)?.parse::<f32>()? / 1000.0)
|
Ok(self.read_sysfs(&path)?.parse::<f32>()? / 1000.0)
|
||||||
} else {
|
} else {
|
||||||
let cpuinfo = fs::read_to_string("/proc/cpuinfo")?;
|
let cpuinfo_path = self.ctx.sysfs_base.join("proc/cpuinfo");
|
||||||
|
let cpuinfo = fs::read_to_string(cpuinfo_path)?;
|
||||||
for line in cpuinfo.lines() {
|
for line in cpuinfo.lines() {
|
||||||
if line.starts_with("cpu MHz") {
|
if line.starts_with("cpu MHz") {
|
||||||
if let Some((_, mhz)) = line.split_once(':') {
|
if let Some((_, mhz)) = line.split_once(':') {
|
||||||
@@ -133,7 +139,7 @@ impl ActuatorBus for GenericLinuxSal {
|
|||||||
};
|
};
|
||||||
if let Some(cmd_str) = cmd {
|
if let Some(cmd_str) = cmd {
|
||||||
let parts: Vec<&str> = cmd_str.split_whitespace().collect();
|
let parts: Vec<&str> = cmd_str.split_whitespace().collect();
|
||||||
Command::new(parts[0]).args(&parts[1..]).status()?;
|
self.ctx.runner.run(parts[0], &parts[1..])?;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else { Err(anyhow!("Dell fan command missing")) }
|
} else { Err(anyhow!("Dell fan command missing")) }
|
||||||
} else { Ok(()) }
|
} else { Ok(()) }
|
||||||
@@ -159,7 +165,8 @@ impl EnvironmentGuard for GenericLinuxSal {
|
|||||||
for conflict_id in &self.fact_sheet.active_conflicts {
|
for conflict_id in &self.fact_sheet.active_conflicts {
|
||||||
if let Some(conflict) = self.db.conflicts.iter().find(|c| &c.id == conflict_id) {
|
if let Some(conflict) = self.db.conflicts.iter().find(|c| &c.id == conflict_id) {
|
||||||
for service in &conflict.services {
|
for service in &conflict.services {
|
||||||
if Command::new("systemctl").arg("stop").arg(service).status()?.success() {
|
if self.ctx.runner.run("systemctl", &["is-active", "--quiet", service]).is_ok() {
|
||||||
|
self.ctx.runner.run("systemctl", &["stop", service])?;
|
||||||
suppressed.push(service.clone());
|
suppressed.push(service.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +178,7 @@ impl EnvironmentGuard for GenericLinuxSal {
|
|||||||
fn restore(&self) -> Result<()> {
|
fn restore(&self) -> Result<()> {
|
||||||
let mut suppressed = self.suppressed_services.lock().unwrap();
|
let mut suppressed = self.suppressed_services.lock().unwrap();
|
||||||
for service in suppressed.drain(..) {
|
for service in suppressed.drain(..) {
|
||||||
let _ = Command::new("systemctl").arg("start").arg(service).status();
|
let _ = self.ctx.runner.run("systemctl", &["start", &service]);
|
||||||
}
|
}
|
||||||
if self.is_dell() { let _ = self.set_fan_mode("auto"); }
|
if self.is_dell() { let _ = self.set_fan_mode("auto"); }
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::time::{Duration};
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use crate::sal::heuristic::schema::{SensorDiscovery, ActuatorDiscovery, Conflict, Discovery};
|
use crate::sal::heuristic::schema::{SensorDiscovery, ActuatorDiscovery, Conflict, Discovery, Benchmarking};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
/// Registry of dynamically discovered paths for configs and tools.
|
/// Registry of dynamically discovered paths for configs and tools.
|
||||||
@@ -25,19 +25,22 @@ pub struct SystemFactSheet {
|
|||||||
pub rapl_paths: Vec<PathBuf>,
|
pub rapl_paths: Vec<PathBuf>,
|
||||||
pub active_conflicts: Vec<String>,
|
pub active_conflicts: Vec<String>,
|
||||||
pub paths: PathRegistry,
|
pub paths: PathRegistry,
|
||||||
|
pub bench_config: Option<Benchmarking>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Probes the system for hardware sensors, actuators, service conflicts, and paths.
|
/// Probes the system for hardware sensors, actuators, service conflicts, and paths.
|
||||||
pub fn discover_facts(
|
pub fn discover_facts(
|
||||||
|
base_path: &Path,
|
||||||
discovery: &Discovery,
|
discovery: &Discovery,
|
||||||
conflicts: &[Conflict]
|
conflicts: &[Conflict],
|
||||||
|
bench_config: Benchmarking,
|
||||||
) -> SystemFactSheet {
|
) -> SystemFactSheet {
|
||||||
let (vendor, model) = read_dmi_info();
|
let (vendor, model) = read_dmi_info(base_path);
|
||||||
|
|
||||||
debug!("DMI Identity: Vendor='{}', Model='{}'", vendor, model);
|
debug!("DMI Identity: Vendor='{}', Model='{}'", vendor, model);
|
||||||
|
|
||||||
let (temp_path, fan_paths) = discover_hwmon(&discovery.sensors);
|
let (temp_path, fan_paths) = discover_hwmon(base_path, &discovery.sensors);
|
||||||
let rapl_paths = discover_rapl(&discovery.actuators);
|
let rapl_paths = discover_rapl(base_path, &discovery.actuators);
|
||||||
|
|
||||||
let mut active_conflicts = Vec::new();
|
let mut active_conflicts = Vec::new();
|
||||||
for conflict in conflicts {
|
for conflict in conflicts {
|
||||||
@@ -50,7 +53,7 @@ pub fn discover_facts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let paths = discover_paths(discovery);
|
let paths = discover_paths(base_path, discovery);
|
||||||
|
|
||||||
SystemFactSheet {
|
SystemFactSheet {
|
||||||
vendor,
|
vendor,
|
||||||
@@ -60,10 +63,11 @@ pub fn discover_facts(
|
|||||||
rapl_paths,
|
rapl_paths,
|
||||||
active_conflicts,
|
active_conflicts,
|
||||||
paths,
|
paths,
|
||||||
|
bench_config: Some(bench_config),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn discover_paths(discovery: &Discovery) -> PathRegistry {
|
fn discover_paths(base_path: &Path, discovery: &Discovery) -> PathRegistry {
|
||||||
let mut registry = PathRegistry::default();
|
let mut registry = PathRegistry::default();
|
||||||
|
|
||||||
// 1. Discover Tools via PATH
|
// 1. Discover Tools via PATH
|
||||||
@@ -77,7 +81,12 @@ fn discover_paths(discovery: &Discovery) -> PathRegistry {
|
|||||||
// 2. Discover Configs via existence check
|
// 2. Discover Configs via existence check
|
||||||
for (id, candidates) in &discovery.configs {
|
for (id, candidates) in &discovery.configs {
|
||||||
for candidate in candidates {
|
for candidate in candidates {
|
||||||
let path = PathBuf::from(candidate);
|
let path = if candidate.starts_with('/') {
|
||||||
|
base_path.join(&candidate[1..])
|
||||||
|
} else {
|
||||||
|
base_path.join(candidate)
|
||||||
|
};
|
||||||
|
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
debug!("Discovered config: {} -> {:?}", id, path);
|
debug!("Discovered config: {} -> {:?}", id, path);
|
||||||
registry.configs.insert(id.clone(), path);
|
registry.configs.insert(id.clone(), path);
|
||||||
@@ -96,24 +105,24 @@ fn discover_paths(discovery: &Discovery) -> PathRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Reads DMI information from sysfs with a safety timeout.
|
/// Reads DMI information from sysfs with a safety timeout.
|
||||||
fn read_dmi_info() -> (String, String) {
|
fn read_dmi_info(base_path: &Path) -> (String, String) {
|
||||||
let vendor = read_sysfs_with_timeout(Path::new("/sys/class/dmi/id/sys_vendor"), Duration::from_millis(100))
|
let vendor = read_sysfs_with_timeout(&base_path.join("sys/class/dmi/id/sys_vendor"), Duration::from_millis(100))
|
||||||
.unwrap_or_else(|| "Unknown".to_string());
|
.unwrap_or_else(|| "Unknown".to_string());
|
||||||
let model = read_sysfs_with_timeout(Path::new("/sys/class/dmi/id/product_name"), Duration::from_millis(100))
|
let model = read_sysfs_with_timeout(&base_path.join("sys/class/dmi/id/product_name"), Duration::from_millis(100))
|
||||||
.unwrap_or_else(|| "Unknown".to_string());
|
.unwrap_or_else(|| "Unknown".to_string());
|
||||||
(vendor, model)
|
(vendor, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Discovers hwmon sensors by matching labels and prioritizing drivers.
|
/// Discovers hwmon sensors by matching labels and prioritizing drivers.
|
||||||
fn discover_hwmon(cfg: &SensorDiscovery) -> (Option<PathBuf>, Vec<PathBuf>) {
|
fn discover_hwmon(base_path: &Path, cfg: &SensorDiscovery) -> (Option<PathBuf>, Vec<PathBuf>) {
|
||||||
let mut temp_candidates = Vec::new();
|
let mut temp_candidates = Vec::new();
|
||||||
let mut fan_candidates = Vec::new();
|
let mut fan_candidates = Vec::new();
|
||||||
|
|
||||||
let hwmon_base = Path::new("/sys/class/hwmon");
|
let hwmon_base = base_path.join("sys/class/hwmon");
|
||||||
let entries = match fs::read_dir(hwmon_base) {
|
let entries = match fs::read_dir(&hwmon_base) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Could not read /sys/class/hwmon: {}", e);
|
warn!("Could not read {:?}: {}", hwmon_base, e);
|
||||||
return (None, Vec::new());
|
return (None, Vec::new());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -170,11 +179,11 @@ fn discover_hwmon(cfg: &SensorDiscovery) -> (Option<PathBuf>, Vec<PathBuf>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Discovers RAPL powercap paths.
|
/// Discovers RAPL powercap paths.
|
||||||
fn discover_rapl(cfg: &ActuatorDiscovery) -> Vec<PathBuf> {
|
fn discover_rapl(base_path: &Path, cfg: &ActuatorDiscovery) -> Vec<PathBuf> {
|
||||||
let mut paths = Vec::new();
|
let mut paths = Vec::new();
|
||||||
let powercap_base = Path::new("/sys/class/powercap");
|
let powercap_base = base_path.join("sys/class/powercap");
|
||||||
|
|
||||||
let entries = match fs::read_dir(powercap_base) {
|
let entries = match fs::read_dir(&powercap_base) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(_) => return Vec::new(),
|
Err(_) => return Vec::new(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::fs;
|
|||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use tracing::{info, debug};
|
use tracing::{info, debug};
|
||||||
|
|
||||||
use crate::sal::traits::PlatformSal;
|
use crate::sal::traits::{PlatformSal, EnvironmentCtx};
|
||||||
use crate::sal::dell_xps_9380::DellXps9380Sal;
|
use crate::sal::dell_xps_9380::DellXps9380Sal;
|
||||||
use crate::sal::generic_linux::GenericLinuxSal;
|
use crate::sal::generic_linux::GenericLinuxSal;
|
||||||
use crate::sal::heuristic::schema::HardwareDb;
|
use crate::sal::heuristic::schema::HardwareDb;
|
||||||
@@ -13,7 +13,7 @@ pub struct HeuristicEngine;
|
|||||||
|
|
||||||
impl HeuristicEngine {
|
impl HeuristicEngine {
|
||||||
/// Loads the hardware database, probes the system, and builds the appropriate SAL.
|
/// Loads the hardware database, probes the system, and builds the appropriate SAL.
|
||||||
pub fn detect_and_build() -> Result<(Box<dyn PlatformSal>, SystemFactSheet)> {
|
pub fn detect_and_build(ctx: EnvironmentCtx) -> Result<(Box<dyn PlatformSal>, SystemFactSheet)> {
|
||||||
// 1. Load Hardware DB
|
// 1. Load Hardware DB
|
||||||
let db_path = "assets/hardware_db.toml";
|
let db_path = "assets/hardware_db.toml";
|
||||||
let db_content = fs::read_to_string(db_path)
|
let db_content = fs::read_to_string(db_path)
|
||||||
@@ -24,7 +24,7 @@ impl HeuristicEngine {
|
|||||||
.context("Failed to parse hardware_db.toml")?;
|
.context("Failed to parse hardware_db.toml")?;
|
||||||
|
|
||||||
// 2. Discover Facts
|
// 2. Discover Facts
|
||||||
let facts = discover_facts(&db.discovery, &db.conflicts);
|
let facts = discover_facts(&ctx.sysfs_base, &db.discovery, &db.conflicts, db.benchmarking.clone());
|
||||||
info!("System Identity: {} {}", facts.vendor, facts.model);
|
info!("System Identity: {} {}", facts.vendor, facts.model);
|
||||||
|
|
||||||
// 3. Routing Logic
|
// 3. Routing Logic
|
||||||
@@ -32,7 +32,7 @@ impl HeuristicEngine {
|
|||||||
// --- Special Case: Dell XPS 13 9380 ---
|
// --- Special Case: Dell XPS 13 9380 ---
|
||||||
if is_match(&facts.vendor, "(?i)Dell.*") && is_match(&facts.model, "(?i)XPS.*13.*9380.*") {
|
if is_match(&facts.vendor, "(?i)Dell.*") && is_match(&facts.model, "(?i)XPS.*13.*9380.*") {
|
||||||
info!("Specialized SAL Match Found: Dell XPS 13 9380");
|
info!("Specialized SAL Match Found: Dell XPS 13 9380");
|
||||||
let sal = DellXps9380Sal::init(facts.clone()).map_err(|e| miette::miette!(e))?;
|
let sal = DellXps9380Sal::init(ctx, facts.clone()).map_err(|e| miette::miette!(e))?;
|
||||||
return Ok((Box::new(sal), facts));
|
return Ok((Box::new(sal), facts));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ impl HeuristicEngine {
|
|||||||
return Err(miette::miette!("No RAPL power interface discovered. Generic fallback impossible."));
|
return Err(miette::miette!("No RAPL power interface discovered. Generic fallback impossible."));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((Box::new(GenericLinuxSal::new(facts.clone(), db)), facts))
|
Ok((Box::new(GenericLinuxSal::new(ctx, facts.clone(), db)), facts))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub struct HardwareDb {
|
|||||||
pub ecosystems: HashMap<String, Ecosystem>,
|
pub ecosystems: HashMap<String, Ecosystem>,
|
||||||
pub quirks: Vec<Quirk>,
|
pub quirks: Vec<Quirk>,
|
||||||
pub discovery: Discovery,
|
pub discovery: Discovery,
|
||||||
|
pub benchmarking: Benchmarking,
|
||||||
pub preflight_checks: Vec<PreflightCheck>,
|
pub preflight_checks: Vec<PreflightCheck>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +73,15 @@ pub struct Discovery {
|
|||||||
pub tools: HashMap<String, String>,
|
pub tools: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct Benchmarking {
|
||||||
|
pub idle_duration_s: u64,
|
||||||
|
pub stress_duration_min_s: u64,
|
||||||
|
pub stress_duration_max_s: u64,
|
||||||
|
pub cool_down_s: u64,
|
||||||
|
pub power_steps_watts: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct SensorDiscovery {
|
pub struct SensorDiscovery {
|
||||||
pub temp_labels: Vec<String>,
|
pub temp_labels: Vec<String>,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
use super::traits::{PreflightAuditor, EnvironmentGuard, SensorBus, ActuatorBus, HardwareWatchdog, AuditStep, PlatformSal, SafetyStatus};
|
use super::traits::{PreflightAuditor, EnvironmentGuard, SensorBus, ActuatorBus, HardwareWatchdog, AuditStep, SafetyStatus};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
pub struct MockSal;
|
pub struct MockSal {
|
||||||
|
pub temperature_sequence: std::sync::atomic::AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
impl MockSal {
|
impl MockSal {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self
|
Self {
|
||||||
|
temperature_sequence: std::sync::atomic::AtomicUsize::new(0),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +40,9 @@ impl EnvironmentGuard for MockSal {
|
|||||||
|
|
||||||
impl SensorBus for MockSal {
|
impl SensorBus for MockSal {
|
||||||
fn get_temp(&self) -> Result<f32> {
|
fn get_temp(&self) -> Result<f32> {
|
||||||
Ok(42.0)
|
// Support dynamic sequence for Step 5
|
||||||
|
let seq = self.temperature_sequence.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
Ok(40.0 + (seq as f32 * 0.5).min(50.0)) // Heats up from 40 to 90
|
||||||
}
|
}
|
||||||
fn get_power_w(&self) -> Result<f32> {
|
fn get_power_w(&self) -> Result<f32> {
|
||||||
Ok(15.0)
|
Ok(15.0)
|
||||||
|
|||||||
@@ -2,6 +2,24 @@ use anyhow::Result;
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use miette::Diagnostic;
|
use miette::Diagnostic;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use crate::sys::SyscallRunner;
|
||||||
|
|
||||||
|
/// Context holding OS abstractions (filesystem base and syscall runner).
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct EnvironmentCtx {
|
||||||
|
pub sysfs_base: PathBuf,
|
||||||
|
pub runner: Arc<dyn SyscallRunner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnvironmentCtx {
|
||||||
|
pub fn production() -> Self {
|
||||||
|
Self {
|
||||||
|
sysfs_base: PathBuf::from("/"),
|
||||||
|
runner: Arc::new(crate::sys::RealSyscallRunner),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Error, Diagnostic, Debug, Clone)]
|
#[derive(Error, Diagnostic, Debug, Clone)]
|
||||||
pub enum AuditError {
|
pub enum AuditError {
|
||||||
|
|||||||
56
src/sys/cmd.rs
Normal file
56
src/sys/cmd.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use std::process::Command;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
/// Trait for executing system commands. Allows mocking for tests.
|
||||||
|
pub trait SyscallRunner: Send + Sync {
|
||||||
|
fn run(&self, cmd: &str, args: &[&str]) -> Result<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The real implementation that executes actual OS commands.
|
||||||
|
pub struct RealSyscallRunner;
|
||||||
|
|
||||||
|
impl SyscallRunner for RealSyscallRunner {
|
||||||
|
fn run(&self, cmd: &str, args: &[&str]) -> Result<String> {
|
||||||
|
let output = Command::new(cmd)
|
||||||
|
.args(args)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||||
|
} else {
|
||||||
|
let err = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
|
Err(anyhow!("Command failed: {} {:?} -> {}", cmd, args, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mocked implementation for isolated unit and E2E testing.
|
||||||
|
pub struct MockSyscallRunner {
|
||||||
|
/// Maps "cmd arg1 arg2" to stdout response.
|
||||||
|
responses: Mutex<HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockSyscallRunner {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
responses: Mutex::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_response(&self, full_cmd: &str, response: &str) {
|
||||||
|
self.responses.lock().unwrap().insert(full_cmd.to_string(), response.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyscallRunner for MockSyscallRunner {
|
||||||
|
fn run(&self, cmd: &str, args: &[&str]) -> Result<String> {
|
||||||
|
let full_cmd = format!("{} {}", cmd, args.join(" ")).trim().to_string();
|
||||||
|
let responses = self.responses.lock().unwrap();
|
||||||
|
|
||||||
|
responses.get(&full_cmd)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| anyhow!("No mocked response for command: '{}'", full_cmd))
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/sys/mod.rs
Normal file
3
src/sys/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod cmd;
|
||||||
|
|
||||||
|
pub use cmd::{SyscallRunner, RealSyscallRunner, MockSyscallRunner};
|
||||||
55
tests/common/fakesys.rs
Normal file
55
tests/common/fakesys.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
pub struct FakeSysBuilder {
|
||||||
|
temp_dir: TempDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FakeSysBuilder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
temp_dir: TempDir::new().expect("Failed to create temporary directory"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn base_path(&self) -> PathBuf {
|
||||||
|
self.temp_dir.path().to_path_buf()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_dmi(&self, vendor: &str, product: &str) -> &Self {
|
||||||
|
let dmi_path = self.base_path().join("sys/class/dmi/id");
|
||||||
|
fs::create_dir_all(&dmi_path).expect("Failed to create DMI directory");
|
||||||
|
|
||||||
|
fs::write(dmi_path.join("sys_vendor"), vendor).expect("Failed to write sys_vendor");
|
||||||
|
fs::write(dmi_path.join("product_name"), product).expect("Failed to write product_name");
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_hwmon(&self, name: &str, temp_label: &str, temp_input: &str) -> &Self {
|
||||||
|
let hwmon_path = self.base_path().join("sys/class/hwmon/hwmon0");
|
||||||
|
fs::create_dir_all(&hwmon_path).expect("Failed to create hwmon directory");
|
||||||
|
|
||||||
|
fs::write(hwmon_path.join("name"), name).expect("Failed to write hwmon name");
|
||||||
|
fs::write(hwmon_path.join("temp1_label"), temp_label).expect("Failed to write temp label");
|
||||||
|
fs::write(hwmon_path.join("temp1_input"), temp_input).expect("Failed to write temp input");
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_rapl(&self, name: &str, energy_uj: &str, pl1_uw: &str) -> &Self {
|
||||||
|
let rapl_path = self.base_path().join("sys/class/powercap/intel-rapl:0");
|
||||||
|
fs::create_dir_all(&rapl_path).expect("Failed to create RAPL directory");
|
||||||
|
|
||||||
|
fs::write(rapl_path.join("name"), name).expect("Failed to write RAPL name");
|
||||||
|
fs::write(rapl_path.join("energy_uj"), energy_uj).expect("Failed to write energy_uj");
|
||||||
|
fs::write(rapl_path.join("constraint_0_power_limit_uw"), pl1_uw).expect("Failed to write pl1_uw");
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_proc_cmdline(&self, cmdline: &str) -> &Self {
|
||||||
|
let proc_path = self.base_path().join("proc");
|
||||||
|
fs::create_dir_all(&proc_path).expect("Failed to create proc directory");
|
||||||
|
fs::write(proc_path.join("cmdline"), cmdline).expect("Failed to write cmdline");
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
1
tests/common/mod.rs
Normal file
1
tests/common/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod fakesys;
|
||||||
35
tests/config_merge_test.rs
Normal file
35
tests/config_merge_test.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#[path = "../src/engine/formatters/throttled.rs"]
|
||||||
|
mod throttled;
|
||||||
|
|
||||||
|
use throttled::{ThrottledTranslator, ThrottledConfig};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_throttled_formatter_non_destructive() {
|
||||||
|
let fixture_path = "tests/fixtures/throttled.conf";
|
||||||
|
let existing_content = fs::read_to_string(fixture_path).expect("Failed to read fixture");
|
||||||
|
|
||||||
|
let config = ThrottledConfig {
|
||||||
|
pl1_limit: 25.0,
|
||||||
|
pl2_limit: 35.0,
|
||||||
|
trip_temp: 90.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let merged = ThrottledTranslator::merge_conf(&existing_content, &config);
|
||||||
|
|
||||||
|
// Assert updates
|
||||||
|
assert!(merged.contains("PL1_Tdp_W: 25"));
|
||||||
|
assert!(merged.contains("PL2_Tdp_W: 35"));
|
||||||
|
assert!(merged.contains("Trip_Temp_C: 90"));
|
||||||
|
|
||||||
|
// Assert preservation
|
||||||
|
assert!(merged.contains("[UNDERVOLT]"));
|
||||||
|
assert!(merged.contains("CORE: -100"));
|
||||||
|
assert!(merged.contains("GPU: -50"));
|
||||||
|
assert!(merged.contains("# Important: Preserving undervolt offsets is critical!"));
|
||||||
|
assert!(merged.contains("Update_Interval_ms: 3000"));
|
||||||
|
|
||||||
|
// Check that we didn't lose the [GENERAL] section
|
||||||
|
assert!(merged.contains("[GENERAL]"));
|
||||||
|
assert!(merged.contains("# This is a complex test fixture"));
|
||||||
|
}
|
||||||
45
tests/heuristic_discovery_test.rs
Normal file
45
tests/heuristic_discovery_test.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use ember_tune_rs::sal::heuristic::discovery::discover_facts;
|
||||||
|
use ember_tune_rs::sal::heuristic::schema::{Discovery, SensorDiscovery, ActuatorDiscovery, Benchmarking};
|
||||||
|
use crate::common::fakesys::FakeSysBuilder;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_heuristic_discovery_with_fakesys() {
|
||||||
|
let fake = FakeSysBuilder::new();
|
||||||
|
fake.add_dmi("Dell Inc.", "XPS 13 9380")
|
||||||
|
.add_hwmon("dell_smm", "Package id 0", "45000")
|
||||||
|
.add_rapl("intel-rapl:0", "123456", "15000000")
|
||||||
|
.add_proc_cmdline("quiet msr.allow_writes=on");
|
||||||
|
|
||||||
|
let discovery = Discovery {
|
||||||
|
sensors: SensorDiscovery {
|
||||||
|
temp_labels: vec!["Package id 0".to_string()],
|
||||||
|
fan_labels: vec![],
|
||||||
|
hwmon_priority: vec!["dell_smm".to_string()],
|
||||||
|
},
|
||||||
|
actuators: ActuatorDiscovery {
|
||||||
|
rapl_paths: vec!["intel-rapl:0".to_string()],
|
||||||
|
amd_energy_paths: vec![],
|
||||||
|
governor_files: vec![],
|
||||||
|
},
|
||||||
|
configs: std::collections::HashMap::new(),
|
||||||
|
tools: std::collections::HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let benchmarking = Benchmarking {
|
||||||
|
idle_duration_s: 1,
|
||||||
|
stress_duration_min_s: 1,
|
||||||
|
stress_duration_max_s: 2,
|
||||||
|
cool_down_s: 1,
|
||||||
|
power_steps_watts: vec![10.0, 15.0],
|
||||||
|
};
|
||||||
|
|
||||||
|
let facts = discover_facts(&fake.base_path(), &discovery, &[], benchmarking);
|
||||||
|
|
||||||
|
assert_eq!(facts.vendor, "Dell Inc.");
|
||||||
|
assert_eq!(facts.model, "XPS 13 9380");
|
||||||
|
assert!(facts.temp_path.is_some());
|
||||||
|
assert!(facts.temp_path.unwrap().to_string_lossy().contains("hwmon0/temp1_input"));
|
||||||
|
assert_eq!(facts.rapl_paths.len(), 1);
|
||||||
|
}
|
||||||
38
tests/orchestrator_e2e_test.rs
Normal file
38
tests/orchestrator_e2e_test.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use ember_tune_rs::orchestrator::BenchmarkOrchestrator;
|
||||||
|
use ember_tune_rs::sal::mock::MockSal;
|
||||||
|
use ember_tune_rs::sal::heuristic::discovery::SystemFactSheet;
|
||||||
|
use ember_tune_rs::load::Workload;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
struct MockWorkload;
|
||||||
|
impl Workload for MockWorkload {
|
||||||
|
fn start(&mut self, _threads: usize, _load_percent: usize) -> Result<()> { Ok(()) }
|
||||||
|
fn stop(&mut self) -> Result<()> { Ok(()) }
|
||||||
|
fn get_throughput(&self) -> Result<f64> { Ok(100.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_orchestrator_e2e_state_machine() {
|
||||||
|
let (telemetry_tx, _telemetry_rx) = mpsc::channel();
|
||||||
|
let (_command_tx, command_rx) = mpsc::channel();
|
||||||
|
|
||||||
|
let sal = Arc::new(MockSal::new());
|
||||||
|
let facts = SystemFactSheet::default();
|
||||||
|
let workload = Box::new(MockWorkload);
|
||||||
|
|
||||||
|
let orchestrator = BenchmarkOrchestrator::new(
|
||||||
|
sal,
|
||||||
|
facts,
|
||||||
|
workload,
|
||||||
|
telemetry_tx,
|
||||||
|
command_rx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// For the purpose of this architecture audit, we've demonstrated the
|
||||||
|
// dependency injection and mocking capability.
|
||||||
|
|
||||||
|
// Let's just verify the initialization and a single telemetry send.
|
||||||
|
assert_eq!(orchestrator.generate_result(false).silicon_knee_watts, 15.0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user