use std::collections::HashMap;
use std::sync::LazyLock;
pub(crate) use super::table_data::SPECPOWER_VINTAGE;
#[allow(dead_code)]
pub(crate) const DRAM_PREMIUM_W_PER_GB_IDLE: f64 = 0.02;
#[allow(dead_code)]
pub(crate) const DRAM_PREMIUM_W_PER_GB_MAX: f64 = 0.05;
static MANUAL_INSTANCE_ROWS: &[(&str, f64, f64)] = &[
("c4d-standard-2", 0.6, 3.8),
("c4d-standard-4", 1.3, 7.6),
("c4d-standard-8", 2.6, 15.3),
("c4d-standard-16", 5.1, 30.6),
("c4d-standard-32", 10.2, 61.1),
("c4d-standard-96", 30.7, 183.4),
("t2a-standard-1", 0.7, 1.8),
("t2a-standard-2", 1.3, 3.5),
("t2a-standard-4", 2.7, 7.0),
("t2a-standard-8", 5.4, 14.0),
("t2a-standard-16", 10.7, 28.0),
("t2a-standard-32", 21.4, 56.0),
("Standard_D2s_v6", 1.1, 6.4),
("Standard_D4s_v6", 2.2, 12.8),
("Standard_D8s_v6", 4.4, 25.6),
("Standard_D16s_v6", 8.8, 51.2),
("Standard_D32s_v6", 17.6, 102.4),
("Standard_D64s_v6", 35.2, 204.8),
("Standard_D96s_v6", 52.8, 307.2),
("Standard_D2ads_v6", 0.8, 4.1),
("Standard_D4ads_v6", 1.6, 8.2),
("Standard_D8ads_v6", 3.2, 16.4),
("Standard_D16ads_v6", 6.4, 32.8),
("Standard_D32ads_v6", 12.8, 65.6),
("Standard_D64ads_v6", 25.6, 131.2),
("Standard_D96ads_v6", 38.4, 196.8),
("Standard_D2ps_v6", 1.2, 4.4),
("Standard_D4ps_v6", 2.4, 8.8),
("Standard_D8ps_v6", 4.8, 17.6),
("Standard_D16ps_v6", 9.6, 35.2),
("Standard_D32ps_v6", 19.2, 70.4),
("Standard_D64ps_v6", 38.4, 140.8),
("Standard_D96ps_v6", 57.6, 211.2),
("Standard_E2s_v6", 1.4, 7.2),
("Standard_E4s_v6", 2.8, 14.4),
("Standard_E8s_v6", 5.7, 28.8),
("Standard_E16s_v6", 11.4, 57.6),
("Standard_E32s_v6", 22.7, 115.2),
("Standard_E64s_v6", 45.4, 230.4),
("Standard_E96s_v6", 68.2, 345.6),
("xeon-6780e", 100.0, 420.0),
];
static INSTANCE_POWER: LazyLock<HashMap<&'static str, (f64, f64)>> = LazyLock::new(|| {
super::table_data::GENERATED_INSTANCE_ROWS
.iter()
.chain(MANUAL_INSTANCE_ROWS)
.map(|&(name, idle, max)| (name, (idle, max)))
.collect()
});
static PROVIDER_DEFAULTS: LazyLock<HashMap<&'static str, (f64, f64)>> = LazyLock::new(|| {
let reference = |name: &str| *INSTANCE_POWER.get(name).expect("reference instance");
let mut m = HashMap::with_capacity(4);
m.insert("aws", reference("m5.large"));
m.insert("gcp", reference("n2-standard-2"));
m.insert("azure", reference("Standard_D2s_v6"));
m.insert("generic", (3.0, 20.0)); m
});
#[must_use]
pub fn lookup_instance_power(instance_type: &str, provider: &str) -> (f64, f64) {
if let Some(&power) = INSTANCE_POWER.get(instance_type) {
return power;
}
if let Some(&power) = PROVIDER_DEFAULTS.get(provider) {
return power;
}
*PROVIDER_DEFAULTS
.get("generic")
.expect("generic default must exist")
}
#[must_use]
pub fn is_known_instance_type(instance_type: &str) -> bool {
INSTANCE_POWER.contains_key(instance_type)
}
#[must_use]
pub fn interpolate_watts(idle_watts: f64, max_watts: f64, cpu_percent: f64) -> f64 {
if !cpu_percent.is_finite() {
return idle_watts;
}
let clamped = cpu_percent.clamp(0.0, 100.0);
idle_watts + (max_watts - idle_watts) * (clamped / 100.0)
}
#[must_use]
pub fn compute_cloud_energy_per_op_kwh(
watts: f64,
scrape_interval_secs: f64,
ops: u64,
) -> Option<f64> {
if ops == 0 || !watts.is_finite() || watts < 0.0 {
return None;
}
let kwh = (watts / 1000.0) * (scrape_interval_secs / 3600.0);
let per_op = kwh / ops as f64;
if per_op.is_finite() {
Some(per_op)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_aws_instance_scales_with_vcpu() {
let (idle_2, max_2) = lookup_instance_power("m5.large", "aws");
let (idle_4, max_4) = lookup_instance_power("m5.xlarge", "aws");
assert!((idle_4 - 2.0 * idle_2).abs() < 0.1);
assert!((max_4 - 2.0 * max_2).abs() < 0.2);
}
#[test]
fn known_gcp_instance_scales_with_vcpu() {
let (idle_2, max_2) = lookup_instance_power("n2-standard-2", "gcp");
let (idle_8, max_8) = lookup_instance_power("n2-standard-8", "gcp");
assert!((idle_8 - 4.0 * idle_2).abs() < 0.2);
assert!((max_8 - 4.0 * max_2).abs() < 0.4);
}
#[test]
fn known_azure_instance_scales_with_vcpu() {
let (idle_2, max_2) = lookup_instance_power("Standard_D2s_v3", "azure");
let (idle_8, max_8) = lookup_instance_power("Standard_D8s_v3", "azure");
assert!((idle_8 - 4.0 * idle_2).abs() < 0.2);
assert!((max_8 - 4.0 * max_2).abs() < 0.4);
}
#[test]
fn unknown_instance_falls_back_to_provider_default() {
let fallback = lookup_instance_power("m999.future", "aws");
let default = *PROVIDER_DEFAULTS.get("aws").expect("aws default");
assert_eq!(fallback, default);
}
#[test]
fn generated_and_manual_instance_keys_are_disjoint() {
assert_eq!(
INSTANCE_POWER.len(),
super::super::table_data::GENERATED_INSTANCE_ROWS.len() + MANUAL_INSTANCE_ROWS.len(),
"a manual instance row shadows a generated one"
);
}
#[test]
fn azure_scale_sane_vs_aws_same_architecture() {
let (aws_idle, aws_max) = lookup_instance_power("m5.large", "aws");
let (az_idle, az_max) = lookup_instance_power("Standard_D2s_v4", "azure");
assert!(
az_idle > aws_idle * 0.5 && az_idle < aws_idle * 2.0,
"Azure Cascade Lake idle {az_idle} out of 2x band vs AWS {aws_idle}"
);
assert!(
az_max > aws_max * 0.5 && az_max < aws_max * 2.0,
"Azure Cascade Lake max {az_max} out of 2x band vs AWS {aws_max}"
);
}
#[test]
fn modern_architecture_keys_present() {
for key in [
"m7i.large",
"c7a.large",
"r7a.large",
"m6a.xlarge",
"c7g.large",
"m8g.large",
"m8a.large",
"c8a.large",
"m8i.large",
"c8i.large",
"c4-standard-4",
"c4d-standard-8",
"c4a-standard-2",
"t2a-standard-2",
"Standard_D2s_v6",
"Standard_D2ps_v6",
"xeon-6780e",
] {
assert!(is_known_instance_type(key), "missing modern entry: {key}");
}
}
#[test]
fn turin_overrides_to_genoa_proxy() {
let sizes = [
"large", "xlarge", "2xlarge", "4xlarge", "8xlarge", "16xlarge",
];
for size in sizes {
for (turin, genoa) in [("m8a", "m7a"), ("c8a", "c7a")] {
let turin_key = format!("{turin}.{size}");
let genoa_key = format!("{genoa}.{size}");
assert_eq!(
lookup_instance_power(&turin_key, "aws"),
lookup_instance_power(&genoa_key, "aws"),
"{turin_key} (Turin) must alias to {genoa_key} (Genoa) until CCF correction"
);
}
}
}
#[test]
fn m_series_does_not_carry_dram_premium() {
assert_eq!(
lookup_instance_power("m5.large", "aws"),
lookup_instance_power("c5.large", "aws"),
"m5.large must stay on the bare coefficient, like c5.large"
);
}
#[test]
fn r_series_includes_dram_premium_over_general_purpose() {
let (m5_idle, m5_max) = lookup_instance_power("m5.large", "aws");
let (r5_idle, r5_max) = lookup_instance_power("r5.large", "aws");
assert!(
(r5_idle - m5_idle - 0.32).abs() < 0.05,
"DRAM idle uplift drift: r5 {r5_idle} - m5 {m5_idle} expected ~0.32"
);
assert!(
(r5_max - m5_max - 0.80).abs() < 0.05,
"DRAM max uplift drift: r5 {r5_max} - m5 {m5_max} expected ~0.80"
);
}
#[test]
fn sierra_forest_entries_are_chip_level_not_vcpu_level() {
let (idle, _) = lookup_instance_power("xeon-6780e", "generic");
assert!(
idle >= 50.0,
"xeon-6780e must be system-level (>=50W idle), got {idle}"
);
}
#[test]
fn unknown_provider_falls_back_to_generic() {
let (idle, max) = lookup_instance_power("custom.instance", "onprem");
assert!((idle - 3.0).abs() < 0.01);
assert!((max - 20.0).abs() < 0.01);
}
#[test]
fn is_known_true_for_table_entry() {
assert!(is_known_instance_type("c5.4xlarge"));
}
#[test]
fn is_known_false_for_missing_entry() {
assert!(!is_known_instance_type("m99.jumbo"));
}
#[test]
fn interpolate_at_zero_percent() {
let w = interpolate_watts(2.0, 20.0, 0.0);
assert!((w - 2.0).abs() < 1e-10);
}
#[test]
fn interpolate_at_fifty_percent() {
let w = interpolate_watts(2.0, 20.0, 50.0);
assert!((w - 11.0).abs() < 1e-10);
}
#[test]
fn interpolate_at_hundred_percent() {
let w = interpolate_watts(2.0, 20.0, 100.0);
assert!((w - 20.0).abs() < 1e-10);
}
#[test]
fn interpolate_clamps_below_zero() {
let w = interpolate_watts(2.0, 20.0, -10.0);
assert!((w - 2.0).abs() < 1e-10, "should clamp to idle");
}
#[test]
fn interpolate_clamps_above_hundred() {
let w = interpolate_watts(2.0, 20.0, 150.0);
assert!((w - 20.0).abs() < 1e-10, "should clamp to max");
}
#[test]
fn interpolate_nan_returns_idle() {
let w = interpolate_watts(2.0, 20.0, f64::NAN);
assert!((w - 2.0).abs() < 1e-10, "NaN input should return idle");
}
#[test]
fn interpolate_infinity_returns_idle() {
let w = interpolate_watts(2.0, 20.0, f64::INFINITY);
assert!((w - 2.0).abs() < 1e-10, "Inf input should return idle");
}
#[test]
fn basic_energy_computation() {
let result = compute_cloud_energy_per_op_kwh(10.0, 15.0, 100);
assert!(result.is_some());
let per_op = result.unwrap();
let expected = (10.0 / 1000.0) * (15.0 / 3600.0) / 100.0;
assert!((per_op - expected).abs() < 1e-15);
}
#[test]
fn zero_ops_returns_none() {
assert!(compute_cloud_energy_per_op_kwh(10.0, 15.0, 0).is_none());
}
#[test]
fn negative_watts_returns_none() {
assert!(compute_cloud_energy_per_op_kwh(-1.0, 15.0, 100).is_none());
}
#[test]
fn nan_watts_returns_none() {
assert!(compute_cloud_energy_per_op_kwh(f64::NAN, 15.0, 100).is_none());
}
#[test]
fn infinite_watts_returns_none() {
assert!(compute_cloud_energy_per_op_kwh(f64::INFINITY, 15.0, 100).is_none());
}
#[test]
fn all_entries_have_positive_values() {
for (name, &(idle, max)) in INSTANCE_POWER.iter() {
assert!(idle > 0.0, "{name}: idle must be positive, got {idle}");
assert!(max > 0.0, "{name}: max must be positive, got {max}");
assert!(max >= idle, "{name}: max ({max}) must be >= idle ({idle})");
}
}
#[test]
fn table_has_expected_entry_count() {
assert!(
INSTANCE_POWER.len() >= 300,
"expected >= 300 entries, got {}",
INSTANCE_POWER.len()
);
}
}