use std::path::Path;
use super::{GpuDb, escape_regex, fmt_us, trunc};
pub fn cmd_layers(db: &GpuDb) {
let sql = "SELECT id, source, file, collected_at, collection_secs FROM layers ORDER BY id";
let rows: Vec<(i64, String, String, String, Option<f64>)> = db.query_vec(sql, [], |row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
});
if rows.is_empty() {
println!("no layers loaded");
return;
}
println!(" # Source File Collected Secs");
println!(" ── ─────── ──────────────────────────────────────── ────────────────── ─────");
for (id, source, file, at, secs) in &rows {
println!(
" {:<2} {:<7} {:<43} {:<18} {}",
id,
source,
trunc(file, 43),
&at[..at.len().min(18)],
secs.map(|s| format!("{s:.1}"))
.unwrap_or_else(|| "?".into())
);
}
let uk = db.unique_kernel_count();
let wm = db.kernels_with_metrics();
let wo = db.kernels_with_ops();
println!("\n Correlation: {uk} unique kernels");
println!(" With metrics: {wm}/{uk}");
println!(" With op map: {wo}/{uk}");
}
pub fn cmd_suggest(db: &GpuDb) {
let uk = db.unique_kernel_count();
let failures = db.failures();
let has_nsys = db.has_layer("nsys");
let has_ncu = db.has_layer("ncu");
let has_torch = db.has_layer("torch");
let target = db.meta("target");
if uk == 0 && failures.is_empty() {
println!("no profile data");
return;
}
let mut n = 1;
if !failures.is_empty() {
println!(" Collection failures:\n");
for (phase, error) in &failures {
println!(" {n}. {phase} failed: {error}");
n += 1;
}
println!();
}
if uk == 0 {
println!(" No kernel data collected.");
return;
}
println!(" Suggestions:\n");
if !has_nsys {
println!(" {n}. No timeline data. Run gdbg with your target.");
println!(" This gives: kernel timeline, memory transfers, GPU idle gaps\n");
n += 1;
}
if !has_ncu {
let tl = db.timeline_filter();
let top_sql = format!(
"SELECT kernel_name, SUM(duration_us) as total
FROM launches WHERE {tl} GROUP BY kernel_name ORDER BY total DESC LIMIT 5"
);
let top: Vec<(String, f64)> =
db.query_vec(&top_sql, [], |row| Ok((row.get(0)?, row.get(1)?)));
if !top.is_empty() {
let gpu_total = db.total_gpu_time_us();
let pct: f64 = top
.iter()
.map(|t| {
if gpu_total > 0.0 {
t.1 / gpu_total * 100.0
} else {
0.0
}
})
.sum();
let regex = top
.iter()
.map(|t| escape_regex(&t.0))
.collect::<Vec<_>>()
.join("|");
println!(
" {n}. Top {} kernels ({pct:.0}% of GPU) lack hardware metrics.",
top.len()
);
println!(" Collect: ncu --set full --kernel-name \"regex:{regex}\" {target}\n");
n += 1;
}
}
if !has_torch && target.ends_with(".py") {
println!(" {n}. No op->kernel mapping. Can't trace kernels back to Python.");
println!(" Needed for: ops, callers, trace commands\n");
n += 1;
}
let tl2 = db.timeline_filter();
let var_sql = format!(
"SELECT kernel_name, COUNT(*) as cnt, AVG(duration_us) as avg,
AVG(duration_us * duration_us) - AVG(duration_us) * AVG(duration_us) as var
FROM launches WHERE {tl2} GROUP BY kernel_name
HAVING cnt > 5 AND var > 0
ORDER BY SUM(duration_us) DESC LIMIT 5"
);
let vars: Vec<(String, f64, f64)> = db.query_vec(&var_sql, [], |row| {
Ok((row.get(0)?, row.get(2)?, row.get(3)?))
});
for (name, avg, var) in &vars {
let stddev = var.max(0.0).sqrt();
let cv = if *avg > 0.0 { stddev / avg } else { 0.0 };
if cv > 0.3 {
println!(" {n}. '{}' has high variance (CV={cv:.2}).", name);
println!(
" May indicate: data-dependent paths, cache effects, or varying input sizes.\n"
);
n += 1;
}
}
if has_nsys {
let gpu_us = db.total_gpu_time_us();
let transfer_filter = db.timeline_filter_for("transfers");
let xfer_us: f64 = db.scalar_f64(&format!(
"SELECT COALESCE(SUM(duration_us),0) FROM transfers WHERE {transfer_filter}"
));
if gpu_us > 0.0 && xfer_us > 0.0 {
let ratio = xfer_us / gpu_us;
if ratio > 5.0 {
println!(" {n}. Transfer:compute ratio is {ratio:.1}:1 — PCIe dominates.");
println!(
" Try: cudaMallocHost (pinned memory), overlap via CUDA streams, or increase batch size.\n"
);
n += 1;
}
}
let tl = db.timeline_filter();
let tiny_sql = format!(
"SELECT COUNT(*) FROM (
SELECT kernel_name FROM launches WHERE {tl}
GROUP BY kernel_name HAVING AVG(duration_us) < 10.0
)"
);
let tiny_count: i64 = db.scalar_f64(&tiny_sql) as i64;
if tiny_count > 10 {
println!(
" {n}. {tiny_count} distinct kernels average under 10us — launch overhead likely dominates."
);
println!(
" Try: torch.compile(), CUDA graphs, or manual kernel fusion. See 'small' and 'fuse'.\n"
);
n += 1;
}
let dom_sql = format!(
"SELECT kernel_name, SUM(duration_us) as t FROM launches WHERE {tl}
GROUP BY kernel_name ORDER BY t DESC LIMIT 1"
);
if let Ok((dom_name, dom_time)) = db.conn.query_row(&dom_sql, [], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, f64>(1)?))
}) {
if gpu_us > 0.0 && dom_time / gpu_us > 0.5 {
let pct = dom_time / gpu_us * 100.0;
println!(
" {n}. One kernel accounts for {pct:.0}% of GPU time: {}",
trunc(&dom_name, 60)
);
println!(
" Try: bound '{}' — optimize the hotspot directly.\n",
trunc(&dom_name, 40)
);
n += 1;
}
}
}
if has_nsys && has_ncu && (has_torch || !target.ends_with(".py")) {
println!(" All layers loaded — full analysis available.");
}
let _ = n;
}
pub fn cmd_save(db: &GpuDb, args: &[&str]) {
let name = match args.first() {
Some(n) => *n,
None => {
println!("usage: save <name>");
return;
}
};
match db.save(name) {
Ok(path) => println!("saved to {}", path.display()),
Err(e) => println!("save failed: {e}"),
}
}
pub fn cmd_list() {
match GpuDb::list_saved() {
Ok(sessions) => {
if sessions.is_empty() {
println!("no saved sessions");
return;
}
println!(" Name Device Kernels Layers Created");
println!(
" ─────────────────────── ─────────────── ──────── ──────────────── ────────────────"
);
for s in &sessions {
let dev = if s.device.is_empty() { "?" } else { &s.device };
println!(
" {:<23} {:<15} {:>7} {:<16} {}",
trunc(&s.name, 23),
trunc(dev, 15),
s.kernel_count,
s.layers.join("+"),
created_prefix(&s.created)
);
}
}
Err(e) => println!("list failed: {e}"),
}
}
fn created_prefix(created: &str) -> String {
created.chars().take(16).collect()
}
#[cfg(test)]
mod tests {
use super::created_prefix;
#[test]
fn saved_date_prefix_is_utf8_safe() {
let created = "2026-07-28T12:évidence";
let prefix = created_prefix(created);
assert_eq!(prefix.chars().count(), 16);
assert!(created.starts_with(&prefix));
}
}
pub fn cmd_diff(db: &GpuDb, args: &[&str]) {
let name = match args.first() {
Some(n) => *n,
None => {
println!("usage: diff <saved_session>");
return;
}
};
let other = if name.ends_with(".gpu.db") || name.contains('/') {
match GpuDb::open(Path::new(name)) {
Ok(saved) => saved,
Err(error) => {
println!("cannot load '{name}': {error}");
return;
}
}
} else {
match GpuDb::load(name) {
Ok(saved) => saved,
Err(error) => {
println!("cannot load '{name}': {error}");
return;
}
}
};
if let Err(e) = db.attach_db(&other, "other") {
println!("cannot load '{name}': {e}");
return;
}
let current_tl = db.timeline_filter();
let other_tl = "other.launches.layer_id = COALESCE(
(SELECT id FROM other.layers WHERE source = 'nsys' ORDER BY id LIMIT 1),
(SELECT id FROM other.layers WHERE source = 'torch' ORDER BY id LIMIT 1),
(SELECT id FROM other.layers WHERE source = 'proton' ORDER BY id LIMIT 1), -1)";
let sql = format!("SELECT
COALESCE(c.kernel_name, o.kernel_name) as name,
COALESCE(o.total, 0) as before,
COALESCE(c.total, 0) as after
FROM
(SELECT kernel_name, SUM(duration_us) as total FROM launches WHERE {current_tl} GROUP BY kernel_name) c
FULL OUTER JOIN
(SELECT kernel_name, SUM(duration_us) as total FROM other.launches WHERE {other_tl} GROUP BY kernel_name) o
ON c.kernel_name = o.kernel_name
ORDER BY ABS(COALESCE(c.total,0) - COALESCE(o.total,0)) DESC
LIMIT 15");
let rows: Vec<(String, f64, f64)> =
match db.query_vec_result(&sql, [], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?))) {
Ok(rows) => rows,
Err(error) => {
println!("diff query failed for '{name}': {error}");
let _ = db.detach("other");
return;
}
};
println!(" Diff: current vs {name}\n");
println!(" Kernel Before After Delta");
println!(" ────────────────────────────────── ────────── ────────── ──────────");
for (kname, before, after) in &rows {
let delta = if *before > 0.0 {
let pct = (after - before) / before * 100.0;
format!("{}{pct:.1}%", if pct >= 0.0 { "+" } else { "" })
} else {
"new".into()
};
println!(
" {:<34} {:>10} {:>10} {:>10}",
trunc(kname, 34),
fmt_us(*before),
fmt_us(*after),
delta
);
}
let _ = db.detach("other");
}