1use crate::{
4 constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS},
5 traces::{CallTraceArena, CallTraceDecoder, CallTraceNode, DecodedCallData},
6};
7use alloy_primitives::map::HashSet;
8use comfy_table::{Cell, Color, Table, modifiers::UTF8_ROUND_CORNERS, presets::ASCII_MARKDOWN};
9use foundry_common::{TestFunctionExt, calc, shell};
10use foundry_evm::traces::CallKind;
11
12use serde::{Deserialize, Serialize};
13use serde_json::json;
14use std::{collections::BTreeMap, fmt::Display};
15
16#[derive(Clone, Debug, Default, Serialize, Deserialize)]
18pub struct GasReport {
19 report_any: bool,
21 report_for: HashSet<String>,
23 ignore: HashSet<String>,
25 include_tests: bool,
27 pub contracts: BTreeMap<String, ContractInfo>,
30}
31
32impl GasReport {
33 pub fn new(
34 report_for: impl IntoIterator<Item = String>,
35 ignore: impl IntoIterator<Item = String>,
36 include_tests: bool,
37 ) -> Self {
38 let report_for = report_for.into_iter().collect::<HashSet<_>>();
39 let ignore = ignore.into_iter().collect::<HashSet<_>>();
40 let report_any = report_for.is_empty() || report_for.contains("*");
41 Self { report_any, report_for, ignore, include_tests, ..Default::default() }
42 }
43
44 #[instrument(level = "trace", skip(self), ret)]
46 fn should_report(&self, contract_name: &str) -> bool {
47 if self.ignore.contains(contract_name) {
48 let contains_anyway = self.report_for.contains(contract_name);
49 if contains_anyway {
50 let _ = sh_warn!(
56 "{contract_name} is listed in both 'gas_reports' and 'gas_reports_ignore'."
57 );
58 }
59 return contains_anyway;
60 }
61 self.report_any || self.report_for.contains(contract_name)
62 }
63
64 pub async fn analyze(
66 &mut self,
67 arenas: impl IntoIterator<Item = &CallTraceArena>,
68 decoder: &CallTraceDecoder,
69 ) {
70 for node in arenas.into_iter().flat_map(|arena| arena.nodes()) {
71 self.analyze_node(node, decoder).await;
72 }
73 }
74
75 async fn analyze_node(&mut self, node: &CallTraceNode, decoder: &CallTraceDecoder) {
76 let trace = &node.trace;
77
78 if trace.address == CHEATCODE_ADDRESS || trace.address == HARDHAT_CONSOLE_ADDRESS {
79 return;
80 }
81
82 let Some(name) = decoder.contracts.get(&node.trace.address) else { return };
83 let contract_name = name.rsplit(':').next().unwrap_or(name);
84
85 if !self.should_report(contract_name) {
86 return;
87 }
88 let contract_info = self.contracts.entry(name.to_string()).or_default();
89 let is_create_call = trace.kind.is_any_create();
90
91 if is_create_call {
93 trace!(contract_name, "adding create size info");
94 contract_info.size = trace.data.len();
95 }
96
97 if trace.depth > 1 && (trace.kind == CallKind::Call || is_create_call) {
100 return;
101 }
102
103 let decoded = || decoder.decode_function(&node.trace);
104
105 if is_create_call {
106 trace!(contract_name, "adding create gas info");
107 contract_info.gas = trace.gas_used;
108 } else if let Some(DecodedCallData { signature, .. }) = decoded().await.call_data {
109 let name = signature.split('(').next().unwrap();
110 if self.include_tests || !name.test_function_kind().is_known() {
112 trace!(contract_name, signature, "adding gas info");
113 let gas_info = contract_info
114 .functions
115 .entry(name.to_string())
116 .or_default()
117 .entry(signature.clone())
118 .or_default();
119 gas_info.frames.push(trace.gas_used);
120 }
121 }
122 }
123
124 #[must_use]
126 pub fn finalize(mut self) -> Self {
127 trace!("finalizing gas report");
128 for contract in self.contracts.values_mut() {
129 for sigs in contract.functions.values_mut() {
130 for func in sigs.values_mut() {
131 func.frames.sort_unstable();
132 func.min = func.frames.first().copied().unwrap_or_default();
133 func.max = func.frames.last().copied().unwrap_or_default();
134 func.mean = calc::mean(&func.frames);
135 func.median = calc::median_sorted(&func.frames);
136 func.calls = func.frames.len() as u64;
137 }
138 }
139 }
140 self
141 }
142}
143
144impl Display for GasReport {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
146 if shell::is_json() {
147 writeln!(f, "{}", &self.format_json_output())?;
148 } else {
149 for (name, contract) in &self.contracts {
150 if contract.functions.is_empty() {
151 trace!(name, "gas report contract without functions");
152 continue;
153 }
154
155 let table = self.format_table_output(contract, name);
156 writeln!(f, "\n{table}")?;
157 }
158 }
159
160 Ok(())
161 }
162}
163
164impl GasReport {
165 fn format_json_output(&self) -> String {
166 serde_json::to_string(
167 &self
168 .contracts
169 .iter()
170 .filter_map(|(name, contract)| {
171 if contract.functions.is_empty() {
172 trace!(name, "gas report contract without functions");
173 return None;
174 }
175
176 let functions = contract
177 .functions
178 .iter()
179 .flat_map(|(_, sigs)| {
180 sigs.iter().map(|(sig, gas_info)| {
181 let display_name = sig.replace(':', "");
182 (display_name, gas_info)
183 })
184 })
185 .collect::<BTreeMap<_, _>>();
186
187 Some(json!({
188 "contract": name,
189 "deployment": {
190 "gas": contract.gas,
191 "size": contract.size,
192 },
193 "functions": functions,
194 }))
195 })
196 .collect::<Vec<_>>(),
197 )
198 .unwrap()
199 }
200
201 fn format_table_output(&self, contract: &ContractInfo, name: &str) -> Table {
202 let mut table = Table::new();
203 if shell::is_markdown() {
204 table.load_preset(ASCII_MARKDOWN);
205 } else {
206 table.apply_modifier(UTF8_ROUND_CORNERS);
207 }
208
209 table.set_header(vec![Cell::new(format!("{name} Contract")).fg(Color::Magenta)]);
210
211 table.add_row(vec![
212 Cell::new("Deployment Cost").fg(Color::Cyan),
213 Cell::new("Deployment Size").fg(Color::Cyan),
214 ]);
215 table.add_row(vec![
216 Cell::new(contract.gas.to_string()),
217 Cell::new(contract.size.to_string()),
218 ]);
219
220 table.add_row(vec![Cell::new("")]);
222
223 table.add_row(vec![
224 Cell::new("Function Name"),
225 Cell::new("Min").fg(Color::Green),
226 Cell::new("Avg").fg(Color::Yellow),
227 Cell::new("Median").fg(Color::Yellow),
228 Cell::new("Max").fg(Color::Red),
229 Cell::new("# Calls").fg(Color::Cyan),
230 ]);
231
232 contract.functions.iter().for_each(|(fname, sigs)| {
233 sigs.iter().for_each(|(sig, gas_info)| {
234 let display_name =
236 if sigs.len() == 1 { fname.to_string() } else { sig.replace(':', "") };
237
238 table.add_row(vec![
239 Cell::new(display_name),
240 Cell::new(gas_info.min.to_string()).fg(Color::Green),
241 Cell::new(gas_info.mean.to_string()).fg(Color::Yellow),
242 Cell::new(gas_info.median.to_string()).fg(Color::Yellow),
243 Cell::new(gas_info.max.to_string()).fg(Color::Red),
244 Cell::new(gas_info.calls.to_string()),
245 ]);
246 })
247 });
248
249 table
250 }
251}
252
253#[derive(Clone, Debug, Default, Serialize, Deserialize)]
254pub struct ContractInfo {
255 pub gas: u64,
256 pub size: usize,
257 pub functions: BTreeMap<String, BTreeMap<String, GasInfo>>,
259}
260
261#[derive(Clone, Debug, Default, Serialize, Deserialize)]
262pub struct GasInfo {
263 pub calls: u64,
264 pub min: u64,
265 pub mean: u64,
266 pub median: u64,
267 pub max: u64,
268
269 #[serde(skip)]
270 pub frames: Vec<u64>,
271}