use std::collections::{BTreeMap, BTreeSet};
use cbh_model::sanitize_segment;
use sha2::{Digest, Sha256};
const FINGERPRINT_VERSION: &str = "mk2";
const FINGERPRINT_HEX_LEN: usize = 16;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HardwareProfile {
pub processors: usize,
pub memory_regions: usize,
pub processor_models: Vec<String>,
pub processor_speeds: Vec<(u64, usize)>,
}
fn canonical(profile: &HardwareProfile) -> String {
format!(
"{FINGERPRINT_VERSION}\nprocessors={}\nmemory_regions={}\nprocessor_models={}\nprocessor_speeds={}",
profile.processors,
profile.memory_regions,
render_models(&profile.processor_models),
render_speed_histogram(&profile.processor_speeds),
)
}
fn normalize_model(model: &str) -> String {
model.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn render_speed_histogram(speeds: &[(u64, usize)]) -> String {
let mut merged: BTreeMap<u64, usize> = BTreeMap::new();
for &(speed, count) in speeds {
if count == 0 {
continue;
}
let total = merged.entry(speed).or_insert(0);
*total = total
.checked_add(count)
.expect("processor count cannot exceed the usize range");
}
merged
.iter()
.map(|(speed, count)| format!("{speed}x{count}"))
.collect::<Vec<_>>()
.join(",")
}
fn speed_histogram(speeds: impl IntoIterator<Item = u64>) -> Vec<(u64, usize)> {
let mut counts: BTreeMap<u64, usize> = BTreeMap::new();
for speed in speeds {
let count = counts.entry(speed).or_insert(0);
*count = count
.checked_add(1)
.expect("processor count cannot exceed the usize range");
}
counts.into_iter().collect()
}
fn render_models(models: &[String]) -> String {
let mut distinct: BTreeSet<String> = BTreeSet::new();
for model in models {
let normalized = normalize_model(model);
if !normalized.is_empty() {
distinct.insert(normalized);
}
}
distinct.into_iter().collect::<Vec<_>>().join(",")
}
fn distinct_models(models: impl IntoIterator<Item = String>) -> Vec<String> {
let mut distinct: BTreeSet<String> = BTreeSet::new();
for model in models {
let normalized = normalize_model(&model);
if !normalized.is_empty() {
distinct.insert(normalized);
}
}
distinct.into_iter().collect()
}
pub(crate) fn fingerprint(profile: &HardwareProfile) -> String {
use std::fmt::Write as _;
let mut hasher = Sha256::new();
hasher.update(canonical(profile).as_bytes());
let digest = hasher.finalize();
let bytes_needed = FINGERPRINT_HEX_LEN.div_ceil(2);
let mut hex = String::with_capacity(FINGERPRINT_HEX_LEN);
for byte in digest.iter().take(bytes_needed) {
write!(hex, "{byte:02x}").expect("writing to a String never fails");
}
hex.truncate(FINGERPRINT_HEX_LEN);
hex
}
#[must_use]
pub fn resolve_machine_key(override_key: Option<&str>, profile: &HardwareProfile) -> String {
match override_key {
Some(key) => sanitize_segment(key),
None => fingerprint(profile),
}
}
#[must_use]
pub fn describe_fingerprint_components(profile: &HardwareProfile) -> String {
let models = render_models(&profile.processor_models);
let models = if models.is_empty() {
"<none>".to_owned()
} else {
models
};
let speeds = render_speed_histogram(&profile.processor_speeds);
let speeds = if speeds.is_empty() {
"<none>".to_owned()
} else {
speeds
};
format!(
"version={FINGERPRINT_VERSION}, processors={}, memory_regions={}, processor_models={models}, processor_speeds={speeds}",
profile.processors, profile.memory_regions,
)
}
#[cfg_attr(test, mutants::skip)] pub(crate) fn system_profile() -> HardwareProfile {
let hardware = many_cpus::SystemHardware::current();
let all_processors = hardware.all_processors();
HardwareProfile {
processors: hardware.max_processor_count(),
memory_regions: hardware.max_memory_region_count(),
processor_models: distinct_models(
all_processors
.iter()
.map(|processor| processor.model().to_owned()),
),
processor_speeds: speed_histogram(
all_processors
.iter()
.map(|processor| processor.relative_speed().as_u64()),
),
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
fn profile(processors: usize, memory_regions: usize, models: &[&str]) -> HardwareProfile {
HardwareProfile {
processors,
memory_regions,
processor_models: models.iter().map(|model| (*model).to_owned()).collect(),
processor_speeds: Vec::new(),
}
}
fn profile_with_speeds(
processors: usize,
memory_regions: usize,
models: &[&str],
processor_speeds: Vec<(u64, usize)>,
) -> HardwareProfile {
HardwareProfile {
processors,
memory_regions,
processor_models: models.iter().map(|model| (*model).to_owned()).collect(),
processor_speeds,
}
}
#[test]
fn fingerprint_is_stable_for_a_fixed_profile() {
let key = fingerprint(&profile(8, 1, &["Test CPU 3000"]));
assert_eq!(key, "1e3277ddba18263f");
}
#[test]
fn fingerprint_is_stable_for_a_fixed_profile_with_speeds() {
let key = fingerprint(&profile_with_speeds(
4,
1,
&["Test CPU 3000"],
vec![(3141, 2), (6283, 2)],
));
assert_eq!(key, "55e2e3746d2a53be");
}
#[test]
fn fingerprint_is_sixteen_lowercase_hex_chars() {
let key = fingerprint(&profile(4, 2, &[]));
assert_eq!(key.len(), FINGERPRINT_HEX_LEN);
assert!(
key.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
"{key}"
);
}
#[test]
fn distinct_factors_produce_distinct_fingerprints() {
let base = fingerprint(&profile(8, 1, &["Model A"]));
assert_ne!(base, fingerprint(&profile(16, 1, &["Model A"])));
assert_ne!(base, fingerprint(&profile(8, 2, &["Model A"])));
assert_ne!(base, fingerprint(&profile(8, 1, &["Model B"])));
assert_ne!(base, fingerprint(&profile(8, 1, &["Model A", "Model B"])));
assert_ne!(base, fingerprint(&profile(8, 1, &[])));
assert_ne!(
base,
fingerprint(&profile_with_speeds(8, 1, &["Model A"], vec![(3141, 8)])),
);
}
#[test]
fn distinct_speed_histograms_produce_distinct_fingerprints() {
let uniform = profile_with_speeds(8, 1, &["Model A"], vec![(3141, 8)]);
let hybrid = profile_with_speeds(8, 1, &["Model A"], vec![(3141, 4), (6283, 4)]);
assert_ne!(fingerprint(&uniform), fingerprint(&hybrid));
}
#[test]
fn speed_histogram_order_does_not_change_the_fingerprint() {
let ascending = profile_with_speeds(8, 1, &["Model A"], vec![(3141, 4), (6283, 4)]);
let descending = profile_with_speeds(8, 1, &["Model A"], vec![(6283, 4), (3141, 4)]);
assert_eq!(fingerprint(&ascending), fingerprint(&descending));
}
#[test]
fn equivalent_speed_histogram_representations_produce_the_same_fingerprint() {
let canonical = profile_with_speeds(8, 1, &["Model A"], vec![(3141, 4)]);
let split = profile_with_speeds(8, 1, &["Model A"], vec![(3141, 1), (3141, 3)]);
let zero_padded = profile_with_speeds(8, 1, &["Model A"], vec![(3141, 4), (6283, 0)]);
assert_eq!(fingerprint(&canonical), fingerprint(&split));
assert_eq!(fingerprint(&canonical), fingerprint(&zero_padded));
}
#[test]
fn model_order_and_duplicates_do_not_change_the_fingerprint() {
let ascending = profile(8, 1, &["Model A", "Model B"]);
let shuffled = profile(8, 1, &["Model B", "Model A", "Model A"]);
assert_eq!(fingerprint(&ascending), fingerprint(&shuffled));
}
#[test]
fn model_whitespace_differences_do_not_change_the_fingerprint() {
assert_eq!(
fingerprint(&profile(8, 1, &["Intel Xeon E5"])),
fingerprint(&profile(8, 1, &[" Intel Xeon E5 "])),
);
}
#[test]
fn canonical_is_versioned_and_ordered() {
let rendered = canonical(&profile(8, 1, &["CPU X"]));
assert_eq!(
rendered,
"mk2\nprocessors=8\nmemory_regions=1\nprocessor_models=CPU X\nprocessor_speeds="
);
}
#[test]
fn canonical_renders_the_speed_histogram_in_order() {
let rendered = canonical(&profile_with_speeds(
4,
1,
&["CPU X"],
vec![(3141, 2), (6283, 2)],
));
assert_eq!(
rendered,
"mk2\nprocessors=4\nmemory_regions=1\nprocessor_models=CPU X\nprocessor_speeds=3141x2,6283x2"
);
}
#[test]
fn canonical_renders_absent_models_as_empty() {
let rendered = canonical(&profile(2, 1, &[]));
assert!(rendered.contains("\nprocessor_models=\n"), "{rendered}");
}
#[test]
fn normalize_model_collapses_and_trims_whitespace() {
assert_eq!(normalize_model(" a b\tc \n"), "a b c");
assert_eq!(normalize_model(" "), "");
}
#[test]
fn resolve_machine_key_uses_fingerprint_without_override() {
let hardware = profile(8, 1, &["CPU X"]);
assert_eq!(resolve_machine_key(None, &hardware), fingerprint(&hardware));
}
#[test]
fn resolve_machine_key_prefers_sanitized_override() {
let hardware = profile(8, 1, &["CPU X"]);
assert_eq!(
resolve_machine_key(Some("ci/pool one"), &hardware),
"ci_pool_one"
);
}
#[test]
fn describe_components_lists_version_and_normalized_factors() {
let described = describe_fingerprint_components(&profile_with_speeds(
8,
1,
&["Intel Xeon E5", "AMD EPYC"],
vec![(3141, 6), (6283, 2)],
));
assert_eq!(
described,
"version=mk2, processors=8, memory_regions=1, processor_models=AMD EPYC,Intel Xeon E5, processor_speeds=3141x6,6283x2"
);
}
#[test]
fn describe_components_marks_absent_factors() {
let described = describe_fingerprint_components(&profile(4, 2, &[]));
assert_eq!(
described,
"version=mk2, processors=4, memory_regions=2, processor_models=<none>, processor_speeds=<none>"
);
}
#[test]
fn speed_histogram_counts_and_orders_speeds() {
let histogram = speed_histogram([6283, 3141, 6283, 3141, 3141]);
assert_eq!(histogram, vec![(3141, 3), (6283, 2)]);
assert_eq!(speed_histogram(std::iter::empty()), Vec::new());
}
#[test]
fn render_speed_histogram_joins_pairs_with_commas() {
assert_eq!(
render_speed_histogram(&[(3141, 4), (6283, 2)]),
"3141x4,6283x2"
);
assert_eq!(render_speed_histogram(&[]), "");
}
#[test]
fn render_speed_histogram_sorts_pairs_ascending() {
assert_eq!(
render_speed_histogram(&[(6283, 2), (3141, 4)]),
"3141x4,6283x2"
);
}
#[test]
fn render_speed_histogram_merges_duplicate_speeds_and_drops_zero_counts() {
assert_eq!(render_speed_histogram(&[(3141, 2), (3141, 2)]), "3141x4");
assert_eq!(render_speed_histogram(&[(3141, 4), (6283, 0)]), "3141x4");
assert_eq!(render_speed_histogram(&[(3141, 0)]), "");
}
#[test]
fn distinct_models_normalizes_sorts_and_dedups() {
let models = distinct_models([
" Intel Xeon ".to_owned(),
"AMD EPYC".to_owned(),
"Intel Xeon".to_owned(),
String::new(),
" ".to_owned(),
]);
assert_eq!(models, vec!["AMD EPYC".to_owned(), "Intel Xeon".to_owned()]);
assert_eq!(
distinct_models(std::iter::empty::<String>()),
Vec::<String>::new()
);
}
#[test]
fn render_models_joins_sorted_distinct_with_commas() {
assert_eq!(
render_models(&["Intel Xeon".to_owned(), "AMD EPYC".to_owned()]),
"AMD EPYC,Intel Xeon"
);
assert_eq!(
render_models(&["AMD EPYC".to_owned(), " AMD EPYC ".to_owned()]),
"AMD EPYC"
);
assert_eq!(render_models(&[]), "");
}
#[test]
#[cfg_attr(miri, ignore)] fn system_profile_reports_at_least_one_processor() {
let hardware = system_profile();
assert!(hardware.processors >= 1, "{hardware:?}");
assert!(hardware.memory_regions >= 1, "{hardware:?}");
assert_eq!(fingerprint(&hardware).len(), FINGERPRINT_HEX_LEN);
}
}