1+ use std:: collections:: HashMap ;
2+
3+ use callgraph_shared:: interfaces:: CostVec ;
14use inferno:: flamegraph:: { self , Options } ;
5+ use petgraph:: {
6+ graph:: NodeIndex ,
7+ visit:: { EdgeRef , IntoEdgeReferences } ,
8+ } ;
29
310use super :: { error:: FlamegraphError , model:: CallGraph } ;
411
5- /// Below this incoming budget a frame rounds to zero cost, so descending
6- /// further would only emit empty lines.
712const MIN_BUDGET : f64 = 1.0 ;
8-
9- /// Fraction of the total cost below which a branch is pruned. Budget is
10- /// conserved and splits across a node's children, so a relative floor bounds
11- /// the number of surviving paths to ~1/this, keeping the fold tractable on
12- /// large graphs with heavily-shared subtrees (e.g. a real Node/V8 profile).
1313const MIN_BUDGET_FRACTION : f64 = 0.0005 ;
1414
1515impl CallGraph {
16- /// Fold the graph into Brendan-Gregg "collapsed stack" lines
17- /// (`root;child;leaf <count>`), the input format for flamegraph tools.
18- ///
19- /// Each line carries a node's SELF cost for one call path; a frame's width
20- /// in the rendered graph is the sum of the self costs of everything beneath
21- /// it, i.e. its inclusive cost. Because Callgrind aggregates cost per
22- /// function (not per call path), a node's self cost is distributed across
23- /// its incoming paths in proportion to each call's inclusive cost.
2416 pub fn to_folded ( & self ) -> Vec < String > {
25- let n = self . nodes . len ( ) ;
26- let names: Vec < & str > = self
27- . nodes
17+ let graph = self . inner . graph . borrow ( ) ;
18+ let indices: Vec < NodeIndex > = graph. node_indices ( ) . collect ( ) ;
19+ let index_by_node: HashMap < NodeIndex , usize > = indices
20+ . iter ( )
21+ . enumerate ( )
22+ . map ( |( index, node) | ( * node, index) )
23+ . collect ( ) ;
24+
25+ let names: Vec < & str > = indices
26+ . iter ( )
27+ . map ( |index| graph[ * index] . name . as_str ( ) )
28+ . collect ( ) ;
29+ let self_costs: Vec < u64 > = indices
2830 . iter ( )
29- . map ( |node| node . function . as_str ( ) )
31+ . map ( |index| first_cost ( & graph [ * index ] . costs . aggregated_cost ( ) ) )
3032 . collect ( ) ;
3133
32- // Adjacency + incoming inclusive cost, keyed by sorted-node index.
33- let mut out: Vec < Vec < ( usize , u64 ) > > = vec ! [ Vec :: new( ) ; n] ;
34- let mut incoming_incl: Vec < u64 > = vec ! [ 0 ; n] ;
35- for e in & self . edges {
36- let ( Some ( c) , Some ( d) ) = ( self . node_index ( & e. caller ) , self . node_index ( & e. callee ) )
37- else {
34+ let n = indices. len ( ) ;
35+ let mut out = vec ! [ Vec :: <( usize , u64 ) >:: new( ) ; n] ;
36+ let mut incoming_incl = vec ! [ 0 ; n] ;
37+ for edge in graph. edge_references ( ) {
38+ let Some ( & caller) = index_by_node. get ( & edge. source ( ) ) else {
3839 continue ;
3940 } ;
40- let ic = e. inclusive_cost . unwrap_or ( 0 ) ;
41- out[ c] . push ( ( d, ic) ) ;
42- incoming_incl[ d] += ic;
41+ let Some ( & callee) = index_by_node. get ( & edge. target ( ) ) else {
42+ continue ;
43+ } ;
44+ let inclusive_cost = first_cost ( & edge. weight ( ) . costs . aggregated_cost ( ) ) ;
45+ out[ caller] . push ( ( callee, inclusive_cost) ) ;
46+ incoming_incl[ callee] += inclusive_cost;
4347 }
4448
45- // incl[i] = self[i] + sum of outgoing inclusive costs; the total cost
46- // attributed to node i across all call paths.
4749 let incl: Vec < u64 > = ( 0 ..n)
48- . map ( |i| self . self_cost ( i ) + out[ i] . iter ( ) . map ( |( _, c ) | * c ) . sum :: < u64 > ( ) )
50+ . map ( |i| self_costs [ i ] + out[ i] . iter ( ) . map ( |( _, cost ) | * cost ) . sum :: < u64 > ( ) )
4951 . collect ( ) ;
5052
51- let roots = self . roots ( & incoming_incl, & incl) ;
52- let total: f64 = roots. iter ( ) . map ( |( _, b ) | * b ) . sum ( ) ;
53+ let roots = roots ( & incoming_incl, & incl) ;
54+ let total: f64 = roots. iter ( ) . map ( |( _, budget ) | * budget ) . sum ( ) ;
5355 let min_budget = MIN_BUDGET . max ( total * MIN_BUDGET_FRACTION ) ;
5456
5557 let mut lines = Vec :: new ( ) ;
56- let mut stack: Vec < usize > = Vec :: new ( ) ;
58+ let mut stack = Vec :: new ( ) ;
5759 let mut on_path = vec ! [ false ; n] ;
5860 for ( root, budget) in roots {
5961 fold_dfs (
@@ -64,15 +66,14 @@ impl CallGraph {
6466 & mut on_path,
6567 & out,
6668 & incl,
67- & self . self_costs ,
69+ & self_costs,
6870 & names,
6971 & mut lines,
7072 ) ;
7173 }
7274 lines
7375 }
7476
75- /// Render the graph to a flamegraph SVG string.
7677 pub fn to_flamegraph ( & self ) -> Result < String , FlamegraphError > {
7778 let lines = self . to_folded ( ) ;
7879 if lines. is_empty ( ) {
@@ -83,13 +84,12 @@ impl CallGraph {
8384 opts. title = "Callgrind" . to_string ( ) ;
8485 opts. count_name = "instructions" . to_string ( ) ;
8586
86- let mut svg: Vec < u8 > = Vec :: new ( ) ;
87+ let mut svg = Vec :: new ( ) ;
8788 flamegraph:: from_lines ( & mut opts, lines. iter ( ) . map ( String :: as_str) , & mut svg)
8889 . map_err ( |e| FlamegraphError :: Inferno ( e. to_string ( ) ) ) ?;
8990 String :: from_utf8 ( svg) . map_err ( |e| FlamegraphError :: Inferno ( e. to_string ( ) ) )
9091 }
9192
92- /// Render the graph to a flamegraph SVG file at `path`.
9393 pub fn to_flamegraph_file (
9494 & self ,
9595 path : impl AsRef < std:: path:: Path > ,
@@ -98,39 +98,30 @@ impl CallGraph {
9898 std:: fs:: write ( path, svg) ?;
9999 Ok ( ( ) )
100100 }
101+ }
101102
102- /// Entry points for the flame stacks, as `(node, budget)` pairs.
103- ///
104- /// A node's `budget` is the part of its inclusive cost NOT accounted for by
105- /// any recorded incoming call, i.e. `incl - sum(incoming edge inclusive)`.
106- /// This is nonzero for true roots (no caller) and, crucially, for frames
107- /// that were already on the stack when `--instr-atstart=no` instrumentation
108- /// began: their entry call predates measurement, so no edge carries their
109- /// cost and they would otherwise be dropped. A fully-cyclic graph has no
110- /// such node, so fall back to the single costliest node.
111- fn roots ( & self , incoming_incl : & [ u64 ] , incl : & [ u64 ] ) -> Vec < ( usize , f64 ) > {
112- let roots: Vec < ( usize , f64 ) > = ( 0 ..self . nodes . len ( ) )
113- . filter_map ( |i| {
114- let uncovered = incl[ i] . saturating_sub ( incoming_incl[ i] ) ;
115- ( uncovered > 0 ) . then_some ( ( i, uncovered as f64 ) )
116- } )
117- . collect ( ) ;
118- if !roots. is_empty ( ) {
119- return roots;
120- }
121- ( 0 ..self . nodes . len ( ) )
122- . filter ( |& i| incl[ i] > 0 )
123- . max_by_key ( |& i| incl[ i] )
124- . map ( |i| ( i, incl[ i] as f64 ) )
125- . into_iter ( )
126- . collect ( )
103+ fn first_cost ( cost : & CostVec ) -> u64 {
104+ cost. 0 . first ( ) . copied ( ) . unwrap_or ( 0 )
105+ }
106+
107+ fn roots ( incoming_incl : & [ u64 ] , incl : & [ u64 ] ) -> Vec < ( usize , f64 ) > {
108+ let roots: Vec < ( usize , f64 ) > = ( 0 ..incl. len ( ) )
109+ . filter_map ( |i| {
110+ let uncovered = incl[ i] . saturating_sub ( incoming_incl[ i] ) ;
111+ ( uncovered > 0 ) . then_some ( ( i, uncovered as f64 ) )
112+ } )
113+ . collect ( ) ;
114+ if !roots. is_empty ( ) {
115+ return roots;
127116 }
117+ ( 0 ..incl. len ( ) )
118+ . filter ( |& i| incl[ i] > 0 )
119+ . max_by_key ( |& i| incl[ i] )
120+ . map ( |i| ( i, incl[ i] as f64 ) )
121+ . into_iter ( )
122+ . collect ( )
128123}
129124
130- /// Emit one collapsed-stack line per node reachable from `node`, distributing
131- /// self cost by `budget / incl` (the share of this node's total cost that flows
132- /// through the current path). A node already on the path is a recursion edge:
133- /// emit it as a leaf carrying the incoming budget rather than descend.
134125#[ allow( clippy:: too_many_arguments) ]
135126fn fold_dfs (
136127 node : usize ,
@@ -151,10 +142,6 @@ fn fold_dfs(
151142 stack. push ( node) ;
152143 on_path[ node] = true ;
153144
154- // Share of this node's aggregated cost that flows through the current path.
155- // In consistent Callgrind data a single incoming edge never exceeds the
156- // node's total inclusive cost, so frac <= 1; clamp guards against malformed
157- // input amplifying cost down the tree.
158145 let frac = ( budget / incl[ node] as f64 ) . min ( 1.0 ) ;
159146 let self_here = ( self_costs[ node] as f64 * frac) . round ( ) as u64 ;
160147 if self_here >= 1 {
@@ -178,7 +165,6 @@ fn fold_dfs(
178165 ) ;
179166 continue ;
180167 }
181- // Recursion: represent the recursive cost without looping.
182168 let recursive = child_budget. round ( ) as u64 ;
183169 if recursive >= 1 {
184170 stack. push ( child) ;
@@ -191,7 +177,6 @@ fn fold_dfs(
191177 stack. pop ( ) ;
192178}
193179
194- /// `name0;name1;...;nameK <count>` for the current index stack.
195180fn fold_line ( stack : & [ usize ] , names : & [ & str ] , count : u64 ) -> String {
196181 let mut line = String :: new ( ) ;
197182 for ( i, & idx) in stack. iter ( ) . enumerate ( ) {
0 commit comments