Skip to main content

resource_accounting/
lib.rs

1//! Building blocks for process-level resource accounting, including memory bounds enforcement and
2//! CPU usage tracking.
3//!
4//! # Overview
5//!
6//! This crate provides a four-pronged approach to process accounting:
7//!
8//! - memory bounds (components declare their _expected_ memory usage)
9//! - allocation tracking (tracking _actual_ memory usage)
10//! - memory limiting (enforcing _maximum_ memory usage)
11//! - CPU tracking (tracking per-component CPU time consumption)
12//!
13//! Through this approach, data planes can be vastly more resilient to memory exhaustion or
14//! exceeding externally applied memory limits, and gain visibility into per-component CPU usage.
15//!
16//! # Memory bounds
17//!
18//! One major problem with resource planning is predicting memory usage. For many applications,
19//! there are a number of factors that can influence memory usage, such as:
20//!
21//! - the workload itself (amount of data coming in)
22//! - application configuration (buffer sizes)
23//! - application changes (new features, bug fixes)
24//!
25//! This requires additional effort by operators, potentially on an ongoing basis, to empirically
26//! determine the right amount of memory to dedicate. What if instead, an application could
27//! determine a reasonable upper bound on its memory usage based on its configuration and report
28//! that to the operator? This is the goal of memory bounds.
29//!
30//! Memory bounds are a way for components to declare their expected memory usage, categorized into
31//! both a minimum required amount and a firm limit. The minimum required amount is the amount of
32//! memory that's required for the component to function correctly, which generally encompasses
33//! things like pre-allocated buffers. The firm limit is meant to indicate the maximum amount of
34//! memory that the component should use, regardless of the workload.
35//!
36//! Providing firm limits does require some additional thought and care, as a component needs to be
37//! able to actually limit itself in order to adhere to those limits. While determining the
38//! bounds themselves is out of scope for this crate, our other two prongs are meant to pick up the
39//! slack where memory bounds fall off.
40//!
41//! # Allocation tracking
42//!
43//! As memory bounds are inherently lossy, and not everything can be fully bounded, we need a way to
44//! track the actual memory used against the expected memory usage. This is where allocation
45//! tracking comes into play and offers a very precise view into per-component memory usage.
46//!
47//! A custom allocator is provided that tracks all memory allocations, and more specifically,
48//! attributes them to a set of registered components. Components register with the allocator and
49//! receive a "token" that can be used to scope allocations to that component.
50//!
51//! By tracking allocations in this way, we end up with the actual usage of each component, which
52//! can then be compared against the memory bounds to determine if a component is exceeding its
53//! bounds or not. In cases where a component is exceeding its bounds, or the application as a whole
54//! is exceeding its configured limit, we need a way to attempt to enforce those limits.
55//!
56//! # Memory limiting
57//!
58//! When the application is approaching its configured memory limit, or is exceeding the limit, a
59//! mechanism is needed to slow down the rate of memory growth. The global memory limiter is a
60//! mechanism for cooperatively applying backpressure in order to limit the rate of work, and
61//! thereby limit the rate of allocations. Components participate by utilizing the global memory
62//! limiter, which conditionally applies small delays in order to artificially generate backpressure.
63//!
64//! # CPU tracking
65//!
66//! CPU tracking provides per-component visibility into CPU time consumption. When running on supported
67//! operating systems, we can granularly track the amount of CPU time spent on a per-thread basis, which
68//! allows us to track CPU usage for resource groups in the same way we track allocations: through the
69//! [`Tracked`] future wrapper.
70//!
71//! This allows operators to understand which components are consuming the most CPU time, aiding in
72//! capacity planning and performance optimization.
73//!
74//! CPU usage tracking is only available on Linux.
75#![deny(warnings)]
76#![deny(missing_docs)]
77
78use std::collections::HashMap;
79
80use serde::Serialize;
81
82#[cfg(test)]
83pub mod test_util;
84
85mod allocator;
86pub use self::allocator::TrackingAllocator;
87
88mod api;
89pub use self::api::ResourceAPIHandler;
90
91mod registry;
92pub use self::registry::{ComponentRegistry, ComponentRegistryHandle, MemoryBoundsBuilder};
93
94mod grant;
95pub use self::grant::MemoryGrant;
96
97mod groups;
98pub use self::groups::{ResourceGroupRegistry, ResourceGroupToken, ResourceTrackingGuard, Track, Tracked};
99
100mod limiter;
101pub use self::limiter::MemoryLimiter;
102
103mod stats;
104pub use self::stats::{ResourceStats, ResourceStatsSnapshot};
105
106mod verifier;
107pub use self::verifier::{BoundsVerifier, VerifiedBounds, VerifierError};
108
109/// Memory bounds for a component.
110///
111/// Components will naturally allocate memory in many phases, from initialization to normal operation. In some cases,
112/// these allocations can be unbounded, leading to potential memory exhaustion.
113///
114/// When a component has a way to bound its memory usage, it can implement this trait to provide that accounting. A
115/// bounds builder exposes a simple interface for tallying up the memory usage of individual pieces of a component, such
116/// as buffers and buffer pools, containers, and more.
117pub trait MemoryBounds {
118    /// Specifies the minimum and firm memory bounds for this component and its subcomponents.
119    fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder);
120}
121
122impl<T> MemoryBounds for &T
123where
124    T: MemoryBounds,
125{
126    fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
127        T::specify_bounds(self, builder);
128    }
129}
130
131impl<T> MemoryBounds for Box<T>
132where
133    T: MemoryBounds + ?Sized,
134{
135    fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
136        T::specify_bounds(self, builder);
137    }
138}
139
140/// Represents a memory usage expression for a component.
141#[derive(Clone, Debug, Serialize)]
142#[serde(tag = "type")]
143pub enum UsageExpr {
144    /// A config value
145    Config {
146        /// The name
147        name: String,
148        /// The value
149        value: usize,
150    },
151
152    /// A struct size
153    StructSize {
154        /// The value
155        name: String,
156        /// The value
157        value: usize,
158    },
159
160    /// A constant value
161    Constant {
162        /// The name
163        name: String,
164        /// The value
165        value: usize,
166    },
167
168    /// A product of subexpressions
169    Product {
170        /// Values to multiply
171        values: Vec<UsageExpr>,
172    },
173
174    /// A sum of subexpressions
175    Sum {
176        /// Values to add
177        values: Vec<UsageExpr>,
178    },
179}
180
181impl UsageExpr {
182    /// Creates a new usage expression that's a config value.
183    pub fn config(s: impl Into<String>, value: usize) -> Self {
184        Self::Config { name: s.into(), value }
185    }
186
187    /// Creates a new usage expression that's a constant value.
188    pub fn constant(s: impl Into<String>, value: usize) -> Self {
189        Self::Constant { name: s.into(), value }
190    }
191
192    /// Creates a new usage expression that's a struct size.
193    pub fn struct_size<T>(s: impl Into<String>) -> Self {
194        Self::StructSize {
195            name: s.into(),
196            value: std::mem::size_of::<T>(),
197        }
198    }
199
200    /// Creates a new usage expression that's the product of two subexpressions.
201    pub fn product(_s: impl Into<String>, lhs: UsageExpr, rhs: UsageExpr) -> Self {
202        Self::Product { values: vec![lhs, rhs] }
203    }
204
205    /// Creates a new usage expression that's the sum of two subexpressions.
206    pub fn sum(_s: impl Into<String>, lhs: UsageExpr, rhs: UsageExpr) -> Self {
207        Self::Sum { values: vec![lhs, rhs] }
208    }
209
210    fn evaluate(&self) -> usize {
211        match self {
212            Self::Config { value, .. } | Self::StructSize { value, .. } | Self::Constant { value, .. } => *value,
213            Self::Product { values } => values.iter().map(UsageExpr::evaluate).product(),
214            Self::Sum { values } => values.iter().map(UsageExpr::evaluate).sum(),
215        }
216    }
217}
218
219/// Memory bounds for a component.
220#[derive(Clone, Debug, Default)]
221pub struct ComponentBounds {
222    self_minimum_required_bytes: Vec<UsageExpr>,
223    self_firm_limit_bytes: Vec<UsageExpr>,
224    subcomponents: HashMap<String, ComponentBounds>,
225}
226
227impl ComponentBounds {
228    /// Gets the total minimum required bytes for this component and all subcomponents.
229    pub fn total_minimum_required_bytes(&self) -> usize {
230        self.self_minimum_required_bytes
231            .iter()
232            .map(UsageExpr::evaluate)
233            .sum::<usize>()
234            + self
235                .subcomponents
236                .values()
237                .map(|cb| cb.total_minimum_required_bytes())
238                .sum::<usize>()
239    }
240
241    /// Gets the total firm limit bytes for this component and all subcomponents.
242    ///
243    /// The firm limit includes the minimum required bytes.
244    pub fn total_firm_limit_bytes(&self) -> usize {
245        self.self_minimum_required_bytes
246            .iter()
247            .map(UsageExpr::evaluate)
248            .sum::<usize>()
249            + self
250                .self_firm_limit_bytes
251                .iter()
252                .map(UsageExpr::evaluate)
253                .sum::<usize>()
254            + self
255                .subcomponents
256                .values()
257                .map(|cb| cb.total_firm_limit_bytes())
258                .sum::<usize>()
259    }
260
261    /// Returns an iterator of all subcomponents within this component.
262    ///
263    /// Only iterates over direct subcomponents, not the subcomponents of those subcomponents, and so on.
264    pub fn subcomponents(&self) -> impl IntoIterator<Item = (&String, &ComponentBounds)> {
265        self.subcomponents.iter()
266    }
267
268    /// Returns a tree of all bound expressions for this component and its subcomponents as JSON.
269    pub fn to_exprs(&self) -> Vec<serde_json::Value> {
270        let path = vec!["root".to_string()];
271        let mut stack = vec![(path, self)];
272        let mut output = Vec::new();
273
274        while let Some((path, cb)) = stack.pop() {
275            for expr in &cb.self_minimum_required_bytes {
276                output.push(serde_json::json!({
277                    "name": format!("{}.min", path.join(".")),
278                    "expr": expr,
279                }));
280            }
281            for expr in &cb.self_firm_limit_bytes {
282                output.push(serde_json::json!({
283                    "name": format!("{}.firm", path.join(".")),
284                    "expr": expr,
285                }));
286            }
287
288            for (name, subcomponent) in cb.subcomponents() {
289                let mut path = path.clone();
290                path.push(name.clone());
291                stack.push((path, subcomponent));
292            }
293        }
294
295        output
296    }
297}