Skip to main content

resource_accounting/
stats.rs

1use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering::Relaxed};
2
3/// Statistics for an resource group.
4pub struct ResourceStats {
5    allocated_bytes: AtomicUsize,
6    allocated_objects: AtomicUsize,
7    deallocated_bytes: AtomicUsize,
8    deallocated_objects: AtomicUsize,
9    cpu_time_nanos: AtomicU64,
10}
11
12impl ResourceStats {
13    pub(super) const fn new() -> Self {
14        Self {
15            allocated_bytes: AtomicUsize::new(0),
16            allocated_objects: AtomicUsize::new(0),
17            deallocated_bytes: AtomicUsize::new(0),
18            deallocated_objects: AtomicUsize::new(0),
19            cpu_time_nanos: AtomicU64::new(0),
20        }
21    }
22
23    /// Returns `true` if the given group has allocated any memory at all.
24    pub fn has_allocated(&self) -> bool {
25        self.allocated_bytes.load(Relaxed) > 0
26    }
27
28    #[inline]
29    pub(super) fn track_allocation(&self, size: usize) {
30        self.allocated_bytes.fetch_add(size, Relaxed);
31        self.allocated_objects.fetch_add(1, Relaxed);
32    }
33
34    #[inline]
35    pub(super) fn track_deallocation(&self, size: usize) {
36        self.deallocated_bytes.fetch_add(size, Relaxed);
37        self.deallocated_objects.fetch_add(1, Relaxed);
38    }
39
40    #[inline]
41    pub(super) fn track_cpu_time(&self, nanos: u64) {
42        self.cpu_time_nanos.fetch_add(nanos, Relaxed);
43    }
44
45    /// Captures a snapshot of the current statistics based on the delta from a previous snapshot.
46    ///
47    /// This can be used to keep a single local snapshot of the last delta, and then both track the delta since that
48    /// snapshot, as well as update the snapshot to the current statistics.
49    ///
50    /// Callers should generally create their snapshot via [`ResourceStatsSnapshot::empty`] and then use this method
51    /// to get their snapshot delta, utilize those delta values in whatever way is necessary, and then merge the
52    /// snapshot delta into the primary snapshot via [`ResourceStatsSnapshot::merge`] to make it current.
53    pub fn snapshot_delta(&self, previous: &ResourceStatsSnapshot) -> ResourceStatsSnapshot {
54        ResourceStatsSnapshot {
55            allocated_bytes: self.allocated_bytes.load(Relaxed) - previous.allocated_bytes,
56            allocated_objects: self.allocated_objects.load(Relaxed) - previous.allocated_objects,
57            deallocated_bytes: self.deallocated_bytes.load(Relaxed) - previous.deallocated_bytes,
58            deallocated_objects: self.deallocated_objects.load(Relaxed) - previous.deallocated_objects,
59            cpu_time_nanos: self.cpu_time_nanos.load(Relaxed) - previous.cpu_time_nanos,
60        }
61    }
62}
63
64/// Snapshot of allocation statistics for a group.
65pub struct ResourceStatsSnapshot {
66    /// Number of allocated bytes since the last snapshot.
67    pub allocated_bytes: usize,
68
69    /// Number of allocated objects since the last snapshot.
70    pub allocated_objects: usize,
71
72    /// Number of deallocated bytes since the last snapshot.
73    pub deallocated_bytes: usize,
74
75    /// Number of deallocated objects since the last snapshot.
76    pub deallocated_objects: usize,
77
78    /// Cumulative CPU time in nanoseconds consumed by this group since the last snapshot.
79    pub cpu_time_nanos: u64,
80}
81
82impl ResourceStatsSnapshot {
83    /// Creates an empty `ResourceStatsSnapshot`.
84    pub const fn empty() -> Self {
85        Self {
86            allocated_bytes: 0,
87            allocated_objects: 0,
88            deallocated_bytes: 0,
89            deallocated_objects: 0,
90            cpu_time_nanos: 0,
91        }
92    }
93
94    /// Returns the number of live allocated bytes.
95    pub fn live_bytes(&self) -> usize {
96        self.allocated_bytes - self.deallocated_bytes
97    }
98
99    /// Returns the number of live allocated objects.
100    pub fn live_objects(&self) -> usize {
101        self.allocated_objects - self.deallocated_objects
102    }
103
104    /// Merges `other` into `self`.
105    ///
106    /// This can be used to accumulate the total number of (de)allocated bytes and objects when handling the deltas
107    /// generated from `ResourceStats::consume`.
108    pub fn merge(&mut self, other: &Self) {
109        self.allocated_bytes += other.allocated_bytes;
110        self.allocated_objects += other.allocated_objects;
111        self.deallocated_bytes += other.deallocated_bytes;
112        self.deallocated_objects += other.deallocated_objects;
113        self.cpu_time_nanos += other.cpu_time_nanos;
114    }
115}
116
117/// Returns the current thread's CPU time in nanoseconds, or `None` if unavailable.
118#[cfg(target_os = "linux")]
119#[inline]
120pub(crate) fn thread_cpu_time_nanos() -> Option<u64> {
121    // NOTE: `CLOCK_THREAD_CPUTIME_ID` is not vDSO accelerated and degrades to a full syscall.
122    //
123    // Practically speaking, this ends up being roughly ~40-50x slower than a vDSO call at around 850ns or so. This is
124    // acceptable for our use case, because we're only tracking thread CPU time on task enter/exit, and only doing so on
125    // root task futures which are running for appreciable amounts of time so the rate at which we're calling this is fairly
126    // low.
127
128    let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
129    // SAFETY: We pass a valid reference to the `timespec` struct, and `CLOCK_THREAD_CPUTIME_ID` has been available
130    // since Linux 2.6.12.
131    let ret = unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) };
132    if ret == 0 {
133        Some(ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64)
134    } else {
135        None
136    }
137}
138
139/// Returns the current thread's CPU time in nanoseconds, or `None` if unavailable.
140#[cfg(not(target_os = "linux"))]
141#[inline]
142pub(crate) fn thread_cpu_time_nanos() -> Option<u64> {
143    None
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[cfg(target_os = "linux")]
151    #[test]
152    fn thread_cpu_time_returns_some() {
153        let t = thread_cpu_time_nanos();
154        assert!(t.is_some());
155        assert!(t.unwrap() > 0);
156    }
157
158    #[cfg(target_os = "linux")]
159    #[test]
160    fn thread_cpu_time_is_monotonic() {
161        let t1 = thread_cpu_time_nanos().unwrap();
162        // Do some work to consume CPU.
163        let mut sum = 0u64;
164        for i in 0..100_000 {
165            sum = sum.wrapping_add(i);
166        }
167        std::hint::black_box(sum);
168        let t2 = thread_cpu_time_nanos().unwrap();
169        assert!(t2 > t1);
170    }
171
172    #[cfg(not(target_os = "linux"))]
173    #[test]
174    fn thread_cpu_time_returns_none_on_non_linux() {
175        assert!(thread_cpu_time_nanos().is_none());
176    }
177}