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 measures_forced_minor_page_fault() {
130            let mapping_len = 4096;
131            // SAFETY: The mapping is private and anonymous, has a non-zero length, and the returned
132            // address is checked before use.
133            let mapping = unsafe {
134                libc::mmap(
135                    std::ptr::null_mut(),
136                    mapping_len,
137                    libc::PROT_READ | libc::PROT_WRITE,
138                    libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
139                    -1,
140                    0,
141                )
142            };
143            assert_ne!(mapping, libc::MAP_FAILED);
144
145            let usage = ThreadResourceUsage::now();
146            // SAFETY: `mapping` points to a valid writable mapping. Its first write requires Linux
147            // to populate the anonymous page, which incurs a minor page fault.
148            unsafe { mapping.cast::<u8>().write_volatile(1) };
149            let elapsed = usage.elapsed();
150
151            // SAFETY: `mapping` and `mapping_len` identify the live mapping created above.
152            assert_eq!(unsafe { libc::munmap(mapping, mapping_len) }, 0);
153            let elapsed = elapsed.expect("per-thread resource usage is supported on Linux");
154            assert!(
155                elapsed.minor_page_faults >= 1,
156                "expected a minor page fault after touching a new anonymous mapping: {elapsed:?}"
157            );
158        }
159
160        #[test]
161        fn converts_timeval_to_duration() {
162            let timeval = libc::timeval { tv_sec: 2, tv_usec: 345_678 };
163            assert_eq!(timeval_to_duration(timeval), Some(Duration::new(2, 345_678_000)));
164        }
165    }
166}
167
168#[cfg(not(target_os = "linux"))]
169mod platform {
170    use super::ThreadResourceUsageSnapshot;
171
172    #[allow(clippy::missing_const_for_fn)]
173    pub(super) fn current_thread_resource_usage() -> Option<ThreadResourceUsageSnapshot> {
174        None
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn computes_resource_usage_delta() {
184        let earlier = ThreadResourceUsageSnapshot {
185            user_cpu_time: Duration::from_micros(10),
186            system_cpu_time: Duration::from_micros(20),
187            minor_page_faults: 30,
188            major_page_faults: 40,
189            voluntary_context_switches: 50,
190            involuntary_context_switches: 60,
191            block_input_operations: 70,
192            block_output_operations: 80,
193        };
194        let current = ThreadResourceUsageSnapshot {
195            user_cpu_time: Duration::from_micros(11),
196            system_cpu_time: Duration::from_micros(22),
197            minor_page_faults: 33,
198            major_page_faults: 44,
199            voluntary_context_switches: 55,
200            involuntary_context_switches: 66,
201            block_input_operations: 77,
202            block_output_operations: 88,
203        };
204
205        assert_eq!(
206            current.saturating_delta(earlier),
207            ThreadResourceUsageDelta {
208                user_cpu_time: Duration::from_micros(1),
209                system_cpu_time: Duration::from_micros(2),
210                minor_page_faults: 3,
211                major_page_faults: 4,
212                voluntary_context_switches: 5,
213                involuntary_context_switches: 6,
214                block_input_operations: 7,
215                block_output_operations: 8,
216            }
217        );
218    }
219
220    #[test]
221    fn resource_usage_delta_saturates() {
222        let earlier = ThreadResourceUsageSnapshot {
223            user_cpu_time: Duration::from_secs(1),
224            system_cpu_time: Duration::from_secs(1),
225            minor_page_faults: 1,
226            major_page_faults: 1,
227            voluntary_context_switches: 1,
228            involuntary_context_switches: 1,
229            block_input_operations: 1,
230            block_output_operations: 1,
231        };
232
233        assert_eq!(
234            ThreadResourceUsageSnapshot::default().saturating_delta(earlier),
235            ThreadResourceUsageDelta::default()
236        );
237    }
238}