//! # Mirror Performance Metrics and Scoring
//!
//! This module implements the performance scoring algorithm for mirror selection optimization.
//! It calculates weighted performance scores based on historical throughput and latency data,
//! with geographic bonuses for country-localized mirrors.
//!
//! ## Core Metrics
//!
//! - **Throughput**: Historical download speeds in bytes per second (weighted average)
//! - **Latency**: Historical response times in milliseconds (median calculation)
//! - **Performance Score**: Combined metric balancing speed vs. responsiveness
//! - **Geographic Bonus**: 8x multiplier for same-country mirrors
//!
//! ## Scoring Algorithm
//!
//! The performance score is calculated as:
//! ```
//! base_score = avg_throughput / avg_latency
//! final_score = base_score * country_multiplier
//! ```
//!
//! ## Key Features
//!
//! - **Weighted Averages**: Recent measurements have higher influence on scoring
//! - **Bounded Values**: Throughput (1KB/s - 10MB/s) and latency (10ms - 500ms) caps
//! - **Usage Tracking**: Thread-safe counters for active downloads and total usage
//! - **Automatic Cleanup**: History limited to last 10 measurements for efficiency
//! - **Geographic Optimization**: Country-based performance multipliers for localization
//!
//! ## History Management
//!
//! - Maintains rolling history of recent performance measurements
//! - Automatically trims old data to prevent unbounded growth
//! - Supports both throughput and latency tracking per mirror
//! - Thread-safe updates for concurrent access during downloads
use std::sync::atomic::Ordering;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::mirror::types::*;
impl Mirror {
// Helper method to update performance metrics
pub fn record_performance(&mut self, throughput: u32, latency: u32) {
const MAX_HISTORY: usize = 10; // Keep last 10 measurements
// Add new throughput if non-zero
if throughput > 0 {
self.stats.throughputs.push(throughput);
if self.stats.throughputs.len() > MAX_HISTORY {
self.stats.throughputs.remove(0);
}
}
// Add new latency if non-zero
if latency > 0 {
self.stats.latencies.push(latency);
if self.stats.latencies.len() > MAX_HISTORY {
self.stats.latencies.remove(0);
}
}
}
// Helper method to calculate weighted average throughput
// Newer measurements have higher weights: sum(i * self.throughputs[i]) / sum(i)
//
// History data variation could be 10x, so
// - meaningless using log data for ETA estimation => course 1M beforehand chunking would be suitable
// - quickly switch to new data for ETA and score calculation => better estimation for ondemand chunking
// (but still not good enough to qualify an ETA based global planner)
//
// History data at different time: 10x variation in throughputs!
// Rank | URL | Score | Throughputs (KB/s) | Latencies (ms)
// -----|------------------------|-------|-------------------------|----------------------
// 1 | repo.huaweicloud.com | 13039 | [1294, 945, 1274KB/s] | [190, 553, 247ms]
// 2 | repo.huaweicloud.com | 6783 | [345, 308, 394KB/s] | [1185, 1154, 1009ms]
// 2 | repo.huaweicloud.com | 10985 | [463, 1078, 333KB/s] | [665, 972, 728ms]
// 8 | repo.huaweicloud.com | 3855 | [81, 47, 126KB/s] | [738, 2287, 1319ms]
//
// History data at same time: throughputs could still have 3-5x variation!
// https://mirrors.tuna.tsinghua.edu.cn/ubuntu///dists/noble-updates/by-hash/SHA256/d8c255df1be42d64603734262d5a7833ef4fb8b2a59d0abfb13b093fdb1c6d2d
// 2025-07-05.16:47:24 offset=10485760 bytes=1048576 dur=3534 tput=303831 ok=1
// 2025-07-05.16:47:26 offset=11534336 bytes=1048576 dur=1087 tput=987802 ok=1
// 2025-07-05.16:47:28 offset=13631488 bytes=1048576 dur=1227 tput=875095 ok=1
// 2025-07-05.16:47:30 offset=14680064 bytes=1048576 dur=1772 tput=605949 ok=1
// 2025-07-05.16:47:32 offset=19922944 bytes=1048576 dur=895 tput=1199711 ok=1
// 2025-07-05.16:47:37 offset=20971520 bytes=1048576 dur=3877 tput=276951 ok=1
// 2025-07-05.16:47:39 offset=22020096 bytes=1048576 dur=1029 tput=1043480 ok=1
// 2025-07-05.16:47:40 offset=25165824 bytes=1048576 dur=828 tput=1296789 ok=1
// 2025-07-05.16:47:42 offset=26214400 bytes=1048576 dur=930 tput=1154561 ok=1
// 2025-07-05.16:47:48 offset=31457280 bytes=1048576 dur=4794 tput=223976 ok=1
// 2025-07-05.16:47:51 offset=37748736 bytes=1048576 dur=1900 tput=565127 ok=1
// 2025-07-05.16:47:58 offset=38797312 bytes=1048576 dur=5030 tput=213467 ok=1
// 2025-07-05.16:48:01 offset=46137344 bytes=1048576 dur=1608 tput=667749 ok=1
// 2025-07-05.16:48:03 offset=48234496 bytes=1048576 dur=1585 tput=677439 ok=1
// 2025-07-05.16:48:05 offset=49283072 bytes=1048576 dur=1059 tput=1013920 ok=1
// 2025-07-05.16:48:10 offset=51380224 bytes=1048576 dur=4856 tput=221116 ok=1
pub fn avg_throughput(&self) -> Option<u32> {
if self.stats.throughputs.is_empty() {
None
} else {
// Calculate sum of weights (1 + 2 + ... + n)
let n = self.stats.throughputs.len();
let sum_weights = (n * (n + 1)) / 2;
// Calculate weighted sum: 1*throughputs[0] + 2*throughputs[1] + ... + n*throughputs[n-1]
let weighted_sum: u64 = self.stats.throughputs.iter().enumerate()
.map(|(i, &t)| (i + 1) as u64 * t as u64)
.sum();
Some((weighted_sum / sum_weights as u64) as u32)
}
}
// Helper method to calculate median latency
pub fn avg_latency(&self) -> Option<u32> {
if self.stats.latencies.is_empty() {
None
} else {
// Create a sorted copy of latencies
let mut sorted = self.stats.latencies.clone();
sorted.sort();
// Get the middle value (median)
let mid = sorted.len() / 2;
if sorted.len() % 2 == 0 && sorted.len() >= 2 {
// Even number of elements, average the two middle values
Some((sorted[mid - 1] + sorted[mid]) / 2)
} else {
// Odd number of elements, return the middle value
Some(sorted[mid])
}
}
}
/// Start tracking usage for this mirror
pub fn start_usage_tracking(&mut self) {
self.shared_usage.active_downloads.fetch_add(1, Ordering::Relaxed);
self.shared_usage.total_uses.fetch_add(1, Ordering::Relaxed);
self.shared_usage.last_used.store(
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
Ordering::Relaxed
);
}
/// Stop tracking usage for this mirror
pub fn stop_usage_tracking(&mut self) {
// Prevent underflow by checking current value before subtracting
let current = self.shared_usage.active_downloads.load(Ordering::Relaxed);
if current > 0 {
self.shared_usage.active_downloads.fetch_sub(1, Ordering::Relaxed);
}
}
pub fn add_skip_url(&mut self, url: &str) {
self.skip_urls.insert(url.to_string());
log::debug!("Added {} to skip_urls for mirror {} (total skip_urls: {})", url, self.url, self.skip_urls.len());
}
pub fn should_skip_url(&self, url: &str) -> bool {
self.skip_urls.contains(url)
}
/// Calculate weighted performance score for mirror selection optimization
///
/// This algorithm combines multiple factors to produce a single score for mirror ranking:
///
/// **Core Performance Metrics:**
/// - **Throughput**: Based on historical download speeds (higher = better)
/// - **Latency**: Based on historical response times (lower is better, squared penalty)
///
/// **Fallback Strategy:**
/// - Uses optimistic defaults for mirrors without historical data to encourage exploration
/// - Default latency: 30ms, Default throughput: estimated from bandwidth specs
///
/// **Score Formula:**
/// ```
/// base_score = avg_throughput / avg_latency
/// final_score = base_score * country_multiplier
/// ```
///
/// **Geographic Optimization:**
/// - 8x bonus for mirrors in the same country as the user
/// - This heavily favors local mirrors for better performance and compliance
///
/// **Constraints:**
/// - Throughput capped between 1KB/s and 100MB/s to prevent outliers
/// - Latency capped between 10ms and 500ms for realistic bounds
/// - Squared latency penalty amplifies the preference for low-latency mirrors
///
/// **Returns:** Calculated score (higher values indicate better mirrors)
pub fn calculate_performance_score(&mut self) -> u64 {
let avg_latency = self.avg_latency().unwrap_or(DEFAULT_LATENCY_MS) as u64;
let avg_throughput = self.avg_throughput().unwrap_or(
self.bandwidth.unwrap_or(DEFAULT_BANDWIDTH_MBPS) * (1024*1024/8/1024)) as u64; // Mbps => B/s; the last /1024 is total_site_bw => my_connection_throughput
// Store the average throughput for filtering
self.stats.avg_throughput = Some(avg_throughput as u32);
// Score based on throughput (higher is better) and latency (lower is better)
let mut throughput_score = (avg_throughput).min(MAX_THROUGHPUT_BPS as u64).max(MIN_THROUGHPUT_BPS as u64); // Cap in [1KB/s, 10MB/s]
// Apply country bonus
// Highly prefer mirrors in same country
if let Ok(user_country_code) = crate::location::get_country_code() {
if self.country_code.as_deref() == Some(user_country_code.as_str()) {
throughput_score *= COUNTRY_BONUS_MULTIPLIER as u64; // Country bonus
}
}
// real tests in CN show there may be 2-3 times difference in same country
// so divide it twice to prefer the near sites
let latency_penalty = (avg_latency).min(MAX_LATENCY_MS as u64).max(MIN_LATENCY_MS as u64); // Cap in 10-500 ms
let performance_score = throughput_score / latency_penalty;
if performance_score > 0 {
self.stats.score = performance_score;
}
self.stats.score
}
}