use super::*;
use std::collections::HashMap;
pub(super) const MAX_TIMING_SAMPLES: usize = 20;
const MAX_TIMING_PROJECTS: usize = 500;
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct TimingRecord {
pub timestamp: u64,
pub duration_ms: u64,
pub remote: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub(super) struct ProjectTimingData {
pub local_samples: Vec<TimingRecord>,
pub remote_samples: Vec<TimingRecord>,
}
#[allow(dead_code)]
impl ProjectTimingData {
pub(super) fn add_sample(&mut self, duration_ms: u64, remote: bool) {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let record = TimingRecord {
timestamp,
duration_ms,
remote,
};
let samples = if remote {
&mut self.remote_samples
} else {
&mut self.local_samples
};
samples.push(record);
if samples.len() > MAX_TIMING_SAMPLES {
samples.remove(0);
}
}
pub(super) fn median_duration(&self, remote: bool) -> Option<u64> {
let samples = if remote {
&self.remote_samples
} else {
&self.local_samples
};
if samples.is_empty() {
return None;
}
let mut durations: Vec<u64> = samples.iter().map(|r| r.duration_ms).collect();
durations.sort_unstable();
let mid = durations.len() / 2;
Some(if durations.len().is_multiple_of(2) {
(durations[mid - 1] + durations[mid]) / 2
} else {
durations[mid]
})
}
pub(super) fn speedup_ratio(&self) -> Option<f64> {
let local_median = self.median_duration(false)?;
let remote_median = self.median_duration(true)?;
if remote_median == 0 {
return None;
}
Some(local_median as f64 / remote_median as f64)
}
fn most_recent_timestamp(&self) -> u64 {
let local_max = self
.local_samples
.iter()
.map(|r| r.timestamp)
.max()
.unwrap_or(0);
let remote_max = self
.remote_samples
.iter()
.map(|r| r.timestamp)
.max()
.unwrap_or(0);
local_max.max(remote_max)
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub(super) struct TimingHistory {
#[serde(default)]
pub entries: HashMap<String, ProjectTimingData>,
}
static TIMING_CACHE: std::sync::OnceLock<std::sync::RwLock<TimingHistory>> =
std::sync::OnceLock::new();
pub(super) fn timing_cache() -> &'static std::sync::RwLock<TimingHistory> {
TIMING_CACHE.get_or_init(|| std::sync::RwLock::new(TimingHistory::load_from_disk()))
}
impl TimingHistory {
fn load_from_disk() -> Self {
let Some(path) = timing_history_path() else {
return Self::default();
};
match std::fs::read_to_string(&path) {
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
Err(_) => Self::default(),
}
}
fn save_to_disk(&self) {
let Some(path) = timing_history_path() else {
return;
};
if let Some(parent) = path.parent()
&& let Err(e) = std::fs::create_dir_all(parent)
{
warn!(
"Failed to create timing history directory {}: {}",
parent.display(),
e
);
return;
}
let temp_path = path.with_extension("tmp");
let content = match serde_json::to_string_pretty(self) {
Ok(c) => c,
Err(e) => {
warn!("Failed to serialize timing history: {}", e);
return;
}
};
if let Err(e) = std::fs::write(&temp_path, &content) {
warn!(
"Failed to write timing history to {}: {}",
temp_path.display(),
e
);
return;
}
if let Err(e) = std::fs::rename(&temp_path, &path) {
warn!(
"Failed to rename timing history {} -> {}: {}",
temp_path.display(),
path.display(),
e
);
}
}
pub(super) fn key(project: &str, kind: Option<CompilationKind>) -> String {
let kind_str = kind
.map(|k| format!("{:?}", k))
.unwrap_or_else(|| "Unknown".to_string());
format!("{}:{}", project, kind_str)
}
pub(super) fn get(
&self,
project: &str,
kind: Option<CompilationKind>,
) -> Option<&ProjectTimingData> {
self.entries.get(&Self::key(project, kind))
}
pub(super) fn record(
&mut self,
project: &str,
kind: Option<CompilationKind>,
duration_ms: u64,
remote: bool,
) {
let key = Self::key(project, kind);
let data = self.entries.entry(key).or_default();
data.add_sample(duration_ms, remote);
if self.entries.len() > MAX_TIMING_PROJECTS {
if let Some(oldest_key) = self
.entries
.iter()
.min_by_key(|(_, data)| data.most_recent_timestamp())
.map(|(k, _)| k.clone())
{
self.entries.remove(&oldest_key);
}
}
}
}
fn timing_history_path() -> Option<PathBuf> {
dirs::cache_dir().map(|dir| dir.join("rch").join("timing_history.json"))
}
pub(super) fn record_build_timing(
project: &str,
kind: Option<CompilationKind>,
duration_ms: u64,
remote: bool,
) {
let _lock = timing_history_path().and_then(|path| {
std::fs::create_dir_all(path.parent()?).ok()?;
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(path.with_extension("lock"))
.ok()?;
file.lock().ok().map(|()| file)
});
let mut history = TimingHistory::load_from_disk();
history.record(project, kind, duration_ms, remote);
history.save_to_disk();
match timing_cache().write() {
Ok(mut cache) => *cache = history,
Err(poison) => {
tracing::warn!(
target: "rch::hook::timing",
"timing cache RwLock poisoned; recovering"
);
*poison.into_inner() = history;
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub(super) struct TimingEstimate {
pub predicted_local_ms: u64,
pub predicted_speedup: Option<f64>,
}
#[allow(dead_code)]
#[allow(unused_variables)] pub(super) fn estimate_timing_for_build(
project: &str,
kind: Option<CompilationKind>,
config: &rch_common::RchConfig,
) -> Option<TimingEstimate> {
let cache = timing_cache();
let history = cache.read().ok()?;
let data = history.get(project, kind)?;
let local_median = data.median_duration(false)?;
let speedup = data.speedup_ratio();
Some(TimingEstimate {
predicted_local_ms: local_median,
predicted_speedup: speedup,
})
}