reth_bench/bench/
output.rs

1//! Contains various benchmark output formats, either for logging or for
2//! serialization to / from files.
3
4use eyre::OptionExt;
5use reth_primitives_traits::constants::GIGAGAS;
6use serde::{ser::SerializeStruct, Serialize};
7use std::time::Duration;
8
9/// This is the suffix for gas output csv files.
10pub(crate) const GAS_OUTPUT_SUFFIX: &str = "total_gas.csv";
11
12/// This is the suffix for combined output csv files.
13pub(crate) const COMBINED_OUTPUT_SUFFIX: &str = "combined_latency.csv";
14
15/// This is the suffix for new payload output csv files.
16pub(crate) const NEW_PAYLOAD_OUTPUT_SUFFIX: &str = "new_payload_latency.csv";
17
18/// This represents the results of a single `newPayload` call in the benchmark, containing the gas
19/// used and the `newPayload` latency.
20#[derive(Debug)]
21pub(crate) struct NewPayloadResult {
22    /// The gas used in the `newPayload` call.
23    pub(crate) gas_used: u64,
24    /// The latency of the `newPayload` call.
25    pub(crate) latency: Duration,
26}
27
28impl NewPayloadResult {
29    /// Returns the gas per second processed in the `newPayload` call.
30    pub(crate) fn gas_per_second(&self) -> f64 {
31        self.gas_used as f64 / self.latency.as_secs_f64()
32    }
33}
34
35impl std::fmt::Display for NewPayloadResult {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(
38            f,
39            "New payload processed at {:.4} Ggas/s, used {} total gas. Latency: {:?}",
40            self.gas_per_second() / GIGAGAS as f64,
41            self.gas_used,
42            self.latency
43        )
44    }
45}
46
47/// This is another [`Serialize`] implementation for the [`NewPayloadResult`] struct, serializing
48/// the duration as microseconds because the csv writer would fail otherwise.
49impl Serialize for NewPayloadResult {
50    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
51    where
52        S: serde::ser::Serializer,
53    {
54        // convert the time to microseconds
55        let time = self.latency.as_micros();
56        let mut state = serializer.serialize_struct("NewPayloadResult", 2)?;
57        state.serialize_field("gas_used", &self.gas_used)?;
58        state.serialize_field("latency", &time)?;
59        state.end()
60    }
61}
62
63/// This represents the combined results of a `newPayload` call and a `forkchoiceUpdated` call in
64/// the benchmark, containing the gas used, the `newPayload` latency, and the `forkchoiceUpdated`
65/// latency.
66#[derive(Debug)]
67pub(crate) struct CombinedResult {
68    /// The block number of the block being processed.
69    pub(crate) block_number: u64,
70    /// The number of transactions in the block.
71    pub(crate) transaction_count: u64,
72    /// The `newPayload` result.
73    pub(crate) new_payload_result: NewPayloadResult,
74    /// The latency of the `forkchoiceUpdated` call.
75    pub(crate) fcu_latency: Duration,
76    /// The latency of both calls combined.
77    pub(crate) total_latency: Duration,
78}
79
80impl CombinedResult {
81    /// Returns the gas per second, including the `newPayload` _and_ `forkchoiceUpdated` duration.
82    pub(crate) fn combined_gas_per_second(&self) -> f64 {
83        self.new_payload_result.gas_used as f64 / self.total_latency.as_secs_f64()
84    }
85}
86
87impl std::fmt::Display for CombinedResult {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        write!(
90            f,
91            "Payload {} processed at {:.4} Ggas/s, used {} total gas. Combined gas per second: {:.4} Ggas/s. fcu latency: {:?}, newPayload latency: {:?}",
92            self.block_number,
93            self.new_payload_result.gas_per_second() / GIGAGAS as f64,
94            self.new_payload_result.gas_used,
95            self.combined_gas_per_second() / GIGAGAS as f64,
96            self.fcu_latency,
97            self.new_payload_result.latency
98        )
99    }
100}
101
102/// This is a [`Serialize`] implementation for the [`CombinedResult`] struct, serializing the
103/// durations as microseconds because the csv writer would fail otherwise.
104impl Serialize for CombinedResult {
105    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
106    where
107        S: serde::ser::Serializer,
108    {
109        // convert the time to microseconds
110        let fcu_latency = self.fcu_latency.as_micros();
111        let new_payload_latency = self.new_payload_result.latency.as_micros();
112        let total_latency = self.total_latency.as_micros();
113        let mut state = serializer.serialize_struct("CombinedResult", 6)?;
114
115        // flatten the new payload result because this is meant for CSV writing
116        state.serialize_field("block_number", &self.block_number)?;
117        state.serialize_field("transaction_count", &self.transaction_count)?;
118        state.serialize_field("gas_used", &self.new_payload_result.gas_used)?;
119        state.serialize_field("new_payload_latency", &new_payload_latency)?;
120        state.serialize_field("fcu_latency", &fcu_latency)?;
121        state.serialize_field("total_latency", &total_latency)?;
122        state.end()
123    }
124}
125
126/// This represents a row of total gas data in the benchmark.
127#[derive(Debug)]
128pub(crate) struct TotalGasRow {
129    /// The block number of the block being processed.
130    pub(crate) block_number: u64,
131    /// The number of transactions in the block.
132    pub(crate) transaction_count: u64,
133    /// The total gas used in the block.
134    pub(crate) gas_used: u64,
135    /// Time since the start of the benchmark.
136    pub(crate) time: Duration,
137}
138
139/// This represents the aggregated output, meant to show gas per second metrics, of a benchmark run.
140#[derive(Debug)]
141pub(crate) struct TotalGasOutput {
142    /// The total gas used in the benchmark.
143    pub(crate) total_gas_used: u64,
144    /// The total duration of the benchmark.
145    pub(crate) total_duration: Duration,
146    /// The total gas used per second.
147    pub(crate) total_gas_per_second: f64,
148    /// The number of blocks processed.
149    pub(crate) blocks_processed: u64,
150}
151
152impl TotalGasOutput {
153    /// Create a new [`TotalGasOutput`] from a list of [`TotalGasRow`].
154    pub(crate) fn new(rows: Vec<TotalGasRow>) -> eyre::Result<Self> {
155        // the duration is obtained from the last row
156        let total_duration = rows.last().map(|row| row.time).ok_or_eyre("empty results")?;
157        let blocks_processed = rows.len() as u64;
158        let total_gas_used: u64 = rows.into_iter().map(|row| row.gas_used).sum();
159        let total_gas_per_second = total_gas_used as f64 / total_duration.as_secs_f64();
160
161        Ok(Self { total_gas_used, total_duration, total_gas_per_second, blocks_processed })
162    }
163
164    /// Return the total gigagas per second.
165    pub(crate) fn total_gigagas_per_second(&self) -> f64 {
166        self.total_gas_per_second / GIGAGAS as f64
167    }
168}
169
170/// This serializes the `time` field of the [`TotalGasRow`] to microseconds.
171///
172/// This is essentially just for the csv writer, which would have headers
173impl Serialize for TotalGasRow {
174    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
175    where
176        S: serde::ser::Serializer,
177    {
178        // convert the time to microseconds
179        let time = self.time.as_micros();
180        let mut state = serializer.serialize_struct("TotalGasRow", 4)?;
181        state.serialize_field("block_number", &self.block_number)?;
182        state.serialize_field("transaction_count", &self.transaction_count)?;
183        state.serialize_field("gas_used", &self.gas_used)?;
184        state.serialize_field("time", &time)?;
185        state.end()
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use csv::Writer;
193    use std::io::BufRead;
194
195    #[test]
196    fn test_write_total_gas_row_csv() {
197        let row = TotalGasRow {
198            block_number: 1,
199            transaction_count: 10,
200            gas_used: 1_000,
201            time: Duration::from_secs(1),
202        };
203
204        let mut writer = Writer::from_writer(vec![]);
205        writer.serialize(row).unwrap();
206        let result = writer.into_inner().unwrap();
207
208        // parse into Lines
209        let mut result = result.as_slice().lines();
210
211        // assert header
212        let expected_first_line = "block_number,transaction_count,gas_used,time";
213        let first_line = result.next().unwrap().unwrap();
214        assert_eq!(first_line, expected_first_line);
215
216        let expected_second_line = "1,10,1000,1000000";
217        let second_line = result.next().unwrap().unwrap();
218        assert_eq!(second_line, expected_second_line);
219    }
220}