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