mod analysis;
mod forecasting;
pub use analysis::*;
pub use forecasting::*;
use crate::{BrokerMetrics, Priority};
use celers_protocol::Message;
pub fn calculate_optimal_batch_size(
avg_message_size: usize,
target_throughput_per_sec: usize,
max_batch_size: usize,
) -> usize {
if target_throughput_per_sec == 0 || avg_message_size == 0 {
return 1;
}
let size_factor = if avg_message_size < 1024 {
10 } else if avg_message_size < 10240 {
5 } else {
2 };
let throughput_factor = (target_throughput_per_sec / 10).max(1);
let calculated = (throughput_factor / size_factor).max(1);
calculated.min(max_batch_size)
}
pub fn match_routing_pattern(routing_key: &str, pattern: &str) -> bool {
let key_parts: Vec<&str> = routing_key.split('.').collect();
let pattern_parts: Vec<&str> = pattern.split('.').collect();
match_parts(&key_parts, &pattern_parts)
}
fn match_parts(key_parts: &[&str], pattern_parts: &[&str]) -> bool {
if pattern_parts.is_empty() {
return key_parts.is_empty();
}
if pattern_parts[0] == "#" {
if pattern_parts.len() == 1 {
return true; }
for i in 0..=key_parts.len() {
if match_parts(&key_parts[i..], &pattern_parts[1..]) {
return true;
}
}
false
} else if key_parts.is_empty() {
false
} else if pattern_parts[0] == "*" || pattern_parts[0] == key_parts[0] {
match_parts(&key_parts[1..], &pattern_parts[1..])
} else {
false
}
}
pub fn analyze_broker_performance(metrics: &BrokerMetrics) -> (f64, f64, f64) {
let total_ops = metrics.messages_published + metrics.messages_consumed;
let total_errors = metrics.publish_errors + metrics.consume_errors;
let success_rate = if total_ops > 0 {
(total_ops - total_errors) as f64 / total_ops as f64
} else {
1.0
};
let error_rate = if total_ops > 0 {
total_errors as f64 / total_ops as f64
} else {
0.0
};
let ack_rate = if metrics.messages_consumed > 0 {
metrics.messages_acknowledged as f64 / metrics.messages_consumed as f64
} else {
0.0
};
(success_rate, error_rate, ack_rate)
}
pub fn estimate_message_size(message: &Message) -> usize {
let base = 200; let task_name = message.task_name().len();
let body = message.body.len();
base + task_name + body
}
pub fn analyze_queue_health(
queue_size: usize,
low_threshold: usize,
high_threshold: usize,
) -> &'static str {
if queue_size < low_threshold {
"healthy"
} else if queue_size < high_threshold {
"warning"
} else {
"critical"
}
}
pub fn calculate_throughput(message_count: u64, duration_secs: f64) -> f64 {
if duration_secs <= 0.0 {
0.0
} else {
message_count as f64 / duration_secs
}
}
pub fn calculate_avg_latency(total_latency_ms: f64, message_count: u64) -> f64 {
if message_count == 0 {
0.0
} else {
total_latency_ms / message_count as f64
}
}
pub fn estimate_drain_time(queue_size: usize, consumption_rate_per_sec: usize) -> f64 {
if consumption_rate_per_sec == 0 {
f64::INFINITY
} else {
queue_size as f64 / consumption_rate_per_sec as f64
}
}
pub fn calculate_backoff_delay(
attempt: u32,
base_delay_ms: u64,
max_delay_ms: u64,
jitter_factor: f64,
) -> u64 {
use std::cmp::min;
let exponential = base_delay_ms.saturating_mul(2u64.saturating_pow(attempt));
let capped = min(exponential, max_delay_ms);
if jitter_factor > 0.0 {
let jitter = (capped as f64 * jitter_factor.clamp(0.0, 1.0)) as u64;
let jitter_amount = jitter / 2 + (attempt as u64 * 17) % (jitter / 2 + 1);
capped.saturating_sub(jitter_amount)
} else {
capped
}
}
pub fn analyze_circuit_breaker(
failures: u64,
successes: u64,
total_requests: u64,
) -> (f64, &'static str) {
if total_requests == 0 {
return (1.0, "healthy");
}
let failure_rate = failures as f64 / total_requests as f64;
let success_rate = successes as f64 / total_requests as f64;
let health_score = success_rate;
let recommendation = if failure_rate > 0.5 {
"critical"
} else if failure_rate > 0.2 {
"warning"
} else {
"healthy"
};
(health_score, recommendation)
}
pub fn calculate_optimal_workers(
queue_size: usize,
avg_processing_time_ms: u64,
target_drain_time_secs: u64,
max_workers: usize,
) -> usize {
if queue_size == 0 || target_drain_time_secs == 0 || avg_processing_time_ms == 0 {
return 1;
}
let msgs_per_sec_per_worker = 1000.0 / avg_processing_time_ms as f64;
let required_throughput = queue_size as f64 / target_drain_time_secs as f64;
let needed_workers = (required_throughput / msgs_per_sec_per_worker).ceil() as usize;
needed_workers.clamp(1, max_workers)
}
pub fn generate_deduplication_id(task_name: &str, args: &[u8]) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
task_name.hash(&mut hasher);
args.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
#[allow(clippy::too_many_arguments)]
pub fn analyze_pool_health(
active_connections: usize,
idle_connections: usize,
max_connections: usize,
total_requests: u64,
timeout_count: u64,
) -> (f64, &'static str) {
let total_connections = active_connections + idle_connections;
if max_connections == 0 {
return (0.0, "invalid");
}
let utilization = total_connections as f64 / max_connections as f64;
let timeout_rate = if total_requests > 0 {
timeout_count as f64 / total_requests as f64
} else {
0.0
};
let timeout_penalty = timeout_rate * 0.5;
let utilization_score = if utilization > 0.9 {
0.7 } else if utilization < 0.1 {
0.8 } else {
1.0 };
let efficiency = (utilization_score - timeout_penalty).clamp(0.0, 1.0);
let status = if timeout_rate > 0.1 {
"critical"
} else if utilization > 0.95 {
"saturated"
} else if utilization < 0.05 {
"underutilized"
} else {
"healthy"
};
(efficiency, status)
}
pub fn calculate_load_distribution(
queue_sizes: &[usize],
total_workers: usize,
) -> Vec<(usize, usize)> {
if queue_sizes.is_empty() || total_workers == 0 {
return vec![];
}
let total_messages: usize = queue_sizes.iter().sum();
if total_messages == 0 {
let workers_per_queue = total_workers / queue_sizes.len();
let remainder = total_workers % queue_sizes.len();
return queue_sizes
.iter()
.enumerate()
.map(|(idx, _)| {
let workers = workers_per_queue + if idx < remainder { 1 } else { 0 };
(idx, workers)
})
.collect();
}
let mut distribution: Vec<(usize, usize)> = queue_sizes
.iter()
.enumerate()
.map(|(idx, &size)| {
let proportion = size as f64 / total_messages as f64;
let workers = (proportion * total_workers as f64).round() as usize;
(idx, workers)
})
.collect();
let assigned: usize = distribution.iter().map(|(_, w)| w).sum();
if assigned < total_workers {
let diff = total_workers - assigned;
let dist_len = distribution.len();
for _i in 0..diff {
if let Some(max_queue) = queue_sizes
.iter()
.enumerate()
.max_by_key(|(_, &size)| size)
.map(|(idx, _)| idx)
{
distribution[max_queue % dist_len].1 += 1;
}
}
}
distribution
}
pub fn match_direct_routing(routing_key: &str, pattern: &str) -> bool {
routing_key == pattern
}
pub fn match_fanout_routing(_routing_key: &str) -> bool {
true }
pub fn estimate_queue_memory(message_count: usize, avg_message_size: usize) -> usize {
let overhead_per_message = 100; message_count * (avg_message_size + overhead_per_message)
}
pub fn calculate_priority_score(priority: Priority, age_seconds: u64, retry_count: u32) -> f64 {
let priority_weight = match priority {
Priority::Highest => 10.0,
Priority::High => 7.0,
Priority::Normal => 5.0,
Priority::Low => 3.0,
Priority::Lowest => 1.0,
};
let age_factor = (age_seconds as f64).ln().max(1.0);
let retry_penalty = 0.9_f64.powi(retry_count as i32);
priority_weight * age_factor * retry_penalty
}
pub fn suggest_batch_groups(message_sizes: &[usize], max_batch_bytes: usize) -> Vec<Vec<usize>> {
let mut groups = Vec::new();
let mut current_group = Vec::new();
let mut current_size = 0;
for (idx, &size) in message_sizes.iter().enumerate() {
if current_size + size <= max_batch_bytes {
current_group.push(idx);
current_size += size;
} else {
if !current_group.is_empty() {
groups.push(current_group);
}
current_group = vec![idx];
current_size = size;
}
}
if !current_group.is_empty() {
groups.push(current_group);
}
groups
}
pub fn calculate_health_score(
success_rate: f64,
queue_size: usize,
error_rate: f64,
latency_ms: u64,
) -> f64 {
let success_component = success_rate;
let queue_component = if queue_size < 100 {
1.0
} else if queue_size < 1000 {
0.8
} else if queue_size < 10000 {
0.5
} else {
0.2
};
let error_component = (1.0 - error_rate).max(0.0);
let latency_component = if latency_ms < 100 {
1.0
} else if latency_ms < 500 {
0.8
} else if latency_ms < 1000 {
0.5
} else {
0.2
};
(success_component * 0.4
+ queue_component * 0.2
+ error_component * 0.2
+ latency_component * 0.2)
.clamp(0.0, 1.0)
}
pub fn identify_stale_messages(message_ages: &[u64], threshold_seconds: u64) -> Vec<usize> {
message_ages
.iter()
.enumerate()
.filter(|(_, &age)| age > threshold_seconds)
.map(|(idx, _)| idx)
.collect()
}
pub fn predict_throughput(historical_throughput: &[f64], periods_ahead: usize) -> f64 {
if historical_throughput.is_empty() {
return 0.0;
}
if historical_throughput.len() < 2 {
return historical_throughput[0];
}
let mut changes = Vec::new();
for i in 1..historical_throughput.len() {
changes.push(historical_throughput[i] - historical_throughput[i - 1]);
}
let avg_change = changes.iter().sum::<f64>() / changes.len() as f64;
let last_value = historical_throughput[historical_throughput.len() - 1];
(last_value + avg_change * periods_ahead as f64).max(0.0)
}
pub fn calculate_rebalancing(current_sizes: &[usize], target_total: usize) -> Vec<usize> {
if current_sizes.is_empty() {
return vec![];
}
let num_queues = current_sizes.len();
let per_queue = target_total / num_queues;
let remainder = target_total % num_queues;
let mut result = vec![per_queue; num_queues];
for item in result.iter_mut().take(remainder) {
*item += 1;
}
result
}
pub fn estimate_completion_time(remaining_messages: usize, current_rate_per_sec: f64) -> u64 {
if current_rate_per_sec <= 0.0 {
return u64::MAX;
}
(remaining_messages as f64 / current_rate_per_sec).ceil() as u64
}
pub fn calculate_efficiency(successful: usize, failed: usize, rejected: usize) -> f64 {
let total = successful + failed + rejected;
if total == 0 {
return 1.0;
}
successful as f64 / total as f64
}
pub fn calculate_queue_capacity(
production_rate_per_sec: usize,
consumption_rate_per_sec: usize,
buffer_duration_secs: usize,
) -> usize {
if production_rate_per_sec <= consumption_rate_per_sec {
production_rate_per_sec * buffer_duration_secs.min(10)
} else {
let backlog_rate = production_rate_per_sec - consumption_rate_per_sec;
backlog_rate * buffer_duration_secs
}
}
pub fn suggest_partition_count(
target_throughput_per_sec: usize,
consumer_count: usize,
max_partition_throughput: usize,
) -> usize {
if max_partition_throughput == 0 {
return consumer_count.max(1);
}
let throughput_partitions = target_throughput_per_sec.div_ceil(max_partition_throughput);
let consumer_partitions = consumer_count;
throughput_partitions.max(consumer_partitions).min(100)
}
#[allow(clippy::too_many_arguments)]
pub fn calculate_cost_estimate(
messages_per_day: usize,
avg_message_size_bytes: usize,
cost_per_message: f64,
storage_cost_per_gb_month: f64,
retention_days: usize,
) -> f64 {
let daily_message_cost = messages_per_day as f64 * cost_per_message;
let daily_storage_gb =
(messages_per_day * avg_message_size_bytes) as f64 / (1024.0 * 1024.0 * 1024.0);
let total_storage_gb = daily_storage_gb * retention_days as f64;
let monthly_storage_cost = total_storage_gb * storage_cost_per_gb_month / 30.0;
(daily_message_cost * 30.0) + monthly_storage_cost
}
pub fn analyze_message_patterns(message_sizes: &[usize]) -> (usize, usize, f64, f64) {
if message_sizes.is_empty() {
return (0, 0, 0.0, 0.0);
}
let min = *message_sizes.iter().min().unwrap();
let max = *message_sizes.iter().max().unwrap();
let sum: usize = message_sizes.iter().sum();
let avg = sum as f64 / message_sizes.len() as f64;
let variance: f64 = message_sizes
.iter()
.map(|&size| {
let diff = size as f64 - avg;
diff * diff
})
.sum::<f64>()
/ message_sizes.len() as f64;
let std_dev = variance.sqrt();
(min, max, avg, std_dev)
}
pub fn calculate_buffer_size(
avg_message_size: usize,
message_overhead: usize,
target_efficiency: f64,
) -> usize {
let effective_message_size = avg_message_size + message_overhead;
let mtu = if effective_message_size < 1400 {
1500
} else {
9000
};
let messages_per_packet = (mtu as f64 / effective_message_size as f64).floor() as usize;
if messages_per_packet == 0 {
return effective_message_size;
}
let packets_needed = (1.0 / (1.0 - target_efficiency.min(0.99))).ceil() as usize;
messages_per_packet * packets_needed * effective_message_size
}
pub fn estimate_memory_footprint(
queue_count: usize,
messages_per_queue: usize,
avg_message_size: usize,
metadata_overhead_per_msg: usize,
) -> usize {
let per_message_total = avg_message_size + metadata_overhead_per_msg;
let per_queue_memory = messages_per_queue * per_message_total;
let queue_overhead = 1024 * 64;
queue_count * (per_queue_memory + queue_overhead)
}
pub fn suggest_ttl(
avg_processing_time_secs: u64,
max_retries: u32,
backoff_multiplier: f64,
) -> u64 {
let mut total_time = avg_processing_time_secs;
for retry in 0..max_retries {
let backoff = avg_processing_time_secs as f64 * backoff_multiplier.powi(retry as i32);
total_time += backoff as u64;
}
(total_time as f64 * 1.5).ceil() as u64
}
pub fn calculate_replication_lag(
throughput_per_sec: usize,
replica_count: usize,
require_strong_consistency: bool,
) -> u64 {
if require_strong_consistency {
return 100; }
let messages_per_ms = throughput_per_sec as f64 / 1000.0;
let max_lag_messages = 1000 * replica_count;
if messages_per_ms <= 0.0 {
return 10000; }
let lag_ms = (max_lag_messages as f64 / messages_per_ms).ceil() as u64;
lag_ms.clamp(100, 60000)
}
pub fn calculate_bandwidth_requirement(
messages_per_sec: usize,
avg_message_size: usize,
protocol_overhead_factor: f64,
) -> usize {
let raw_bandwidth = messages_per_sec * avg_message_size;
(raw_bandwidth as f64 * protocol_overhead_factor) as usize
}
pub fn suggest_retry_policy(failure_rate: f64, avg_recovery_time_secs: u64) -> (u32, u64, u64) {
let max_retries = if failure_rate > 0.3 {
10
} else if failure_rate > 0.1 {
5
} else {
3
};
let initial_delay_ms = (avg_recovery_time_secs * 100).max(100);
let max_delay_ms = (avg_recovery_time_secs * 1000 * 5).min(300000);
(max_retries, initial_delay_ms, max_delay_ms)
}
pub fn analyze_consumer_lag(
queue_size: usize,
consumption_rate_per_sec: usize,
production_rate_per_sec: usize,
) -> (u64, bool, &'static str) {
let lag_seconds = if consumption_rate_per_sec > 0 {
(queue_size as f64 / consumption_rate_per_sec as f64).ceil() as u64
} else {
u64::MAX };
let is_falling_behind = production_rate_per_sec > consumption_rate_per_sec;
let recommended_action = if is_falling_behind && lag_seconds > 10 {
"scale_up" } else if !is_falling_behind
&& queue_size < 100
&& consumption_rate_per_sec > production_rate_per_sec * 2
{
"scale_down" } else {
"stable" };
(lag_seconds, is_falling_behind, recommended_action)
}
pub fn calculate_message_velocity(
current_size: usize,
previous_size: usize,
time_window_secs: u64,
) -> (i64, &'static str) {
if time_window_secs == 0 {
return (0, "stable");
}
let delta = current_size as i64 - previous_size as i64;
let velocity = delta / time_window_secs as i64;
let trend = if velocity > 5 {
"growing"
} else if velocity < -5 {
"shrinking"
} else {
"stable"
};
(velocity, trend)
}
#[allow(clippy::comparison_chain)]
pub fn suggest_worker_scaling(
queue_size: usize,
current_workers: usize,
avg_processing_time_ms: u64,
target_latency_secs: u64,
) -> (usize, &'static str) {
if avg_processing_time_ms == 0 || target_latency_secs == 0 {
return (current_workers.max(1), "maintain");
}
let messages_per_worker_per_sec = 1000 / avg_processing_time_ms.max(1);
let required_throughput = (queue_size as f64 / target_latency_secs as f64).ceil() as u64;
let recommended_workers = if messages_per_worker_per_sec > 0 {
((required_throughput as f64 / messages_per_worker_per_sec as f64).ceil() as usize).max(1)
} else {
current_workers.max(1)
};
let recommended_workers = ((recommended_workers as f64 * 1.2).ceil() as usize).max(1);
let scaling_action = if recommended_workers > current_workers {
"add"
} else if recommended_workers < current_workers {
"remove"
} else {
"maintain"
};
(recommended_workers, scaling_action)
}
pub fn calculate_message_age_distribution(message_ages_secs: &[u64]) -> (u64, u64, u64, u64) {
if message_ages_secs.is_empty() {
return (0, 0, 0, 0);
}
let mut sorted_ages = message_ages_secs.to_vec();
sorted_ages.sort_unstable();
let len = sorted_ages.len();
let p50_idx = (len as f64 * 0.50) as usize;
let p95_idx = (len as f64 * 0.95) as usize;
let p99_idx = (len as f64 * 0.99) as usize;
let p50 = sorted_ages.get(p50_idx.min(len - 1)).copied().unwrap_or(0);
let p95 = sorted_ages.get(p95_idx.min(len - 1)).copied().unwrap_or(0);
let p99 = sorted_ages.get(p99_idx.min(len - 1)).copied().unwrap_or(0);
let max_age = sorted_ages.last().copied().unwrap_or(0);
(p50, p95, p99, max_age)
}
pub fn estimate_processing_capacity(
num_workers: usize,
avg_processing_time_ms: u64,
concurrency_per_worker: usize,
) -> (u64, u64, u64) {
if avg_processing_time_ms == 0 || num_workers == 0 || concurrency_per_worker == 0 {
return (0, 0, 0);
}
let msgs_per_sec_per_task = 1000 / avg_processing_time_ms;
let total_tasks = num_workers * concurrency_per_worker;
let capacity_per_sec = msgs_per_sec_per_task * total_tasks as u64;
let capacity_per_min = capacity_per_sec * 60;
let capacity_per_hour = capacity_per_min * 60;
(capacity_per_sec, capacity_per_min, capacity_per_hour)
}
pub fn detect_anomalies(
current_samples: &[u64],
baseline_samples: &[u64],
threshold_multiplier: f64,
) -> (bool, f64, String) {
if current_samples.is_empty() || baseline_samples.is_empty() {
return (false, 0.0, "Insufficient data".to_string());
}
let baseline_avg = baseline_samples.iter().sum::<u64>() as f64 / baseline_samples.len() as f64;
let baseline_variance = baseline_samples
.iter()
.map(|&x| {
let diff = x as f64 - baseline_avg;
diff * diff
})
.sum::<f64>()
/ baseline_samples.len() as f64;
let baseline_stddev = baseline_variance.sqrt();
let current_avg = current_samples.iter().sum::<u64>() as f64 / current_samples.len() as f64;
let deviation = (current_avg - baseline_avg).abs();
let threshold = baseline_stddev * threshold_multiplier;
if deviation > threshold {
let severity = (deviation / (baseline_stddev * 3.0)).min(1.0);
let direction = if current_avg > baseline_avg {
"spike"
} else {
"drop"
};
let description = format!(
"Anomaly detected: {} in message rate (current: {:.0}, baseline: {:.0}, deviation: {:.0})",
direction, current_avg, baseline_avg, deviation
);
(true, severity, description)
} else {
(false, 0.0, "Normal pattern".to_string())
}
}
pub fn calculate_sla_compliance(
processing_times_ms: &[u64],
sla_target_ms: u64,
) -> (f64, usize, f64) {
if processing_times_ms.is_empty() {
return (100.0, 0, 0.0);
}
let violations = processing_times_ms
.iter()
.filter(|&&t| t > sla_target_ms)
.count();
let compliance = 100.0 * (1.0 - (violations as f64 / processing_times_ms.len() as f64));
let avg_time =
processing_times_ms.iter().sum::<u64>() as f64 / processing_times_ms.len() as f64;
(compliance, violations, avg_time)
}
pub fn estimate_infrastructure_cost(
messages_per_day: u64,
cost_per_million_messages: f64,
days: u32,
) -> f64 {
let total_messages = messages_per_day * days as u64;
let cost = (total_messages as f64 / 1_000_000.0) * cost_per_million_messages;
(cost * 100.0).round() / 100.0 }
pub fn calculate_error_budget(
sla_target_percentage: f64,
total_requests: u64,
failed_requests: u64,
requests_per_hour: u64,
) -> (f64, u64, f64) {
if total_requests == 0 || requests_per_hour == 0 {
return (100.0, 0, 0.0);
}
let error_budget = 100.0 - sla_target_percentage;
let current_error_rate = (failed_requests as f64 / total_requests as f64) * 100.0;
let budget_remaining = ((error_budget - current_error_rate) / error_budget * 100.0).max(0.0);
let total_errors_allowed = (total_requests as f64 * (error_budget / 100.0)) as u64;
let errors_remaining = total_errors_allowed.saturating_sub(failed_requests);
let time_to_exhaustion = if current_error_rate > 0.0 {
(errors_remaining as f64 / (current_error_rate / 100.0)) / requests_per_hour as f64
} else {
f64::INFINITY
};
(budget_remaining, errors_remaining, time_to_exhaustion)
}
pub fn predict_queue_saturation(
historical_sizes: &[u64],
max_capacity: u64,
hours_per_sample: f64,
) -> (f64, f64) {
if historical_sizes.len() < 2 {
return (f64::INFINITY, 0.0);
}
let current_size = *historical_sizes.last().unwrap();
if current_size >= max_capacity {
return (0.0, 0.0);
}
let n = historical_sizes.len() as f64;
let sum_x: f64 = (0..historical_sizes.len()).map(|i| i as f64).sum();
let sum_y: f64 = historical_sizes.iter().map(|&s| s as f64).sum();
let sum_xy: f64 = historical_sizes
.iter()
.enumerate()
.map(|(i, &s)| i as f64 * s as f64)
.sum();
let sum_x_squared: f64 = (0..historical_sizes.len())
.map(|i| (i as f64) * (i as f64))
.sum();
let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x_squared - sum_x * sum_x);
let growth_per_sample = slope;
let growth_per_hour = growth_per_sample / hours_per_sample;
if growth_per_hour <= 0.0 {
return (f64::INFINITY, growth_per_hour);
}
let remaining_capacity = max_capacity.saturating_sub(current_size) as f64;
let hours_to_saturation = remaining_capacity / growth_per_hour;
(hours_to_saturation, growth_per_hour)
}
pub fn calculate_consumer_efficiency(
processing_time_ms: u64,
wait_time_ms: u64,
messages_processed: u64,
) -> (f64, &'static str) {
if messages_processed == 0 {
return (0.0, "no_data");
}
let total_time = processing_time_ms + wait_time_ms;
if total_time == 0 {
return (0.0, "no_data");
}
let efficiency_percentage = (processing_time_ms as f64 / total_time as f64) * 100.0;
let avg_processing_ms = processing_time_ms as f64 / messages_processed as f64;
let recommendation = if efficiency_percentage >= 80.0 {
"efficient" } else if efficiency_percentage >= 60.0 {
"good" } else if efficiency_percentage >= 40.0 {
"fair" } else if avg_processing_ms < 10.0 {
"bottleneck" } else {
"underutilized" };
(efficiency_percentage, recommendation)
}