use crate::astro::*;
use crate::city::CityDatabase;
use chrono::{DateTime, Datelike, Utc};
use chrono_tz::Tz;
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct BenchmarkResult {
pub total_cities: usize,
pub successful: usize,
pub failed: usize,
pub total_duration_ms: u128,
pub avg_duration_per_city_ms: f64,
pub min_duration_ms: u128,
pub max_duration_ms: u128,
pub cities_per_second: f64,
pub failed_cities: Vec<String>,
}
#[derive(Debug, Clone)]
struct CityBenchmark {
pub city_name: String,
pub duration_ms: u128,
pub success: bool,
pub error: Option<String>,
}
pub fn run_benchmark() -> BenchmarkResult {
let db = match CityDatabase::load() {
Ok(db) => db,
Err(e) => {
return BenchmarkResult {
total_cities: 0,
successful: 0,
failed: 1,
total_duration_ms: 0,
avg_duration_per_city_ms: 0.0,
min_duration_ms: 0,
max_duration_ms: 0,
cities_per_second: 0.0,
failed_cities: vec![format!("Failed to load city database: {}", e)],
};
}
};
let cities = db.cities();
let total_cities = cities.len();
let mut results = Vec::with_capacity(total_cities);
let benchmark_start = Instant::now();
for city in cities {
let city_start = Instant::now();
let result = std::panic::catch_unwind(|| {
benchmark_city(city.name.as_str(), city.lat, city.lon, &city.tz)
});
let duration_ms = city_start.elapsed().as_millis();
match result {
Ok(Ok(_)) => {
results.push(CityBenchmark {
city_name: city.name.clone(),
duration_ms,
success: true,
error: None,
});
}
Ok(Err(e)) => {
results.push(CityBenchmark {
city_name: city.name.clone(),
duration_ms,
success: false,
error: Some(e.to_string()),
});
}
Err(_) => {
results.push(CityBenchmark {
city_name: city.name.clone(),
duration_ms,
success: false,
error: Some("Panic occurred".to_string()),
});
}
}
}
let total_duration_ms = benchmark_start.elapsed().as_millis();
let successful = results.iter().filter(|r| r.success).count();
let failed = results.iter().filter(|r| !r.success).count();
let durations: Vec<u128> = results.iter().map(|r| r.duration_ms).collect();
let min_duration_ms = *durations.iter().min().unwrap_or(&0);
let max_duration_ms = *durations.iter().max().unwrap_or(&0);
let avg_duration_per_city_ms = if !results.is_empty() {
total_duration_ms as f64 / total_cities as f64
} else {
0.0
};
let cities_per_second = if total_duration_ms > 0 {
(total_cities as f64 / total_duration_ms as f64) * 1000.0
} else {
0.0
};
let failed_cities: Vec<String> = results
.iter()
.filter(|r| !r.success)
.map(|r| {
if let Some(err) = &r.error {
format!("{}: {}", r.city_name, err)
} else {
r.city_name.clone()
}
})
.collect();
BenchmarkResult {
total_cities,
successful,
failed,
total_duration_ms,
avg_duration_per_city_ms,
min_duration_ms,
max_duration_ms,
cities_per_second,
failed_cities,
}
}
fn benchmark_city(_name: &str, lat: f64, lon: f64, tz_str: &str) -> anyhow::Result<()> {
let tz: Tz = tz_str.parse()?;
let location =
Location::new(lat, lon).map_err(|e| anyhow::anyhow!("Invalid location: {}", e))?;
let now_utc: DateTime<Utc> = Utc::now();
let now_tz = now_utc.with_timezone(&tz);
let _solar_pos = sun::solar_position(&location, &now_tz);
let _lunar_pos = moon::lunar_position(&location, &now_tz);
let _sunrise = sun::solar_event_time(&location, &now_tz, sun::SolarEvent::Sunrise);
let _sunset = sun::solar_event_time(&location, &now_tz, sun::SolarEvent::Sunset);
let _solar_noon = sun::solar_noon(&location, &now_tz);
let _civil_dawn = sun::solar_event_time(&location, &now_tz, sun::SolarEvent::CivilDawn);
let _civil_dusk = sun::solar_event_time(&location, &now_tz, sun::SolarEvent::CivilDusk);
let _nautical_dawn = sun::solar_event_time(&location, &now_tz, sun::SolarEvent::NauticalDawn);
let _nautical_dusk = sun::solar_event_time(&location, &now_tz, sun::SolarEvent::NauticalDusk);
let _astro_dawn = sun::solar_event_time(&location, &now_tz, sun::SolarEvent::AstronomicalDawn);
let _astro_dusk = sun::solar_event_time(&location, &now_tz, sun::SolarEvent::AstronomicalDusk);
let _moonrise = moon::lunar_event_time(&location, &now_tz, moon::LunarEvent::Moonrise);
let _moonset = moon::lunar_event_time(&location, &now_tz, moon::LunarEvent::Moonset);
let _phases = moon::lunar_phases(now_tz.year(), now_tz.month());
Ok(())
}
pub fn generate_html_report(result: &BenchmarkResult) -> String {
let success_rate = if result.total_cities > 0 {
(result.successful as f64 / result.total_cities as f64) * 100.0
} else {
0.0
};
let mut html = String::new();
html.push_str("<!DOCTYPE html>\n");
html.push_str("<html>\n");
html.push_str("<head>\n");
html.push_str("<meta charset=\"UTF-8\">\n");
html.push_str("<title>Solunatus Benchmark Report</title>\n");
html.push_str("<style>\n");
html.push_str("body { font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; max-width: 1200px; margin: 40px auto; padding: 20px; background: #0a0e1a; color: #e0e6f0; }\n");
html.push_str(
"h1 { color: #60a5fa; border-bottom: 2px solid #1e40af; padding-bottom: 10px; }\n",
);
html.push_str("h2 { color: #34d399; margin-top: 30px; }\n");
html.push_str(".summary { background: #1e293b; padding: 20px; border-radius: 8px; margin: 20px 0; border: 1px solid #334155; }\n");
html.push_str(".stat { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #334155; }\n");
html.push_str(".stat:last-child { border-bottom: none; }\n");
html.push_str(".label { color: #94a3b8; }\n");
html.push_str(".value { color: #e0e6f0; font-weight: bold; }\n");
html.push_str(".success { color: #34d399; }\n");
html.push_str(".failure { color: #f87171; }\n");
html.push_str(".warning { background: #451a03; border: 1px solid #ea580c; padding: 15px; border-radius: 8px; margin: 20px 0; }\n");
html.push_str(".warning h3 { color: #fb923c; margin-top: 0; }\n");
html.push_str(".error-list { background: #1e293b; padding: 10px; border-radius: 4px; max-height: 400px; overflow-y: auto; }\n");
html.push_str(".error-item { padding: 5px 0; font-size: 0.9em; color: #f87171; }\n");
html.push_str("</style>\n");
html.push_str("</head>\n");
html.push_str("<body>\n");
html.push_str("<h1>🚀 Solunatus Benchmark Report</h1>\n");
html.push_str("<div class=\"summary\">\n");
html.push_str("<h2>Summary</h2>\n");
html.push_str(&format!("<div class=\"stat\"><span class=\"label\">Total Cities</span><span class=\"value\">{}</span></div>\n", result.total_cities));
html.push_str(&format!("<div class=\"stat\"><span class=\"label\">Successful</span><span class=\"value success\">{}</span></div>\n", result.successful));
html.push_str(&format!("<div class=\"stat\"><span class=\"label\">Failed</span><span class=\"value failure\">{}</span></div>\n", result.failed));
html.push_str(&format!("<div class=\"stat\"><span class=\"label\">Success Rate</span><span class=\"value success\">{:.2}%</span></div>\n", success_rate));
html.push_str("</div>\n");
html.push_str("<div class=\"summary\">\n");
html.push_str("<h2>Performance</h2>\n");
html.push_str(&format!("<div class=\"stat\"><span class=\"label\">Total Duration</span><span class=\"value\">{:.2} s</span></div>\n", result.total_duration_ms as f64 / 1000.0));
html.push_str(&format!("<div class=\"stat\"><span class=\"label\">Average per City</span><span class=\"value\">{:.2} ms</span></div>\n", result.avg_duration_per_city_ms));
html.push_str(&format!("<div class=\"stat\"><span class=\"label\">Min Duration</span><span class=\"value\">{} ms</span></div>\n", result.min_duration_ms));
html.push_str(&format!("<div class=\"stat\"><span class=\"label\">Max Duration</span><span class=\"value\">{} ms</span></div>\n", result.max_duration_ms));
html.push_str(&format!("<div class=\"stat\"><span class=\"label\">Throughput</span><span class=\"value\">{:.2} cities/sec</span></div>\n", result.cities_per_second));
html.push_str("</div>\n");
if !result.failed_cities.is_empty() {
html.push_str("<div class=\"warning\">\n");
html.push_str("<h3>⚠️ Failed Cities</h3>\n");
html.push_str("<div class=\"error-list\">\n");
for error in &result.failed_cities {
html.push_str(&format!("<div class=\"error-item\">{}</div>\n", error));
}
html.push_str("</div>\n");
html.push_str("</div>\n");
}
html.push_str("</body>\n");
html.push_str("</html>\n");
html
}