Skip to main content

reth_metrics/
thread.rs

1//! Per-thread resource usage measurement.
2
3use std::{marker::PhantomData, rc::Rc, time::Duration};
4
5/// A snapshot used to measure resource usage on the current thread.
6///
7/// This type is thread-bound because [`Self::elapsed`] must sample the same thread as
8/// [`Self::now`].
9#[derive(Debug)]
10#[must_use = "call elapsed() to obtain the resource usage delta"]
11pub struct ThreadResourceUsage {
12    start: Option<ThreadResourceUsageSnapshot>,
13    _not_send: PhantomData<Rc<()>>,
14}
15
16impl ThreadResourceUsage {
17    /// Captures the current thread's resource usage.
18    pub fn now() -> Self {
19        Self { start: platform::current_thread_resource_usage(), _not_send: PhantomData }
20    }
21
22    /// Returns the resource usage accumulated by the current thread since this snapshot.
23    ///
24    /// Returns `None` when per-thread resource usage is unsupported or could not be sampled.
25    pub fn elapsed(&self) -> Option<ThreadResourceUsageDelta> {
26        let start = self.start?;
27        Some(platform::current_thread_resource_usage()?.saturating_delta(start))
28    }
29}
30
31/// Resource usage accumulated by a thread over a measured interval.
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
33pub struct ThreadResourceUsageDelta {
34    /// Time spent executing in user mode.
35    pub user_cpu_time: Duration,
36    /// Time spent executing in kernel mode.
37    pub system_cpu_time: Duration,
38    /// Page faults serviced without I/O.
39    pub minor_page_faults: u64,
40    /// Page faults that required I/O.
41    pub major_page_faults: u64,
42    /// Voluntary context switches.
43    pub voluntary_context_switches: u64,
44    /// Involuntary context switches.
45    pub involuntary_context_switches: u64,
46    /// Block input operations.
47    pub block_input_operations: u64,
48    /// Block output operations.
49    pub block_output_operations: u64,
50}
51
52#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
53struct ThreadResourceUsageSnapshot {
54    user_cpu_time: Duration,
55    system_cpu_time: Duration,
56    minor_page_faults: u64,
57    major_page_faults: u64,
58    voluntary_context_switches: u64,
59    involuntary_context_switches: u64,
60    block_input_operations: u64,
61    block_output_operations: u64,
62}
63
64impl ThreadResourceUsageSnapshot {
65    const fn saturating_delta(self, earlier: Self) -> ThreadResourceUsageDelta {
66        ThreadResourceUsageDelta {
67            user_cpu_time: self.user_cpu_time.saturating_sub(earlier.user_cpu_time),
68            system_cpu_time: self.system_cpu_time.saturating_sub(earlier.system_cpu_time),
69            minor_page_faults: self.minor_page_faults.saturating_sub(earlier.minor_page_faults),
70            major_page_faults: self.major_page_faults.saturating_sub(earlier.major_page_faults),
71            voluntary_context_switches: self
72                .voluntary_context_switches
73                .saturating_sub(earlier.voluntary_context_switches),
74            involuntary_context_switches: self
75                .involuntary_context_switches
76                .saturating_sub(earlier.involuntary_context_switches),
77            block_input_operations: self
78                .block_input_operations
79                .saturating_sub(earlier.block_input_operations),
80            block_output_operations: self
81                .block_output_operations
82                .saturating_sub(earlier.block_output_operations),
83        }
84    }
85}
86
87#[cfg(target_os = "linux")]
88mod platform {
89    use super::ThreadResourceUsageSnapshot;
90    use std::{mem::MaybeUninit, time::Duration};
91
92    pub(super) fn current_thread_resource_usage() -> Option<ThreadResourceUsageSnapshot> {
93        let mut usage = MaybeUninit::<libc::rusage>::uninit();
94        // SAFETY: `usage` is a valid writable pointer and is only initialized when getrusage
95        // succeeds.
96        if unsafe { libc::getrusage(libc::RUSAGE_THREAD, usage.as_mut_ptr()) } != 0 {
97            return None
98        }
99        // SAFETY: getrusage returned success and initialized the structure.
100        let usage = unsafe { usage.assume_init() };
101
102        Some(ThreadResourceUsageSnapshot {
103            user_cpu_time: timeval_to_duration(usage.ru_utime)?,
104            system_cpu_time: timeval_to_duration(usage.ru_stime)?,
105            minor_page_faults: usage.ru_minflt.try_into().ok()?,
106            major_page_faults: usage.ru_majflt.try_into().ok()?,
107            voluntary_context_switches: usage.ru_nvcsw.try_into().ok()?,
108            involuntary_context_switches: usage.ru_nivcsw.try_into().ok()?,
109            block_input_operations: usage.ru_inblock.try_into().ok()?,
110            block_output_operations: usage.ru_oublock.try_into().ok()?,
111        })
112    }
113
114    fn timeval_to_duration(timeval: libc::timeval) -> Option<Duration> {
115        let seconds = timeval.tv_sec.try_into().ok()?;
116        let microseconds: u32 = timeval.tv_usec.try_into().ok()?;
117        if microseconds >= 1_000_000 {
118            return None
119        }
120        Some(Duration::new(seconds, microseconds * 1_000))
121    }
122
123    #[cfg(test)]
124    mod tests {
125        use super::*;
126        use crate::thread::ThreadResourceUsage;
127
128        #[test]
129        fn samples_current_thread() {
130            assert!(ThreadResourceUsage::now().elapsed().is_some());
131        }
132
133        #[test]
134        fn measures_forced_minor_page_fault() {
135            let mapping_len = 4096;
136            // SAFETY: The mapping is private and anonymous, has a non-zero length, and the returned
137            // address is checked before use.
138            let mapping = unsafe {
139                libc::mmap(
140                    std::ptr::null_mut(),
141                    mapping_len,
142                    libc::PROT_READ | libc::PROT_WRITE,
143                    libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
144                    -1,
145                    0,
146                )
147            };
148            assert_ne!(mapping, libc::MAP_FAILED);
149
150            let usage = ThreadResourceUsage::now();
151            // SAFETY: `mapping` points to a valid writable mapping. Its first write requires Linux
152            // to populate the anonymous page, which incurs a minor page fault.
153            unsafe { mapping.cast::<u8>().write_volatile(1) };
154            let elapsed = usage.elapsed();
155
156            // SAFETY: `mapping` and `mapping_len` identify the live mapping created above.
157            assert_eq!(unsafe { libc::munmap(mapping, mapping_len) }, 0);
158            let elapsed = elapsed.expect("per-thread resource usage is supported on Linux");
159            assert!(
160                elapsed.minor_page_faults >= 1,
161                "expected a minor page fault after touching a new anonymous mapping: {elapsed:?}"
162            );
163        }
164
165        #[test]
166        fn converts_timeval_to_duration() {
167            let timeval = libc::timeval { tv_sec: 2, tv_usec: 345_678 };
168            assert_eq!(timeval_to_duration(timeval), Some(Duration::new(2, 345_678_000)));
169        }
170    }
171}
172
173#[cfg(not(target_os = "linux"))]
174mod platform {
175    use super::ThreadResourceUsageSnapshot;
176
177    #[allow(clippy::missing_const_for_fn)]
178    pub(super) fn current_thread_resource_usage() -> Option<ThreadResourceUsageSnapshot> {
179        None
180    }
181
182    #[cfg(test)]
183    mod tests {
184        use crate::thread::ThreadResourceUsage;
185
186        #[test]
187        fn unsupported_platform_returns_none() {
188            assert!(ThreadResourceUsage::now().elapsed().is_none());
189        }
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn computes_resource_usage_delta() {
199        let earlier = ThreadResourceUsageSnapshot {
200            user_cpu_time: Duration::from_micros(10),
201            system_cpu_time: Duration::from_micros(20),
202            minor_page_faults: 30,
203            major_page_faults: 40,
204            voluntary_context_switches: 50,
205            involuntary_context_switches: 60,
206            block_input_operations: 70,
207            block_output_operations: 80,
208        };
209        let current = ThreadResourceUsageSnapshot {
210            user_cpu_time: Duration::from_micros(11),
211            system_cpu_time: Duration::from_micros(22),
212            minor_page_faults: 33,
213            major_page_faults: 44,
214            voluntary_context_switches: 55,
215            involuntary_context_switches: 66,
216            block_input_operations: 77,
217            block_output_operations: 88,
218        };
219
220        assert_eq!(
221            current.saturating_delta(earlier),
222            ThreadResourceUsageDelta {
223                user_cpu_time: Duration::from_micros(1),
224                system_cpu_time: Duration::from_micros(2),
225                minor_page_faults: 3,
226                major_page_faults: 4,
227                voluntary_context_switches: 5,
228                involuntary_context_switches: 6,
229                block_input_operations: 7,
230                block_output_operations: 8,
231            }
232        );
233    }
234
235    #[test]
236    fn resource_usage_delta_saturates() {
237        let earlier = ThreadResourceUsageSnapshot {
238            user_cpu_time: Duration::from_secs(1),
239            system_cpu_time: Duration::from_secs(1),
240            minor_page_faults: 1,
241            major_page_faults: 1,
242            voluntary_context_switches: 1,
243            involuntary_context_switches: 1,
244            block_input_operations: 1,
245            block_output_operations: 1,
246        };
247
248        assert_eq!(
249            ThreadResourceUsageSnapshot::default().saturating_delta(earlier),
250            ThreadResourceUsageDelta::default()
251        );
252    }
253}