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
16const GAS_BUCKET_THRESHOLDS: [u64; 5] =
19 [5 * MEGAGAS, 10 * MEGAGAS, 20 * MEGAGAS, 30 * MEGAGAS, 40 * MEGAGAS];
20
21const NUM_GAS_BUCKETS: usize = GAS_BUCKET_THRESHOLDS.len() + 1;
23
24#[derive(Debug, Default)]
26pub struct EngineApiMetrics {
27 pub engine: EngineMetrics,
29 pub executor: ExecutorMetrics,
31 pub block_validation: BlockValidationMetrics,
33 pub tree: TreeMetrics,
35 #[allow(dead_code)]
37 pub(crate) bal: BalMetrics,
38 pub(crate) execution_gas_buckets: ExecutionGasBucketMetrics,
40 pub(crate) block_validation_gas_buckets: BlockValidationGasBucketMetrics,
42 pub(crate) prewarm_reuse: PrewarmReuseMetrics,
44}
45
46impl EngineApiMetrics {
47 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 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 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 pub const fn executor_metrics(&self) -> &ExecutorMetrics {
79 &self.executor
80 }
81
82 pub fn record_pre_execution(&self, elapsed: Duration) {
84 self.executor.pre_execution_histogram.record(elapsed);
85 }
86
87 pub fn record_post_execution(&self, elapsed: Duration) {
89 self.executor.post_execution_histogram.record(elapsed);
90 }
91
92 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 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 pub fn record_transaction_wait(&self, elapsed: Duration) {
110 self.executor.transaction_wait_histogram.record(elapsed);
111 }
112
113 pub fn record_transaction_execution(&self, elapsed: Duration) {
115 self.executor.transaction_execution_histogram.record(elapsed);
116 }
117}
118
119#[derive(Metrics)]
121#[metrics(scope = "sync.prewarm.reuse")]
122pub(crate) struct PrewarmReuseMetrics {
123 reused_transactions: Counter,
125 missing_transactions: Counter,
127 stale_transactions: Counter,
129}
130
131impl PrewarmReuseMetrics {
132 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#[derive(Debug, Default, Clone, Copy)]
142pub(crate) struct PrewarmReuseStats {
143 reused: u64,
144 missing: u64,
145 stale: u64,
146}
147
148impl PrewarmReuseStats {
149 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#[derive(Metrics)]
161#[metrics(scope = "blockchain_tree")]
162pub struct TreeMetrics {
163 pub canonical_chain_height: Gauge,
165 #[metric(skip)]
167 pub reorgs: ReorgMetrics,
168 pub latest_reorg_depth: Gauge,
170 pub safe_block_height: Gauge,
172 pub finalized_block_height: Gauge,
174}
175
176#[derive(Debug)]
178pub struct ReorgMetrics {
179 pub head: Counter,
181 pub safe: Counter,
183 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#[derive(Metrics)]
199#[metrics(scope = "consensus.engine.beacon")]
200pub struct EngineMetrics {
201 #[metric(skip)]
203 pub(crate) forkchoice_updated: ForkchoiceUpdatedMetrics,
204 #[metric(skip)]
206 pub(crate) new_payload: NewPayloadStatusMetrics,
207 pub(crate) executed_blocks: Gauge,
209 pub(crate) inserted_already_executed_blocks: Counter,
211 pub(crate) pipeline_runs: Counter,
213 pub(crate) executed_new_block_cache_miss: Counter,
215 pub(crate) persistence_duration: Histogram,
217 pub(crate) backpressure_active: Gauge,
219 pub(crate) backpressure_stall_duration: Histogram,
221 pub(crate) failed_new_payload_response_deliveries: Counter,
227 pub(crate) failed_forkchoice_updated_response_deliveries: Counter,
229 pub(crate) block_insert_total_duration: Histogram,
231}
232
233#[derive(Metrics)]
235#[metrics(scope = "consensus.engine.beacon")]
236pub(crate) struct ForkchoiceUpdatedMetrics {
237 #[metric(skip)]
239 pub(crate) latest_finish_at: Option<Instant>,
240 #[metric(skip)]
242 pub(crate) latest_start_at: Option<Instant>,
243 pub(crate) forkchoice_updated_messages: Counter,
245 pub(crate) forkchoice_with_attributes_updated_messages: Counter,
247 pub(crate) forkchoice_updated_valid: Counter,
250 pub(crate) forkchoice_updated_invalid: Counter,
253 pub(crate) forkchoice_updated_syncing: Counter,
256 pub(crate) forkchoice_updated_error: Counter,
259 pub(crate) forkchoice_updated_latency: Histogram,
261 pub(crate) forkchoice_updated_last: Gauge,
263 pub(crate) new_payload_forkchoice_updated_time_diff: Histogram,
265 pub(crate) time_between_forkchoice_updated: Histogram,
268 pub(crate) forkchoice_updated_interval: Histogram,
271}
272
273impl ForkchoiceUpdatedMetrics {
274 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#[derive(Clone, Metrics)]
316#[metrics(scope = "consensus.engine.beacon")]
317pub(crate) struct NewPayloadGasBucketMetrics {
318 pub(crate) new_payload_gas_bucket_latency: Histogram,
320 pub(crate) new_payload_gas_bucket_gas_per_second: Histogram,
322}
323
324#[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 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 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#[derive(Clone, Metrics)]
376#[metrics(scope = "sync.execution")]
377pub(crate) struct ExecutionGasBucketSeries {
378 pub(crate) execution_gas_bucket_histogram: Histogram,
380}
381
382#[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#[derive(Clone, Metrics)]
401#[metrics(scope = "sync.block_validation")]
402pub(crate) struct BlockValidationGasBucketSeries {
403 pub(crate) state_root_gas_bucket_histogram: Histogram,
405}
406
407#[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#[derive(Metrics)]
426#[metrics(scope = "consensus.engine.beacon")]
427pub(crate) struct NewPayloadStatusMetrics {
428 #[metric(skip)]
430 pub(crate) latest_finish_at: Option<Instant>,
431 #[metric(skip)]
433 pub(crate) latest_start_at: Option<Instant>,
434 #[metric(skip)]
436 pub(crate) gas_bucket: GasBucketMetrics,
437 #[metric(skip)]
439 thread_resource_usage: NewPayloadThreadResourceMetrics,
440 pub(crate) new_payload_messages: Counter,
442 pub(crate) new_payload_valid: Counter,
445 pub(crate) new_payload_invalid: Counter,
448 pub(crate) new_payload_syncing: Counter,
451 pub(crate) new_payload_accepted: Counter,
454 pub(crate) new_payload_error: Counter,
457 pub(crate) new_payload_total_gas: Histogram,
459 pub(crate) new_payload_total_gas_last: Gauge,
461 pub(crate) new_payload_gas_per_second: Histogram,
463 pub(crate) new_payload_gas_per_second_last: Gauge,
465 pub(crate) new_payload_latency: Histogram,
467 pub(crate) new_payload_last: Gauge,
469 pub(crate) time_between_new_payloads: Histogram,
471 pub(crate) new_payload_interval: Histogram,
473 pub(crate) forkchoice_updated_new_payload_time_diff: Histogram,
475}
476
477impl NewPayloadStatusMetrics {
478 pub(crate) fn measure_thread_resource_usage(&self) -> NewPayloadThreadResourceGuard {
480 self.thread_resource_usage.measure()
481 }
482
483 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#[derive(Clone, Metrics)]
534#[metrics(scope = "consensus.engine.beacon")]
535struct NewPayloadThreadResourceMetrics {
536 new_payload_thread_user_cpu_seconds: Histogram,
538 new_payload_thread_system_cpu_seconds: Histogram,
540 new_payload_thread_minor_page_faults: Histogram,
542 new_payload_thread_major_page_faults: Histogram,
544 new_payload_thread_voluntary_context_switches: Histogram,
546 new_payload_thread_involuntary_context_switches: Histogram,
548 new_payload_thread_block_input_operations: Histogram,
550 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
576pub(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#[allow(dead_code)]
594#[derive(Metrics, Clone)]
595#[metrics(scope = "execution.block_access_list")]
596pub(crate) struct BalMetrics {
597 pub(crate) size_bytes: Gauge,
599 pub(crate) valid_total: Counter,
601 pub(crate) invalid_total: Counter,
603 pub(crate) validation_time_seconds: Histogram,
605 pub(crate) account_changes: Gauge,
607 pub(crate) storage_changes: Gauge,
609 pub(crate) balance_changes: Gauge,
611 pub(crate) nonce_changes: Gauge,
613 pub(crate) code_changes: Gauge,
615}
616
617#[derive(Metrics, Clone)]
619#[metrics(scope = "sync.block_validation")]
620pub struct BlockValidationMetrics {
621 pub state_root_storage_tries_updated_total: Counter,
623 pub state_root_task_fallback_success_total: Counter,
625 pub state_root_task_timeout_total: Counter,
627 pub state_root_duration: Gauge,
629 pub state_root_histogram: Histogram,
631 pub deferred_trie_compute_duration: Histogram,
633 pub payload_validation_duration: Gauge,
635 pub payload_validation_histogram: Histogram,
637 pub spawn_payload_processor: Histogram,
639 pub post_execution_validation_duration: Histogram,
641 pub total_duration: Histogram,
643 pub hashed_post_state_size: Histogram,
645 pub trie_updates_sorted_size: Histogram,
647}
648
649impl BlockValidationMetrics {
650 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 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#[derive(Metrics)]
668#[metrics(scope = "blockchain_tree.block_buffer")]
669pub(crate) struct BlockBufferMetrics {
670 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 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 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}