tor_memquota_cost/memory_cost_derive.rs
1//! Deriving `HasMemoryCost`
2
3use crate::{EnabledToken, HasMemoryCost};
4use derive_deftly::define_derive_deftly;
5use itertools::chain;
6use paste::paste;
7
8//---------- main public items ----------
9
10/// Types whose `HasMemoryCost` is derived structurally
11///
12/// Usually implemented using
13/// [`#[derive_deftly(HasMemoryCost)]`](crate::derive_deftly_template_HasMemoryCost).
14///
15/// For `Copy` types, it can also be implemented with
16/// `memory_cost_structural_copy!`.
17///
18/// When this trait is implemented, a blanket impl provides [`HasMemoryCost`].
19///
20/// ### Structural memory cost
21///
22/// We call the memory cost "structural"
23/// when it is derived from the type's structure.
24///
25/// The memory cost of a `HasMemoryCostStructural` type is:
26///
27/// - The number of bytes in its own size [`size_of`]; plus
28///
29/// - The (structural) memory cost of all the out-of-line data that it owns;
30/// that's what's returned by
31/// [`indirect_memory_cost`](HasMemoryCostStructural::indirect_memory_cost)
32///
33/// For example, `String`s out-of-line memory cost is just its capacity,
34/// so its memory cost is the size of its three word layout plus its capacity.
35///
36/// This calculation is performed by the blanket impl of `HasMemoryCost`.
37///
38/// ### Shared data - non-`'static` types, `Arc`
39///
40/// It is probably a mistake to implement this trait (or `HasMemoryCost`)
41/// for types with out-of-line data that they don't exclusively own.
42/// After all, the memory cost must be known and fixed,
43/// and if there is shared data it's not clear how it should be accounted.
44pub trait HasMemoryCostStructural {
45 /// Memory cost of data stored out-of-line
46 ///
47 /// The total memory cost is the cost of the layout of `self` plus this.
48 fn indirect_memory_cost(&self, _: EnabledToken) -> usize;
49}
50
51/// Compile-time check for `Copy + 'static` - helper for macros
52///
53/// Used by `#[deftly(has_memory_cost(copy))]`
54/// and `memory_cost_structural_copy!`
55/// to check that the type really is suitable.
56pub fn assert_copy_static<T: Copy + 'static>(_: &T) {}
57
58impl<T: HasMemoryCostStructural> HasMemoryCost for T {
59 fn memory_cost(&self, et: EnabledToken) -> usize {
60 size_of::<T>() //
61 .saturating_add(
62 //
63 <T as HasMemoryCostStructural>::indirect_memory_cost(self, et),
64 )
65 }
66}
67
68//---------- specific implementations ----------
69
70/// Implement [`HasMemoryCostStructural`] for `Copy` types
71///
72/// The [`indirect_memory_cost`](HasMemoryCostStructural::indirect_memory_cost)
73/// of a `Copy + 'static` type is zero.
74///
75/// This macro implements that.
76///
77/// Ideally, we would `impl <T: Copy + 'static> MemoryCostStructural for T`.
78/// But that falls foul of trait coherence rules.
79/// So instead we provide `memory_cost_structural_copy!`
80/// and the `#[deftly(has_memory_cost(copy))]` attribute.
81///
82/// This macro can only be used within `tor-memquota`, or for types local to your crate.
83/// For other types, use `#[deftly(has_memory_cost(copy))]` on each field of that type.
84//
85// Unfortunately we can't provide a blanket impl of `HasMemoryCostStructural`
86// for all `Copy` types, because we want to provide `HasMemoryCostStructural`
87// for `Vec` and `Box` -
88// and rustic thinks that those might become `Copy` in the future.
89#[macro_export]
90macro_rules! memory_cost_structural_copy { { $($ty:ty),* $(,)? } => { $(
91 impl $crate::HasMemoryCostStructural for $ty {
92 fn indirect_memory_cost(&self, _et: $crate::EnabledToken) -> usize {
93 $crate::assert_copy_static::<$ty>(self);
94 0
95 }
96 }
97)* } }
98
99memory_cost_structural_copy! {
100 u8, u16, u32, u64, usize,
101 i8, i16, i32, i64, isize,
102 // Generic NonZero<T> impl isn't possible, so we use qualified types. See:
103 // https://github.com/rust-lang/rust/issues/142966
104 std::num::NonZeroU8, std::num::NonZeroU16, std::num::NonZeroU32, std::num::NonZeroU64,
105 std::num::NonZeroI8, std::num::NonZeroI16, std::num::NonZeroI32, std::num::NonZeroI64,
106 std::num::NonZeroUsize,
107 std::num::NonZeroIsize,
108 std::net::IpAddr, std::net::Ipv4Addr, std::net::Ipv6Addr,
109}
110
111/// Implement HasMemoryCost for tuples
112macro_rules! memory_cost_structural_tuples { {
113 // Recursive case: do base case for this input, and then the next inputs
114 $($T:ident)* - $U0:ident $($UN:ident)*
115} => {
116 memory_cost_structural_tuples! { $($T)* - }
117 memory_cost_structural_tuples! { $($T)* $U0 - $($UN)* }
118}; {
119 // Base case, implement for the tuple with contents types $T
120 $($T:ident)* -
121} => { paste! {
122 impl < $(
123 $T: HasMemoryCostStructural,
124 )* > HasMemoryCostStructural for ( $(
125 $T,
126 )* ) {
127 fn indirect_memory_cost(&self, #[allow(unused)] et: EnabledToken) -> usize {
128 let ( $(
129 [< $T:lower >],
130 )* ) = self;
131 0_usize $(
132 .saturating_add([< $T:lower >].indirect_memory_cost(et))
133 )*
134 }
135 }
136} } }
137memory_cost_structural_tuples! { - A B C D E F G H I J K L M N O P Q R S T U V W X Y Z }
138
139impl<T: HasMemoryCostStructural> HasMemoryCostStructural for Option<T> {
140 fn indirect_memory_cost(&self, et: EnabledToken) -> usize {
141 if let Some(t) = self {
142 <T as HasMemoryCostStructural>::indirect_memory_cost(t, et)
143 } else {
144 0
145 }
146 }
147}
148
149impl<T: HasMemoryCostStructural, const N: usize> HasMemoryCostStructural for [T; N] {
150 fn indirect_memory_cost(&self, et: EnabledToken) -> usize {
151 self.iter()
152 .map(|t| t.indirect_memory_cost(et))
153 .fold(0, usize::saturating_add)
154 }
155}
156
157impl<T: HasMemoryCostStructural> HasMemoryCostStructural for Box<T> {
158 fn indirect_memory_cost(&self, et: EnabledToken) -> usize {
159 <T as HasMemoryCost>::memory_cost(&**self, et)
160 }
161}
162
163impl<T: HasMemoryCostStructural> HasMemoryCostStructural for Vec<T> {
164 fn indirect_memory_cost(&self, et: EnabledToken) -> usize {
165 chain!(
166 [size_of::<T>().saturating_mul(self.capacity())],
167 self.iter().map(|t| t.indirect_memory_cost(et)),
168 )
169 .fold(0, usize::saturating_add)
170 }
171}
172
173impl HasMemoryCostStructural for String {
174 fn indirect_memory_cost(&self, _et: EnabledToken) -> usize {
175 self.capacity()
176 }
177}
178
179//------------------- derive macro ----------
180
181define_derive_deftly! {
182 /// Derive `HasMemoryCost`
183 ///
184 /// Each field must implement [`HasMemoryCostStructural`].
185 ///
186 /// Valid for structs and enums.
187 ///
188 /// ### Top-level attributes
189 ///
190 /// * **`#[deftly(has_memory_cost(bounds = "BOUNDS"))]`**:
191 /// Additional bounds to apply to the implementation.
192 ///
193 /// ### Field attributes
194 ///
195 /// * **`#[deftly(has_memory_cost(copy))]`**:
196 /// This field is `Copy + 'static` so does not reference any data that should be accounted.
197 /// * **`#[deftly(has_memory_cost(indirect_fn = "FUNCTION"))]`**:
198 /// `FUNCTION` is a function with the signature and semantics of
199 /// [`HasMemoryCostStructural::indirect_memory_cost`],
200 /// * **`#[deftly(has_memory_cost(indirect_size = "EXPR"))]`**:
201 /// `EXPR` is an expression of type usize with the semantics of a return value from
202 /// [`HasMemoryCostStructural::indirect_memory_cost`].
203 ///
204 /// With one of these, the field doesn't need to implement `HasMemoryCostStructural`.
205 ///
206 /// # Example
207 ///
208 /// ```
209 /// use derive_deftly::Deftly;
210 /// use std::mem::size_of;
211 /// use tor_memquota_cost::{HasMemoryCost, HasMemoryCostStructural};
212 /// use tor_memquota_cost::derive_deftly_template_HasMemoryCost;
213 ///
214 /// #[derive(Deftly)]
215 /// #[derive_deftly(HasMemoryCost)]
216 /// #[deftly(has_memory_cost(bounds = "Data: HasMemoryCostStructural"))]
217 /// struct Struct<Data> {
218 /// data: Data,
219 ///
220 /// #[deftly(has_memory_cost(indirect_size = "0"))] // this is a good guess
221 /// num: serde_json::Number,
222 ///
223 /// #[deftly(has_memory_cost(copy))]
224 /// msg: &'static str,
225 ///
226 /// #[deftly(has_memory_cost(indirect_fn = "|info, _et| String::capacity(info)"))]
227 /// info: safelog::Sensitive<String>,
228 /// }
229 ///
230 /// let s = Struct {
231 /// data: String::with_capacity(33),
232 /// num: serde_json::Number::from_f64(0.0).unwrap(),
233 /// msg: "hello",
234 /// info: String::with_capacity(12).into(),
235 /// };
236 ///
237 /// let Some(et) = tor_memquota_cost::EnabledToken::new_if_compiled_in() else { return };
238 ///
239 /// assert_eq!(
240 /// s.memory_cost(et),
241 /// size_of::<Struct<String>>() + 33 + 12,
242 /// );
243 /// ```
244 export HasMemoryCost expect items, beta_deftly:
245
246 ${impl $crate::HasMemoryCostStructural
247 where $twheres
248 ${tmeta(has_memory_cost(bounds)) as token_stream, default {}}
249 }
250 {
251 fn indirect_memory_cost(&self, #[allow(unused)] et: $crate::EnabledToken) -> usize {
252 ${define F_INDIRECT_COST {
253 ${select1
254 fmeta(has_memory_cost(copy)) {
255 {
256 $crate::assert_copy_static::<$ftype>(&$fpatname);
257 0
258 }
259 }
260 fmeta(has_memory_cost(indirect_fn)) {
261 ${fmeta(has_memory_cost(indirect_fn)) as expr}(&$fpatname, et)
262 }
263 fmeta(has_memory_cost(indirect_size)) {
264 ${fmeta(has_memory_cost(indirect_size)) as expr}
265 }
266 else {
267 <$ftype as $crate::HasMemoryCostStructural>::indirect_memory_cost(&$fpatname, et)
268 }
269 }
270 }}
271
272 match self {
273 $(
274 $vpat => {
275 0_usize
276 ${for fields {
277 .saturating_add( $F_INDIRECT_COST )
278 }}
279 }
280 )
281 }
282 }
283 }
284}
285
286#[cfg(all(test, feature = "memquota"))]
287mod test {
288 // @@ begin test lint list maintained by maint/add_warning @@
289 #![allow(clippy::bool_assert_comparison)]
290 #![allow(clippy::clone_on_copy)]
291 #![allow(clippy::dbg_macro)]
292 #![allow(clippy::mixed_attributes_style)]
293 #![allow(clippy::print_stderr)]
294 #![allow(clippy::print_stdout)]
295 #![allow(clippy::single_char_pattern)]
296 #![allow(clippy::unwrap_used)]
297 #![allow(clippy::unchecked_time_subtraction)]
298 #![allow(clippy::useless_vec)]
299 #![allow(clippy::needless_pass_by_value)]
300 #![allow(clippy::string_slice)] // See arti#2571
301 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
302 #![allow(clippy::arithmetic_side_effects)] // don't mind potential panicking ops in tests
303
304 use super::*;
305 use derive_deftly::Deftly;
306
307 #[derive(Deftly)]
308 #[derive_deftly(HasMemoryCost)]
309 enum E {
310 U(usize),
311 B(Box<u32>),
312 }
313
314 #[derive(Deftly, Default)]
315 #[derive_deftly(HasMemoryCost)]
316 struct S {
317 u: usize,
318 b: Box<u32>,
319 v: Vec<u32>,
320 ev: Vec<E>,
321 }
322
323 const ET: EnabledToken = EnabledToken::new();
324
325 // The size of a u32 is always 4 bytes, so we just write "4" rather than u32::SIZE.
326
327 #[test]
328 fn structs() {
329 assert_eq!(S::default().memory_cost(ET), size_of::<S>() + 4);
330 assert_eq!(E::U(0).memory_cost(ET), size_of::<E>());
331 assert_eq!(E::B(Box::default()).memory_cost(ET), size_of::<E>() + 4);
332 }
333
334 #[test]
335 fn values() {
336 let mut v: Vec<u32> = Vec::with_capacity(10);
337 v.push(1);
338
339 let s = S {
340 u: 0,
341 b: Box::new(42),
342 v,
343 ev: vec![],
344 };
345
346 assert_eq!(
347 s.memory_cost(ET),
348 size_of::<S>() + 4 /* b */ + 10 * 4, /* v buffer */
349 );
350 }
351
352 #[test]
353 #[allow(clippy::identity_op)]
354 fn nest() {
355 let mut ev = Vec::with_capacity(10);
356 ev.push(E::U(42));
357 ev.push(E::B(Box::new(42)));
358
359 let s = S { ev, ..S::default() };
360
361 assert_eq!(
362 s.memory_cost(ET),
363 size_of::<S>() + 4 /* b */ + 0 /* v */ + size_of::<E>() * 10 /* ev buffer */ + 4 /* E::B */
364 );
365 }
366}