1use alloy_primitives::B256;
5use csv::Writer;
6use eyre::OptionExt;
7use reth_primitives_traits::constants::GIGAGAS;
8use serde::{ser::SerializeStruct, Deserialize, Serialize};
9use std::{fs, path::Path, time::Duration};
10use tracing::info;
11
12pub(crate) const GAS_OUTPUT_SUFFIX: &str = "total_gas.csv";
14
15pub(crate) const COMBINED_OUTPUT_SUFFIX: &str = "combined_latency.csv";
17
18pub(crate) const NEW_PAYLOAD_OUTPUT_SUFFIX: &str = "new_payload_latency.csv";
20
21#[derive(Debug, Serialize, Deserialize)]
23pub(crate) struct GasRampPayloadFile {
24 pub(crate) version: u8,
26 pub(crate) block_hash: B256,
28 pub(crate) params: serde_json::Value,
30}
31
32#[derive(Debug)]
35pub(crate) struct NewPayloadResult {
36 pub(crate) gas_used: u64,
38 pub(crate) latency: Duration,
40}
41
42impl NewPayloadResult {
43 pub(crate) fn gas_per_second(&self) -> f64 {
45 self.gas_used as f64 / self.latency.as_secs_f64()
46 }
47}
48
49impl std::fmt::Display for NewPayloadResult {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 write!(
52 f,
53 "New payload processed at {:.4} Ggas/s, used {} total gas. Latency: {:?}",
54 self.gas_per_second() / GIGAGAS as f64,
55 self.gas_used,
56 self.latency
57 )
58 }
59}
60
61impl Serialize for NewPayloadResult {
64 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
65 where
66 S: serde::ser::Serializer,
67 {
68 let time = self.latency.as_micros();
70 let mut state = serializer.serialize_struct("NewPayloadResult", 2)?;
71 state.serialize_field("gas_used", &self.gas_used)?;
72 state.serialize_field("latency", &time)?;
73 state.end()
74 }
75}
76
77#[derive(Debug)]
81pub(crate) struct CombinedResult {
82 pub(crate) block_number: u64,
84 pub(crate) gas_limit: u64,
86 pub(crate) transaction_count: u64,
88 pub(crate) new_payload_result: NewPayloadResult,
90 pub(crate) fcu_latency: Duration,
92 pub(crate) total_latency: Duration,
94}
95
96impl CombinedResult {
97 pub(crate) fn combined_gas_per_second(&self) -> f64 {
99 self.new_payload_result.gas_used as f64 / self.total_latency.as_secs_f64()
100 }
101}
102
103impl std::fmt::Display for CombinedResult {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 write!(
106 f,
107 "Block {} processed at {:.4} Ggas/s, used {} total gas. Combined: {:.4} Ggas/s. fcu: {:?}, newPayload: {:?}",
108 self.block_number,
109 self.new_payload_result.gas_per_second() / GIGAGAS as f64,
110 self.new_payload_result.gas_used,
111 self.combined_gas_per_second() / GIGAGAS as f64,
112 self.fcu_latency,
113 self.new_payload_result.latency
114 )
115 }
116}
117
118impl Serialize for CombinedResult {
121 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
122 where
123 S: serde::ser::Serializer,
124 {
125 let fcu_latency = self.fcu_latency.as_micros();
127 let new_payload_latency = self.new_payload_result.latency.as_micros();
128 let total_latency = self.total_latency.as_micros();
129 let mut state = serializer.serialize_struct("CombinedResult", 7)?;
130
131 state.serialize_field("block_number", &self.block_number)?;
133 state.serialize_field("gas_limit", &self.gas_limit)?;
134 state.serialize_field("transaction_count", &self.transaction_count)?;
135 state.serialize_field("gas_used", &self.new_payload_result.gas_used)?;
136 state.serialize_field("new_payload_latency", &new_payload_latency)?;
137 state.serialize_field("fcu_latency", &fcu_latency)?;
138 state.serialize_field("total_latency", &total_latency)?;
139 state.end()
140 }
141}
142
143#[derive(Debug)]
145pub(crate) struct TotalGasRow {
146 pub(crate) block_number: u64,
148 pub(crate) transaction_count: u64,
150 pub(crate) gas_used: u64,
152 pub(crate) time: Duration,
154}
155
156#[derive(Debug)]
158pub(crate) struct TotalGasOutput {
159 pub(crate) total_gas_used: u64,
161 pub(crate) total_duration: Duration,
163 pub(crate) execution_duration: Duration,
165 pub(crate) blocks_processed: u64,
167}
168
169impl TotalGasOutput {
170 pub(crate) fn new(rows: Vec<TotalGasRow>) -> eyre::Result<Self> {
175 let total_duration = rows.last().map(|row| row.time).ok_or_eyre("empty results")?;
176 let blocks_processed = rows.len() as u64;
177 let total_gas_used: u64 = rows.into_iter().map(|row| row.gas_used).sum();
178
179 Ok(Self {
180 total_gas_used,
181 total_duration,
182 execution_duration: total_duration,
183 blocks_processed,
184 })
185 }
186
187 pub(crate) fn with_combined_results(
192 rows: Vec<TotalGasRow>,
193 combined_results: &[CombinedResult],
194 ) -> eyre::Result<Self> {
195 let total_duration = rows.last().map(|row| row.time).ok_or_eyre("empty results")?;
196 let blocks_processed = rows.len() as u64;
197 let total_gas_used: u64 = rows.into_iter().map(|row| row.gas_used).sum();
198
199 let execution_duration: Duration = combined_results.iter().map(|r| r.total_latency).sum();
201
202 Ok(Self { total_gas_used, total_duration, execution_duration, blocks_processed })
203 }
204
205 pub(crate) fn total_gigagas_per_second(&self) -> f64 {
207 self.total_gas_used as f64 / self.total_duration.as_secs_f64() / GIGAGAS as f64
208 }
209
210 pub(crate) fn execution_gigagas_per_second(&self) -> f64 {
212 self.total_gas_used as f64 / self.execution_duration.as_secs_f64() / GIGAGAS as f64
213 }
214}
215
216pub(crate) fn write_benchmark_results(
222 output_dir: &Path,
223 gas_results: &[TotalGasRow],
224 combined_results: &[CombinedResult],
225) -> eyre::Result<()> {
226 fs::create_dir_all(output_dir)?;
227
228 let output_path = output_dir.join(COMBINED_OUTPUT_SUFFIX);
229 info!(target: "reth-bench", "Writing engine api call latency output to file: {:?}", output_path);
230 let mut writer = Writer::from_path(&output_path)?;
231 for result in combined_results {
232 writer.serialize(result)?;
233 }
234 writer.flush()?;
235
236 let output_path = output_dir.join(GAS_OUTPUT_SUFFIX);
237 info!(target: "reth-bench", "Writing total gas output to file: {:?}", output_path);
238 let mut writer = Writer::from_path(&output_path)?;
239 for row in gas_results {
240 writer.serialize(row)?;
241 }
242 writer.flush()?;
243
244 info!(target: "reth-bench", "Finished writing benchmark output files to {:?}.", output_dir);
245 Ok(())
246}
247
248impl Serialize for TotalGasRow {
252 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
253 where
254 S: serde::ser::Serializer,
255 {
256 let time = self.time.as_micros();
258 let mut state = serializer.serialize_struct("TotalGasRow", 4)?;
259 state.serialize_field("block_number", &self.block_number)?;
260 state.serialize_field("transaction_count", &self.transaction_count)?;
261 state.serialize_field("gas_used", &self.gas_used)?;
262 state.serialize_field("time", &time)?;
263 state.end()
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use csv::Writer;
271 use std::io::BufRead;
272
273 #[test]
274 fn test_write_total_gas_row_csv() {
275 let row = TotalGasRow {
276 block_number: 1,
277 transaction_count: 10,
278 gas_used: 1_000,
279 time: Duration::from_secs(1),
280 };
281
282 let mut writer = Writer::from_writer(vec![]);
283 writer.serialize(row).unwrap();
284 let result = writer.into_inner().unwrap();
285
286 let mut result = result.as_slice().lines();
288
289 let expected_first_line = "block_number,transaction_count,gas_used,time";
291 let first_line = result.next().unwrap().unwrap();
292 assert_eq!(first_line, expected_first_line);
293
294 let expected_second_line = "1,10,1000,1000000";
295 let second_line = result.next().unwrap().unwrap();
296 assert_eq!(second_line, expected_second_line);
297 }
298}