-
Notifications
You must be signed in to change notification settings - Fork 2k
Support for Flamegraph #8640
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Support for Flamegraph #8640
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
c230018
first pass
zemse 2993ed3
fix: bug while processing call node
zemse f1a168e
handle contract creation in flamegraph
zemse 96abca1
store in tmp file and open file
zemse 0c50af8
enable decode_internal
zemse c32ca29
remove pub from internal method
zemse c94387e
use temp_dir
zemse af94f28
ref: combine fst code into single file
zemse 86ca11b
remove redundant option
zemse 6acbec6
fix: handle non-empty step_exits
zemse d9e4387
some docs
zemse 2fa980c
revert revm-inspectors version change
zemse ca61b6d
switch to flamegraph and flamechart boolean flags
zemse 8ea22d3
Merge branch 'master' into flamegraph-2
zerosnacks 7baeda5
Update crates/evm/traces/src/folded_stack_trace.rs
zemse d944ce5
Update crates/evm/traces/src/folded_stack_trace.rs
zemse a62c080
save to cache dir and gracefully handle opener outcome
zemse 595c09a
disable default features in inferno
zemse 9c0b387
fixes
klkvr aab2e5c
license
klkvr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,301 @@ | ||
use alloy_primitives::hex::ToHexExt; | ||
use revm_inspectors::tracing::{ | ||
types::{CallTraceNode, CallTraceStep, DecodedTraceStep, TraceMemberOrder}, | ||
CallTraceArena, | ||
}; | ||
|
||
/// Builds a folded stack trace from a call trace arena. | ||
pub fn build(arena: &CallTraceArena) -> Vec<String> { | ||
let mut fst = EvmFoldedStackTraceBuilder::default(); | ||
fst.process_call_node(arena.nodes(), 0); | ||
fst.build() | ||
} | ||
|
||
/// Wrapper for building a folded stack trace using EVM call trace node. | ||
#[derive(Default)] | ||
pub struct EvmFoldedStackTraceBuilder { | ||
/// Raw folded stack trace builder. | ||
fst: FoldedStackTraceBuilder, | ||
} | ||
|
||
impl EvmFoldedStackTraceBuilder { | ||
/// Returns the folded stack trace. | ||
pub fn build(self) -> Vec<String> { | ||
self.fst.build() | ||
} | ||
|
||
/// Creates an entry for a EVM CALL in the folded stack trace. This method recursively processes | ||
/// all the children nodes of the call node and at the end it exits. | ||
pub fn process_call_node(&mut self, nodes: &[CallTraceNode], idx: usize) { | ||
let node = &nodes[idx]; | ||
|
||
let func_name = if node.trace.kind.is_any_create() { | ||
let default_contract_name = "Contract".to_string(); | ||
let contract_name = node.trace.decoded.label.as_ref().unwrap_or(&default_contract_name); | ||
format!("new {contract_name}") | ||
} else { | ||
let selector = node | ||
.selector() | ||
.map(|selector| selector.encode_hex_with_prefix()) | ||
.unwrap_or("fallback".to_string()); | ||
let signature = | ||
node.trace.decoded.call_data.as_ref().map(|dc| &dc.signature).unwrap_or(&selector); | ||
|
||
if let Some(label) = &node.trace.decoded.label { | ||
format!("{label}.{signature}") | ||
} else { | ||
signature.clone() | ||
} | ||
}; | ||
|
||
self.fst.enter(func_name, node.trace.gas_used as i64); | ||
|
||
// Track internal function step exits to do in this call context. | ||
let mut step_exits = vec![]; | ||
|
||
// Process children nodes. | ||
for order in &node.ordering { | ||
match order { | ||
TraceMemberOrder::Call(child_idx) => { | ||
let child_node_idx = node.children[*child_idx]; | ||
self.process_call_node(nodes, child_node_idx); | ||
} | ||
TraceMemberOrder::Step(step_idx) => { | ||
self.exit_previous_steps(&mut step_exits, *step_idx); | ||
self.process_step(&node.trace.steps, *step_idx, &mut step_exits) | ||
} | ||
TraceMemberOrder::Log(_) => {} | ||
} | ||
} | ||
|
||
// Exit pending internal function calls if any. | ||
for _ in 0..step_exits.len() { | ||
self.fst.exit(); | ||
} | ||
|
||
// Exit from this call context in the folded stack trace. | ||
self.fst.exit(); | ||
} | ||
|
||
/// Creates an entry for an internal function call in the folded stack trace. This method only | ||
/// enters the function in the folded stack trace, we cannot exit since we need to exit at a | ||
/// future step. Hence, we keep track of the step end index in the `step_exits`. | ||
fn process_step( | ||
&mut self, | ||
steps: &[CallTraceStep], | ||
step_idx: usize, | ||
step_exits: &mut Vec<usize>, | ||
) { | ||
let step = &steps[step_idx]; | ||
if let Some(decoded_step) = &step.decoded { | ||
match decoded_step { | ||
DecodedTraceStep::InternalCall(decoded_internal_call, step_end_idx) => { | ||
let gas_used = steps[*step_end_idx].gas_used.saturating_sub(step.gas_used); | ||
self.fst.enter(decoded_internal_call.func_name.clone(), gas_used as i64); | ||
step_exits.push(*step_end_idx); | ||
} | ||
DecodedTraceStep::Line(_) => {} | ||
} | ||
} | ||
} | ||
|
||
/// Exits all the previous internal calls that should end before starting step_idx. | ||
fn exit_previous_steps(&mut self, step_exits: &mut Vec<usize>, step_idx: usize) { | ||
let initial_length = step_exits.len(); | ||
step_exits.retain(|&number| number > step_idx); | ||
|
||
let num_exits = initial_length - step_exits.len(); | ||
for _ in 0..num_exits { | ||
self.fst.exit(); | ||
} | ||
} | ||
} | ||
|
||
/// Helps to translate a function enter-exit flow into a folded stack trace. | ||
/// | ||
/// Example: | ||
/// fn top() { child_a(); child_b() } // consumes 500 gas | ||
/// fn child_a() {} // consumes 100 gas | ||
/// fn child_b() {} // consumes 200 gas | ||
/// | ||
/// For execution of the `top` function looks like: | ||
/// 1. enter `top` | ||
/// 2. enter `child_a` | ||
/// 3. exit `child_a` | ||
/// 4. enter `child_b` | ||
/// 5. exit `child_b` | ||
/// 6. exit `top` | ||
/// | ||
/// The translated folded stack trace lines look like: | ||
/// 1. top | ||
/// 2. top;child_a | ||
/// 3. top;child_b | ||
/// | ||
/// Including the gas consumed by the function by itself. | ||
/// 1. top 200 // 500 - 100 - 200 | ||
/// 2. top;child_a 100 | ||
/// 3. top;child_b 200 | ||
#[derive(Debug, Default)] | ||
pub struct FoldedStackTraceBuilder { | ||
/// Trace entries. | ||
traces: Vec<TraceEntry>, | ||
/// Number of exits to be done before entering a new function. | ||
exits: usize, | ||
} | ||
|
||
#[derive(Debug, Default)] | ||
struct TraceEntry { | ||
/// Names of all functions in the call stack of this trace. | ||
names: Vec<String>, | ||
/// Gas consumed by this function, allowed to be negative due to refunds. | ||
gas: i64, | ||
} | ||
|
||
impl FoldedStackTraceBuilder { | ||
/// Enter execution of a function call that consumes `gas`. | ||
pub fn enter(&mut self, label: String, gas: i64) { | ||
let mut names = self.traces.last().map(|entry| entry.names.clone()).unwrap_or_default(); | ||
|
||
while self.exits > 0 { | ||
names.pop(); | ||
self.exits -= 1; | ||
} | ||
|
||
names.push(label); | ||
self.traces.push(TraceEntry { names, gas }); | ||
} | ||
|
||
/// Exit execution of a function call. | ||
pub fn exit(&mut self) { | ||
self.exits += 1; | ||
} | ||
|
||
/// Returns folded stack trace. | ||
pub fn build(mut self) -> Vec<String> { | ||
self.subtract_children(); | ||
self.build_without_subtraction() | ||
} | ||
|
||
/// Internal method to build the folded stack trace without subtracting gas consumed by | ||
/// the children function calls. | ||
fn build_without_subtraction(&mut self) -> Vec<String> { | ||
let mut lines = Vec::new(); | ||
for TraceEntry { names, gas } in self.traces.iter() { | ||
lines.push(format!("{} {}", names.join(";"), gas)); | ||
} | ||
lines | ||
} | ||
|
||
/// Subtracts gas consumed by the children function calls from the parent function calls. | ||
fn subtract_children(&mut self) { | ||
// Iterate over each trace to find the children and subtract their values from the parents. | ||
for i in 0..self.traces.len() { | ||
let (left, right) = self.traces.split_at_mut(i); | ||
let TraceEntry { names, gas } = &right[0]; | ||
if names.len() > 1 { | ||
let parent_trace_to_match = &names[..names.len() - 1]; | ||
for parent in left.iter_mut().rev() { | ||
if parent.names == parent_trace_to_match { | ||
parent.gas -= gas; | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
mod tests { | ||
#[test] | ||
fn test_fst_1() { | ||
let mut trace = super::FoldedStackTraceBuilder::default(); | ||
trace.enter("top".to_string(), 500); | ||
trace.enter("child_a".to_string(), 100); | ||
trace.exit(); | ||
trace.enter("child_b".to_string(), 200); | ||
|
||
assert_eq!( | ||
trace.build_without_subtraction(), | ||
vec![ | ||
"top 500", // | ||
"top;child_a 100", | ||
"top;child_b 200", | ||
] | ||
); | ||
assert_eq!( | ||
trace.build(), | ||
vec![ | ||
"top 200", // 500 - 100 - 200 | ||
"top;child_a 100", | ||
"top;child_b 200", | ||
] | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_fst_2() { | ||
let mut trace = super::FoldedStackTraceBuilder::default(); | ||
trace.enter("top".to_string(), 500); | ||
trace.enter("child_a".to_string(), 300); | ||
trace.enter("child_b".to_string(), 100); | ||
trace.exit(); | ||
trace.exit(); | ||
trace.enter("child_c".to_string(), 100); | ||
|
||
assert_eq!( | ||
trace.build_without_subtraction(), | ||
vec![ | ||
"top 500", // | ||
"top;child_a 300", | ||
"top;child_a;child_b 100", | ||
"top;child_c 100", | ||
] | ||
); | ||
|
||
assert_eq!( | ||
trace.build(), | ||
vec![ | ||
"top 100", // 500 - 300 - 100 | ||
"top;child_a 200", // 300 - 100 | ||
"top;child_a;child_b 100", | ||
"top;child_c 100", | ||
] | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_fst_3() { | ||
let mut trace = super::FoldedStackTraceBuilder::default(); | ||
trace.enter("top".to_string(), 1700); | ||
trace.enter("child_a".to_string(), 500); | ||
trace.exit(); | ||
trace.enter("child_b".to_string(), 500); | ||
trace.enter("child_c".to_string(), 500); | ||
trace.exit(); | ||
trace.exit(); | ||
trace.exit(); | ||
trace.enter("top2".to_string(), 1700); | ||
|
||
assert_eq!( | ||
trace.build_without_subtraction(), | ||
vec![ | ||
"top 1700", // | ||
"top;child_a 500", | ||
"top;child_b 500", | ||
"top;child_b;child_c 500", | ||
"top2 1700", | ||
] | ||
); | ||
|
||
assert_eq!( | ||
trace.build(), | ||
vec![ | ||
"top 700", // | ||
"top;child_a 500", | ||
"top;child_b 0", | ||
"top;child_b;child_c 500", | ||
"top2 1700", | ||
] | ||
); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.