use crate::graph::unified::concurrent::GraphSnapshot;
use crate::graph::unified::edge::EdgeKind;
use crate::graph::unified::node::{NodeId, NodeKind};
use crate::graph::unified::string::StringId;
const MAX_CHAIN_DEPTH: usize = 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HotspotRank {
pub node: NodeId,
pub name: StringId,
pub kind: NodeKind,
pub score: usize,
}
fn compute_call_chain_reach(calls_out: &mut [Vec<u32>]) -> Vec<usize> {
for list in calls_out.iter_mut() {
list.sort_unstable();
}
enum Step {
Enter(usize),
Exit(usize),
}
let n = calls_out.len();
let mut reach = vec![0usize; n];
let mut computed = vec![false; n];
let mut on_stack = vec![false; n];
for start in 0..n {
if computed[start] {
continue;
}
let mut stack = vec![Step::Enter(start)];
while let Some(step) = stack.pop() {
match step {
Step::Enter(node) => {
if computed[node] {
continue;
}
on_stack[node] = true;
stack.push(Step::Exit(node));
for &callee in &calls_out[node] {
let c = callee as usize;
if c < n && !computed[c] && !on_stack[c] {
stack.push(Step::Enter(c));
}
}
}
Step::Exit(node) => {
let mut best = 0usize;
for &callee in &calls_out[node] {
let c = callee as usize;
if c < n && computed[c] {
best = best.max(1 + reach[c]);
}
}
on_stack[node] = false;
reach[node] = best.min(MAX_CHAIN_DEPTH);
computed[node] = true;
}
}
}
}
reach
}
fn node_complexity(callees: &[u32], reach: &[usize]) -> usize {
let mut max_depth = 0usize;
for &callee in callees {
let depth = reach
.get(callee as usize)
.map_or(1, |&r| (1 + r).min(MAX_CHAIN_DEPTH));
max_depth = max_depth.max(depth);
}
callees.len() + max_depth
}
#[must_use]
pub fn rank_hotspots(snapshot: &GraphSnapshot, top: usize) -> Vec<HotspotRank> {
let slots = snapshot.nodes().slot_count();
let mut calls_out: Vec<Vec<u32>> = vec![Vec::new(); slots];
for edge_ref in snapshot.edges().all_live_forward_edges() {
if !matches!(edge_ref.kind, EdgeKind::Calls { .. }) {
continue;
}
if let Some(list) = calls_out.get_mut(edge_ref.source.index() as usize) {
list.push(edge_ref.target.index());
}
}
let reach = compute_call_chain_reach(&mut calls_out);
let mut ranked: Vec<HotspotRank> = Vec::new();
for (id, entry) in snapshot.iter_nodes() {
if entry.is_unified_loser() {
continue;
}
if !matches!(entry.kind, NodeKind::Function | NodeKind::Method) {
continue;
}
let callees = calls_out
.get(id.index() as usize)
.map_or(&[][..], Vec::as_slice);
let score = node_complexity(callees, &reach);
if score == 0 {
continue;
}
ranked.push(HotspotRank {
node: id,
name: entry.name,
kind: entry.kind,
score,
});
}
ranked.sort_by(|a, b| {
b.score
.cmp(&a.score) .then_with(|| a.node.index().cmp(&b.node.index())) });
if top != 0 && ranked.len() > top {
ranked.truncate(top);
}
ranked
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::Language;
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::edge::{EdgeKind, ResolvedVia};
use crate::graph::unified::file::FileId;
use crate::graph::unified::storage::arena::NodeEntry;
use std::path::PathBuf;
fn calls() -> EdgeKind {
EdgeKind::Calls {
argument_count: 0,
is_async: false,
resolved_via: ResolvedVia::Direct,
}
}
fn register_file(graph: &mut CodeGraph, path: &str) -> FileId {
graph
.files_mut()
.register_with_language(&PathBuf::from(path), Some(Language::Rust))
.expect("register file")
}
fn add_fn(graph: &mut CodeGraph, name: &str, file: FileId) -> NodeId {
let sid = graph.strings_mut().intern(name).expect("intern name");
let entry = NodeEntry::new(NodeKind::Function, sid, file)
.with_definition(true)
.with_byte_range(0, 1);
let id = graph.nodes_mut().alloc(entry).expect("alloc node");
graph
.indices_mut()
.add(id, NodeKind::Function, sid, Some(sid), file);
id
}
fn edge(graph: &CodeGraph, from: NodeId, to: NodeId, file: FileId) {
graph.edges().add_edge(from, to, calls(), file);
}
#[test]
fn ranks_by_fan_out_complexity_descending() {
let mut graph = CodeGraph::new();
let f = register_file(&mut graph, "crate/src/lib.rs");
let driver = add_fn(&mut graph, "driver", f);
let a = add_fn(&mut graph, "a", f);
let b = add_fn(&mut graph, "b", f);
let leaf = add_fn(&mut graph, "leaf", f);
edge(&graph, driver, a, f);
edge(&graph, a, b, f);
edge(&graph, b, leaf, f);
let snapshot = graph.snapshot();
let hotspots = rank_hotspots(&snapshot, 10);
assert_eq!(hotspots.len(), 3, "leaf (score 0) is excluded");
assert_eq!(hotspots[0].node, driver, "highest score ranks first");
assert_eq!(hotspots[0].score, 4);
assert!(
hotspots[0].score >= hotspots[1].score && hotspots[1].score >= hotspots[2].score,
"scores are non-increasing"
);
}
#[test]
fn top_bound_truncates() {
let mut graph = CodeGraph::new();
let f = register_file(&mut graph, "crate/src/lib.rs");
let a = add_fn(&mut graph, "a", f);
let b = add_fn(&mut graph, "b", f);
let c = add_fn(&mut graph, "c", f);
edge(&graph, a, b, f);
edge(&graph, b, c, f);
let snapshot = graph.snapshot();
let hotspots = rank_hotspots(&snapshot, 1);
assert_eq!(
hotspots.len(),
1,
"top=1 keeps only the highest-scoring node"
);
}
#[test]
fn cyclic_call_graph_terminates() {
let mut graph = CodeGraph::new();
let f = register_file(&mut graph, "crate/src/lib.rs");
let a = add_fn(&mut graph, "a", f);
let b = add_fn(&mut graph, "b", f);
edge(&graph, a, b, f);
edge(&graph, b, a, f);
let snapshot = graph.snapshot();
let hotspots = rank_hotspots(&snapshot, 0);
assert_eq!(hotspots.len(), 2);
}
#[test]
fn densely_cyclic_call_graph_terminates_fast() {
let mut graph = CodeGraph::new();
let f = register_file(&mut graph, "crate/src/lib.rs");
let fns: Vec<NodeId> = (0..6)
.map(|i| add_fn(&mut graph, &format!("f{i}"), f))
.collect();
for &from in &fns {
for &to in &fns {
if from != to {
edge(&graph, from, to, f);
}
}
}
let snapshot = graph.snapshot();
let hotspots = rank_hotspots(&snapshot, 0);
assert_eq!(hotspots.len(), 6);
for h in &hotspots {
assert!(h.score >= 5 && h.score <= 5 + MAX_CHAIN_DEPTH);
}
assert_eq!(hotspots, rank_hotspots(&snapshot, 0));
}
#[test]
fn reach_is_order_independent_deterministic() {
let mut sorted_first = vec![vec![1u32, 2], vec![2], vec![1]];
let mut reversed_first = vec![vec![2u32, 1], vec![2], vec![1]];
let reach_a = compute_call_chain_reach(&mut sorted_first);
let reach_b = compute_call_chain_reach(&mut reversed_first);
assert_eq!(
reach_a,
vec![2, 0, 1],
"canonical reach for the branching cycle"
);
assert_eq!(
reach_a, reach_b,
"reach must be identical regardless of input callee order"
);
}
#[test]
fn branching_cycle_scores_are_canonical() {
let mut graph = CodeGraph::new();
let f = register_file(&mut graph, "crate/src/lib.rs");
let v0 = add_fn(&mut graph, "v0", f);
let v1 = add_fn(&mut graph, "v1", f);
let v2 = add_fn(&mut graph, "v2", f);
edge(&graph, v2, v1, f);
edge(&graph, v1, v2, f);
edge(&graph, v0, v2, f);
edge(&graph, v0, v1, f);
let snapshot = graph.snapshot();
let hotspots = rank_hotspots(&snapshot, 0);
let score_of = |node: NodeId| {
hotspots
.iter()
.find(|h| h.node == node)
.map(|h| h.score)
.unwrap_or_else(|| panic!("node {node:?} missing from ranking"))
};
assert_eq!(score_of(v0), 4, "v0 canonical score");
assert_eq!(score_of(v1), 3, "v1 canonical score");
assert_eq!(score_of(v2), 2, "v2 canonical score");
assert_eq!(
hotspots.iter().map(|h| h.node).collect::<Vec<_>>(),
vec![v0, v1, v2],
"canonical ranking order independent of edge emission order"
);
}
}