use crate::app::blame::{BlameRequest, BlameUseCase};
use crate::domain::{BinaryBlame, DependencyChangeType};
use perfgate_types::{CompareReceipt, Metric, MetricStatus};
use std::path::PathBuf;
pub struct ExplainRequest {
pub compare: PathBuf,
pub baseline_lock: Option<PathBuf>,
pub current_lock: Option<PathBuf>,
}
pub struct ExplainOutcome {
pub markdown: String,
}
pub struct ExplainUseCase;
impl ExplainUseCase {
pub fn execute(&self, req: ExplainRequest) -> anyhow::Result<ExplainOutcome> {
let compare: CompareReceipt = perfgate_types::read_json_file(&req.compare)?;
let mut md = String::new();
md.push_str(&format!(
"# Performance Analysis for `{}`\n\n",
compare.bench.name
));
if compare.verdict.status == perfgate_types::VerdictStatus::Pass {
md.push_str("✅ **Great news!** No significant performance regressions were detected in this run.\n");
return Ok(ExplainOutcome { markdown: md });
}
md.push_str("⚠️ **Performance Regressions Detected**\n\n");
md.push_str("The following metrics exceeded their budgets. Below are automated playbooks to help diagnose and resolve the issues:\n\n");
let blame = if let (Some(base), Some(curr)) = (req.baseline_lock, req.current_lock) {
let blame_usecase = BlameUseCase;
match blame_usecase.execute(BlameRequest {
baseline_lock: base,
current_lock: curr,
}) {
Ok(o) => Some(o.blame),
Err(err) => {
eprintln!("warning: blame analysis failed: {err}");
None
}
}
} else {
None
};
for (metric, delta) in &compare.deltas {
if delta.status == MetricStatus::Fail || delta.status == MetricStatus::Warn {
let threshold = compare
.budgets
.get(metric)
.map(|b| {
if delta.status == MetricStatus::Fail {
b.threshold
} else {
b.warn_threshold
}
})
.unwrap_or(0.0);
md.push_str(&format!("## {}\n", metric.as_str()));
md.push_str(&format!(
"**Regression**: {:.2}% (Threshold: {:.2}%)\n\n",
delta.regression * 100.0,
threshold * 100.0
));
md.push_str(&Self::playbook_for_metric(metric, blame.as_ref()));
md.push('\n');
}
}
md.push_str("---\n\n");
md.push_str("### 🤖 LLM Prompt\n");
md.push_str("Copy the text below and paste it into an LLM (like Gemini, ChatGPT, or Claude) along with your PR diff to get a detailed explanation:\n\n");
md.push_str("```text\n");
md.push_str("Act as a senior performance engineer. I have a performance regression in my pull request.\n\n");
md.push_str(&format!("Benchmark: {}\n", compare.bench.name));
for (metric, delta) in &compare.deltas {
if delta.status == MetricStatus::Fail || delta.status == MetricStatus::Warn {
md.push_str(&format!(
"- {} degraded by {:.2}%\n",
metric.as_str(),
delta.regression * 100.0
));
}
}
if let Some(b) = &blame {
md.push_str("\nDetected Dependency Changes (Binary Blame):\n");
for change in &b.changes {
match change.change_type {
DependencyChangeType::Added => {
md.push_str(&format!(
" - Added: {} v{}\n",
change.name,
change.new_version.as_deref().unwrap_or("?")
));
}
DependencyChangeType::Removed => {
md.push_str(&format!(
" - Removed: {} v{}\n",
change.name,
change.old_version.as_deref().unwrap_or("?")
));
}
DependencyChangeType::Updated => {
md.push_str(&format!(
" - Updated: {} ({} -> {})\n",
change.name,
change.old_version.as_deref().unwrap_or("?"),
change.new_version.as_deref().unwrap_or("?")
));
}
}
}
}
md.push_str("\nPlease analyze the attached code diff and explain what changes might have caused these specific metric regressions. Suggest code optimizations to fix the issue.\n");
md.push_str("```\n");
Ok(ExplainOutcome { markdown: md })
}
fn playbook_for_metric(metric: &Metric, blame: Option<&BinaryBlame>) -> String {
match metric {
Metric::WallMs => "### Wall Time Playbook\n- **Check for blocking I/O**: Are you doing disk or network operations on the main thread?\n- **Algorithm Complexity**: Did you add nested loops or expensive sorts?\n- **Lock Contention**: Check for deadlocks or heavy mutex usage in concurrent code.".to_string(),
Metric::CpuMs => "### CPU Time Playbook\n- **Hot Loops**: Profile the code (e.g. using `perf` or `flamegraph`) to find where CPU time is spent.\n- **Allocation Overhead**: Did you add unnecessary clones or heap allocations inside a loop?\n- **Inlining**: Ensure small, frequently called functions are inlined.".to_string(),
Metric::MaxRssKb => "### Peak Memory (RSS) Playbook\n- **Memory Leaks**: Check if you are retaining references to objects that should be dropped.\n- **Buffer Sizing**: Are you pre-allocating extremely large buffers? Consider streaming or chunking data.\n- **Data Structures**: Can you use more memory-efficient data structures (e.g. `Box<[T]>` instead of `Vec<T>`)?".to_string(),
Metric::IoReadBytes => "### Disk Read Playbook\n- **Redundant Reads**: Are you reading the same file multiple times?\n- **Buffering**: Use buffered readers (`BufReader`) to reduce syscalls.\n- **Lazy Loading**: Delay reading file contents until strictly necessary.".to_string(),
Metric::IoWriteBytes => "### Disk Write Playbook\n- **Redundant Writes**: Can you batch writes in memory before flushing to disk?\n- **Buffering**: Use `BufWriter` for many small writes.\n- **Log Level**: Did you accidentally leave verbose logging enabled?".to_string(),
Metric::NetworkPackets => "### Network Playbook\n- **Batching**: Are you making N+1 API queries? Consolidate them into a bulk endpoint.\n- **Caching**: Cache immutable remote resources instead of fetching them repeatedly.\n- **Connection Pooling**: Are you opening a new TCP connection for every request? Reuse connections.".to_string(),
Metric::CtxSwitches => "### Context Switch Playbook\n- **Thread Thrashing**: Are you spawning too many threads for CPU-bound work? (Match thread count to physical cores).\n- **Async Yielding**: Are you yielding too often in an async executor?\n- **Lock Contention**: High context switches often point to threads repeatedly waking up and going back to sleep on a lock.".to_string(),
Metric::PageFaults => "### Page Faults Playbook\n- **Memory Thrashing**: You might be allocating memory faster than the OS can provide physical pages. Pre-allocate and reuse memory buffers.\n- **Memory Mapping**: If using `mmap`, sequential access is better than random access for triggering pre-fetching.".to_string(),
Metric::BinaryBytes => {
let mut playbook = "### Binary Size Playbook\n- **Dependency Bloat**: Run `cargo tree` to see if a heavy dependency was introduced. Use the perfgate Binary Blame feature.\n- **Monomorphization**: Heavy use of generics can lead to code bloat. Try using trait objects (`dyn Trait`) in cold paths.\n- **Debug Info**: Ensure you are stripping debug symbols in release builds.".to_string();
if let Some(b) = blame {
playbook.push_str("\n\n**Binary Blame Analysis**:\n");
if b.changes.is_empty() {
playbook.push_str("No dependency changes detected in Cargo.lock.\n");
} else {
playbook.push_str(&format!("Detected {} dependency changes:\n", b.changes.len()));
for change in b.changes.iter().take(10) {
playbook.push_str(&format!("- {} ({:?})\n", change.name, change.change_type));
}
if b.changes.len() > 10 {
playbook.push_str(&format!("- ... and {} more.\n", b.changes.len() - 10));
}
}
}
playbook
},
Metric::ThroughputPerS => "### Throughput Playbook\n- **Bottlenecks**: A drop in throughput usually indicates a bottleneck in CPU or I/O. Consult the Wall Time and CPU playbooks.\n- **Concurrency Limit**: Check if a semaphore or connection pool is artificially limiting concurrent work units.".to_string(),
Metric::EnergyUj => "### Energy Efficiency Playbook\n- **Busy Waiting**: Are you using `spin` loops? Use OS-backed blocking primitives instead.\n- **High CPU Utilization**: Energy correlates strongly with CPU time. Optimize your algorithms to do less work.\n- **Polling**: Switch from polling models to event-driven (interrupt-based) architectures.".to_string(),
}
}
}