1use std::collections::{HashMap, VecDeque};
22
23#[derive(Debug, Clone)]
30pub struct CallGraph {
31 ids: Vec<String>,
32 id_to_idx: HashMap<String, u32>,
33 adj: Vec<Vec<u32>>,
34 pub total_callee_names: usize,
36 pub resolved_callee_names: usize,
38}
39
40#[derive(Debug, Clone, PartialEq)]
42pub struct GraphMetrics {
43 pub fan_in: usize,
45 pub fan_out: usize,
47 pub pagerank: f64,
49 pub betweenness: f64,
51}
52
53impl CallGraph {
54 pub fn new() -> Self {
56 CallGraph {
57 ids: Vec::new(),
58 id_to_idx: HashMap::new(),
59 adj: Vec::new(),
60 total_callee_names: 0,
61 resolved_callee_names: 0,
62 }
63 }
64
65 pub fn intern(&mut self, id: String) -> u32 {
67 if let Some(&idx) = self.id_to_idx.get(&id) {
68 return idx;
69 }
70 let idx = self.ids.len() as u32;
71 self.id_to_idx.insert(id.clone(), idx);
72 self.ids.push(id);
73 self.adj.push(Vec::new());
74 idx
75 }
76
77 pub fn node_count(&self) -> usize {
79 self.ids.len()
80 }
81
82 pub fn edge_count(&self) -> usize {
84 self.adj.iter().map(|v| v.len()).sum()
85 }
86
87 pub fn contains(&self, id: &str) -> bool {
89 self.id_to_idx.contains_key(id)
90 }
91
92 pub fn callees_of<'a>(&'a self, id: &str) -> Option<impl Iterator<Item = &'a str>> {
94 let idx = *self.id_to_idx.get(id)? as usize;
95 Some(self.adj[idx].iter().map(|&i| self.ids[i as usize].as_str()))
96 }
97
98 pub fn add_adj(&mut self, caller_idx: u32, callee_idx: u32) {
103 self.adj[caller_idx as usize].push(callee_idx);
104 }
105
106 pub fn all_ids(&self) -> impl Iterator<Item = &str> {
108 self.ids.iter().map(|s| s.as_str())
109 }
110
111 pub fn add_node(&mut self, function_id: String) {
113 self.intern(function_id);
114 }
115
116 pub fn add_edge(&mut self, caller: String, callee: String) {
118 let caller_idx = self.intern(caller);
119 let callee_idx = self.intern(callee);
120 self.adj[caller_idx as usize].push(callee_idx);
121 }
122
123 pub fn fan_in(&self, function_id: &str) -> usize {
125 match self.id_to_idx.get(function_id) {
126 None => 0,
127 Some(&target) => self.adj.iter().filter(|c| c.contains(&target)).count(),
128 }
129 }
130
131 pub fn fan_out(&self, function_id: &str) -> usize {
133 match self.id_to_idx.get(function_id) {
134 None => 0,
135 Some(&idx) => self.adj[idx as usize].len(),
136 }
137 }
138
139 pub fn pagerank(
150 &self,
151 damping: f64,
152 max_iterations: usize,
153 epsilon: f64,
154 ) -> HashMap<String, f64> {
155 let n = self.ids.len();
156 if n == 0 {
157 return HashMap::new();
158 }
159
160 let mut rev_adj: Vec<Vec<u32>> = vec![Vec::new(); n];
162 for (caller_idx, callees) in self.adj.iter().enumerate() {
163 for &callee_idx in callees {
164 rev_adj[callee_idx as usize].push(caller_idx as u32);
165 }
166 }
167 for callers in rev_adj.iter_mut() {
169 callers.sort();
170 }
171
172 let fan_out: Vec<f64> = self.adj.iter().map(|v| v.len().max(1) as f64).collect();
174
175 let initial_rank = 1.0 / n as f64;
176 let mut ranks = vec![initial_rank; n];
177 let mut new_ranks = vec![0.0f64; n];
178
179 for _ in 0..max_iterations {
180 for i in 0..n {
181 let mut rank = (1.0 - damping) / n as f64;
182 for &caller_idx in &rev_adj[i] {
183 rank += damping * ranks[caller_idx as usize] / fan_out[caller_idx as usize];
184 }
185 new_ranks[i] = rank;
186 }
187
188 let max_delta = ranks
189 .iter()
190 .zip(new_ranks.iter())
191 .map(|(old, new)| (new - old).abs())
192 .fold(0.0_f64, f64::max);
193
194 std::mem::swap(&mut ranks, &mut new_ranks);
195
196 if max_delta < epsilon {
197 break;
198 }
199 }
200
201 self.ids
202 .iter()
203 .enumerate()
204 .map(|(i, id)| (id.clone(), ranks[i]))
205 .collect()
206 }
207
208 pub fn betweenness_centrality_approx(&self, k: usize) -> HashMap<String, f64> {
224 let n = self.ids.len();
225 if n <= k {
226 return self.betweenness_centrality();
227 }
228
229 let mut sorted_indices: Vec<u32> = (0..n as u32).collect();
231 sorted_indices.sort_by_key(|&i| &self.ids[i as usize]);
232
233 let scale = n as f64 / k as f64;
234 let mut betweenness = vec![0.0f64; n];
235
236 let mut stack: Vec<u32> = Vec::with_capacity(n);
238 let mut pred: Vec<Vec<u32>> = vec![Vec::new(); n];
239 let mut sigma = vec![0.0f64; n];
240 let mut dist = vec![-1i32; n];
241 let mut delta = vec![0.0f64; n];
242 let mut queue: VecDeque<u32> = VecDeque::with_capacity(n);
243
244 for i in 0..k {
245 let source_idx = sorted_indices[(i * n) / k];
246 brandes_bfs_inplace(
247 source_idx, &self.adj, &mut stack, &mut pred, &mut sigma, &mut dist, &mut queue,
248 );
249 brandes_accumulate_inplace(&stack, &pred, &sigma, &mut delta);
250 for &w in &stack {
251 if w != source_idx {
252 betweenness[w as usize] += delta[w as usize] * scale;
253 }
254 }
255 }
256
257 if n > 2 {
258 let normalization = 1.0 / ((n - 1) * (n - 2)) as f64;
259 for v in betweenness.iter_mut() {
260 *v *= normalization;
261 }
262 }
263
264 self.ids
265 .iter()
266 .enumerate()
267 .map(|(i, id)| (id.clone(), betweenness[i]))
268 .collect()
269 }
270
271 pub fn betweenness_centrality(&self) -> HashMap<String, f64> {
278 let n = self.ids.len();
279 let mut betweenness = vec![0.0f64; n];
280
281 if n == 0 {
282 return HashMap::new();
283 }
284
285 let mut stack: Vec<u32> = Vec::with_capacity(n);
286 let mut pred: Vec<Vec<u32>> = vec![Vec::new(); n];
287 let mut sigma = vec![0.0f64; n];
288 let mut dist = vec![-1i32; n];
289 let mut delta = vec![0.0f64; n];
290 let mut queue: VecDeque<u32> = VecDeque::with_capacity(n);
291
292 for source_idx in 0..n as u32 {
293 brandes_bfs_inplace(
294 source_idx, &self.adj, &mut stack, &mut pred, &mut sigma, &mut dist, &mut queue,
295 );
296 brandes_accumulate_inplace(&stack, &pred, &sigma, &mut delta);
297 for &w in &stack {
298 if w != source_idx {
299 betweenness[w as usize] += delta[w as usize];
300 }
301 }
302 }
303
304 if n > 2 {
305 let normalization = 1.0 / ((n - 1) * (n - 2)) as f64;
306 for v in betweenness.iter_mut() {
307 *v *= normalization;
308 }
309 }
310
311 self.ids
312 .iter()
313 .enumerate()
314 .map(|(i, id)| (id.clone(), betweenness[i]))
315 .collect()
316 }
317
318 pub fn find_strongly_connected_components(&self) -> HashMap<String, (usize, usize)> {
323 let n = self.ids.len();
324
325 let sorted_adj: Vec<Vec<u32>> = self
327 .adj
328 .iter()
329 .map(|v| {
330 let mut s = v.clone();
331 s.sort_by_key(|&i| &self.ids[i as usize]);
332 s
333 })
334 .collect();
335
336 let mut node_index: Vec<i64> = vec![-1; n]; let mut lowlink: Vec<u32> = vec![0; n];
338 let mut on_stack: Vec<bool> = vec![false; n];
339 let mut tarjan_stack: Vec<u32> = Vec::new();
340 let mut index_counter: u32 = 0;
341 let mut scc_id: usize = 0;
342 let mut node_scc: Vec<usize> = vec![0; n];
343 let mut scc_sizes: Vec<usize> = Vec::new();
344
345 let mut sorted_nodes: Vec<u32> = (0..n as u32).collect();
347 sorted_nodes.sort_by_key(|&i| &self.ids[i as usize]);
348
349 let mut work: Vec<(u32, usize)> = Vec::new();
351
352 for &start in &sorted_nodes {
353 if node_index[start as usize] >= 0 {
354 continue;
355 }
356
357 work.push((start, 0));
358 node_index[start as usize] = index_counter as i64;
359 lowlink[start as usize] = index_counter;
360 index_counter += 1;
361 tarjan_stack.push(start);
362 on_stack[start as usize] = true;
363
364 while !work.is_empty() {
365 let (v, si) = *work.last().unwrap();
366 let vi = v as usize;
367
368 if si < sorted_adj[vi].len() {
369 let w = sorted_adj[vi][si];
370 work.last_mut().unwrap().1 += 1;
371 let wi = w as usize;
372
373 if node_index[wi] < 0 {
374 work.push((w, 0));
376 node_index[wi] = index_counter as i64;
377 lowlink[wi] = index_counter;
378 index_counter += 1;
379 tarjan_stack.push(w);
380 on_stack[wi] = true;
381 } else if on_stack[wi] {
382 lowlink[vi] = lowlink[vi].min(node_index[wi] as u32);
383 }
384 } else {
385 work.pop();
386 if let Some(&(parent, _)) = work.last() {
388 lowlink[parent as usize] = lowlink[parent as usize].min(lowlink[vi]);
389 }
390 if lowlink[vi] == node_index[vi] as u32 {
392 let mut size = 0;
393 loop {
394 let w = tarjan_stack.pop().unwrap();
395 on_stack[w as usize] = false;
396 node_scc[w as usize] = scc_id;
397 size += 1;
398 if w == v {
399 break;
400 }
401 }
402 scc_sizes.push(size);
403 scc_id += 1;
404 }
405 }
406 }
407 }
408
409 self.ids
410 .iter()
411 .enumerate()
412 .map(|(i, id)| {
413 let sid = node_scc[i];
414 let size = scc_sizes.get(sid).copied().unwrap_or(1);
415 (id.clone(), (sid, size))
416 })
417 .collect()
418 }
419
420 pub fn compute_dependency_depth(&self) -> HashMap<String, Option<usize>> {
424 let n = self.ids.len();
425 let mut depths: Vec<Option<usize>> = vec![None; n];
426 let mut queue: VecDeque<(u32, usize)> = VecDeque::new();
427
428 let mut entry_indices: Vec<u32> = (0..n as u32)
430 .filter(|&i| self.is_entry_point(&self.ids[i as usize]))
431 .collect();
432
433 if entry_indices.is_empty() {
434 let mut fan_in = vec![0usize; n];
435 for callees in &self.adj {
436 for &c in callees {
437 fan_in[c as usize] += 1;
438 }
439 }
440 entry_indices = (0..n as u32).filter(|&i| fan_in[i as usize] == 0).collect();
441 }
442
443 for entry in entry_indices {
444 depths[entry as usize] = Some(0);
445 queue.push_back((entry, 0));
446 }
447
448 while let Some((node_idx, depth)) = queue.pop_front() {
449 for &callee_idx in &self.adj[node_idx as usize] {
450 let ci = callee_idx as usize;
451 let current = depths[ci];
452 if current.is_none() || current.unwrap() > depth + 1 {
453 depths[ci] = Some(depth + 1);
454 queue.push_back((callee_idx, depth + 1));
455 }
456 }
457 }
458
459 self.ids
460 .iter()
461 .enumerate()
462 .map(|(i, id)| (id.clone(), depths[i]))
463 .collect()
464 }
465
466 pub fn build_fan_in_map(&self) -> HashMap<String, usize> {
468 let n = self.ids.len();
469 let mut counts = vec![0usize; n];
470 for callees in &self.adj {
471 for &callee in callees {
472 counts[callee as usize] += 1;
473 }
474 }
475 self.ids
476 .iter()
477 .enumerate()
478 .map(|(i, id)| (id.clone(), counts[i]))
479 .collect()
480 }
481
482 pub fn is_entry_point(&self, function_id: &str) -> bool {
484 let function_name = function_id.split("::").last().unwrap_or("").to_lowercase();
485
486 let entry_point_names = [
487 "main",
488 "start",
489 "init",
490 "initialize",
491 "run",
492 "execute",
493 "bootstrap",
494 ];
495
496 let handler_patterns = [
497 "handle",
498 "handler",
499 "onrequest",
500 "onmessage",
501 "onevent",
502 "middleware",
503 "controller",
504 ];
505
506 if entry_point_names.contains(&function_name.as_str()) {
507 return true;
508 }
509
510 for pattern in &handler_patterns {
511 if function_name.contains(pattern) {
512 return true;
513 }
514 }
515
516 false
517 }
518
519 pub fn metrics_for(
521 &self,
522 function_id: &str,
523 pagerank_scores: &HashMap<String, f64>,
524 betweenness_scores: &HashMap<String, f64>,
525 ) -> GraphMetrics {
526 GraphMetrics {
527 fan_in: self.fan_in(function_id),
528 fan_out: self.fan_out(function_id),
529 pagerank: pagerank_scores.get(function_id).copied().unwrap_or(0.0),
530 betweenness: betweenness_scores.get(function_id).copied().unwrap_or(0.0),
531 }
532 }
533}
534
535fn brandes_bfs_inplace(
541 source: u32,
542 adj: &[Vec<u32>],
543 stack: &mut Vec<u32>,
544 pred: &mut [Vec<u32>],
545 sigma: &mut [f64],
546 dist: &mut [i32],
547 queue: &mut VecDeque<u32>,
548) {
549 for &i in stack.iter() {
551 pred[i as usize].clear();
552 sigma[i as usize] = 0.0;
553 dist[i as usize] = -1;
554 }
555 stack.clear();
556 queue.clear();
557
558 let s = source as usize;
559 dist[s] = 0;
560 sigma[s] = 1.0;
561 queue.push_back(source);
562
563 while let Some(v) = queue.pop_front() {
564 let vi = v as usize;
565 stack.push(v);
566 for &w in &adj[vi] {
567 let wi = w as usize;
568 if dist[wi] < 0 {
569 queue.push_back(w);
570 dist[wi] = dist[vi] + 1;
571 }
572 if dist[wi] == dist[vi] + 1 {
573 sigma[wi] += sigma[vi];
574 pred[wi].push(v);
575 }
576 }
577 }
578}
579
580fn brandes_accumulate_inplace(stack: &[u32], pred: &[Vec<u32>], sigma: &[f64], delta: &mut [f64]) {
582 for &w in stack {
583 delta[w as usize] = 0.0;
584 }
585 for &w in stack.iter().rev() {
586 let wi = w as usize;
587 for &v in &pred[wi] {
588 let vi = v as usize;
589 delta[vi] += (sigma[vi] / sigma[wi].max(1e-300)) * (1.0 + delta[wi]);
590 }
591 }
592}
593
594impl Default for CallGraph {
595 fn default() -> Self {
596 Self::new()
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603
604 #[test]
605 fn test_empty_graph() {
606 let graph = CallGraph::new();
607 assert_eq!(graph.node_count(), 0);
608 assert_eq!(graph.edge_count(), 0);
609 }
610
611 #[test]
612 fn test_fan_in_fan_out() {
613 let mut graph = CallGraph::new();
614
615 graph.add_edge("A".to_string(), "B".to_string());
619 graph.add_edge("A".to_string(), "C".to_string());
620 graph.add_edge("B".to_string(), "C".to_string());
621
622 assert_eq!(graph.fan_in("A"), 0); assert_eq!(graph.fan_out("A"), 2); assert_eq!(graph.fan_in("B"), 1); assert_eq!(graph.fan_out("B"), 1); assert_eq!(graph.fan_in("C"), 2); assert_eq!(graph.fan_out("C"), 0); }
631
632 #[test]
633 fn test_pagerank() {
634 let mut graph = CallGraph::new();
635
636 graph.add_edge("A".to_string(), "B".to_string());
638 graph.add_edge("B".to_string(), "C".to_string());
639
640 let ranks = graph.pagerank(0.85, 20, 1e-6);
641
642 assert!(ranks.get("C").copied().unwrap_or(0.0) > ranks.get("B").copied().unwrap_or(0.0));
645 assert!(ranks.get("B").copied().unwrap_or(0.0) > ranks.get("A").copied().unwrap_or(0.0));
646 }
647
648 #[test]
649 fn test_build_fan_in_map() {
650 let mut graph = CallGraph::new();
651 graph.add_edge("A".to_string(), "B".to_string());
653 graph.add_edge("A".to_string(), "C".to_string());
654 graph.add_edge("B".to_string(), "C".to_string());
655
656 let fan_in = graph.build_fan_in_map();
657 assert_eq!(fan_in.get("A").copied().unwrap_or(0), 0); assert_eq!(fan_in.get("B").copied().unwrap_or(0), 1); assert_eq!(fan_in.get("C").copied().unwrap_or(0), 2); }
661
662 #[test]
663 fn test_betweenness_linear_chain() {
664 let mut graph = CallGraph::new();
667 graph.add_edge("a".to_string(), "b".to_string());
668 graph.add_edge("b".to_string(), "c".to_string());
669
670 let scores = graph.betweenness_centrality();
671 assert!(
672 (scores["b"] - 0.5).abs() < 1e-10,
673 "b betweenness should be 0.5"
674 );
675 assert!(scores["a"].abs() < 1e-10, "a betweenness should be 0.0");
676 assert!(scores["c"].abs() < 1e-10, "c betweenness should be 0.0");
677 }
678
679 #[test]
680 fn test_approx_betweenness_equals_exact_when_k_geq_n() {
681 let mut graph = CallGraph::new();
683 graph.add_edge("a".to_string(), "b".to_string());
684 graph.add_edge("b".to_string(), "c".to_string());
685 graph.add_edge("c".to_string(), "d".to_string());
686
687 let exact = graph.betweenness_centrality();
688 let approx = graph.betweenness_centrality_approx(100); for (node, &exact_val) in &exact {
691 let approx_val = approx.get(node).copied().unwrap_or(0.0);
692 assert!(
693 (exact_val - approx_val).abs() < 1e-10,
694 "node {node}: exact={exact_val}, approx={approx_val}"
695 );
696 }
697 }
698
699 #[test]
700 fn test_approx_betweenness_identifies_bridge() {
701 let mut graph = CallGraph::new();
707 graph.add_edge("a".to_string(), "bridge".to_string());
708 graph.add_edge("bridge".to_string(), "y".to_string());
709 graph.add_edge("bridge".to_string(), "z".to_string());
710
711 let approx = graph.betweenness_centrality_approx(2);
712 let bridge_score = approx.get("bridge").copied().unwrap_or(0.0);
713 for (node, &score) in &approx {
714 if node != "bridge" {
715 assert!(
716 bridge_score >= score,
717 "bridge ({bridge_score}) should dominate {node} ({score})"
718 );
719 }
720 }
721 }
722
723 #[test]
724 fn test_approx_betweenness_pivot_covers_tail() {
725 let mut graph = CallGraph::new();
739 graph.add_edge("a_in".to_string(), "hub".to_string());
740 graph.add_edge("b_in".to_string(), "hub".to_string());
741 graph.add_edge("z_source".to_string(), "hub".to_string());
742 graph.add_edge("hub".to_string(), "x_out".to_string());
743 graph.add_edge("hub".to_string(), "y_out".to_string());
744
745 let n = graph.node_count();
746 let exact = graph.betweenness_centrality();
747
748 let approx_full = graph.betweenness_centrality_approx(n);
750 for (node, &val) in &exact {
751 let av = approx_full.get(node).copied().unwrap_or(0.0);
752 assert!(
753 (val - av).abs() < 1e-10,
754 "k=n mismatch for {node}: exact={val}, approx={av}"
755 );
756 }
757
758 let hub_score = exact.get("hub").copied().unwrap_or(0.0);
760 assert!(hub_score > 0.0, "hub should have non-zero betweenness");
761 for (node, &val) in &exact {
762 if node != "hub" {
763 assert!(
764 hub_score >= val,
765 "hub ({hub_score}) should have highest betweenness, but {node}={val}"
766 );
767 }
768 }
769 }
770
771 #[test]
772 fn test_approx_betweenness_top_hubs_rank_preserved() {
773 let mut graph = CallGraph::new();
787 for (hub, size) in [("hub_a", 50usize), ("hub_b", 30), ("hub_c", 15)] {
788 for i in 0..size {
789 graph.add_edge(format!("in_{hub}_{i}"), hub.to_string());
790 graph.add_edge(hub.to_string(), format!("out_{hub}_{i}"));
791 }
792 }
793
794 assert!(
795 graph.node_count() > 32,
796 "graph must be large enough that k=32 is a real approximation"
797 );
798
799 let exact = graph.betweenness_centrality();
800 let approx = graph.betweenness_centrality_approx(32);
801
802 let ex_a = exact.get("hub_a").copied().unwrap_or(0.0);
804 let ex_b = exact.get("hub_b").copied().unwrap_or(0.0);
805 let ex_c = exact.get("hub_c").copied().unwrap_or(0.0);
806 assert!(
807 ex_a > ex_b && ex_b > ex_c,
808 "exact: hub_a={ex_a} hub_b={ex_b} hub_c={ex_c}"
809 );
810
811 let ap_a = approx.get("hub_a").copied().unwrap_or(0.0);
813 let ap_b = approx.get("hub_b").copied().unwrap_or(0.0);
814 let ap_c = approx.get("hub_c").copied().unwrap_or(0.0);
815 assert!(
816 ap_a > ap_b && ap_b > ap_c,
817 "approx rank broken: hub_a={ap_a} hub_b={ap_b} hub_c={ap_c}"
818 );
819
820 let mut ranked: Vec<(&str, f64)> = approx.iter().map(|(k, &v)| (k.as_str(), v)).collect();
822 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
823 let top3: Vec<&str> = ranked.iter().take(3).map(|(name, _)| *name).collect();
824 assert!(
825 top3.contains(&"hub_a"),
826 "hub_a missing from top-3: {top3:?}"
827 );
828 assert!(
829 top3.contains(&"hub_b"),
830 "hub_b missing from top-3: {top3:?}"
831 );
832 assert!(
833 top3.contains(&"hub_c"),
834 "hub_c missing from top-3: {top3:?}"
835 );
836 }
837}