use std::collections::{HashMap, HashSet};
use super::config::ScaphandreConfig;
use super::parser::ProcessPower;
use super::state::{ScaphandreState, ServiceEnergy};
#[must_use]
pub fn compute_energy_per_op_kwh(
power_microwatts: f64,
scrape_interval_secs: f64,
ops: u64,
) -> Option<f64> {
if ops == 0 || !power_microwatts.is_finite() || power_microwatts < 0.0 {
return None;
}
let power_watts = power_microwatts / 1_000_000.0;
let joules = power_watts * scrape_interval_secs;
let kwh = joules / 3_600_000.0;
Some(kwh / ops as f64)
}
#[allow(clippy::implicit_hasher)]
pub fn apply_scrape(
state: &ScaphandreState,
power_readings: &[ProcessPower],
op_deltas: &HashMap<String, u64>,
cfg: &ScaphandreConfig,
multi_match_warned: &mut HashSet<String>,
now_ms: u64,
) {
let interval_secs = cfg.scrape_interval.as_secs_f64();
let mut next = state.current_owned();
let mut any_change = false;
for (service, matcher) in &cfg.process_map {
let Some(ops) = op_deltas.get(service).copied() else {
continue; };
let mut unique: Option<&ProcessPower> = None;
let mut ambiguous = false;
let mut candidate_count = 0usize;
for p in power_readings.iter().filter(|p| {
p.exe.contains(&matcher.exe_contains)
&& matcher
.cmdline_contains
.as_ref()
.is_none_or(|c| p.cmdline.contains(c))
}) {
candidate_count += 1;
if unique.is_some() {
ambiguous = true;
break;
}
unique = Some(p);
}
let reading = match (ambiguous, unique) {
(false, None) => continue, (false, Some(p)) => {
multi_match_warned.remove(service);
p
}
(true, _) => {
if multi_match_warned.insert(service.clone()) {
tracing::warn!(
service = %service,
candidates = candidate_count,
exe_contains = %matcher.exe_contains,
cmdline_contains = ?matcher.cmdline_contains,
"[green.scaphandre] process_map matcher is ambiguous: \
several Scaphandre processes match this service. Add or \
tighten cmdline_contains to disambiguate. Skipping this \
tick. Subsequent ambiguous ticks for this service will \
log at debug level until the matcher resolves cleanly."
);
} else {
tracing::debug!(
service = %service,
candidates = candidate_count,
"[green.scaphandre] process_map matcher still ambiguous"
);
}
continue;
}
};
let Some(energy_per_op) =
compute_energy_per_op_kwh(reading.power_microwatts, interval_secs, ops)
else {
continue; };
next.insert(
service.clone(),
ServiceEnergy {
energy_per_op_kwh: energy_per_op,
last_update_ms: now_ms,
},
);
any_change = true;
}
if any_change {
state.publish(next);
}
}