use serde::{Deserialize, Serialize};
use std::io::Write;
use std::path::Path;
#[derive(Debug, Serialize, Deserialize)]
pub struct BenchEntry {
pub ts: u64,
pub session_id: String,
pub project: String,
pub task: String,
pub model: String,
pub mode: String,
pub iter: u32,
pub secs: u64,
pub tools: u32,
pub tools_ok: u32,
pub tokens_in: u64,
pub tokens_out: u64,
pub api_calls: u32,
pub cache_pct: u8,
pub ctx_pct: u8,
pub compactions: u32,
pub cancelled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compaction_quality: Option<CompactionQuality>,
#[serde(default)]
pub tool_latency_avg_ms: f64,
#[serde(default)]
pub tool_latency_max_ms: u64,
#[serde(default)]
pub api_latency_avg_ms: f64,
#[serde(default)]
pub api_latency_max_ms: u64,
#[serde(default = "default_success_rate")]
pub tool_success_rate: f32,
#[serde(default)]
pub tokens_per_iteration: f64,
#[serde(default)]
pub tools_per_iteration: f64,
#[serde(default)]
pub top_tools: Vec<(String, u32)>,
#[serde(default)]
pub continuation_round: u32,
#[serde(default = "default_outcome")]
pub outcome: String,
#[serde(default)]
pub provider: String,
}
fn default_success_rate() -> f32 {
100.0
}
fn default_outcome() -> String {
"success".to_string()
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CompactionQuality {
pub preserved_files: f32,
pub preserved_decisions: f32,
pub preserved_errors: f32,
pub token_reduction: f32,
}
pub fn append_entry(path: &Path, entry: &BenchEntry) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
let line = serde_json::to_string(entry).map_err(std::io::Error::other)?;
writeln!(file, "{line}")
}
pub fn prune_old_entries(path: &Path, retain_days: u32) {
if !path.exists() || retain_days == 0 {
return;
}
let cutoff = unix_now().saturating_sub(retain_days as u64 * 86_400);
let Ok(content) = std::fs::read_to_string(path) else {
return;
};
let kept: Vec<&str> = content
.lines()
.filter(|line| {
serde_json::from_str::<BenchEntry>(line)
.map(|e| e.ts >= cutoff)
.unwrap_or(true) })
.collect();
let new_content = if kept.is_empty() {
String::new()
} else {
kept.join("\n") + "\n"
};
let _ = std::fs::write(path, new_content);
}
pub fn unix_now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub fn short_path(working_dir: &str) -> String {
let path = std::path::Path::new(working_dir);
let parts: Vec<&str> = path
.components()
.filter_map(|c| c.as_os_str().to_str())
.collect();
match parts.len() {
0 => working_dir.to_string(),
1 => parts[0].to_string(),
n => format!("{}/{}", parts[n - 2], parts[n - 1]),
}
}