Skip to content

Commit 0079a11

Browse files
zemsezerosnacksklkvr
authored
Support for Flamegraph (#8640)
* first pass bump revm-inspectors * fix: bug while processing call node * handle contract creation in flamegraph * store in tmp file and open file * enable decode_internal * remove pub from internal method * use temp_dir * ref: combine fst code into single file * remove redundant option * fix: handle non-empty step_exits * some docs * revert revm-inspectors version change * switch to flamegraph and flamechart boolean flags * Update crates/evm/traces/src/folded_stack_trace.rs Co-authored-by: Arsenii Kulikov <[email protected]> * Update crates/evm/traces/src/folded_stack_trace.rs Co-authored-by: Arsenii Kulikov <[email protected]> * save to cache dir and gracefully handle opener outcome * disable default features in inferno * fixes * license --------- Co-authored-by: zerosnacks <[email protected]> Co-authored-by: Arsenii Kulikov <[email protected]>
1 parent 96105b4 commit 0079a11

File tree

7 files changed

+443
-19
lines changed

7 files changed

+443
-19
lines changed

Cargo.lock

Lines changed: 43 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
use alloy_primitives::hex::ToHexExt;
2+
use revm_inspectors::tracing::{
3+
types::{CallTraceNode, CallTraceStep, DecodedTraceStep, TraceMemberOrder},
4+
CallTraceArena,
5+
};
6+
7+
/// Builds a folded stack trace from a call trace arena.
8+
pub fn build(arena: &CallTraceArena) -> Vec<String> {
9+
let mut fst = EvmFoldedStackTraceBuilder::default();
10+
fst.process_call_node(arena.nodes(), 0);
11+
fst.build()
12+
}
13+
14+
/// Wrapper for building a folded stack trace using EVM call trace node.
15+
#[derive(Default)]
16+
pub struct EvmFoldedStackTraceBuilder {
17+
/// Raw folded stack trace builder.
18+
fst: FoldedStackTraceBuilder,
19+
}
20+
21+
impl EvmFoldedStackTraceBuilder {
22+
/// Returns the folded stack trace.
23+
pub fn build(self) -> Vec<String> {
24+
self.fst.build()
25+
}
26+
27+
/// Creates an entry for a EVM CALL in the folded stack trace. This method recursively processes
28+
/// all the children nodes of the call node and at the end it exits.
29+
pub fn process_call_node(&mut self, nodes: &[CallTraceNode], idx: usize) {
30+
let node = &nodes[idx];
31+
32+
let func_name = if node.trace.kind.is_any_create() {
33+
let default_contract_name = "Contract".to_string();
34+
let contract_name = node.trace.decoded.label.as_ref().unwrap_or(&default_contract_name);
35+
format!("new {contract_name}")
36+
} else {
37+
let selector = node
38+
.selector()
39+
.map(|selector| selector.encode_hex_with_prefix())
40+
.unwrap_or("fallback".to_string());
41+
let signature =
42+
node.trace.decoded.call_data.as_ref().map(|dc| &dc.signature).unwrap_or(&selector);
43+
44+
if let Some(label) = &node.trace.decoded.label {
45+
format!("{label}.{signature}")
46+
} else {
47+
signature.clone()
48+
}
49+
};
50+
51+
self.fst.enter(func_name, node.trace.gas_used as i64);
52+
53+
// Track internal function step exits to do in this call context.
54+
let mut step_exits = vec![];
55+
56+
// Process children nodes.
57+
for order in &node.ordering {
58+
match order {
59+
TraceMemberOrder::Call(child_idx) => {
60+
let child_node_idx = node.children[*child_idx];
61+
self.process_call_node(nodes, child_node_idx);
62+
}
63+
TraceMemberOrder::Step(step_idx) => {
64+
self.exit_previous_steps(&mut step_exits, *step_idx);
65+
self.process_step(&node.trace.steps, *step_idx, &mut step_exits)
66+
}
67+
TraceMemberOrder::Log(_) => {}
68+
}
69+
}
70+
71+
// Exit pending internal function calls if any.
72+
for _ in 0..step_exits.len() {
73+
self.fst.exit();
74+
}
75+
76+
// Exit from this call context in the folded stack trace.
77+
self.fst.exit();
78+
}
79+
80+
/// Creates an entry for an internal function call in the folded stack trace. This method only
81+
/// enters the function in the folded stack trace, we cannot exit since we need to exit at a
82+
/// future step. Hence, we keep track of the step end index in the `step_exits`.
83+
fn process_step(
84+
&mut self,
85+
steps: &[CallTraceStep],
86+
step_idx: usize,
87+
step_exits: &mut Vec<usize>,
88+
) {
89+
let step = &steps[step_idx];
90+
if let Some(decoded_step) = &step.decoded {
91+
match decoded_step {
92+
DecodedTraceStep::InternalCall(decoded_internal_call, step_end_idx) => {
93+
let gas_used = steps[*step_end_idx].gas_used.saturating_sub(step.gas_used);
94+
self.fst.enter(decoded_internal_call.func_name.clone(), gas_used as i64);
95+
step_exits.push(*step_end_idx);
96+
}
97+
DecodedTraceStep::Line(_) => {}
98+
}
99+
}
100+
}
101+
102+
/// Exits all the previous internal calls that should end before starting step_idx.
103+
fn exit_previous_steps(&mut self, step_exits: &mut Vec<usize>, step_idx: usize) {
104+
let initial_length = step_exits.len();
105+
step_exits.retain(|&number| number > step_idx);
106+
107+
let num_exits = initial_length - step_exits.len();
108+
for _ in 0..num_exits {
109+
self.fst.exit();
110+
}
111+
}
112+
}
113+
114+
/// Helps to translate a function enter-exit flow into a folded stack trace.
115+
///
116+
/// Example:
117+
/// fn top() { child_a(); child_b() } // consumes 500 gas
118+
/// fn child_a() {} // consumes 100 gas
119+
/// fn child_b() {} // consumes 200 gas
120+
///
121+
/// For execution of the `top` function looks like:
122+
/// 1. enter `top`
123+
/// 2. enter `child_a`
124+
/// 3. exit `child_a`
125+
/// 4. enter `child_b`
126+
/// 5. exit `child_b`
127+
/// 6. exit `top`
128+
///
129+
/// The translated folded stack trace lines look like:
130+
/// 1. top
131+
/// 2. top;child_a
132+
/// 3. top;child_b
133+
///
134+
/// Including the gas consumed by the function by itself.
135+
/// 1. top 200 // 500 - 100 - 200
136+
/// 2. top;child_a 100
137+
/// 3. top;child_b 200
138+
#[derive(Debug, Default)]
139+
pub struct FoldedStackTraceBuilder {
140+
/// Trace entries.
141+
traces: Vec<TraceEntry>,
142+
/// Number of exits to be done before entering a new function.
143+
exits: usize,
144+
}
145+
146+
#[derive(Debug, Default)]
147+
struct TraceEntry {
148+
/// Names of all functions in the call stack of this trace.
149+
names: Vec<String>,
150+
/// Gas consumed by this function, allowed to be negative due to refunds.
151+
gas: i64,
152+
}
153+
154+
impl FoldedStackTraceBuilder {
155+
/// Enter execution of a function call that consumes `gas`.
156+
pub fn enter(&mut self, label: String, gas: i64) {
157+
let mut names = self.traces.last().map(|entry| entry.names.clone()).unwrap_or_default();
158+
159+
while self.exits > 0 {
160+
names.pop();
161+
self.exits -= 1;
162+
}
163+
164+
names.push(label);
165+
self.traces.push(TraceEntry { names, gas });
166+
}
167+
168+
/// Exit execution of a function call.
169+
pub fn exit(&mut self) {
170+
self.exits += 1;
171+
}
172+
173+
/// Returns folded stack trace.
174+
pub fn build(mut self) -> Vec<String> {
175+
self.subtract_children();
176+
self.build_without_subtraction()
177+
}
178+
179+
/// Internal method to build the folded stack trace without subtracting gas consumed by
180+
/// the children function calls.
181+
fn build_without_subtraction(&mut self) -> Vec<String> {
182+
let mut lines = Vec::new();
183+
for TraceEntry { names, gas } in self.traces.iter() {
184+
lines.push(format!("{} {}", names.join(";"), gas));
185+
}
186+
lines
187+
}
188+
189+
/// Subtracts gas consumed by the children function calls from the parent function calls.
190+
fn subtract_children(&mut self) {
191+
// Iterate over each trace to find the children and subtract their values from the parents.
192+
for i in 0..self.traces.len() {
193+
let (left, right) = self.traces.split_at_mut(i);
194+
let TraceEntry { names, gas } = &right[0];
195+
if names.len() > 1 {
196+
let parent_trace_to_match = &names[..names.len() - 1];
197+
for parent in left.iter_mut().rev() {
198+
if parent.names == parent_trace_to_match {
199+
parent.gas -= gas;
200+
break;
201+
}
202+
}
203+
}
204+
}
205+
}
206+
}
207+
208+
mod tests {
209+
#[test]
210+
fn test_fst_1() {
211+
let mut trace = super::FoldedStackTraceBuilder::default();
212+
trace.enter("top".to_string(), 500);
213+
trace.enter("child_a".to_string(), 100);
214+
trace.exit();
215+
trace.enter("child_b".to_string(), 200);
216+
217+
assert_eq!(
218+
trace.build_without_subtraction(),
219+
vec![
220+
"top 500", //
221+
"top;child_a 100",
222+
"top;child_b 200",
223+
]
224+
);
225+
assert_eq!(
226+
trace.build(),
227+
vec![
228+
"top 200", // 500 - 100 - 200
229+
"top;child_a 100",
230+
"top;child_b 200",
231+
]
232+
);
233+
}
234+
235+
#[test]
236+
fn test_fst_2() {
237+
let mut trace = super::FoldedStackTraceBuilder::default();
238+
trace.enter("top".to_string(), 500);
239+
trace.enter("child_a".to_string(), 300);
240+
trace.enter("child_b".to_string(), 100);
241+
trace.exit();
242+
trace.exit();
243+
trace.enter("child_c".to_string(), 100);
244+
245+
assert_eq!(
246+
trace.build_without_subtraction(),
247+
vec![
248+
"top 500", //
249+
"top;child_a 300",
250+
"top;child_a;child_b 100",
251+
"top;child_c 100",
252+
]
253+
);
254+
255+
assert_eq!(
256+
trace.build(),
257+
vec![
258+
"top 100", // 500 - 300 - 100
259+
"top;child_a 200", // 300 - 100
260+
"top;child_a;child_b 100",
261+
"top;child_c 100",
262+
]
263+
);
264+
}
265+
266+
#[test]
267+
fn test_fst_3() {
268+
let mut trace = super::FoldedStackTraceBuilder::default();
269+
trace.enter("top".to_string(), 1700);
270+
trace.enter("child_a".to_string(), 500);
271+
trace.exit();
272+
trace.enter("child_b".to_string(), 500);
273+
trace.enter("child_c".to_string(), 500);
274+
trace.exit();
275+
trace.exit();
276+
trace.exit();
277+
trace.enter("top2".to_string(), 1700);
278+
279+
assert_eq!(
280+
trace.build_without_subtraction(),
281+
vec![
282+
"top 1700", //
283+
"top;child_a 500",
284+
"top;child_b 500",
285+
"top;child_b;child_c 500",
286+
"top2 1700",
287+
]
288+
);
289+
290+
assert_eq!(
291+
trace.build(),
292+
vec![
293+
"top 700", //
294+
"top;child_a 500",
295+
"top;child_b 0",
296+
"top;child_b;child_c 500",
297+
"top2 1700",
298+
]
299+
);
300+
}
301+
}

crates/evm/traces/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ pub use decoder::{CallTraceDecoder, CallTraceDecoderBuilder};
4242
pub mod debug;
4343
pub use debug::DebugTraceIdentifier;
4444

45+
pub mod folded_stack_trace;
46+
4547
pub type Traces = Vec<(TraceKind, SparsedTraceArena)>;
4648

4749
/// Trace arena keeping track of ignored trace items.

0 commit comments

Comments
 (0)