memory_accounting/grant.rs
1use ordered_float::NotNan;
2use snafu::Snafu;
3
4// We limit grants to a size of 2^53 (~9PB) because at numbers bigger than that, we end up with floating point loss when
5// we do scaling calculations. Since we are not going to support grants that large -- and if we ever have to, well,
6// then, I'll eat my synpatic implant or whatever makes sense to eat 40 years from now -- we just limit them like this
7// for now.
8const MAX_GRANT_BYTES: usize = 2usize.pow(f64::MANTISSA_DIGITS);
9
10#[derive(Debug, Snafu)]
11pub enum GrantError {
12 #[snafu(display("Slop factor must be between 0.0 and 1.0 inclusive."))]
13 InvalidSlopFactor,
14
15 #[snafu(display("Initial limit must be less than or equal to 9PiB (2^53 bytes)."))]
16 InitialLimitTooHigh,
17}
18
19/// A memory grant.
20///
21/// Grants define a number of bytes which have been granted for use, with an accompanying "slop factor."
22///
23/// ## Slop factor
24///
25/// There can be numerous sources of "slop" in terms of memory allocations, and limiting the memory usage of a process.
26/// Not all components can effectively track their true firm/hard limit, memory allocators have fragmentation that may
27/// be reduced over time but never fully eliminated, and so on.
28///
29/// In order to protect against this issue, we utilize a slop factor when calculating the effective limits that we
30/// should verify memory bounds again. For example, if we seek to use no more than 64MB of memory from the OS
31/// perspective (RSS), then intuitively we know that we might only be able to allocate 55-60MB of memory before
32/// allocator fragmentation causes us to reach 64MB RSS.
33///
34/// By specifying a slop factor, we can provide ourselves breathing room to ensure that we don't try to allocate every
35/// last byte of the given global limit, inevitably leading to _exceeding_ that limit and potentially causing
36/// out-of-memory crashes, etc.
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub struct MemoryGrant {
39 initial_limit_bytes: usize,
40 slop_factor: NotNan<f64>,
41 effective_limit_bytes: usize,
42}
43
44impl MemoryGrant {
45 /// Creates a new memory grant based on the given effective limit.
46 ///
47 /// This grant will have a slop factor of 0.0 to indicate that the effective limit is already inclusive of any
48 /// necessary slop factor.
49 ///
50 /// If the effective limit is greater than 9007199254740992 bytes (2^53 bytes, or roughly 9 petabytes), then `None`
51 /// is returned. This is a hardcoded limit.
52 pub fn effective(effective_limit_bytes: usize) -> Result<Self, GrantError> {
53 Self::with_slop_factor(effective_limit_bytes, 0.0)
54 }
55
56 /// Creates a new memory grant based on the given initial limit and slop factor.
57 ///
58 /// The slop factor accounts for the percentage of the initial limit that can effectively be used. For example, a
59 /// slop factor of 0.1 would indicate that only 90% of the initial limit should be used, and a slop factor of 0.25
60 /// would indicate that only 75% of the initial limit should be used, and so on.
61 ///
62 /// If the slop factor is not valid (must be 0.0 < slop_factor <= 1.0), then `None` is returned. If the effective
63 /// limit is greater than 9007199254740992 bytes (2^53 bytes, or roughly 9 petabytes), then `None` is returned. This
64 /// is a hardcoded limit.
65 pub fn with_slop_factor(initial_limit_bytes: usize, slop_factor: f64) -> Result<Self, GrantError> {
66 let slop_factor = if !(0.0..1.0).contains(&slop_factor) {
67 return Err(GrantError::InvalidSlopFactor);
68 } else {
69 NotNan::new(slop_factor).map_err(|_| GrantError::InvalidSlopFactor)?
70 };
71
72 if initial_limit_bytes > MAX_GRANT_BYTES {
73 return Err(GrantError::InitialLimitTooHigh);
74 }
75
76 let effective_limit_bytes = (initial_limit_bytes as f64 * (1.0 - slop_factor.into_inner())) as usize;
77 Ok(Self {
78 initial_limit_bytes,
79 slop_factor,
80 effective_limit_bytes,
81 })
82 }
83
84 /// Initial number of bytes granted.
85 ///
86 /// This value is purely informational, and should not be used to calculating the memory available for use. For that
87 /// value, see [`effective_limit_bytes`][Self::effective_limit_bytes].
88 pub fn initial_limit_bytes(&self) -> usize {
89 self.initial_limit_bytes
90 }
91
92 /// The slop factor for the initial limit.
93 ///
94 /// This value is purely informational.
95 pub fn slop_factor(&self) -> f64 {
96 self.slop_factor.into_inner()
97 }
98
99 /// Effective number of bytes granted.
100 ///
101 /// This is the value which should be followed for memory usage purposes, as it accounts for the configured slop
102 /// factor.
103 pub fn effective_limit_bytes(&self) -> usize {
104 self.effective_limit_bytes
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::MemoryGrant;
111
112 #[test]
113 fn effective() {
114 assert!(MemoryGrant::effective(1).is_ok());
115 assert!(MemoryGrant::effective(2usize.pow(f64::MANTISSA_DIGITS)).is_ok());
116 assert!(MemoryGrant::effective(2usize.pow(f64::MANTISSA_DIGITS) + 1).is_err());
117 }
118
119 #[test]
120 fn slop_factor() {
121 assert!(MemoryGrant::with_slop_factor(1, 0.1).is_ok());
122 assert!(MemoryGrant::with_slop_factor(1, 0.9).is_ok());
123 assert!(MemoryGrant::with_slop_factor(1, f64::NAN).is_err());
124 assert!(MemoryGrant::with_slop_factor(1, -0.1).is_err());
125 assert!(MemoryGrant::with_slop_factor(1, 1.001).is_err());
126 assert!(MemoryGrant::with_slop_factor(2usize.pow(f64::MANTISSA_DIGITS), 0.25).is_ok());
127 assert!(MemoryGrant::with_slop_factor(2usize.pow(f64::MANTISSA_DIGITS) + 1, 0.25).is_err());
128 }
129}