Skip to main content

reth_engine_tree/tree/
metrics.rs

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