blob: e0431e3a73e2773ac2ec97b8389af57cac4b8632 [file]
use crate::config::Environment;
use crate::parser::profile_parser;
use serde_json::{json, Value};
/// Compare two profiles: which operators got slower, where rows exploded, where skew appeared.
pub async fn run(slow_qid: &str, fast_qid: &str, env: &Environment) -> anyhow::Result<Value> {
// Fetch both profiles — diff inherits fan-out across FEs automatically.
let slow_fetched = super::fetch::fetch_profile_text(slow_qid, env)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
let fast_fetched = super::fetch::fetch_profile_text(fast_qid, env)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
let slow_profile = profile_parser::parse(&slow_fetched.text);
let fast_profile = profile_parser::parse(&fast_fetched.text);
let slow_ops = profile_parser::flatten_operators(&slow_profile);
let fast_ops = profile_parser::flatten_operators(&fast_profile);
let slow_total_ms = slow_profile.summary.total_time_ms.unwrap_or(0.0);
let fast_total_ms = fast_profile.summary.total_time_ms.unwrap_or(0.0);
let time_ratio = if fast_total_ms > 0.0 {
slow_total_ms / fast_total_ms
} else {
0.0
};
// Match operators by name+fragment for comparison
let mut operator_diffs = Vec::new();
for slow_op in &slow_ops {
// Find matching operator in fast run (by name and fragment)
let fast_op = fast_ops.iter().find(|f| {
f.name == slow_op.name && f.frag == slow_op.frag && f.pipeline == slow_op.pipeline
});
if let Some(fast_op) = fast_op {
let time_delta_ms = slow_op.exec_time_avg_ms - fast_op.exec_time_avg_ms;
let time_ratio_op = if fast_op.exec_time_avg_ms > 0.0 {
slow_op.exec_time_avg_ms / fast_op.exec_time_avg_ms
} else {
0.0
};
let rows_delta = match (slow_op.input_rows, fast_op.input_rows) {
(Some(s), Some(f)) if f > 0.0 => Some(round2(s / f)),
_ => None,
};
let skew_delta = match (slow_op.skew_ratio, fast_op.skew_ratio) {
(Some(s), Some(f)) => Some(round2(s - f)),
_ => None,
};
// Only include operators with meaningful differences
let significant = time_delta_ms.abs() > 1.0
|| rows_delta.map(|r| (r - 1.0).abs() > 0.1).unwrap_or(false)
|| skew_delta.map(|s| s.abs() > 0.5).unwrap_or(false);
if significant {
operator_diffs.push(json!({
"name": slow_op.name,
"frag": slow_op.frag,
"slow_ms": slow_op.exec_time_avg_ms,
"fast_ms": fast_op.exec_time_avg_ms,
"time_delta_ms": round2(time_delta_ms),
"time_ratio": round2(time_ratio_op),
"rows_ratio": rows_delta,
"skew_delta": skew_delta,
"slow_selectivity": slow_op.selectivity,
"fast_selectivity": fast_op.selectivity,
"slow_spilled": slow_op.spilled,
"fast_spilled": fast_op.spilled,
}));
}
} else if slow_op.exec_time_avg_ms > 1.0 {
// Operator exists only in slow run
operator_diffs.push(json!({
"name": slow_op.name,
"frag": slow_op.frag,
"slow_ms": slow_op.exec_time_avg_ms,
"fast_ms": null,
"time_delta_ms": slow_op.exec_time_avg_ms,
"note": "only in slow run",
}));
}
}
// Sort by time_delta descending (biggest regressions first)
operator_diffs.sort_by(|a, b| {
let da = a
.get("time_delta_ms")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let db = b
.get("time_delta_ms")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
db.partial_cmp(&da).unwrap_or(std::cmp::Ordering::Equal)
});
Ok(json!({
"slow": {
"query_id": slow_qid,
"total_time_ms": slow_total_ms,
"total_time": slow_profile.summary.total_time,
"operator_count": slow_ops.len(),
},
"fast": {
"query_id": fast_qid,
"total_time_ms": fast_total_ms,
"total_time": fast_profile.summary.total_time,
"operator_count": fast_ops.len(),
},
"time_ratio": round2(time_ratio),
"operator_diffs": operator_diffs,
}))
}
fn round2(v: f64) -> f64 {
(v * 100.0).round() / 100.0
}