crates/dwarves-tune-graph/src/xcheck.rs
//! The comparison of the possible calls (graph) with the seen ones (probe).
//! Guides review and tests only: nothing here ever adds a rule to a filter.
use crate::{Gap, Graph, NearMiss, Report, Seen, Untested, REPORT_FORMAT};
/// Calls a vDSO usually answers without entering the kernel.
const VDSO: &[&str] = &["clock_gettime", "clock_getres", "gettimeofday", "time", "getcpu"];
fn gap_hint(g: &Graph) -> String {
let mut why = Vec::new();
if !g.unresolved_sites.is_empty() {
why.push(format!(
"{} unresolved site(s): a syscall number the graph could not resolve, or a call it widened (dyn dispatch, a call through a value)",
g.unresolved_sites.len()
));
}
if g.dlopen {
why.push("the program can dlopen code the graph never saw".to_string());
}
if !g.missing_libs.is_empty() {
why.push(format!("libraries not found: {}", g.missing_libs.join(", ")));
}
if !g.unresolved_imports.is_empty() {
why.push(format!(
"{} imported function(s) no loaded module defines",
g.unresolved_imports.len()
));
}
if why.is_empty() {
why.push(
"code the graph cannot follow (an indirect call it could not resolve, a JIT, or a foreign module)"
.to_string(),
);
}
why.join("; ")
}
/// Compare the possible calls with the seen ones.
pub fn cross_check(g: &Graph, s: &Seen) -> Report {
let mut confirmed = Vec::new();
let mut gaps = Vec::new();
let mut near_miss = Vec::new();
let hint = gap_hint(g);
for (name, runs) in &s.calls {
let reach = g.syscalls.get(name);
if reach.is_some() {
confirmed.push(name.clone());
} else {
gaps.push(Gap {
syscall: name.clone(),
runs: runs.iter().cloned().collect(),
hint: hint.clone(),
});
}
let only_error = !runs.is_empty() && runs.iter().all(|r| s.error_runs.contains(r));
if only_error {
near_miss.push(NearMiss {
syscall: name.clone(),
runs: runs.iter().cloned().collect(),
graph: match reach {
Some(r) if r.direct => "direct".into(),
Some(_) => "indirect".into(),
None => "not in graph".into(),
},
path: reach.map(|r| r.path.clone()).unwrap_or_default(),
});
}
}
let mut untested: Vec<Untested> = g
.syscalls
.iter()
.filter(|(n, _)| !s.calls.contains_key(*n))
.map(|(n, r)| Untested {
syscall: n.clone(),
direct: r.direct,
path: r.path.clone(),
site: r.site.clone(),
note: if VDSO.contains(&n.as_str()) {
"vdso: usually answered without a syscall".into()
} else {
String::new()
},
})
.collect();
// Direct paths first, then by name (stable, deterministic).
untested.sort_by(|a, b| b.direct.cmp(&a.direct).then(a.syscall.cmp(&b.syscall)));
let mut notes = Vec::new();
if s.kind == "oci" {
notes.push("the profile is an OCI file: no runs, so no near-miss view".to_string());
}
if !s.unknown.is_empty() {
notes.push(format!("the probe saw {} call(s) it could not name", s.unknown.len()));
}
Report {
format: REPORT_FORMAT.into(),
binary: g.binary.clone(),
profile: s.source.clone(),
workload: s.workload.clone(),
entries: g.entries.clone(),
modules: g.modules.clone(),
possible: g.syscalls.len(),
possible_direct: g.syscalls.values().filter(|r| r.direct).count(),
seen: s.calls.len(),
confirmed,
untested,
gaps,
near_miss,
unresolved_sites: g.unresolved_sites.len(),
missing_libs: g.missing_libs.clone(),
dlopen: g.dlopen,
guides_only: true,
notes,
}
}
/// The report for a terminal.
pub fn render_text(r: &Report) -> String {
let mut o = String::new();
o.push_str(&format!(
"graph cross-check: {} vs {}\n",
r.binary, r.profile
));
o.push_str("This report guides review and tests only; it never widens a filter.\n");
o.push_str(&format!(
"possible {} ({} direct), seen {}, confirmed {}, untested {}, gaps {}\n",
r.possible,
r.possible_direct,
r.seen,
r.confirmed.len(),
r.untested.len(),
r.gaps.len()
));
if !r.gaps.is_empty() {
o.push_str("\nSeen but not in the graph (graph gaps):\n");
for g in &r.gaps {
o.push_str(&format!(" {} runs: {} ({})\n", g.syscall, g.runs.join(", "), g.hint));
}
}
if !r.near_miss.is_empty() {
o.push_str("\nSeen only on error-path runs (near-misses):\n");
for n in &r.near_miss {
o.push_str(&format!(" {} graph: {} runs: {}\n", n.syscall, n.graph, n.runs.join(", ")));
}
}
if !r.untested.is_empty() {
o.push_str("\nPossible but unseen (untested paths, direct first):\n");
for u in &r.untested {
let kind = if u.direct { "direct" } else { "indirect" };
let note = if u.note.is_empty() { String::new() } else { format!(" [{}]", u.note) };
o.push_str(&format!(" {} {} {}{}\n", u.syscall, kind, u.path.join(" > "), note));
}
}
for n in &r.notes {
o.push_str(&format!("note: {n}\n"));
}
o
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Reach;
use std::collections::{BTreeMap, BTreeSet};
fn reach(direct: bool, path: &[&str]) -> Reach {
Reach {
nr: 0,
direct,
path: path.iter().map(|s| s.to_string()).collect(),
site: "main+0x0".into(),
sites: 1,
uncertain: None,
}
}
fn graph() -> Graph {
let mut syscalls = BTreeMap::new();
syscalls.insert("write".into(), reach(true, &["main"]));
syscalls.insert("sethostname".into(), reach(true, &["main", "rare_path"]));
syscalls.insert("pivot_root".into(), reach(false, &["(indirect call)", "via_pointer"]));
syscalls.insert("clock_gettime".into(), reach(true, &["main"]));
Graph {
format: None,
binary: "prog".into(),
arch: "x86_64".into(),
modules: vec![],
entries: vec!["main".into()],
syscalls,
unnamed: BTreeSet::new(),
unresolved_sites: vec![],
missing_libs: vec![],
unresolved_imports: vec![],
dlopen: true,
indirect_sites: 1,
reached_functions: None,
}
}
fn seen() -> Seen {
let mut calls = BTreeMap::new();
let set = |v: &[&str]| v.iter().map(|s| s.to_string()).collect::<BTreeSet<_>>();
calls.insert("write".into(), set(&["basic"]));
calls.insert("io_uring_setup".into(), set(&["basic"]));
calls.insert("sethostname".into(), set(&["bad-input"]));
Seen {
source: "p.json".into(),
kind: "probe".into(),
workload: "prog".into(),
calls,
error_runs: set(&["bad-input"]),
unknown: vec![],
}
}
#[test]
fn untested_gaps_and_near_misses_are_named() {
let r = cross_check(&graph(), &seen());
assert!(r.guides_only);
assert_eq!(r.confirmed, vec!["sethostname", "write"]);
assert_eq!(r.gaps.len(), 1);
assert_eq!(r.gaps[0].syscall, "io_uring_setup");
assert!(r.gaps[0].hint.contains("dlopen"), "{}", r.gaps[0].hint);
let names: Vec<_> = r.untested.iter().map(|u| (u.syscall.as_str(), u.direct)).collect();
assert_eq!(names, vec![("clock_gettime", true), ("pivot_root", false)], "direct first");
assert!(r.untested[0].note.starts_with("vdso"));
assert_eq!(r.near_miss.len(), 1);
assert_eq!(r.near_miss[0].graph, "direct");
assert_eq!((r.possible, r.possible_direct, r.seen), (4, 3, 3));
let t = render_text(&r);
assert!(t.contains("never widens") && t.contains("io_uring_setup"), "{t}");
}
}