1use std::{marker::PhantomData, rc::Rc, time::Duration};
4
5#[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 pub fn now() -> Self {
19 Self { start: platform::current_thread_resource_usage(), _not_send: PhantomData }
20 }
21
22 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
33pub struct ThreadResourceUsageDelta {
34 pub user_cpu_time: Duration,
36 pub system_cpu_time: Duration,
38 pub minor_page_faults: u64,
40 pub major_page_faults: u64,
42 pub voluntary_context_switches: u64,
44 pub involuntary_context_switches: u64,
46 pub block_input_operations: u64,
48 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 if unsafe { libc::getrusage(libc::RUSAGE_THREAD, usage.as_mut_ptr()) } != 0 {
97 return None
98 }
99 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 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 unsafe { mapping.cast::<u8>().write_volatile(1) };
154 let elapsed = usage.elapsed();
155
156 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}