Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

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}");
    }
}