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 measures_forced_minor_page_fault() {
130 let mapping_len = 4096;
131 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 unsafe { mapping.cast::<u8>().write_volatile(1) };
149 let elapsed = usage.elapsed();
150
151 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}