use std::collections::{BTreeMap, BTreeSet};
use sha2::{Digest, Sha256};
const FINGERPRINT_VERSION: &str = "mk3";
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={}",
profile.processors,
profile.memory_regions,
render_models(&profile.processor_models),
)
}
fn normalize_model(model: &str) -> String {
model.split_whitespace().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()
}
fn distinct_memory_regions(regions: impl IntoIterator<Item = many_cpus::MemoryRegionId>) -> usize {
regions.into_iter().collect::<BTreeSet<_>>().len()
}
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(profile: &HardwareProfile) -> String {
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
};
format!(
"version={FINGERPRINT_VERSION}, processors={}, memory_regions={}, \
processor_models={models}",
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: all_processors.len(),
memory_regions: distinct_memory_regions(
all_processors
.iter()
.map(many_cpus::Processor::memory_region_id),
),
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, "4ffd697efb295d32");
}
#[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, &[])));
}
#[test]
fn processor_speeds_do_not_change_the_fingerprint() {
let absent = profile(8, 1, &["Model A"]);
let uniform = profile_with_speeds(8, 1, &["Model A"], vec![(3141, 8)]);
let drifted = profile_with_speeds(8, 1, &["Model A"], vec![(3142, 8)]);
let hybrid = profile_with_speeds(8, 1, &["Model A"], vec![(3141, 4), (6283, 4)]);
assert_eq!(fingerprint(&absent), fingerprint(&uniform));
assert_eq!(fingerprint(&absent), fingerprint(&drifted));
assert_eq!(fingerprint(&absent), fingerprint(&hybrid));
}
#[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,
"mk3\nprocessors=8\nmemory_regions=1\nprocessor_models=CPU X"
);
}
#[test]
fn canonical_ignores_the_provenance_speed_histogram() {
let rendered = canonical(&profile_with_speeds(
8,
1,
&["CPU X"],
vec![(3141, 2), (6283, 6)],
));
assert_eq!(
rendered,
"mk3\nprocessors=8\nmemory_regions=1\nprocessor_models=CPU X"
);
}
#[test]
fn canonical_renders_absent_models_as_empty() {
let rendered = canonical(&profile(2, 1, &[]));
assert!(rendered.ends_with("\nprocessor_models="), "{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() {
let hardware = profile(8, 1, &["CPU X"]);
assert_eq!(resolve_machine_key(&hardware), fingerprint(&hardware));
}
#[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=mk3, processors=8, memory_regions=1, \
processor_models=AMD EPYC,Intel Xeon E5"
);
}
#[test]
fn describe_components_marks_absent_factors() {
let described = describe_fingerprint_components(&profile(4, 2, &[]));
assert_eq!(
described,
"version=mk3, processors=4, memory_regions=2, processor_models=<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 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]
fn distinct_memory_regions_counts_each_region_once() {
assert_eq!(distinct_memory_regions([0, 0, 0]), 1);
assert_eq!(distinct_memory_regions([3, 0, 3, 7]), 3);
assert_eq!(distinct_memory_regions([]), 0);
}
#[test]
fn a_production_arm_runner_keeps_one_key_across_its_calibration_readings() {
let uniform = profile_with_speeds(4, 1, &["Cobalt 100"], vec![(10678, 4)]);
let recalibrated = profile_with_speeds(4, 1, &["Cobalt 100"], vec![(10678, 3), (10681, 1)]);
assert_eq!(
canonical(&uniform),
"mk3\nprocessors=4\nmemory_regions=1\nprocessor_models=Cobalt 100",
);
assert_eq!(
canonical(&recalibrated),
canonical(&uniform),
"the 10681 reading is provenance, so it must leave the factor string alone",
);
assert_eq!(
resolve_machine_key(&uniform),
"2e3ad42f4e2cd3e1",
"this runner's production history is filed under this key",
);
assert_eq!(
resolve_machine_key(&recalibrated),
resolve_machine_key(&uniform),
"one machine's two calibration readings are one machine",
);
}
}