use std::collections::HashMap;
use std::time::Instant;
use lightcycle::hashes::ConsistentHasher;
use lightcycle::{HasId, HashRing, RendezvousRing};
#[derive(Debug, Clone)]
struct TestNode {
id: String,
}
impl HasId for TestNode {
fn id(&self) -> &str {
&self.id
}
}
#[derive(Debug)]
struct HasherEvaluation {
name: String,
hash_speed_ns: f64,
distribution_chi_squared: f64,
distribution_stddev: f64,
weighted_accuracy: f64,
consistency_score: f64,
}
fn measure_hash_speed<H: ConsistentHasher>(hasher: &H, iterations: usize) -> f64 {
let test_keys: Vec<String> = (0..1000).map(|i| format!("test-key-{}", i)).collect();
let start = Instant::now();
for _ in 0..iterations {
for key in &test_keys {
let _ = hasher.hash(key.as_bytes());
}
}
let duration = start.elapsed();
duration.as_nanos() as f64 / (iterations * test_keys.len()) as f64
}
fn test_distribution_quality<H: ConsistentHasher>(hasher: H, nodes: usize, keys: usize) -> (f64, f64) {
let mut ring = RendezvousRing::new_with_hasher(hasher);
for i in 0..nodes {
ring.add(Box::new(TestNode {
id: format!("node-{}", i),
}));
}
let mut counts: HashMap<String, usize> = HashMap::new();
for i in 0..keys {
let key = format!("key-{}", i);
if let Some(node) = ring.locate(&key) {
*counts.entry(node.id().to_string()).or_insert(0) += 1;
}
}
let expected = keys as f64 / nodes as f64;
let mut chi_squared = 0.0;
let mut sum_sq_diff = 0.0;
for i in 0..nodes {
let node_id = format!("node-{}", i);
let observed = counts.get(&node_id).copied().unwrap_or(0) as f64;
let diff = observed - expected;
chi_squared += (diff * diff) / expected;
sum_sq_diff += diff * diff;
}
let stddev = (sum_sq_diff / nodes as f64).sqrt();
(chi_squared, stddev)
}
fn test_weighted_distribution<H: ConsistentHasher>(hasher: H) -> f64 {
let mut ring = RendezvousRing::new_with_hasher(hasher);
ring.add_weighted(
Box::new(TestNode {
id: "light".to_string(),
}),
1.0,
);
ring.add_weighted(
Box::new(TestNode {
id: "medium".to_string(),
}),
5.0,
);
ring.add_weighted(
Box::new(TestNode {
id: "heavy".to_string(),
}),
10.0,
);
let mut counts: HashMap<String, usize> = HashMap::new();
for i in 0..10000 {
let key = format!("weighted-key-{}", i);
if let Some(node) = ring.locate(&key) {
*counts.entry(node.id().to_string()).or_insert(0) += 1;
}
}
let light_count = counts.get("light").copied().unwrap_or(0) as f64;
let medium_count = counts.get("medium").copied().unwrap_or(0) as f64;
let heavy_count = counts.get("heavy").copied().unwrap_or(0) as f64;
let total_weight = 16.0; let expected_light = 10000.0 / total_weight;
let expected_medium = 5.0 * 10000.0 / total_weight;
let expected_heavy = 10.0 * 10000.0 / total_weight;
let light_error = ((light_count - expected_light).abs() / expected_light) * 100.0;
let medium_error = ((medium_count - expected_medium).abs() / expected_medium) * 100.0;
let heavy_error = ((heavy_count - expected_heavy).abs() / expected_heavy) * 100.0;
100.0 - (light_error + medium_error + heavy_error) / 3.0
}
fn test_consistency<H: ConsistentHasher>(hasher: H) -> f64 {
let mut ring = RendezvousRing::new_with_hasher(hasher);
for i in 0..10 {
ring.add(Box::new(TestNode {
id: format!("node-{}", i),
}));
}
let mut consistency_count = 0;
let test_count = 1000;
for i in 0..test_count {
let key = format!("consistency-key-{}", i);
let first_location = ring.locate(&key).map(|n| n.id().to_string());
let mut consistent = true;
for _ in 0..10 {
let location = ring.locate(&key).map(|n| n.id().to_string());
if location != first_location {
consistent = false;
break;
}
}
if consistent {
consistency_count += 1;
}
}
(consistency_count as f64 / test_count as f64) * 100.0
}
fn evaluate_hasher<H: ConsistentHasher>(hasher: H) -> HasherEvaluation {
let name = hasher.name().to_string();
println!("\nEvaluating {}...", name);
let hash_speed_ns = measure_hash_speed(&hasher, 100);
println!(" Hash speed: {:.2} ns/op", hash_speed_ns);
let (chi_squared, stddev) = test_distribution_quality(hasher.clone(), 10, 10000);
println!(" Distribution χ²: {:.2}, σ: {:.2}", chi_squared, stddev);
let weighted_accuracy = test_weighted_distribution(hasher.clone());
println!(" Weighted accuracy: {:.1}%", weighted_accuracy);
let consistency_score = test_consistency(hasher);
println!(" Consistency: {:.1}%", consistency_score);
HasherEvaluation {
name,
hash_speed_ns,
distribution_chi_squared: chi_squared,
distribution_stddev: stddev,
weighted_accuracy,
consistency_score,
}
}
#[test]
#[ignore]
fn evaluate_algorithms() {
println!("\n=== Hash Algorithm Evaluation for Rendezvous Hashing ===");
let mut results = Vec::new();
#[cfg(feature = "hash-blake3")]
{
use lightcycle::hashes::Blake3Hasher;
results.push(evaluate_hasher(Blake3Hasher::new()));
}
#[cfg(feature = "hash-xxhash")]
{
use lightcycle::hashes::XXHasher;
results.push(evaluate_hasher(XXHasher::new()));
}
#[cfg(feature = "hash-metrohash")]
{
use lightcycle::hashes::MetroHasher;
results.push(evaluate_hasher(MetroHasher::new()));
}
#[cfg(feature = "hash-rapidhash")]
{
use lightcycle::hashes::{RapidHashFastHasher, RapidHashQualityHasher};
results.push(evaluate_hasher(RapidHashQualityHasher::new()));
results.push(evaluate_hasher(RapidHashFastHasher::new()));
}
#[cfg(feature = "hash-murmur3")]
{
use lightcycle::hashes::Murmur3Hasher;
results.push(evaluate_hasher(Murmur3Hasher::new()));
}
println!("\n=== Summary Results ===");
println!(
"{:<12} | {:>10} | {:>10} | {:>10} | {:>12} | {:>12}",
"Hasher", "Speed(ns)", "χ²", "σ", "Weighted%", "Consistent%"
);
println!(
"{:-<12}-+-{:-<10}-+-{:-<10}-+-{:-<10}-+-{:-<12}-+-{:-<12}",
"", "", "", "", "", ""
);
for result in &results {
println!(
"{:<12} | {:>10.2} | {:>10.2} | {:>10.2} | {:>12.1} | {:>12.1}",
result.name,
result.hash_speed_ns,
result.distribution_chi_squared,
result.distribution_stddev,
result.weighted_accuracy,
result.consistency_score
);
}
println!("\n=== Analysis ===");
let fastest = results
.iter()
.min_by(|a, b| {
a.hash_speed_ns
.partial_cmp(&b.hash_speed_ns)
.expect("Hash speed values should be comparable")
})
.expect("Results should not be empty");
println!("Fastest hasher: {} ({:.2} ns/op)", fastest.name, fastest.hash_speed_ns);
let best_distribution = results
.iter()
.min_by(|a, b| {
a.distribution_chi_squared
.partial_cmp(&b.distribution_chi_squared)
.expect("Chi-squared values should be comparable")
})
.expect("Results should not be empty");
println!(
"Best distribution: {} (χ² = {:.2})",
best_distribution.name, best_distribution.distribution_chi_squared
);
let best_weighted = results
.iter()
.max_by(|a, b| {
a.weighted_accuracy
.partial_cmp(&b.weighted_accuracy)
.expect("Weighted accuracy values should be comparable")
})
.expect("Results should not be empty");
println!(
"Best weighted accuracy: {} ({:.1}%)",
best_weighted.name, best_weighted.weighted_accuracy
);
println!("\n=== Recommendation ===");
let mut scores: Vec<(String, f64)> = results
.iter()
.map(|r| {
let speed_score = r.hash_speed_ns / fastest.hash_speed_ns; let distribution_score = r.distribution_chi_squared / best_distribution.distribution_chi_squared;
let weighted_score = best_weighted.weighted_accuracy / r.weighted_accuracy;
let consistency_penalty = if r.consistency_score < 100.0 { 10.0 } else { 1.0 };
let total_score = speed_score + distribution_score + weighted_score * consistency_penalty;
(r.name.clone(), total_score)
})
.collect();
scores.sort_by(|a, b| a.1.partial_cmp(&b.1).expect("Score values should be comparable"));
println!("Top 3 recommended hashers for rendezvous hashing:");
for (i, (name, score)) in scores.iter().take(3).enumerate() {
println!(" {}. {} (score: {:.2})", i + 1, name, score);
}
for result in &results {
assert_eq!(
result.consistency_score, 100.0,
"{} hasher should have perfect consistency",
result.name
);
}
}