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