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