Skip to main content

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:
245
246    impl<$tgens> $crate::HasMemoryCostStructural for $ttype
247    where $twheres ${if tmeta(has_memory_cost(bounds)) {
248              ${tmeta(has_memory_cost(bounds)) as token_stream}
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    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
301    #![allow(clippy::arithmetic_side_effects)] // don't mind potential panicking ops in tests
302
303    use super::*;
304    use derive_deftly::Deftly;
305
306    #[derive(Deftly)]
307    #[derive_deftly(HasMemoryCost)]
308    enum E {
309        U(usize),
310        B(Box<u32>),
311    }
312
313    #[derive(Deftly, Default)]
314    #[derive_deftly(HasMemoryCost)]
315    struct S {
316        u: usize,
317        b: Box<u32>,
318        v: Vec<u32>,
319        ev: Vec<E>,
320    }
321
322    const ET: EnabledToken = EnabledToken::new();
323
324    // The size of a u32 is always 4 bytes, so we just write "4" rather than u32::SIZE.
325
326    #[test]
327    fn structs() {
328        assert_eq!(S::default().memory_cost(ET), size_of::<S>() + 4);
329        assert_eq!(E::U(0).memory_cost(ET), size_of::<E>());
330        assert_eq!(E::B(Box::default()).memory_cost(ET), size_of::<E>() + 4);
331    }
332
333    #[test]
334    fn values() {
335        let mut v: Vec<u32> = Vec::with_capacity(10);
336        v.push(1);
337
338        let s = S {
339            u: 0,
340            b: Box::new(42),
341            v,
342            ev: vec![],
343        };
344
345        assert_eq!(
346            s.memory_cost(ET),
347            size_of::<S>() + 4 /* b */ + 10 * 4, /* v buffer */
348        );
349    }
350
351    #[test]
352    #[allow(clippy::identity_op)]
353    fn nest() {
354        let mut ev = Vec::with_capacity(10);
355        ev.push(E::U(42));
356        ev.push(E::B(Box::new(42)));
357
358        let s = S { ev, ..S::default() };
359
360        assert_eq!(
361            s.memory_cost(ET),
362            size_of::<S>() + 4 /* b */ + 0 /* v */ + size_of::<E>() * 10 /* ev buffer */ + 4 /* E::B */
363        );
364    }
365}