1#![cfg_attr(target_os = "macos", allow(unsafe_code))]
9
10use std::fmt::Write as _;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Source {
15 LinuxProc,
17 MacOsProcTaskInfo,
19}
20
21impl Source {
22 const fn group(self) -> &'static str {
23 match self {
24 Self::LinuxProc => "process (Linux procfs)",
25 Self::MacOsProcTaskInfo => "process (macOS proc_taskinfo)",
26 }
27 }
28}
29
30#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
31struct MacRaw {
32 unix_syscalls: u32,
33 mach_syscalls: u32,
34 faults: u32,
35 pageins: u32,
36}
37
38#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
39enum Raw {
40 #[default]
41 None,
42 MacOs(MacRaw),
43}
44
45#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
47pub struct Snapshot {
48 source: Option<Source>,
49 raw: Raw,
50 pub read_syscalls: Option<u64>,
52 pub write_syscalls: Option<u64>,
54 pub bytes_through_read: Option<u64>,
56 pub bytes_through_write: Option<u64>,
58 pub bytes_from_device: Option<u64>,
60 pub minor_faults: Option<u64>,
62 pub major_faults: Option<u64>,
64 pub page_faults: Option<u64>,
66 pub pageins: Option<u64>,
68 pub total_syscalls: Option<u64>,
70}
71
72impl Snapshot {
73 #[must_use]
78 pub fn now() -> Self {
79 #[cfg(target_os = "linux")]
80 {
81 linux_snapshot()
82 }
83 #[cfg(target_os = "macos")]
84 {
85 macos_snapshot()
86 }
87 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
88 {
89 Self::default()
90 }
91 }
92
93 #[must_use]
95 pub const fn source(&self) -> Option<Source> {
96 self.source
97 }
98
99 #[must_use]
101 pub const fn is_available(&self) -> bool {
102 self.source.is_some()
103 }
104
105 #[must_use]
107 pub fn since(&self, earlier: &Self) -> Self {
108 let Some(source) = self.source.filter(|source| Some(*source) == earlier.source) else {
109 return Self::default();
110 };
111
112 if let (Raw::MacOs(later), Raw::MacOs(before)) = (self.raw, earlier.raw) {
113 let unix = later.unix_syscalls.wrapping_sub(before.unix_syscalls);
114 let mach = later.mach_syscalls.wrapping_sub(before.mach_syscalls);
115 let faults = later.faults.wrapping_sub(before.faults);
116 let pageins = later.pageins.wrapping_sub(before.pageins);
117 return Self {
118 source: Some(source),
119 raw: Raw::MacOs(MacRaw {
120 unix_syscalls: unix,
121 mach_syscalls: mach,
122 faults,
123 pageins,
124 }),
125 total_syscalls: Some(u64::from(unix) + u64::from(mach)),
126 page_faults: Some(u64::from(faults)),
127 pageins: Some(u64::from(pageins)),
128 ..Self::default()
129 };
130 }
131
132 Self {
133 source: Some(source),
134 read_syscalls: delta(self.read_syscalls, earlier.read_syscalls),
135 write_syscalls: delta(self.write_syscalls, earlier.write_syscalls),
136 bytes_through_read: delta(self.bytes_through_read, earlier.bytes_through_read),
137 bytes_through_write: delta(self.bytes_through_write, earlier.bytes_through_write),
138 bytes_from_device: delta(self.bytes_from_device, earlier.bytes_from_device),
139 minor_faults: delta(self.minor_faults, earlier.minor_faults),
140 major_faults: delta(self.major_faults, earlier.major_faults),
141 page_faults: delta(self.page_faults, earlier.page_faults),
142 pageins: delta(self.pageins, earlier.pageins),
143 total_syscalls: delta(self.total_syscalls, earlier.total_syscalls),
144 raw: Raw::None,
145 }
146 }
147
148 #[must_use]
150 pub fn cache_hit_ratio(&self) -> Option<f64> {
151 let through = self.bytes_through_read?;
152 if through == 0 {
153 return None;
154 }
155 let device = self.bytes_from_device?;
156 Some(super::ratio(through.saturating_sub(device), through))
157 }
158
159 #[must_use]
161 pub fn render(&self) -> String {
162 let Some(source) = self.source else {
163 return "process counters: not available on this platform\n".to_string();
164 };
165 let group = source.group();
166 let mut rows = Vec::new();
167 push_metric(&mut rows, group, "read syscalls", self.read_syscalls);
168 push_metric(&mut rows, group, "write syscalls", self.write_syscalls);
169 push_metric(&mut rows, group, "bytes through read", self.bytes_through_read);
170 push_metric(&mut rows, group, "bytes through write", self.bytes_through_write);
171 push_metric(&mut rows, group, "bytes from device", self.bytes_from_device);
172 push_metric(&mut rows, group, "minor page faults", self.minor_faults);
173 push_metric(&mut rows, group, "major page faults", self.major_faults);
174 push_metric(&mut rows, group, "page faults", self.page_faults);
175 push_metric(&mut rows, group, "page-ins", self.pageins);
176 push_metric(&mut rows, group, "syscalls (total)", self.total_syscalls);
177
178 if rows.is_empty() {
179 return format!("process counters: {} supplied no supported metrics\n", source.group());
180 }
181 let mut output = super::render_rows(&rows);
182 if let Some(ratio) = self.cache_hit_ratio() {
183 let _ = writeln!(output, " {:<24} {:>13.1}%", "page cache served", ratio * 100.0);
184 }
185 output
186 }
187}
188
189fn delta(later: Option<u64>, earlier: Option<u64>) -> Option<u64> {
190 Some(later?.saturating_sub(earlier?))
191}
192
193fn push_metric(
194 rows: &mut Vec<(&'static str, &'static str, u64)>,
195 group: &'static str,
196 label: &'static str,
197 value: Option<u64>,
198) {
199 if let Some(value) = value {
200 rows.push((group, label, value));
201 }
202}
203
204#[cfg(target_os = "linux")]
205fn linux_snapshot() -> Snapshot {
206 let mut snapshot = Snapshot::default();
207 if let Ok(io) = std::fs::read_to_string("/proc/self/io") {
208 for line in io.lines() {
209 let Some((key, value)) = line.split_once(':') else { continue };
210 let Ok(value) = value.trim().parse::<u64>() else { continue };
211 match key {
212 "syscr" => snapshot.read_syscalls = Some(value),
213 "syscw" => snapshot.write_syscalls = Some(value),
214 "rchar" => snapshot.bytes_through_read = Some(value),
215 "wchar" => snapshot.bytes_through_write = Some(value),
216 "read_bytes" => snapshot.bytes_from_device = Some(value),
217 _ => {}
218 }
219 }
220 }
221 if let Ok(stat) = std::fs::read_to_string("/proc/self/stat") {
222 if let Some(rest) = stat.rsplit_once(')').map(|(_, rest)| rest) {
223 let fields: Vec<&str> = rest.split_whitespace().collect();
224 snapshot.minor_faults = fields.get(7).and_then(|value| value.parse().ok());
225 snapshot.major_faults = fields.get(9).and_then(|value| value.parse().ok());
226 }
227 }
228 if snapshot.read_syscalls.is_some()
229 || snapshot.write_syscalls.is_some()
230 || snapshot.minor_faults.is_some()
231 || snapshot.major_faults.is_some()
232 {
233 snapshot.source = Some(Source::LinuxProc);
234 }
235 snapshot
236}
237
238#[cfg(target_os = "macos")]
239fn macos_snapshot() -> Snapshot {
240 let mut info = libc::proc_taskinfo {
241 pti_virtual_size: 0,
242 pti_resident_size: 0,
243 pti_total_user: 0,
244 pti_total_system: 0,
245 pti_threads_user: 0,
246 pti_threads_system: 0,
247 pti_policy: 0,
248 pti_faults: 0,
249 pti_pageins: 0,
250 pti_cow_faults: 0,
251 pti_messages_sent: 0,
252 pti_messages_received: 0,
253 pti_syscalls_mach: 0,
254 pti_syscalls_unix: 0,
255 pti_csw: 0,
256 pti_threadnum: 0,
257 pti_numrunning: 0,
258 pti_priority: 0,
259 };
260 let Ok(size) = i32::try_from(size_of::<libc::proc_taskinfo>()) else {
261 return Snapshot::default();
262 };
263 let filled = unsafe {
266 libc::proc_pidinfo(
267 i32::try_from(std::process::id()).unwrap_or(-1),
268 libc::PROC_PIDTASKINFO,
269 0,
270 (&raw mut info).cast(),
271 size,
272 )
273 };
274 if filled != size {
275 return Snapshot::default();
276 }
277
278 let raw = MacRaw {
279 unix_syscalls: signed_counter_bits(info.pti_syscalls_unix),
280 mach_syscalls: signed_counter_bits(info.pti_syscalls_mach),
281 faults: signed_counter_bits(info.pti_faults),
282 pageins: signed_counter_bits(info.pti_pageins),
283 };
284 Snapshot {
285 source: Some(Source::MacOsProcTaskInfo),
286 raw: Raw::MacOs(raw),
287 total_syscalls: Some(u64::from(raw.unix_syscalls) + u64::from(raw.mach_syscalls)),
288 page_faults: Some(u64::from(raw.faults)),
289 pageins: Some(u64::from(raw.pageins)),
290 ..Snapshot::default()
291 }
292}
293
294#[cfg(target_os = "macos")]
295fn signed_counter_bits(value: i32) -> u32 {
296 u32::from_ne_bytes(value.to_ne_bytes())
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn a_snapshot_is_readable_and_differences_are_sane() {
305 let before = Snapshot::now();
306 if !before.is_available() {
307 assert!(before.render().contains("not available"));
308 return;
309 }
310
311 let scratch = tempfile::NamedTempFile::new().expect("scratch file");
312 std::fs::write(scratch.path(), vec![b'x'; 4096]).expect("write scratch file");
313 let mut total = 0_usize;
314 for _ in 0..64 {
315 total += std::fs::read(scratch.path()).map_or(0, |bytes| bytes.len());
316 }
317 assert!(total > 0);
318
319 #[cfg(any(target_os = "linux", target_os = "macos"))]
320 let difference = Snapshot::now().since(&before);
321 #[cfg(target_os = "linux")]
322 {
323 assert!(difference.read_syscalls.is_some_and(|value| value > 0));
324 assert!(difference.bytes_through_read.is_some_and(|value| value > 0));
325 assert_eq!(difference.total_syscalls, None);
326 }
327 #[cfg(target_os = "macos")]
328 {
329 assert!(difference.total_syscalls.is_some_and(|value| value > 0));
330 assert_eq!(difference.read_syscalls, None);
331 assert_eq!(difference.bytes_through_read, None);
332 }
333 }
334
335 #[test]
336 fn render_omits_metrics_the_source_does_not_supply() {
337 let mac = Snapshot {
338 source: Some(Source::MacOsProcTaskInfo),
339 total_syscalls: Some(12),
340 page_faults: Some(3),
341 pageins: Some(1),
342 ..Snapshot::default()
343 };
344
345 let rendered = mac.render();
346 assert!(rendered.contains("syscalls (total)"));
347 assert!(rendered.contains("page-ins"));
348 assert!(!rendered.contains("read syscalls"));
349 assert!(!rendered.contains("bytes from device"));
350 }
351
352 #[test]
353 fn macos_source_width_wraps_without_becoming_zero() {
354 let before_raw = MacRaw {
355 unix_syscalls: 0x7fff_fffe,
356 mach_syscalls: 0xffff_fffe,
357 faults: 0x7fff_ffff,
358 pageins: 0xffff_ffff,
359 };
360 let later_raw = MacRaw {
361 unix_syscalls: 0x8000_0001,
362 mach_syscalls: 1,
363 faults: 0x8000_0001,
364 pageins: 2,
365 };
366 let snapshot = |raw| Snapshot {
367 source: Some(Source::MacOsProcTaskInfo),
368 raw: Raw::MacOs(raw),
369 ..Snapshot::default()
370 };
371
372 let difference = snapshot(later_raw).since(&snapshot(before_raw));
373 assert_eq!(difference.total_syscalls, Some(6));
374 assert_eq!(difference.page_faults, Some(2));
375 assert_eq!(difference.pageins, Some(3));
376 }
377
378 #[test]
379 fn linux_differences_saturate_instead_of_wrapping() {
380 let later = Snapshot {
381 source: Some(Source::LinuxProc),
382 read_syscalls: Some(5),
383 ..Snapshot::default()
384 };
385 let earlier = Snapshot {
386 source: Some(Source::LinuxProc),
387 read_syscalls: Some(9),
388 ..Snapshot::default()
389 };
390 assert_eq!(later.since(&earlier).read_syscalls, Some(0));
391 }
392}