use std::collections::HashMap;
use std::sync::LazyLock;
static INSTANCE_POWER: LazyLock<HashMap<&'static str, (f64, f64)>> = LazyLock::new(|| {
let entries: &[(&str, f64, f64)] = &[
("t3.nano", 2.0, 12.5),
("t3.micro", 2.0, 13.0),
("t3.small", 2.0, 14.0),
("t3.medium", 2.0, 16.0),
("t3.large", 2.0, 20.0),
("t3.xlarge", 4.0, 39.9),
("t3.2xlarge", 8.0, 79.8),
("t3a.nano", 1.7, 10.5),
("t3a.micro", 1.7, 10.8),
("t3a.small", 1.7, 11.4),
("t3a.medium", 1.7, 12.6),
("t3a.large", 1.7, 15.0),
("t3a.xlarge", 3.3, 29.9),
("t3a.2xlarge", 6.7, 59.8),
("m5.large", 2.0, 20.0),
("m5.xlarge", 4.0, 39.9),
("m5.2xlarge", 8.0, 79.8),
("m5.4xlarge", 16.0, 159.6),
("m5.8xlarge", 32.0, 319.3),
("m5.12xlarge", 48.0, 478.9),
("m5.16xlarge", 64.0, 638.5),
("m5.24xlarge", 96.0, 957.8),
("m5a.large", 1.7, 15.0),
("m5a.xlarge", 3.3, 29.9),
("m5a.2xlarge", 6.7, 59.8),
("m5a.4xlarge", 13.3, 119.6),
("m5a.8xlarge", 26.7, 239.3),
("m5a.12xlarge", 40.0, 358.9),
("m5a.16xlarge", 53.3, 478.5),
("m5a.24xlarge", 80.0, 717.8),
("c5.large", 2.7, 18.0),
("c5.xlarge", 5.3, 35.9),
("c5.2xlarge", 10.7, 71.9),
("c5.4xlarge", 21.3, 143.7),
("c5.9xlarge", 48.0, 323.4),
("c5.12xlarge", 48.0, 466.6),
("c5.18xlarge", 96.0, 646.8),
("c5.24xlarge", 96.0, 933.2),
("c5a.large", 1.2, 9.5),
("c5a.xlarge", 2.3, 19.0),
("c5a.2xlarge", 4.7, 38.0),
("c5a.4xlarge", 9.3, 76.1),
("c5a.8xlarge", 18.7, 152.1),
("c5a.12xlarge", 28.0, 228.2),
("c5a.16xlarge", 37.3, 304.3),
("c5a.24xlarge", 56.0, 456.4),
("r5.large", 2.0, 27.9),
("r5.xlarge", 4.0, 55.9),
("r5.2xlarge", 8.0, 111.8),
("r5.4xlarge", 16.0, 223.6),
("r5.8xlarge", 32.0, 447.2),
("r5.12xlarge", 48.0, 670.8),
("r5.16xlarge", 64.0, 894.4),
("r5.24xlarge", 96.0, 1341.6),
("r5a.large", 1.7, 19.8),
("r5a.xlarge", 3.3, 39.5),
("r5a.2xlarge", 6.7, 79.0),
("r5a.4xlarge", 13.3, 158.0),
("r5a.8xlarge", 26.7, 316.1),
("r5a.12xlarge", 40.0, 474.1),
("r5a.16xlarge", 53.3, 632.1),
("r5a.24xlarge", 80.0, 948.2),
("m6i.large", 1.9, 16.2),
("m6i.xlarge", 3.8, 32.4),
("m6i.2xlarge", 7.5, 64.9),
("m6i.4xlarge", 15.0, 129.8),
("m6i.8xlarge", 30.0, 259.6),
("m6i.12xlarge", 45.0, 389.4),
("m6i.16xlarge", 60.0, 519.2),
("m6i.24xlarge", 90.0, 778.7),
("m6i.32xlarge", 120.0, 1038.3),
("c6i.large", 1.9, 16.2),
("c6i.xlarge", 3.8, 32.4),
("c6i.2xlarge", 7.5, 64.9),
("c6i.4xlarge", 15.0, 129.8),
("c6i.8xlarge", 30.0, 259.6),
("c6i.12xlarge", 45.0, 389.4),
("c6i.16xlarge", 60.0, 519.2),
("c6i.24xlarge", 90.0, 778.7),
("c6i.32xlarge", 120.0, 1038.3),
("r6i.large", 1.9, 18.6),
("r6i.xlarge", 3.8, 37.3),
("r6i.2xlarge", 7.5, 74.6),
("r6i.4xlarge", 15.0, 149.3),
("r6i.8xlarge", 30.0, 298.5),
("r6i.12xlarge", 45.0, 447.8),
("r6i.16xlarge", 60.0, 597.1),
("r6i.24xlarge", 90.0, 895.6),
("n2-standard-2", 1.3, 7.3),
("n2-standard-4", 2.6, 14.6),
("n2-standard-8", 5.1, 29.1),
("n2-standard-16", 10.2, 58.3),
("n2-standard-32", 20.4, 116.5),
("n2-standard-48", 30.6, 174.8),
("n2-standard-64", 40.8, 233.1),
("n2-standard-80", 51.0, 291.4),
("n2-standard-96", 61.2, 349.6),
("n2-standard-128", 81.7, 466.2),
("n2-highcpu-2", 1.3, 7.3),
("n2-highcpu-4", 2.6, 14.6),
("n2-highcpu-8", 5.1, 29.1),
("n2-highcpu-16", 10.2, 58.3),
("n2-highcpu-32", 20.4, 116.5),
("n2-highcpu-48", 30.6, 174.8),
("n2-highcpu-64", 40.8, 233.1),
("n2-highcpu-80", 51.0, 291.4),
("n2-highcpu-96", 61.2, 349.6),
("n2-highmem-2", 1.3, 7.3),
("n2-highmem-4", 2.6, 14.6),
("n2-highmem-8", 5.1, 29.1),
("n2-highmem-16", 10.2, 58.3),
("n2-highmem-32", 20.4, 116.5),
("n2-highmem-48", 30.6, 174.8),
("n2-highmem-64", 40.8, 233.1),
("n2-highmem-80", 51.0, 291.4),
("n2-highmem-96", 61.2, 349.6),
("n2-highmem-128", 81.7, 466.2),
("e2-standard-2", 0.9, 3.2),
("e2-standard-4", 1.9, 6.3),
("e2-standard-8", 3.8, 12.6),
("e2-standard-16", 7.6, 25.2),
("e2-standard-32", 15.2, 50.4),
("c2-standard-4", 2.6, 14.6),
("c2-standard-8", 5.1, 29.1),
("c2-standard-16", 10.2, 58.3),
("c2-standard-30", 19.1, 109.3),
("c2-standard-60", 38.3, 218.5),
("Standard_D2s_v3", 1.3, 8.4),
("Standard_D4s_v3", 2.6, 16.8),
("Standard_D8s_v3", 5.2, 33.5),
("Standard_D16s_v3", 10.3, 67.1),
("Standard_D32s_v3", 20.6, 134.2),
("Standard_D48s_v3", 31.0, 201.3),
("Standard_D64s_v3", 41.3, 268.4),
("Standard_D2s_v4", 1.3, 7.9),
("Standard_D4s_v4", 2.6, 15.9),
("Standard_D8s_v4", 5.1, 31.7),
("Standard_D16s_v4", 10.2, 63.5),
("Standard_D32s_v4", 20.4, 126.9),
("Standard_D48s_v4", 30.7, 190.4),
("Standard_D64s_v4", 40.9, 253.9),
("Standard_D2s_v5", 1.3, 7.9),
("Standard_D4s_v5", 2.6, 15.9),
("Standard_D8s_v5", 5.1, 31.7),
("Standard_D16s_v5", 10.2, 63.5),
("Standard_D32s_v5", 20.4, 126.9),
("Standard_D48s_v5", 30.7, 190.4),
("Standard_D64s_v5", 40.9, 253.9),
("Standard_D96s_v5", 61.3, 380.8),
("Standard_D2as_v5", 0.9, 4.0),
("Standard_D4as_v5", 1.8, 8.1),
("Standard_D8as_v5", 3.6, 16.2),
("Standard_D16as_v5", 7.1, 32.3),
("Standard_D32as_v5", 14.2, 64.6),
("Standard_D48as_v5", 21.4, 96.9),
("Standard_D64as_v5", 28.5, 129.2),
("Standard_D96as_v5", 42.7, 193.8),
("Standard_E2s_v3", 1.3, 8.4),
("Standard_E4s_v3", 2.6, 16.8),
("Standard_E8s_v3", 5.2, 33.5),
("Standard_E16s_v3", 10.3, 67.1),
("Standard_E32s_v3", 20.6, 134.2),
("Standard_E48s_v3", 31.0, 201.3),
("Standard_E64s_v3", 41.3, 268.4),
("Standard_E2s_v4", 1.3, 7.9),
("Standard_E4s_v4", 2.6, 15.9),
("Standard_E8s_v4", 5.1, 31.7),
("Standard_E16s_v4", 10.2, 63.5),
("Standard_E32s_v4", 20.4, 126.9),
("Standard_E48s_v4", 30.7, 190.4),
("Standard_E64s_v4", 40.9, 253.9),
("Standard_E2s_v5", 1.3, 7.9),
("Standard_E4s_v5", 2.6, 15.9),
("Standard_E8s_v5", 5.1, 31.7),
("Standard_E16s_v5", 10.2, 63.5),
("Standard_E32s_v5", 20.4, 126.9),
("Standard_E48s_v5", 30.7, 190.4),
("Standard_E64s_v5", 40.9, 253.9),
("Standard_E96s_v5", 61.3, 380.8),
("Standard_F2s_v2", 1.3, 7.9),
("Standard_F4s_v2", 2.6, 15.9),
("Standard_F8s_v2", 5.1, 31.7),
("Standard_F16s_v2", 10.2, 63.5),
("Standard_F32s_v2", 20.4, 126.9),
("Standard_F48s_v2", 30.7, 190.4),
("Standard_F64s_v2", 40.9, 253.9),
("Standard_F72s_v2", 46.0, 285.6),
];
let mut m = HashMap::with_capacity(entries.len());
for &(name, idle, max) in entries {
m.insert(name, (idle, max));
}
m
});
static PROVIDER_DEFAULTS: LazyLock<HashMap<&'static str, (f64, f64)>> = LazyLock::new(|| {
let mut m = HashMap::with_capacity(4);
m.insert("aws", (2.0, 20.0)); m.insert("gcp", (1.3, 7.3)); m.insert("azure", (1.3, 7.9)); 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() {
let (idle, max) = lookup_instance_power("m5.large", "aws");
assert!((idle - 2.0).abs() < 0.01);
assert!((max - 20.0).abs() < 0.01);
}
#[test]
fn known_gcp_instance() {
let (idle, max) = lookup_instance_power("n2-standard-8", "gcp");
assert!((idle - 5.1).abs() < 0.01);
assert!((max - 29.1).abs() < 0.01);
}
#[test]
fn known_azure_instance() {
let (idle, max) = lookup_instance_power("Standard_D8s_v3", "azure");
assert!((idle - 5.2).abs() < 0.01);
assert!((max - 33.5).abs() < 0.1);
}
#[test]
fn unknown_instance_falls_back_to_provider_default() {
let (idle, max) = lookup_instance_power("m7i.large", "aws");
assert!((idle - 2.0).abs() < 0.01);
assert!((max - 20.0).abs() < 0.01);
}
#[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() >= 150,
"expected >= 150 entries, got {}",
INSTANCE_POWER.len()
);
}
}