Skip to main content

reth_engine_tree/tree/
metrics.rs

1use crate::tree::{error::InsertBlockFatalError, TreeOutcome};
2use alloy_rpc_types_engine::{PayloadStatus, PayloadStatusEnum};
3use reth_engine_primitives::{ForkchoiceStatus, OnForkChoiceUpdated};
4use reth_errors::ProviderError;
5use reth_evm::metrics::ExecutorMetrics;
6use reth_execution_types::BlockExecutionOutput;
7use reth_metrics::{
8    metrics::{Counter, Gauge, Histogram},
9    thread::{ThreadResourceUsage, ThreadResourceUsageDelta},
10    Metrics,
11};
12use reth_primitives_traits::{constants::gas_units::MEGAGAS, FastInstant as Instant};
13use reth_trie::updates::TrieUpdates;
14use std::time::Duration;
15
16/// Upper bounds for each gas bucket. The last bucket is a catch-all for
17/// everything above the final threshold: <5M, 5-10M, 10-20M, 20-30M, 30-40M, >40M.
18const GAS_BUCKET_THRESHOLDS: [u64; 5] =
19    [5 * MEGAGAS, 10 * MEGAGAS, 20 * MEGAGAS, 30 * MEGAGAS, 40 * MEGAGAS];
20
21/// Total number of gas buckets (thresholds + 1 catch-all).
22const NUM_GAS_BUCKETS: usize = GAS_BUCKET_THRESHOLDS.len() + 1;
23
24/// Metrics for the `EngineApi`.
25#[derive(Debug, Default)]
26pub struct EngineApiMetrics {
27    /// Engine API-specific metrics.
28    pub engine: EngineMetrics,
29    /// Block executor metrics.
30    pub executor: ExecutorMetrics,
31    /// Metrics for block validation
32    pub block_validation: BlockValidationMetrics,
33    /// Canonical chain and reorg related metrics
34    pub tree: TreeMetrics,
35    /// Metrics for EIP-7928 Block-Level Access Lists (BAL).
36    #[allow(dead_code)]
37    pub(crate) bal: BalMetrics,
38    /// Gas-bucketed execution sub-phase metrics.
39    pub(crate) execution_gas_buckets: ExecutionGasBucketMetrics,
40    /// Gas-bucketed block validation sub-phase metrics.
41    pub(crate) block_validation_gas_buckets: BlockValidationGasBucketMetrics,
42}
43
44impl EngineApiMetrics {
45    /// Records metrics for block execution.
46    ///
47    /// This method updates metrics for execution time, gas usage, and the number
48    /// of accounts, storage slots and bytecodes updated.
49    pub fn record_block_execution<R>(
50        &self,
51        output: &BlockExecutionOutput<R>,
52        execution_duration: Duration,
53    ) {
54        let execution_secs = execution_duration.as_secs_f64();
55        let gas_used = output.result.gas_used;
56
57        // Update gas metrics
58        self.executor.gas_processed_total.increment(gas_used);
59        self.executor.gas_per_second.set(gas_used as f64 / execution_secs);
60        self.executor.gas_used_histogram.record(gas_used as f64);
61        self.executor.execution_histogram.record(execution_secs);
62        self.executor.execution_duration.set(execution_secs);
63
64        // Update the metrics for the number of accounts, storage slots and bytecodes
65        let accounts = output.state.state.len();
66        let storage_slots =
67            output.state.state.values().map(|account| account.storage.len()).sum::<usize>();
68        let bytecodes = output.state.contracts.len();
69
70        self.executor.accounts_updated_histogram.record(accounts as f64);
71        self.executor.storage_slots_updated_histogram.record(storage_slots as f64);
72        self.executor.bytecodes_updated_histogram.record(bytecodes as f64);
73    }
74
75    /// Returns a reference to the executor metrics for use in state hooks.
76    pub const fn executor_metrics(&self) -> &ExecutorMetrics {
77        &self.executor
78    }
79
80    /// Records the duration of block pre-execution changes (e.g., beacon root update).
81    pub fn record_pre_execution(&self, elapsed: Duration) {
82        self.executor.pre_execution_histogram.record(elapsed);
83    }
84
85    /// Records the duration of block post-execution changes (e.g., finalization).
86    pub fn record_post_execution(&self, elapsed: Duration) {
87        self.executor.post_execution_histogram.record(elapsed);
88    }
89
90    /// Records execution duration into the gas-bucketed execution histogram.
91    pub fn record_block_execution_gas_bucket(&self, gas_used: u64, elapsed: Duration) {
92        let idx = GasBucketMetrics::bucket_index(gas_used);
93        self.execution_gas_buckets.buckets[idx]
94            .execution_gas_bucket_histogram
95            .record(elapsed.as_secs_f64());
96    }
97
98    /// Records state root duration into the gas-bucketed block validation histogram.
99    pub fn record_state_root_gas_bucket(&self, gas_used: u64, elapsed_secs: f64) {
100        let idx = GasBucketMetrics::bucket_index(gas_used);
101        self.block_validation_gas_buckets.buckets[idx]
102            .state_root_gas_bucket_histogram
103            .record(elapsed_secs);
104    }
105
106    /// Records the time spent waiting for the next transaction from the iterator.
107    pub fn record_transaction_wait(&self, elapsed: Duration) {
108        self.executor.transaction_wait_histogram.record(elapsed);
109    }
110
111    /// Records the duration of a single transaction execution.
112    pub fn record_transaction_execution(&self, elapsed: Duration) {
113        self.executor.transaction_execution_histogram.record(elapsed);
114    }
115}
116
117/// Metrics for the entire blockchain tree
118#[derive(Metrics)]
119#[metrics(scope = "blockchain_tree")]
120pub struct TreeMetrics {
121    /// The highest block number in the canonical chain
122    pub canonical_chain_height: Gauge,
123    /// Metrics for reorgs.
124    #[metric(skip)]
125    pub reorgs: ReorgMetrics,
126    /// The latest reorg depth
127    pub latest_reorg_depth: Gauge,
128    /// The current safe block height (this is required by optimism)
129    pub safe_block_height: Gauge,
130    /// The current finalized block height (this is required by optimism)
131    pub finalized_block_height: Gauge,
132}
133
134/// Metrics for reorgs.
135#[derive(Debug)]
136pub struct ReorgMetrics {
137    /// The number of head block reorgs
138    pub head: Counter,
139    /// The number of safe block reorgs
140    pub safe: Counter,
141    /// The number of finalized block reorgs
142    pub finalized: Counter,
143}
144
145impl Default for ReorgMetrics {
146    fn default() -> Self {
147        Self {
148            head: metrics::counter!("blockchain_tree_reorgs", "commitment" => "head"),
149            safe: metrics::counter!("blockchain_tree_reorgs", "commitment" => "safe"),
150            finalized: metrics::counter!("blockchain_tree_reorgs", "commitment" => "finalized"),
151        }
152    }
153}
154
155/// Metrics for the `EngineApi`.
156#[derive(Metrics)]
157#[metrics(scope = "consensus.engine.beacon")]
158pub struct EngineMetrics {
159    /// Engine API forkchoiceUpdated response type metrics
160    #[metric(skip)]
161    pub(crate) forkchoice_updated: ForkchoiceUpdatedMetrics,
162    /// Engine API newPayload response type metrics
163    #[metric(skip)]
164    pub(crate) new_payload: NewPayloadStatusMetrics,
165    /// How many executed blocks are currently stored.
166    pub(crate) executed_blocks: Gauge,
167    /// How many already executed blocks were directly inserted into the tree.
168    pub(crate) inserted_already_executed_blocks: Counter,
169    /// The number of times the pipeline was run.
170    pub(crate) pipeline_runs: Counter,
171    /// Newly arriving block hash is not present in executed blocks cache storage
172    pub(crate) executed_new_block_cache_miss: Counter,
173    /// Histogram of persistence operation durations (in seconds)
174    pub(crate) persistence_duration: Histogram,
175    /// Whether the engine loop is currently stalled on persistence backpressure.
176    pub(crate) backpressure_active: Gauge,
177    /// Time spent blocked waiting on persistence because backpressure was active.
178    pub(crate) backpressure_stall_duration: Histogram,
179    /// Tracks the how often we failed to deliver a newPayload response.
180    ///
181    /// This effectively tracks how often the message sender dropped the channel and indicates a CL
182    /// request timeout (e.g. it took more than 8s to send the response and the CL terminated the
183    /// request which resulted in a closed channel).
184    pub(crate) failed_new_payload_response_deliveries: Counter,
185    /// Tracks the how often we failed to deliver a forkchoice update response.
186    pub(crate) failed_forkchoice_updated_response_deliveries: Counter,
187    /// block insert duration
188    pub(crate) block_insert_total_duration: Histogram,
189}
190
191/// Metrics for engine forkchoiceUpdated responses.
192#[derive(Metrics)]
193#[metrics(scope = "consensus.engine.beacon")]
194pub(crate) struct ForkchoiceUpdatedMetrics {
195    /// Finish time of the latest forkchoice updated call.
196    #[metric(skip)]
197    pub(crate) latest_finish_at: Option<Instant>,
198    /// Start time of the latest forkchoice updated call.
199    #[metric(skip)]
200    pub(crate) latest_start_at: Option<Instant>,
201    /// The total count of forkchoice updated messages received.
202    pub(crate) forkchoice_updated_messages: Counter,
203    /// The total count of forkchoice updated messages with payload received.
204    pub(crate) forkchoice_with_attributes_updated_messages: Counter,
205    /// The total count of forkchoice updated messages that we responded to with
206    /// [`Valid`](ForkchoiceStatus::Valid).
207    pub(crate) forkchoice_updated_valid: Counter,
208    /// The total count of forkchoice updated messages that we responded to with
209    /// [`Invalid`](ForkchoiceStatus::Invalid).
210    pub(crate) forkchoice_updated_invalid: Counter,
211    /// The total count of forkchoice updated messages that we responded to with
212    /// [`Syncing`](ForkchoiceStatus::Syncing).
213    pub(crate) forkchoice_updated_syncing: Counter,
214    /// The total count of forkchoice updated messages that were unsuccessful, i.e. we responded
215    /// with an error type that is not a [`PayloadStatusEnum`].
216    pub(crate) forkchoice_updated_error: Counter,
217    /// Latency for the forkchoice updated calls.
218    pub(crate) forkchoice_updated_latency: Histogram,
219    /// Latency for the last forkchoice updated call.
220    pub(crate) forkchoice_updated_last: Gauge,
221    /// Time diff between new payload call response and the next forkchoice updated call request.
222    pub(crate) new_payload_forkchoice_updated_time_diff: Histogram,
223    /// Time from previous forkchoice updated finish to current forkchoice updated start (idle
224    /// time).
225    pub(crate) time_between_forkchoice_updated: Histogram,
226    /// Time from previous forkchoice updated start to current forkchoice updated start (total
227    /// interval).
228    pub(crate) forkchoice_updated_interval: Histogram,
229}
230
231impl ForkchoiceUpdatedMetrics {
232    /// Increment the forkchoiceUpdated counter based on the given result
233    pub(crate) fn update_response_metrics(
234        &mut self,
235        start: Instant,
236        latest_new_payload_at: &mut Option<Instant>,
237        has_attrs: bool,
238        result: &Result<TreeOutcome<OnForkChoiceUpdated>, ProviderError>,
239    ) {
240        let finish = Instant::now();
241        let elapsed = finish - start;
242
243        if let Some(prev_finish) = self.latest_finish_at {
244            self.time_between_forkchoice_updated.record(start - prev_finish);
245        }
246        if let Some(prev_start) = self.latest_start_at {
247            self.forkchoice_updated_interval.record(start - prev_start);
248        }
249        self.latest_finish_at = Some(finish);
250        self.latest_start_at = Some(start);
251
252        match result {
253            Ok(outcome) => match outcome.outcome.forkchoice_status() {
254                ForkchoiceStatus::Valid => self.forkchoice_updated_valid.increment(1),
255                ForkchoiceStatus::Invalid => self.forkchoice_updated_invalid.increment(1),
256                ForkchoiceStatus::Syncing => self.forkchoice_updated_syncing.increment(1),
257            },
258            Err(_) => self.forkchoice_updated_error.increment(1),
259        }
260        self.forkchoice_updated_messages.increment(1);
261        if has_attrs {
262            self.forkchoice_with_attributes_updated_messages.increment(1);
263        }
264        self.forkchoice_updated_latency.record(elapsed);
265        self.forkchoice_updated_last.set(elapsed);
266        if let Some(latest_new_payload_at) = latest_new_payload_at.take() {
267            self.new_payload_forkchoice_updated_time_diff.record(start - latest_new_payload_at);
268        }
269    }
270}
271
272/// Per-gas-bucket newPayload metrics, initialized once via [`Self::new_with_labels`].
273#[derive(Clone, Metrics)]
274#[metrics(scope = "consensus.engine.beacon")]
275pub(crate) struct NewPayloadGasBucketMetrics {
276    /// Latency for new payload calls in this gas bucket.
277    pub(crate) new_payload_gas_bucket_latency: Histogram,
278    /// Gas per second for new payload calls in this gas bucket.
279    pub(crate) new_payload_gas_bucket_gas_per_second: Histogram,
280}
281
282/// Holds pre-initialized [`NewPayloadGasBucketMetrics`] instances, one per gas bucket.
283#[derive(Debug)]
284pub(crate) struct GasBucketMetrics {
285    buckets: [NewPayloadGasBucketMetrics; NUM_GAS_BUCKETS],
286}
287
288impl Default for GasBucketMetrics {
289    fn default() -> Self {
290        Self {
291            buckets: std::array::from_fn(|i| {
292                let label = Self::bucket_label(i);
293                NewPayloadGasBucketMetrics::new_with_labels(&[("gas_bucket", label)])
294            }),
295        }
296    }
297}
298
299impl GasBucketMetrics {
300    fn record(&self, gas_used: u64, elapsed: Duration) {
301        let idx = Self::bucket_index(gas_used);
302        self.buckets[idx].new_payload_gas_bucket_latency.record(elapsed);
303        self.buckets[idx]
304            .new_payload_gas_bucket_gas_per_second
305            .record(gas_used as f64 / elapsed.as_secs_f64());
306    }
307
308    /// Returns the bucket index for a given gas value.
309    pub(crate) fn bucket_index(gas_used: u64) -> usize {
310        GAS_BUCKET_THRESHOLDS
311            .iter()
312            .position(|&threshold| gas_used < threshold)
313            .unwrap_or(GAS_BUCKET_THRESHOLDS.len())
314    }
315
316    /// Returns a human-readable label like `<5M`, `5-10M`, … `>40M`.
317    pub(crate) fn bucket_label(index: usize) -> String {
318        if index == 0 {
319            let hi = GAS_BUCKET_THRESHOLDS[0] / MEGAGAS;
320            format!("<{hi}M")
321        } else if index < GAS_BUCKET_THRESHOLDS.len() {
322            let lo = GAS_BUCKET_THRESHOLDS[index - 1] / MEGAGAS;
323            let hi = GAS_BUCKET_THRESHOLDS[index] / MEGAGAS;
324            format!("{lo}-{hi}M")
325        } else {
326            let lo = GAS_BUCKET_THRESHOLDS[GAS_BUCKET_THRESHOLDS.len() - 1] / MEGAGAS;
327            format!(">{lo}M")
328        }
329    }
330}
331
332/// Per-gas-bucket execution duration metric.
333#[derive(Clone, Metrics)]
334#[metrics(scope = "sync.execution")]
335pub(crate) struct ExecutionGasBucketSeries {
336    /// Gas-bucketed EVM execution duration.
337    pub(crate) execution_gas_bucket_histogram: Histogram,
338}
339
340/// Holds pre-initialized [`ExecutionGasBucketSeries`] instances, one per gas bucket.
341#[derive(Debug)]
342pub(crate) struct ExecutionGasBucketMetrics {
343    buckets: [ExecutionGasBucketSeries; NUM_GAS_BUCKETS],
344}
345
346impl Default for ExecutionGasBucketMetrics {
347    fn default() -> Self {
348        Self {
349            buckets: std::array::from_fn(|i| {
350                let label = GasBucketMetrics::bucket_label(i);
351                ExecutionGasBucketSeries::new_with_labels(&[("gas_bucket", label)])
352            }),
353        }
354    }
355}
356
357/// Per-gas-bucket block validation metrics (state root).
358#[derive(Clone, Metrics)]
359#[metrics(scope = "sync.block_validation")]
360pub(crate) struct BlockValidationGasBucketSeries {
361    /// Gas-bucketed state root computation duration.
362    pub(crate) state_root_gas_bucket_histogram: Histogram,
363}
364
365/// Holds pre-initialized [`BlockValidationGasBucketSeries`] instances, one per gas bucket.
366#[derive(Debug)]
367pub(crate) struct BlockValidationGasBucketMetrics {
368    buckets: [BlockValidationGasBucketSeries; NUM_GAS_BUCKETS],
369}
370
371impl Default for BlockValidationGasBucketMetrics {
372    fn default() -> Self {
373        Self {
374            buckets: std::array::from_fn(|i| {
375                let label = GasBucketMetrics::bucket_label(i);
376                BlockValidationGasBucketSeries::new_with_labels(&[("gas_bucket", label)])
377            }),
378        }
379    }
380}
381
382/// Metrics for engine newPayload responses.
383#[derive(Metrics)]
384#[metrics(scope = "consensus.engine.beacon")]
385pub(crate) struct NewPayloadStatusMetrics {
386    /// Finish time of the latest new payload call.
387    #[metric(skip)]
388    pub(crate) latest_finish_at: Option<Instant>,
389    /// Start time of the latest new payload call.
390    #[metric(skip)]
391    pub(crate) latest_start_at: Option<Instant>,
392    /// Gas-bucket-labeled latency and gas/s histograms.
393    #[metric(skip)]
394    pub(crate) gas_bucket: GasBucketMetrics,
395    /// Resource usage on the engine thread while processing new payloads.
396    #[metric(skip)]
397    thread_resource_usage: NewPayloadThreadResourceMetrics,
398    /// The total count of new payload messages received.
399    pub(crate) new_payload_messages: Counter,
400    /// The total count of new payload messages that we responded to with
401    /// [Valid](PayloadStatusEnum::Valid).
402    pub(crate) new_payload_valid: Counter,
403    /// The total count of new payload messages that we responded to with
404    /// [Invalid](PayloadStatusEnum::Invalid).
405    pub(crate) new_payload_invalid: Counter,
406    /// The total count of new payload messages that we responded to with
407    /// [Syncing](PayloadStatusEnum::Syncing).
408    pub(crate) new_payload_syncing: Counter,
409    /// The total count of new payload messages that we responded to with
410    /// [Accepted](PayloadStatusEnum::Accepted).
411    pub(crate) new_payload_accepted: Counter,
412    /// The total count of new payload messages that were unsuccessful, i.e. we responded with an
413    /// error type that is not a [`PayloadStatusEnum`].
414    pub(crate) new_payload_error: Counter,
415    /// The total gas of valid new payload messages received.
416    pub(crate) new_payload_total_gas: Histogram,
417    /// The gas used for the last valid new payload.
418    pub(crate) new_payload_total_gas_last: Gauge,
419    /// The gas per second of valid new payload messages received.
420    pub(crate) new_payload_gas_per_second: Histogram,
421    /// The gas per second for the last new payload call.
422    pub(crate) new_payload_gas_per_second_last: Gauge,
423    /// Latency for the new payload calls.
424    pub(crate) new_payload_latency: Histogram,
425    /// Latency for the last new payload call.
426    pub(crate) new_payload_last: Gauge,
427    /// Time from previous payload finish to current payload start (idle time).
428    pub(crate) time_between_new_payloads: Histogram,
429    /// Time from previous payload start to current payload start (total interval).
430    pub(crate) new_payload_interval: Histogram,
431    /// Time diff between forkchoice updated call response and the next new payload call request.
432    pub(crate) forkchoice_updated_new_payload_time_diff: Histogram,
433}
434
435impl NewPayloadStatusMetrics {
436    /// Starts measuring resource usage on the current thread.
437    pub(crate) fn measure_thread_resource_usage(&self) -> NewPayloadThreadResourceGuard {
438        self.thread_resource_usage.measure()
439    }
440
441    /// Increment the newPayload counter based on the given result
442    pub(crate) fn update_response_metrics(
443        &mut self,
444        start: Instant,
445        latest_forkchoice_updated_at: &mut Option<Instant>,
446        result: &Result<TreeOutcome<PayloadStatus>, InsertBlockFatalError>,
447        gas_used: u64,
448    ) {
449        let finish = Instant::now();
450        let elapsed = finish - start;
451
452        if let Some(prev_finish) = self.latest_finish_at {
453            self.time_between_new_payloads.record(start - prev_finish);
454        }
455        if let Some(prev_start) = self.latest_start_at {
456            self.new_payload_interval.record(start - prev_start);
457        }
458        self.latest_finish_at = Some(finish);
459        self.latest_start_at = Some(start);
460        match result {
461            Ok(outcome) => match outcome.outcome.status {
462                PayloadStatusEnum::Valid => {
463                    self.new_payload_valid.increment(1);
464                    if !outcome.already_seen {
465                        self.new_payload_total_gas.record(gas_used as f64);
466                        self.new_payload_total_gas_last.set(gas_used as f64);
467                        let gas_per_second = gas_used as f64 / elapsed.as_secs_f64();
468                        self.new_payload_gas_per_second.record(gas_per_second);
469                        self.new_payload_gas_per_second_last.set(gas_per_second);
470
471                        self.new_payload_latency.record(elapsed);
472                        self.new_payload_last.set(elapsed);
473                        self.gas_bucket.record(gas_used, elapsed);
474                    }
475                }
476                PayloadStatusEnum::Syncing => self.new_payload_syncing.increment(1),
477                PayloadStatusEnum::Accepted => self.new_payload_accepted.increment(1),
478                PayloadStatusEnum::Invalid { .. } => self.new_payload_invalid.increment(1),
479            },
480            Err(_) => self.new_payload_error.increment(1),
481        }
482        self.new_payload_messages.increment(1);
483        if let Some(latest_forkchoice_updated_at) = latest_forkchoice_updated_at.take() {
484            self.forkchoice_updated_new_payload_time_diff
485                .record(start - latest_forkchoice_updated_at);
486        }
487    }
488}
489
490/// Per-newPayload engine thread resource usage metrics.
491#[derive(Clone, Metrics)]
492#[metrics(scope = "consensus.engine.beacon")]
493struct NewPayloadThreadResourceMetrics {
494    /// User-mode CPU time used while processing a new payload.
495    new_payload_thread_user_cpu_seconds: Histogram,
496    /// Kernel-mode CPU time used while processing a new payload.
497    new_payload_thread_system_cpu_seconds: Histogram,
498    /// Minor page faults incurred while processing a new payload.
499    new_payload_thread_minor_page_faults: Histogram,
500    /// Major page faults incurred while processing a new payload.
501    new_payload_thread_major_page_faults: Histogram,
502    /// Voluntary context switches while processing a new payload.
503    new_payload_thread_voluntary_context_switches: Histogram,
504    /// Involuntary context switches while processing a new payload.
505    new_payload_thread_involuntary_context_switches: Histogram,
506    /// Block input operations while processing a new payload.
507    new_payload_thread_block_input_operations: Histogram,
508    /// Block output operations while processing a new payload.
509    new_payload_thread_block_output_operations: Histogram,
510}
511
512impl NewPayloadThreadResourceMetrics {
513    fn measure(&self) -> NewPayloadThreadResourceGuard {
514        let metrics = self.clone();
515        let start = ThreadResourceUsage::now();
516        NewPayloadThreadResourceGuard { start, metrics }
517    }
518
519    fn record(&self, usage: &ThreadResourceUsageDelta) {
520        self.new_payload_thread_user_cpu_seconds.record(usage.user_cpu_time);
521        self.new_payload_thread_system_cpu_seconds.record(usage.system_cpu_time);
522        self.new_payload_thread_minor_page_faults.record(usage.minor_page_faults as f64);
523        self.new_payload_thread_major_page_faults.record(usage.major_page_faults as f64);
524        self.new_payload_thread_voluntary_context_switches
525            .record(usage.voluntary_context_switches as f64);
526        self.new_payload_thread_involuntary_context_switches
527            .record(usage.involuntary_context_switches as f64);
528        self.new_payload_thread_block_input_operations.record(usage.block_input_operations as f64);
529        self.new_payload_thread_block_output_operations
530            .record(usage.block_output_operations as f64);
531    }
532}
533
534/// Records engine thread resource usage when dropped.
535pub(crate) struct NewPayloadThreadResourceGuard {
536    start: ThreadResourceUsage,
537    metrics: NewPayloadThreadResourceMetrics,
538}
539
540impl Drop for NewPayloadThreadResourceGuard {
541    fn drop(&mut self) {
542        if let Some(usage) = self.start.elapsed() {
543            self.metrics.record(&usage);
544        }
545    }
546}
547
548/// Metrics for EIP-7928 Block-Level Access Lists (BAL).
549///
550/// See also <https://github.com/ethereum/execution-metrics/issues/5>
551#[allow(dead_code)]
552#[derive(Metrics, Clone)]
553#[metrics(scope = "execution.block_access_list")]
554pub(crate) struct BalMetrics {
555    /// Size of the BAL in bytes for the current block.
556    pub(crate) size_bytes: Gauge,
557    /// Total number of blocks with valid BALs.
558    pub(crate) valid_total: Counter,
559    /// Total number of blocks with invalid BALs.
560    pub(crate) invalid_total: Counter,
561    /// Time taken to validate the BAL against actual execution.
562    pub(crate) validation_time_seconds: Histogram,
563    /// Number of account changes in the BAL.
564    pub(crate) account_changes: Gauge,
565    /// Number of storage changes in the BAL.
566    pub(crate) storage_changes: Gauge,
567    /// Number of balance changes in the BAL.
568    pub(crate) balance_changes: Gauge,
569    /// Number of nonce changes in the BAL.
570    pub(crate) nonce_changes: Gauge,
571    /// Number of code changes in the BAL.
572    pub(crate) code_changes: Gauge,
573}
574
575/// Metrics for non-execution related block validation.
576#[derive(Metrics, Clone)]
577#[metrics(scope = "sync.block_validation")]
578pub struct BlockValidationMetrics {
579    /// Total number of storage tries updated in the state root calculation
580    pub state_root_storage_tries_updated_total: Counter,
581    /// Total number of times the state root task failed but the fallback succeeded.
582    pub state_root_task_fallback_success_total: Counter,
583    /// Total number of times the state root task timed out and a sequential fallback was spawned.
584    pub state_root_task_timeout_total: Counter,
585    /// Latest state root duration, ie the time spent blocked waiting for the state root.
586    pub state_root_duration: Gauge,
587    /// Histogram for state root duration ie the time spent blocked waiting for the state root
588    pub state_root_histogram: Histogram,
589    /// Histogram of deferred trie computation duration.
590    pub deferred_trie_compute_duration: Histogram,
591    /// Payload conversion and validation latency
592    pub payload_validation_duration: Gauge,
593    /// Histogram of payload validation latency
594    pub payload_validation_histogram: Histogram,
595    /// Payload processor spawning duration
596    pub spawn_payload_processor: Histogram,
597    /// Post-execution validation duration
598    pub post_execution_validation_duration: Histogram,
599    /// Total duration of the new payload call
600    pub total_duration: Histogram,
601    /// Size of `HashedPostStateSorted` (`total_len`)
602    pub hashed_post_state_size: Histogram,
603    /// Size of `TrieUpdatesSorted` (`total_len`)
604    pub trie_updates_sorted_size: Histogram,
605}
606
607impl BlockValidationMetrics {
608    /// Records a new state root time, updating both the histogram and state root gauge
609    pub fn record_state_root(&self, trie_output: &TrieUpdates, elapsed_as_secs: f64) {
610        self.state_root_storage_tries_updated_total
611            .increment(trie_output.storage_tries_ref().len() as u64);
612        self.state_root_duration.set(elapsed_as_secs);
613        self.state_root_histogram.record(elapsed_as_secs);
614    }
615
616    /// Records a new payload validation time, updating both the histogram and the payload
617    /// validation gauge
618    pub fn record_payload_validation(&self, elapsed_as_secs: f64) {
619        self.payload_validation_duration.set(elapsed_as_secs);
620        self.payload_validation_histogram.record(elapsed_as_secs);
621    }
622}
623
624/// Metrics for the blockchain tree block buffer
625#[derive(Metrics)]
626#[metrics(scope = "blockchain_tree.block_buffer")]
627pub(crate) struct BlockBufferMetrics {
628    /// Total blocks in the block buffer
629    pub blocks: Gauge,
630}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635    use alloy_eips::eip7685::Requests;
636    use metrics_util::debugging::{DebuggingRecorder, Snapshotter};
637    use reth_ethereum_primitives::Receipt;
638    use reth_execution_types::BlockExecutionResult;
639    use reth_revm::db::BundleState;
640
641    fn setup_test_recorder() -> Snapshotter {
642        let recorder = DebuggingRecorder::new();
643        let snapshotter = recorder.snapshotter();
644        recorder.install().unwrap();
645        snapshotter
646    }
647
648    #[test]
649    fn test_record_block_execution_metrics() {
650        let snapshotter = setup_test_recorder();
651        let metrics = EngineApiMetrics::default();
652
653        // Pre-populate some metrics to ensure they exist
654        metrics.executor.gas_processed_total.increment(0);
655        metrics.executor.gas_per_second.set(0.0);
656        metrics.executor.gas_used_histogram.record(0.0);
657
658        let output = BlockExecutionOutput::<Receipt> {
659            state: BundleState::default(),
660            result: BlockExecutionResult {
661                receipts: vec![],
662                requests: Requests::default(),
663                gas_used: 21000,
664                blob_gas_used: 0,
665            },
666        };
667
668        metrics.record_block_execution(&output, Duration::from_millis(100));
669        metrics.engine.new_payload.thread_resource_usage.record(&ThreadResourceUsageDelta {
670            user_cpu_time: Duration::from_millis(1),
671            system_cpu_time: Duration::from_millis(2),
672            minor_page_faults: 3,
673            major_page_faults: 4,
674            voluntary_context_switches: 5,
675            involuntary_context_switches: 6,
676            block_input_operations: 7,
677            block_output_operations: 8,
678        });
679
680        let snapshot = snapshotter.snapshot().into_vec();
681
682        // Verify that metrics were registered
683        let mut found_execution_metrics = false;
684        let mut found_thread_resource_metrics = false;
685        for (key, _unit, _desc, _value) in snapshot {
686            let metric_name = key.key().name();
687            if metric_name.starts_with("sync.execution") {
688                found_execution_metrics = true;
689            }
690            if metric_name == "consensus.engine.beacon.new_payload_thread_major_page_faults" {
691                found_thread_resource_metrics = true;
692            }
693        }
694
695        assert!(found_execution_metrics, "Expected to find sync.execution metrics");
696        assert!(found_thread_resource_metrics, "Expected to find thread resource metrics");
697    }
698}