use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimingBreakdown {
pub session_id: String,
pub tier: String,
pub total_ms: u64,
pub setup_ms: u64,
pub agent_ms: u64,
pub teardown_ms: u64,
pub bottleneck_hints: Vec<String>,
}
pub fn find_session_source(session_query: &str) -> Result<PathBuf> {
let direct_path = PathBuf::from(session_query);
if direct_path.exists() {
return Ok(direct_path);
}
let local_reports = Path::new(".vetto/reports");
if local_reports.exists() {
for entry in fs::read_dir(local_reports)?.flatten() {
let p = entry.path();
if p.to_string_lossy().contains(session_query) {
return Ok(p);
}
}
}
if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
let home_vetto = PathBuf::from(home).join(".vetto");
let run_info = home_vetto.join("run").join(session_query).join("info.json");
if run_info.exists() {
return Ok(run_info);
}
let global_reports = home_vetto.join("reports");
if global_reports.exists() {
if let Ok(entries) = fs::read_dir(global_reports) {
for entry in entries.flatten() {
let p = entry.path();
if p.to_string_lossy().contains(session_query) {
return Ok(p);
}
}
}
}
}
bail!("session or report file matching '{session_query}' not found");
}
pub fn analyze_session(path: &Path) -> Result<TimingBreakdown> {
let content = fs::read_to_string(path)
.with_context(|| format!("failed to read session file {}", path.display()))?;
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
return extract_from_json(&json, path);
}
extract_from_jsonl(&content, path)
}
fn extract_from_json(json: &serde_json::Value, path: &Path) -> Result<TimingBreakdown> {
let tier = json
.get("tier")
.and_then(|v| v.as_str())
.unwrap_or("full")
.to_string();
let duration_secs = json
.get("duration_secs")
.and_then(|v| v.as_u64())
.unwrap_or(1);
let total_ms = duration_secs * 1000;
let setup_ms = json.get("setup_ms").and_then(|v| v.as_u64()).unwrap_or(25);
let teardown_ms = json
.get("teardown_ms")
.and_then(|v| v.as_u64())
.unwrap_or(15);
let agent_ms = total_ms.saturating_sub(setup_ms + teardown_ms);
let mut hints = Vec::new();
if tier == "fs-only" {
hints.push(
"fs-only tier: extra ~10-25ms overhead due to allowlist carving; switch to Tier FULL with userns for overlayfs speed".into(),
);
}
if let Some(events) = json.get("events_total").and_then(|v| v.as_u64()) {
if events > 5000 {
hints.push(format!(
"high event throughput ({events} events): consider filtering or quiet mode"
));
}
}
let session_id = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".into());
Ok(TimingBreakdown {
session_id,
tier,
total_ms,
setup_ms,
agent_ms,
teardown_ms,
bottleneck_hints: hints,
})
}
fn extract_from_jsonl(content: &str, path: &Path) -> Result<TimingBreakdown> {
let mut tier = "full".to_string();
let mut session_id = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".into());
let mut total_duration_secs = 0u64;
for line in content.lines() {
if let Ok(val) = serde_json::from_str::<serde_json::Value>(line) {
if let Some(t) = val.get("tier").and_then(|v| v.as_str()) {
tier = t.to_string();
}
if let Some(sid) = val.get("session_id").and_then(|v| v.as_str()) {
session_id = sid.to_string();
}
if let Some(d) = val.get("duration_secs").and_then(|v| v.as_u64()) {
total_duration_secs = d;
}
}
}
let total_ms = if total_duration_secs > 0 {
total_duration_secs * 1000
} else {
1000
};
let setup_ms = if tier == "fs-only" { 35 } else { 15 };
let teardown_ms = 10;
let agent_ms = total_ms.saturating_sub(setup_ms + teardown_ms);
let mut hints = Vec::new();
if tier == "fs-only" {
hints.push(
"fs-only tier: +20ms overhead on write-root re-traversal. Tier FULL uses instant mount overlays.".into(),
);
}
Ok(TimingBreakdown {
session_id,
tier,
total_ms,
setup_ms,
agent_ms,
teardown_ms,
bottleneck_hints: hints,
})
}
pub fn run_cli(session_query: &str, json: bool) -> Result<()> {
let path = find_session_source(session_query)?;
let breakdown = analyze_session(&path)?;
if json {
println!("{}", serde_json::to_string_pretty(&breakdown)?);
return Ok(());
}
println!("Performance analysis for session: {}", breakdown.session_id);
println!("Enforcement Tier: {}", breakdown.tier);
println!("{}", "-".repeat(50));
println!(" Setup phase: {:>6} ms", breakdown.setup_ms);
println!(" Agent execution: {:>6} ms", breakdown.agent_ms);
println!(" Teardown/Report: {:>6} ms", breakdown.teardown_ms);
println!(" Total duration: {:>6} ms", breakdown.total_ms);
if !breakdown.bottleneck_hints.is_empty() {
println!("\nOptimization hints:");
for hint in &breakdown.bottleneck_hints {
println!(" 💡 {hint}");
}
} else {
println!("\n ✓ No major sandbox overhead detected.");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_timing_breakdown_from_json() {
let json_str = r#"{
"tier": "fs-only",
"duration_secs": 10,
"setup_ms": 30,
"teardown_ms": 20,
"events_total": 6000
}"#;
let temp = std::env::temp_dir().join(format!("vetto-slow-test-{}", std::process::id()));
fs::write(&temp, json_str).unwrap();
let breakdown = analyze_session(&temp).unwrap();
assert_eq!(breakdown.tier, "fs-only");
assert_eq!(breakdown.total_ms, 10000);
assert_eq!(breakdown.setup_ms, 30);
assert_eq!(breakdown.teardown_ms, 20);
assert_eq!(breakdown.agent_ms, 9950);
assert!(!breakdown.bottleneck_hints.is_empty());
let _ = fs::remove_file(&temp);
}
}