Skip to main content

reth_trie/hashed_cursor/
metrics.rs

1use super::{HashedCursor, HashedCursorFactory, HashedStorageCursor};
2use crate::TrieType;
3use alloy_primitives::B256;
4use reth_metrics::metrics::{self, Histogram};
5use reth_primitives_traits::FastInstant as Instant;
6use reth_storage_errors::db::DatabaseError;
7use std::time::Duration;
8
9/// Prometheus metrics for hashed cursor operations.
10///
11/// Tracks the number of cursor operations for monitoring and performance analysis.
12#[derive(Clone, Debug)]
13pub struct HashedCursorMetrics {
14    /// Histogram tracking overall time spent in database operations, in seconds.
15    overall_duration_seconds: Histogram,
16    /// Histogram for `next()` operations
17    next_histogram: Histogram,
18    /// Histogram for `seek()` operations
19    seek_histogram: Histogram,
20    /// Histogram for `is_storage_empty()` operations
21    is_storage_empty_histogram: Histogram,
22}
23
24impl HashedCursorMetrics {
25    /// Create a new metrics instance with the specified trie type label.
26    pub fn new(trie_type: TrieType) -> Self {
27        let trie_type_str = trie_type.as_str();
28
29        Self {
30            overall_duration_seconds: metrics::histogram!(
31                "trie.hashed_cursor.overall_duration_seconds",
32                "type" => trie_type_str
33            ),
34            next_histogram: metrics::histogram!(
35                "trie.hashed_cursor.operations",
36                "type" => trie_type_str,
37                "operation" => "next"
38            ),
39            seek_histogram: metrics::histogram!(
40                "trie.hashed_cursor.operations",
41                "type" => trie_type_str,
42                "operation" => "seek"
43            ),
44            is_storage_empty_histogram: metrics::histogram!(
45                "trie.hashed_cursor.operations",
46                "type" => trie_type_str,
47                "operation" => "is_storage_empty"
48            ),
49        }
50    }
51
52    /// Wraps a cursor with owned counters that are recorded when the cursor is dropped.
53    pub fn instrument<C>(&self, cursor: C) -> InstrumentedHashedCursor<C> {
54        InstrumentedHashedCursor {
55            cursor,
56            metrics: self.clone(),
57            next_count: 0,
58            seek_count: 0,
59            is_storage_empty_count: 0,
60            total_duration: Duration::ZERO,
61        }
62    }
63}
64
65/// A wrapper around a [`HashedCursor`] that tracks metrics for cursor operations.
66///
67/// Counters are recorded into the supplied metric handles when the cursor is dropped.
68///
69/// This implementation counts the number of times each cursor operation is called:
70/// - `next()` - Move to the next entry
71/// - `seek()` - Seek to a key or the next greater key
72#[derive(Debug)]
73pub struct InstrumentedHashedCursor<C> {
74    /// The underlying cursor being wrapped.
75    cursor: C,
76    /// Metric handles used to record counters when the cursor is dropped.
77    metrics: HashedCursorMetrics,
78    /// Counter for `next()` calls.
79    next_count: usize,
80    /// Counter for `seek()` calls.
81    seek_count: usize,
82    /// Counter for `is_storage_empty` calls.
83    is_storage_empty_count: usize,
84    /// Total duration spent in cursor operations.
85    total_duration: Duration,
86}
87
88impl<C> HashedCursor for InstrumentedHashedCursor<C>
89where
90    C: HashedCursor,
91{
92    type Value = C::Value;
93
94    fn seek(&mut self, key: B256) -> Result<Option<(B256, Self::Value)>, DatabaseError> {
95        let start = Instant::now();
96        self.seek_count += 1;
97        let result = self.cursor.seek(key);
98        self.total_duration += start.elapsed();
99        result
100    }
101
102    fn next(&mut self) -> Result<Option<(B256, Self::Value)>, DatabaseError> {
103        let start = Instant::now();
104        self.next_count += 1;
105        let result = self.cursor.next();
106        self.total_duration += start.elapsed();
107        result
108    }
109
110    fn reset(&mut self) {
111        self.cursor.reset()
112    }
113}
114
115impl<C> HashedStorageCursor for InstrumentedHashedCursor<C>
116where
117    C: HashedStorageCursor,
118{
119    fn is_storage_empty(&mut self) -> Result<bool, DatabaseError> {
120        let start = Instant::now();
121        self.is_storage_empty_count += 1;
122        let result = self.cursor.is_storage_empty();
123        self.total_duration += start.elapsed();
124        result
125    }
126
127    fn set_hashed_address(&mut self, hashed_address: B256) {
128        self.cursor.set_hashed_address(hashed_address)
129    }
130}
131
132impl<C> Drop for InstrumentedHashedCursor<C> {
133    fn drop(&mut self) {
134        self.metrics.next_histogram.record(self.next_count as f64);
135        self.metrics.seek_histogram.record(self.seek_count as f64);
136        self.metrics.is_storage_empty_histogram.record(self.is_storage_empty_count as f64);
137        self.metrics.overall_duration_seconds.record(self.total_duration.as_secs_f64());
138    }
139}
140
141/// Instruments every account and storage cursor opened by the wrapped factory.
142///
143/// Counters are owned by each cursor and recorded into the supplied handles on drop. Wrap the
144/// outermost overlay factory to include both overlay and database operations in these counters.
145#[derive(Clone, Debug)]
146pub struct InstrumentedHashedCursorFactory<F> {
147    factory: F,
148    account_metrics: HashedCursorMetrics,
149    storage_metrics: HashedCursorMetrics,
150}
151
152impl<F> InstrumentedHashedCursorFactory<F> {
153    /// Wraps a factory with account and storage metric handles shared by its cursors.
154    pub const fn new(
155        factory: F,
156        account_metrics: HashedCursorMetrics,
157        storage_metrics: HashedCursorMetrics,
158    ) -> Self {
159        Self { factory, account_metrics, storage_metrics }
160    }
161}
162
163impl<F: HashedCursorFactory> HashedCursorFactory for InstrumentedHashedCursorFactory<F> {
164    type AccountCursor<'a>
165        = InstrumentedHashedCursor<F::AccountCursor<'a>>
166    where
167        Self: 'a;
168    type StorageCursor<'a>
169        = InstrumentedHashedCursor<F::StorageCursor<'a>>
170    where
171        Self: 'a;
172
173    fn hashed_account_cursor(&self) -> Result<Self::AccountCursor<'_>, DatabaseError> {
174        Ok(self.account_metrics.instrument(self.factory.hashed_account_cursor()?))
175    }
176
177    fn hashed_storage_cursor(
178        &self,
179        hashed_address: B256,
180    ) -> Result<Self::StorageCursor<'_>, DatabaseError> {
181        Ok(self.storage_metrics.instrument(self.factory.hashed_storage_cursor(hashed_address)?))
182    }
183}