From 2c5d00ffb5b7f3ed8f89382c4bbea79c173da621 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 11 Feb 2026 14:00:07 -0500 Subject: [PATCH] feat(engine): add gas bucket label to newPayload metrics (#22067) Co-authored-by: Amp --- crates/engine/tree/src/tree/metrics.rs | 69 ++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/crates/engine/tree/src/tree/metrics.rs b/crates/engine/tree/src/tree/metrics.rs index 387418d910..cd2a8bce5c 100644 --- a/crates/engine/tree/src/tree/metrics.rs +++ b/crates/engine/tree/src/tree/metrics.rs @@ -8,9 +8,17 @@ use reth_metrics::{ metrics::{Counter, Gauge, Histogram}, Metrics, }; +use reth_primitives_traits::constants::gas_units::MEGAGAS; use reth_trie::updates::TrieUpdates; use std::time::{Duration, Instant}; +/// Width of each gas bucket in gas units (10 Mgas). +const GAS_BUCKET_SIZE: u64 = 10 * MEGAGAS; + +/// Number of gas buckets. The last bucket is a catch-all for everything above +/// `(NUM_GAS_BUCKETS - 1) * GAS_BUCKET_SIZE`. +const NUM_GAS_BUCKETS: usize = 5; + /// Metrics for the `EngineApi`. #[derive(Debug, Default)] pub struct EngineApiMetrics { @@ -235,6 +243,63 @@ impl ForkchoiceUpdatedMetrics { } } +/// Per-gas-bucket newPayload metrics, initialized once via [`Self::new_with_labels`]. +#[derive(Clone, Metrics)] +#[metrics(scope = "consensus.engine.beacon")] +pub(crate) struct NewPayloadGasBucketMetrics { + /// Latency for new payload calls in this gas bucket. + pub(crate) new_payload_gas_bucket_latency: Histogram, + /// Gas per second for new payload calls in this gas bucket. + pub(crate) new_payload_gas_bucket_gas_per_second: Histogram, +} + +/// Holds pre-initialized [`NewPayloadGasBucketMetrics`] instances, one per gas bucket. +#[derive(Debug)] +pub(crate) struct GasBucketMetrics { + buckets: [NewPayloadGasBucketMetrics; NUM_GAS_BUCKETS], +} + +impl Default for GasBucketMetrics { + fn default() -> Self { + Self { + buckets: std::array::from_fn(|i| { + let label = Self::bucket_label(i); + NewPayloadGasBucketMetrics::new_with_labels(&[("gas_bucket", label)]) + }), + } + } +} + +impl GasBucketMetrics { + fn record(&self, gas_used: u64, elapsed: Duration) { + let idx = Self::bucket_index(gas_used); + self.buckets[idx].new_payload_gas_bucket_latency.record(elapsed); + self.buckets[idx] + .new_payload_gas_bucket_gas_per_second + .record(gas_used as f64 / elapsed.as_secs_f64()); + } + + fn bucket_index(gas_used: u64) -> usize { + let idx = gas_used / GAS_BUCKET_SIZE; + (idx as usize).min(NUM_GAS_BUCKETS - 1) + } + + /// Returns a human-readable label like `<10M`, `10-20M`, … `>40M`. + fn bucket_label(index: usize) -> String { + let m = GAS_BUCKET_SIZE / 1_000_000; + if index == 0 { + format!("<{m}M") + } else if index < NUM_GAS_BUCKETS - 1 { + let lo = m * index as u64; + let hi = lo + m; + format!("{lo}-{hi}M") + } else { + let lo = m * index as u64; + format!(">{lo}M") + } + } +} + /// Metrics for engine newPayload responses. #[derive(Metrics)] #[metrics(scope = "consensus.engine.beacon")] @@ -245,6 +310,9 @@ pub(crate) struct NewPayloadStatusMetrics { /// Start time of the latest new payload call. #[metric(skip)] pub(crate) latest_start_at: Option, + /// Gas-bucket-labeled latency and gas/s histograms. + #[metric(skip)] + pub(crate) gas_bucket: GasBucketMetrics, /// The total count of new payload messages received. pub(crate) new_payload_messages: Counter, /// The total count of new payload messages that we responded to with @@ -321,6 +389,7 @@ impl NewPayloadStatusMetrics { self.new_payload_messages.increment(1); self.new_payload_latency.record(elapsed); self.new_payload_last.set(elapsed); + self.gas_bucket.record(gas_used, elapsed); if let Some(latest_forkchoice_updated_at) = latest_forkchoice_updated_at.take() { self.forkchoice_updated_new_payload_time_diff .record(start - latest_forkchoice_updated_at);