1use std::time::Duration;
5
6const DEFAULT_SAMPLE_INTERVAL: Duration = Duration::from_millis(100);
7
8#[derive(Debug, dial9_trace_format::TraceEvent)]
10#[traceevent(wire_slot)]
11#[cfg_attr(not(feature = "unstable-events"), non_exhaustive)]
12pub struct ProcessResourceUsageEvent {
13 #[traceevent(timestamp)]
15 pub timestamp_ns: u64,
16 #[traceevent(unit = "ns", kind = "counter")]
18 pub user_cpu_ns: u64,
19 #[traceevent(unit = "ns", kind = "counter")]
21 pub system_cpu_ns: u64,
22 #[traceevent(unit = "bytes", kind = "gauge")]
24 pub max_rss_bytes: u64,
25 #[traceevent(kind = "counter")]
27 pub minor_faults: u64,
28 #[traceevent(kind = "counter")]
30 pub major_faults: u64,
31 #[traceevent(kind = "counter")]
33 pub block_input_ops: u64,
34 #[traceevent(kind = "counter")]
36 pub block_output_ops: u64,
37 #[traceevent(kind = "counter")]
39 pub voluntary_context_switches: u64,
40 #[traceevent(kind = "counter")]
42 pub involuntary_context_switches: u64,
43}
44
45#[derive(Debug, Clone, bon::Builder)]
50pub struct ProcessResourceUsageConfig {
51 #[builder(default = DEFAULT_SAMPLE_INTERVAL)]
53 sample_interval: Duration,
54}
55
56impl Default for ProcessResourceUsageConfig {
57 fn default() -> Self {
58 Self::builder().build()
59 }
60}
61
62impl ProcessResourceUsageConfig {
63 pub fn sample_interval(&self) -> Duration {
65 self.sample_interval
66 }
67}
68
69#[cfg(unix)]
70mod unix {
71 use super::{ProcessResourceUsageConfig, ProcessResourceUsageEvent};
72 use dial9_core::clock::clock_monotonic_ns;
73 use dial9_core::rate_limited;
74 use dial9_core::source::{FlushContext, Source};
75 use std::io;
76 use std::mem::MaybeUninit;
77 use std::time::{Duration, Instant};
78
79 #[cfg(target_vendor = "apple")]
80 const RU_MAXRSS_MULTIPLIER: u64 = 1;
81 #[cfg(not(target_vendor = "apple"))]
82 const RU_MAXRSS_MULTIPLIER: u64 = 1024;
83
84 #[derive(Debug)]
86 pub struct ProcessResourceUsageSource {
87 config: ProcessResourceUsageConfig,
88 last_sample: Option<Instant>,
89 }
90
91 #[derive(Debug, Clone, Copy)]
92 struct ProcessResourceUsageSnapshot {
93 user_cpu_ns: u64,
94 system_cpu_ns: u64,
95 max_rss_bytes: u64,
96 minor_faults: u64,
97 major_faults: u64,
98 block_input_ops: u64,
99 block_output_ops: u64,
100 voluntary_context_switches: u64,
101 involuntary_context_switches: u64,
102 }
103
104 impl ProcessResourceUsageSource {
105 pub fn new(config: ProcessResourceUsageConfig) -> Self {
107 Self {
108 config,
109 last_sample: None,
110 }
111 }
112 }
113
114 impl Source for ProcessResourceUsageSource {
115 fn flush(&mut self, ctx: &FlushContext<'_>) {
116 let now = Instant::now();
117 if let Some(last_sample) = self.last_sample
118 && now.duration_since(last_sample) < self.config.sample_interval
119 {
120 return;
121 }
122 self.last_sample = Some(now);
123
124 match read_process_resource_usage() {
125 Ok(snapshot) => {
126 let event = snapshot.into_event(clock_monotonic_ns());
127 ctx.record_event(&event);
128 }
129 Err(e) => rate_limited!(Duration::from_secs(60), {
130 tracing::warn!("failed to read process resource usage via getrusage: {e}");
131 }),
132 }
133 }
134
135 fn name(&self) -> &'static str {
136 "process_resource_usage"
137 }
138 }
139
140 impl ProcessResourceUsageSnapshot {
141 fn into_event(self, timestamp_ns: u64) -> ProcessResourceUsageEvent {
142 ProcessResourceUsageEvent {
143 timestamp_ns,
144 user_cpu_ns: self.user_cpu_ns,
145 system_cpu_ns: self.system_cpu_ns,
146 max_rss_bytes: self.max_rss_bytes,
147 minor_faults: self.minor_faults,
148 major_faults: self.major_faults,
149 block_input_ops: self.block_input_ops,
150 block_output_ops: self.block_output_ops,
151 voluntary_context_switches: self.voluntary_context_switches,
152 involuntary_context_switches: self.involuntary_context_switches,
153 }
154 }
155 }
156
157 fn read_process_resource_usage() -> io::Result<ProcessResourceUsageSnapshot> {
158 snapshot_from_rusage(read_rusage()?)
159 }
160
161 fn read_rusage() -> io::Result<libc::rusage> {
162 let mut usage = MaybeUninit::<libc::rusage>::uninit();
163
164 let rc = unsafe { libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) };
167 if rc != 0 {
168 return Err(io::Error::last_os_error());
169 }
170
171 Ok(unsafe { usage.assume_init() })
173 }
174
175 fn snapshot_from_rusage(usage: libc::rusage) -> io::Result<ProcessResourceUsageSnapshot> {
176 Ok(ProcessResourceUsageSnapshot {
177 user_cpu_ns: timeval_to_ns(usage.ru_utime, "ru_utime")?,
178 system_cpu_ns: timeval_to_ns(usage.ru_stime, "ru_stime")?,
179 max_rss_bytes: max_rss_to_bytes(usage.ru_maxrss)?,
180 minor_faults: nonnegative(usage.ru_minflt, "ru_minflt")?,
181 major_faults: nonnegative(usage.ru_majflt, "ru_majflt")?,
182 block_input_ops: nonnegative(usage.ru_inblock, "ru_inblock")?,
183 block_output_ops: nonnegative(usage.ru_oublock, "ru_oublock")?,
184 voluntary_context_switches: nonnegative(usage.ru_nvcsw, "ru_nvcsw")?,
185 involuntary_context_switches: nonnegative(usage.ru_nivcsw, "ru_nivcsw")?,
186 })
187 }
188
189 fn timeval_to_ns(tv: libc::timeval, field: &'static str) -> io::Result<u64> {
190 let seconds = nonnegative(tv.tv_sec, field)?;
191 let micros = nonnegative(tv.tv_usec, field)?;
192 let second_ns = checked_mul(seconds, 1_000_000_000, field)?;
193 let micro_ns = checked_mul(micros, 1_000, field)?;
194 second_ns.checked_add(micro_ns).ok_or_else(|| {
195 io::Error::new(
196 io::ErrorKind::InvalidData,
197 format!("{field} overflowed u64"),
198 )
199 })
200 }
201
202 fn max_rss_to_bytes(max_rss: libc::c_long) -> io::Result<u64> {
203 let value = nonnegative(max_rss, "ru_maxrss")?;
204 checked_mul(value, RU_MAXRSS_MULTIPLIER, "ru_maxrss")
205 }
206
207 fn checked_mul(value: u64, multiplier: u64, field: &'static str) -> io::Result<u64> {
208 value.checked_mul(multiplier).ok_or_else(|| {
209 io::Error::new(
210 io::ErrorKind::InvalidData,
211 format!("{field} overflowed u64"),
212 )
213 })
214 }
215
216 fn nonnegative<T>(value: T, field: &'static str) -> io::Result<u64>
217 where
218 T: TryInto<u64>,
219 {
220 value.try_into().map_err(|_| {
221 io::Error::new(
222 io::ErrorKind::InvalidData,
223 format!("getrusage returned negative {field}"),
224 )
225 })
226 }
227
228 #[cfg(all(test, feature = "test-util"))]
229 mod tests {
230 use super::*;
231 use dial9_core::shared_state::SharedState;
232 use dial9_core::test_util;
233 use serde::Deserialize;
234
235 #[derive(Debug, Deserialize)]
236 #[serde(tag = "event")]
237 enum DecodedEvent {
238 ProcessResourceUsageEvent(DecodedProcessResourceUsageEvent),
239 #[serde(other)]
240 Other,
241 }
242
243 #[derive(Debug, Deserialize)]
244 #[allow(dead_code)]
245 struct DecodedProcessResourceUsageEvent {
246 timestamp_ns: u64,
247 user_cpu_ns: u64,
248 system_cpu_ns: u64,
249 max_rss_bytes: u64,
250 minor_faults: u64,
251 major_faults: u64,
252 block_input_ops: u64,
253 block_output_ops: u64,
254 voluntary_context_switches: u64,
255 involuntary_context_switches: u64,
256 }
257
258 fn decode_process_resource_usage_events(
259 bytes: &[u8],
260 ) -> Vec<DecodedProcessResourceUsageEvent> {
261 let mut decoder = dial9_trace_format::decoder::Decoder::new(bytes)
262 .expect("encoded process resource usage batch should have a valid trace header");
263 let mut events = Vec::new();
264 decoder
265 .for_each_event(|raw| {
266 match raw
267 .deserialize()
268 .expect("encoded process resource usage event should deserialize")
269 {
270 DecodedEvent::ProcessResourceUsageEvent(event) => events.push(event),
271 DecodedEvent::Other => {}
272 }
273 })
274 .expect("encoded process resource usage batch should decode");
275 events
276 }
277
278 #[test]
279 fn read_process_resource_usage_returns_metrics() {
280 let snapshot = read_process_resource_usage()
281 .expect("getrusage should succeed for the current process");
282 assert!(snapshot.max_rss_bytes > 0);
283 }
284
285 #[test]
286 fn source_emits_process_resource_usage_event() {
287 let shared = SharedState::new(0);
288 let ctx = shared.flush_context();
289 let mut source = ProcessResourceUsageSource::new(ProcessResourceUsageConfig::default());
290
291 source.flush(&ctx);
292 let events: Vec<_> = test_util::drain_encoded_batches(&shared)
293 .iter()
294 .flat_map(|b| decode_process_resource_usage_events(b))
295 .collect();
296
297 assert_eq!(events.len(), 1);
298 let event = &events[0];
299 assert!(event.timestamp_ns > 0);
300 assert!(event.max_rss_bytes > 0);
301 }
302
303 #[test]
304 fn source_respects_sample_interval() {
305 let shared = SharedState::new(0);
306 let ctx = shared.flush_context();
307 let config = ProcessResourceUsageConfig::builder()
308 .sample_interval(Duration::from_secs(60))
309 .build();
310 let mut source = ProcessResourceUsageSource::new(config);
311
312 source.flush(&ctx);
313 source.flush(&ctx);
314 let events: Vec<_> = test_util::drain_encoded_batches(&shared)
315 .iter()
316 .flat_map(|b| decode_process_resource_usage_events(b))
317 .collect();
318
319 assert_eq!(events.len(), 1);
320 }
321 }
322}
323
324#[cfg(unix)]
325pub use unix::ProcessResourceUsageSource;
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn default_sample_interval_is_100ms() {
333 assert_eq!(
334 ProcessResourceUsageConfig::default().sample_interval(),
335 DEFAULT_SAMPLE_INTERVAL
336 );
337 }
338
339 #[test]
340 fn process_resource_usage_annotations() {
341 use dial9_trace_format::TraceEvent;
342 let entry = ProcessResourceUsageEvent::schema_entry();
343 let annotations = |key| {
344 entry
345 .annotations()
346 .iter()
347 .filter(|annotation| annotation.key() == key)
348 .map(|annotation| {
349 (
350 entry.fields()[annotation.field_index() as usize].name(),
351 annotation.value(),
352 )
353 })
354 .collect::<Vec<_>>()
355 };
356 assert_eq!(
357 annotations("unit"),
358 vec![
359 ("user_cpu_ns", "ns"),
360 ("system_cpu_ns", "ns"),
361 ("max_rss_bytes", "bytes"),
362 ]
363 );
364 assert_eq!(
365 annotations("kind"),
366 vec![
367 ("user_cpu_ns", "counter"),
368 ("system_cpu_ns", "counter"),
369 ("max_rss_bytes", "gauge"),
370 ("minor_faults", "counter"),
371 ("major_faults", "counter"),
372 ("block_input_ops", "counter"),
373 ("block_output_ops", "counter"),
374 ("voluntary_context_switches", "counter"),
375 ("involuntary_context_switches", "counter"),
376 ]
377 );
378 }
379}