1
//! Deriving `HasMemoryCost`
2

            
3
use crate::{EnabledToken, HasMemoryCost};
4
use derive_deftly::define_derive_deftly;
5
use itertools::chain;
6
use 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.
44
pub 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.
56
417349106
pub fn assert_copy_static<T: Copy + 'static>(_: &T) {}
57

            
58
impl<T: HasMemoryCostStructural> HasMemoryCost for T {
59
314608
    fn memory_cost(&self, et: EnabledToken) -> usize {
60
314608
        size_of::<T>() //
61
314608
            .saturating_add(
62
                //
63
314608
                <T as HasMemoryCostStructural>::indirect_memory_cost(self, et),
64
            )
65
314608
    }
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]
90
macro_rules! memory_cost_structural_copy { { $($ty:ty),* $(,)? } => { $(
91
    impl $crate::HasMemoryCostStructural for $ty {
92
417338970
        fn indirect_memory_cost(&self, _et: $crate::EnabledToken) -> usize {
93
417338970
            $crate::assert_copy_static::<$ty>(self);
94
417338970
            0
95
417338970
        }
96
    }
97
)* } }
98

            
99
memory_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
112
macro_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
9092
        fn indirect_memory_cost(&self, #[allow(unused)] et: EnabledToken) -> usize {
128
            let ( $(
129
9092
                [< $T:lower >],
130
9092
            )* ) = self;
131
            0_usize $(
132
9092
                .saturating_add([< $T:lower >].indirect_memory_cost(et))
133
            )*
134
9092
        }
135
    }
136
} } }
137
memory_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

            
139
impl<T: HasMemoryCostStructural> HasMemoryCostStructural for Option<T> {
140
10460
    fn indirect_memory_cost(&self, et: EnabledToken) -> usize {
141
10460
        if let Some(t) = self {
142
8996
            <T as HasMemoryCostStructural>::indirect_memory_cost(t, et)
143
        } else {
144
1464
            0
145
        }
146
10460
    }
147
}
148

            
149
impl<T: HasMemoryCostStructural, const N: usize> HasMemoryCostStructural for [T; N] {
150
295488
    fn indirect_memory_cost(&self, et: EnabledToken) -> usize {
151
295488
        self.iter()
152
150403392
            .map(|t| t.indirect_memory_cost(et))
153
295488
            .fold(0, usize::saturating_add)
154
295488
    }
155
}
156

            
157
impl<T: HasMemoryCostStructural> HasMemoryCostStructural for Box<T> {
158
295498
    fn indirect_memory_cost(&self, et: EnabledToken) -> usize {
159
295498
        <T as HasMemoryCost>::memory_cost(&**self, et)
160
295498
    }
161
}
162

            
163
impl<T: HasMemoryCostStructural> HasMemoryCostStructural for Vec<T> {
164
379996
    fn indirect_memory_cost(&self, et: EnabledToken) -> usize {
165
379996
        chain!(
166
379996
            [size_of::<T>().saturating_mul(self.capacity())],
167
185110573
            self.iter().map(|t| t.indirect_memory_cost(et)),
168
        )
169
379996
        .fold(0, usize::saturating_add)
170
379996
    }
171
}
172

            
173
impl HasMemoryCostStructural for String {
174
    fn indirect_memory_cost(&self, _et: EnabledToken) -> usize {
175
        self.capacity()
176
    }
177
}
178

            
179
//------------------- derive macro ----------
180

            
181
define_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
1105618
        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"))]
287
mod 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
}