Skip to main content

mem_dbg/
lib.rs

1/*
2 * SPDX-FileCopyrightText: 2023 Tommaso Fontana
3 * SPDX-FileCopyrightText: 2023 Inria
4 * SPDX-FileCopyrightText: 2023 Sebastiano Vigna
5 *
6 * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
7 */
8#![doc = include_str!("../README.md")]
9#![deny(unconditional_recursion)]
10#![cfg_attr(not(feature = "std"), no_std)]
11#[cfg(not(feature = "std"))]
12extern crate alloc;
13
14#[cfg(not(feature = "std"))]
15use alloc::string::String;
16
17// HashMap for pointer deduplication in mem_size (re-exported for derive macro)
18pub use hashbrown::HashMap;
19// HashSet for pointer deduplication in mem_dbg
20pub use hashbrown::HashSet;
21
22#[cfg(feature = "derive")]
23pub use mem_dbg_derive::{MemDbg, MemSize};
24
25mod impl_mem_dbg;
26mod impl_mem_size;
27
28mod utils;
29pub use utils::*;
30
31/// Internal trait used within [`FlatType`] to implement [`MemSize`] depending
32/// on whether a type is flat or not.
33///
34/// It has only two implementations, [`True`] and [`False`].
35///
36/// The `And` associated type computes the logical AND with another [`Boolean`]:
37/// - `True::And<B> = B` (true AND x = x)
38/// - `False::And<B> = False` (false AND x = false)
39///
40/// This is used to determine [`FlatType`] for composite types like tuples:
41/// a tuple is `FlatType<Flat=True>` only if all its components are
42/// `FlatType<Flat=True>`.
43pub trait Boolean {
44    type And<B: Boolean>: Boolean;
45    const VALUE: bool;
46}
47
48/// One of the two possible implementations of [`Boolean`].
49pub struct True {}
50impl Boolean for True {
51    type And<B: Boolean> = B;
52    const VALUE: bool = true;
53}
54
55/// One of the two possible implementations of [`Boolean`].
56pub struct False {}
57impl Boolean for False {
58    type And<B: Boolean> = False;
59    const VALUE: bool = false;
60}
61
62/// How to display a reference address in [`MemDbgImpl::_mem_dbg_depth_on_impl`].
63#[derive(Clone, Copy)]
64pub enum RefDisplay {
65    /// No address display.
66    None,
67    /// First encounter of a reference: display `@ 0x...` after type name.
68    FirstEncounter(usize),
69    /// Back-reference to already-seen pointer: display `→ 0x...`, use pointer size, skip recursion.
70    BackReference(usize),
71}
72
73/// Marker trait for flat types.
74///
75/// "Flat" means that the type has no heap indirection to account for, so its
76/// size can be computed using [`size_of`]. The scope of the trait is slightly
77/// wider than that of [`Copy`] because, for example, atomic types are not
78/// [`Copy`] but they are considered to be flat. In a non-flat type the
79/// computation of size must be done by iterating recursively on the size of the
80/// fields.
81///
82/// The trait comes in two flavors: `FlatType<Flat=True>` and
83/// `FlatType<Flat=False>`. In the first case, [`MemSize::mem_size`] can be
84/// computed on arrays, vectors, slices, and supported containers by multiplying
85/// the length or capacity by the size of the element type; in the second case,
86/// it is necessary to iterate on each element.
87///
88/// Since we cannot use negative trait bounds, every type that is used as a
89/// parameter of an array, vector, slice, or container type, must implement
90/// either `FlatType<Flat=True>` or `FlatType<Flat=False>`. If you do not
91/// implement either of these traits, you will not be able to compute the size
92/// of arrays, vectors, slices, and containers, but error messages will be very
93/// unhelpful due to the contrived way we have to implement mutually exclusive
94/// types [working around the bug that prevents the compiler from understanding
95/// that implementations for the two flavors of `FlatType` are mutually
96/// exclusive](https://github.com/rust-lang/rfcs/pull/1672#issuecomment-1405377983).
97///
98/// If you use the provided derive macros all this logic will be hidden from
99/// you. You'll just have to add the attribute `#[mem_size(flat)]` to your
100/// structures if they are flat types that do not contain non-`'static`
101/// references (typically [`Copy`] + `'static` types, but this is not enforced).
102///
103/// When all fields of a struct or enum implement `FlatType<Flat=True>` but the
104/// type itself is not annotated with `#[mem_size(flat)]`, a compile-time error
105/// will suggest adding `#[mem_size(flat)]` (if the type is flat) or
106/// `#[mem_size(rec)]` (to explicitly opt out of the optimization and silence
107/// the error).
108///
109/// For example, the following will not compile because `usize` implements
110/// `FlatType<Flat=True>`, but the struct is not annotated with
111/// `#[mem_size(flat)]` or `#[mem_size(rec)]`:
112///
113/// ```compile_fail
114/// #[derive(mem_dbg::MemSize)]
115/// struct MyStruct(usize);
116/// ```
117///
118/// Note that this approach forces us to compute the size of non-flat types that
119/// contain references by iteration _even if you do not specify_
120/// [`SizeFlags::FOLLOW_REFS`].
121pub trait FlatType {
122    type Flat: Boolean;
123}
124
125bitflags::bitflags! {
126    /// Flags for [`MemSize`].
127    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
128    pub struct SizeFlags: u32 {
129        /// Follow references.
130        ///
131        /// By default [`MemSize::mem_size`] does not follow references and
132        /// computes only the size of the reference itself. Note that the size
133        /// of every reference will be added once (i.e., if you have two
134        /// identical references to the same memory region, the size of that
135        /// region will be added only once).
136        const FOLLOW_REFS = 1 << 0;
137        /// Return capacity instead of size.
138        ///
139        /// Size does not include memory allocated but not used: for example, in
140        /// the case of a vector [`MemSize::mem_size`] calls [`Vec::len`] rather
141        /// than [`Vec::capacity`].
142        ///
143        /// However, when this flag is specified [`MemSize::mem_size`] will
144        /// return the size of all memory allocated, even if it is not used: for
145        /// example, in the case of a vector this option makes
146        /// [`MemSize::mem_size`] call [`Vec::capacity`] rather than
147        /// [`Vec::len`].
148        const CAPACITY = 1 << 1;
149        /// Follow counted references (i.e., [`Rc`](std::rc::Rc) and
150        /// [`Arc`](std::sync::Arc)).
151        ///
152        /// By default [`MemSize::mem_size`] does not follow counted references
153        /// and computes only the size of the reference itself. Note that the
154        /// size of every counted reference will be added once (i.e., if you
155        /// have two identical counted references to the same memory region,
156        /// the size of that region will be added only once).
157        const FOLLOW_RCS = 1 << 2;
158    }
159}
160
161impl Default for SizeFlags {
162    /// The default set of flags is the empty set.
163    fn default() -> Self {
164        Self::empty()
165    }
166}
167
168/// A trait to compute recursively the overall size or capacity of a structure,
169/// as opposed to the stack size returned by [`core::mem::size_of()`].
170///
171/// You can derive this trait with `#[derive(MemSize)]` if all the fields of
172/// your type implement [`MemSize`].
173///
174/// When implementing this trait manually, you should implement
175/// [`mem_size_rec`](MemSize::mem_size_rec).
176pub trait MemSize {
177    /// Returns the (recursively computed) overall memory size of the structure
178    /// in bytes.
179    fn mem_size(&self, flags: SizeFlags) -> usize {
180        let mut refs = HashMap::new();
181        let base = self.mem_size_rec(flags, &mut refs);
182        base + refs.into_values().sum::<usize>()
183    }
184
185    /// Recursive implementation that tracks visited references for deduplication.
186    ///
187    /// The parameter `refs` is a map from pointer addresses (coming from
188    /// references) to their computed size that is used to count the space
189    /// occupied by references only once in case any of the flags
190    /// [`SizeFlags::FOLLOW_REFS`] or [`SizeFlags::FOLLOW_RCS`] is set.
191    ///
192    /// In case of custom (non-derive) implementations: types that do not
193    /// contain references can simply ignore the `refs` parameter; otherwise,
194    /// please have a look at the [implementation for
195    /// references](#impl-MemSize-for-%26T).
196    fn mem_size_rec(&self, flags: SizeFlags, refs: &mut HashMap<usize, usize>) -> usize;
197}
198
199bitflags::bitflags! {
200    /// Flags for [`MemDbg`].
201    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
202    pub struct DbgFlags: u32 {
203        /// Follow references. See [`SizeFlags::FOLLOW_REFS`].
204        const FOLLOW_REFS = 1 << 0;
205        /// Print memory usage in human readable format.
206        const HUMANIZE = 1 << 1;
207        /// Print memory usage as a percentage.
208        const PERCENTAGE = 1 << 2;
209        /// Print the type name.
210        const TYPE_NAME = 1 << 3;
211        /// Display capacity instead of size. See [`SizeFlags::CAPACITY`].
212        const CAPACITY = 1 << 4;
213        /// Add an underscore every 3 digits, when `HUMANIZE` is not set.
214        const SEPARATOR = 1 << 5;
215        /// Print fields in memory order (i.e., using the layout chosen by the
216        /// compiler), rather than in declaration order.
217        const RUST_LAYOUT = 1 << 6;
218        /// Use colors to distinguish sizes.
219        const COLOR = 1 << 7;
220        /// Follow counted references. See [`SizeFlags::FOLLOW_RCS`].
221        const FOLLOW_RCS = 1 << 8;
222    }
223}
224
225impl DbgFlags {
226    /// Translates flags that are in common with [`MemSize`] into [`SizeFlags`].
227    pub const fn to_size_flags(&self) -> SizeFlags {
228        let mut flags = SizeFlags::empty();
229        if self.contains(DbgFlags::FOLLOW_REFS) {
230            flags = flags.union(SizeFlags::FOLLOW_REFS);
231        }
232        if self.contains(DbgFlags::CAPACITY) {
233            flags = flags.union(SizeFlags::CAPACITY);
234        }
235        if self.contains(DbgFlags::FOLLOW_RCS) {
236            flags = flags.union(SizeFlags::FOLLOW_RCS);
237        }
238        flags
239    }
240}
241
242impl Default for DbgFlags {
243    /// The default set of flags contains [`DbgFlags::TYPE_NAME`],
244    /// [`DbgFlags::SEPARATOR`], and [`DbgFlags::PERCENTAGE`].
245    fn default() -> Self {
246        Self::TYPE_NAME | Self::SEPARATOR | Self::PERCENTAGE
247    }
248}
249
250/// A trait providing methods to display recursively the content and size of a
251/// structure.
252///
253/// You can derive this trait with `#[derive(MemDbg)]` if all the fields of your
254/// type implement [`MemDbg`]. Note that you will also need to derive
255/// [`MemSize`].
256pub trait MemDbg: MemDbgImpl {
257    /// Writes to stderr debug info about the structure memory usage, expanding
258    /// all levels of nested structures.
259    #[cfg(feature = "std")]
260    fn mem_dbg(&self, flags: DbgFlags) -> core::fmt::Result {
261        self._mem_dbg_depth(
262            <Self as MemSize>::mem_size(self, flags.to_size_flags()),
263            usize::MAX,
264            core::mem::size_of_val(self),
265            flags,
266        )
267    }
268
269    /// Writes to a [`core::fmt::Write`] debug info about the structure memory
270    /// usage, expanding all levels of nested structures.
271    fn mem_dbg_on(&self, writer: &mut impl core::fmt::Write, flags: DbgFlags) -> core::fmt::Result {
272        let mut dbg_refs = HashSet::new();
273        self._mem_dbg_depth_on(
274            writer,
275            <Self as MemSize>::mem_size(self, flags.to_size_flags()),
276            usize::MAX,
277            &mut String::new(),
278            Some("⏺"),
279            true,
280            core::mem::size_of_val(self),
281            flags,
282            &mut dbg_refs,
283        )
284    }
285
286    /// Writes to stderr debug info about the structure memory usage as
287    /// [`mem_dbg`](MemDbg::mem_dbg), but expanding only up to `max_depth`
288    /// levels of nested structures.
289    #[cfg(feature = "std")]
290    fn mem_dbg_depth(&self, max_depth: usize, flags: DbgFlags) -> core::fmt::Result {
291        self._mem_dbg_depth(
292            <Self as MemSize>::mem_size(self, flags.to_size_flags()),
293            max_depth,
294            core::mem::size_of_val(self),
295            flags,
296        )
297    }
298
299    /// Writes to a [`core::fmt::Write`] debug info about the structure memory
300    /// usage as [`mem_dbg_on`](MemDbg::mem_dbg_on), but expanding only up to
301    /// `max_depth` levels of nested structures.
302    fn mem_dbg_depth_on(
303        &self,
304        writer: &mut impl core::fmt::Write,
305        max_depth: usize,
306        flags: DbgFlags,
307    ) -> core::fmt::Result {
308        let mut dbg_refs = HashSet::new();
309        self._mem_dbg_depth_on(
310            writer,
311            <Self as MemSize>::mem_size(self, flags.to_size_flags()),
312            max_depth,
313            &mut String::new(),
314            Some("⏺"),
315            true,
316            core::mem::size_of_val(self),
317            flags,
318            &mut dbg_refs,
319        )
320    }
321}
322
323/// Implements [`MemDbg`] for all types that implement [`MemDbgImpl`].
324///
325/// This is done so that no one can change the implementation of [`MemDbg`],
326/// which ensures consistency in printing.
327impl<T: MemDbgImpl> MemDbg for T {}
328
329/// Inner trait used to implement [`MemDbg`].
330///
331/// This trait should not be implemented by users, which should use the
332/// [`MemDbg`](mem_dbg_derive::MemDbg) derive macro instead.
333///
334/// The default no-op implementation is used by all types in which it does not
335/// make sense, or it is impossible, to recurse.
336#[allow(clippy::too_many_arguments)]
337pub trait MemDbgImpl: MemSize {
338    fn _mem_dbg_rec_on(
339        &self,
340        _writer: &mut impl core::fmt::Write,
341        _total_size: usize,
342        _max_depth: usize,
343        _prefix: &mut String,
344        _is_last: bool,
345        _flags: DbgFlags,
346        _dbg_refs: &mut HashSet<usize>,
347    ) -> core::fmt::Result {
348        Ok(())
349    }
350
351    #[cfg(feature = "std")]
352    #[doc(hidden)]
353    fn _mem_dbg_depth(
354        &self,
355        total_size: usize,
356        max_depth: usize,
357        padded_size: usize,
358        flags: DbgFlags,
359    ) -> core::fmt::Result {
360        struct Wrapper(std::io::Stderr);
361        impl core::fmt::Write for Wrapper {
362            #[inline(always)]
363            fn write_str(&mut self, s: &str) -> core::fmt::Result {
364                use std::io::Write;
365                self.0
366                    .lock()
367                    .write(s.as_bytes())
368                    .map_err(|_| core::fmt::Error)
369                    .map(|_| ())
370            }
371        }
372        let mut dbg_refs = HashSet::new();
373        self._mem_dbg_depth_on(
374            &mut Wrapper(std::io::stderr()),
375            total_size,
376            max_depth,
377            &mut String::new(),
378            Some("⏺"),
379            true,
380            padded_size,
381            flags,
382            &mut dbg_refs,
383        )
384    }
385
386    fn _mem_dbg_depth_on(
387        &self,
388        writer: &mut impl core::fmt::Write,
389        total_size: usize,
390        max_depth: usize,
391        prefix: &mut String,
392        field_name: Option<&str>,
393        is_last: bool,
394        padded_size: usize,
395        flags: DbgFlags,
396        dbg_refs: &mut HashSet<usize>,
397    ) -> core::fmt::Result {
398        self._mem_dbg_depth_on_impl(
399            writer,
400            total_size,
401            max_depth,
402            prefix,
403            field_name,
404            is_last,
405            padded_size,
406            flags,
407            dbg_refs,
408            RefDisplay::None,
409        )
410    }
411
412    /// Internal implementation for depth display.
413    ///
414    /// The `ref_display` parameter controls how reference addresses are shown:
415    /// - `RefDisplay::None`: no address display
416    /// - `RefDisplay::FirstEncounter(ptr)`: show `@ 0x...` after type name
417    /// - `RefDisplay::BackReference(ptr)`: show `→ 0x...`, use pointer size, skip recursion
418    #[allow(clippy::too_many_arguments)]
419    fn _mem_dbg_depth_on_impl(
420        &self,
421        writer: &mut impl core::fmt::Write,
422        total_size: usize,
423        max_depth: usize,
424        prefix: &mut String,
425        field_name: Option<&str>,
426        is_last: bool,
427        padded_size: usize,
428        flags: DbgFlags,
429        dbg_refs: &mut HashSet<usize>,
430        ref_display: RefDisplay,
431    ) -> core::fmt::Result {
432        // Each depth level adds 2 characters to prefix ("│ " or "  ")
433        // Use chars().count() since prefix contains multi-byte UTF-8 chars
434        if prefix.chars().count() / 2 > max_depth {
435            return Ok(());
436        }
437
438        // For back-references, use pointer size; otherwise compute full size
439        let is_backref = matches!(ref_display, RefDisplay::BackReference(_));
440        let display_size = if is_backref {
441            core::mem::size_of_val(self)
442        } else {
443            <Self as MemSize>::mem_size(self, flags.to_size_flags())
444        };
445
446        if flags.contains(DbgFlags::COLOR) {
447            let color = utils::color(display_size);
448            writer.write_fmt(format_args!("{color}"))?;
449        };
450
451        if flags.contains(DbgFlags::HUMANIZE) {
452            let (value, uom) = crate::utils::humanize_float(display_size);
453            if uom == " B" {
454                writer.write_fmt(format_args!("{:>5}  B ", display_size))?;
455            } else {
456                let precision = if value >= 100.0 {
457                    1
458                } else if value >= 10.0 {
459                    2
460                } else if value >= 1.0 {
461                    3
462                } else {
463                    4
464                };
465                writer.write_fmt(format_args!("{0:>4.1$} {2} ", value, precision, uom))?;
466            }
467        } else if flags.contains(DbgFlags::SEPARATOR) {
468            let mut align = crate::utils::n_of_digits(total_size);
469            let mut size_for_sep = display_size;
470            align += align / 3;
471            let mut digits = crate::utils::n_of_digits(size_for_sep);
472            let digit_align = digits + digits / 3;
473            for _ in digit_align..align {
474                writer.write_char(' ')?;
475            }
476
477            let first_digits = digits % 3;
478            let mut multiplier = 10_usize.pow((digits - first_digits) as u32);
479            if first_digits != 0 {
480                writer.write_fmt(format_args!("{}", size_for_sep / multiplier))?;
481            } else {
482                multiplier /= 1000;
483                digits -= 3;
484                writer.write_fmt(format_args!(" {}", size_for_sep / multiplier))?;
485            }
486
487            while digits >= 3 {
488                size_for_sep %= multiplier;
489                multiplier /= 1000;
490                writer.write_fmt(format_args!("_{:03}", size_for_sep / multiplier))?;
491                digits -= 3;
492            }
493
494            writer.write_str(" B ")?;
495        } else {
496            let align = crate::utils::n_of_digits(total_size);
497            writer.write_fmt(format_args!("{:>align$} B ", display_size))?;
498        }
499
500        if flags.contains(DbgFlags::PERCENTAGE) {
501            writer.write_fmt(format_args!(
502                "{:>6.2}% ",
503                if total_size == 0 {
504                    100.0
505                } else {
506                    100.0 * display_size as f64 / total_size as f64
507                }
508            ))?;
509        }
510        if flags.contains(DbgFlags::COLOR) {
511            let reset_color = utils::reset_color();
512            writer.write_fmt(format_args!("{reset_color}"))?;
513        };
514        if !prefix.is_empty() {
515            // Find the byte index of the 3rd character
516            let start_byte = prefix
517                .char_indices()
518                .nth(2) // Skip 2 characters to get to the 3rd
519                .map(|(idx, _)| idx)
520                .unwrap_or(prefix.len());
521            writer.write_str(&prefix[start_byte..])?;
522            if is_last {
523                writer.write_char('╰')?;
524            } else {
525                writer.write_char('├')?;
526            }
527            writer.write_char('╴')?;
528        }
529
530        if let Some(field_name) = field_name {
531            writer.write_fmt(format_args!("{}", field_name))?;
532        }
533
534        if flags.contains(DbgFlags::TYPE_NAME) {
535            if flags.contains(DbgFlags::COLOR) {
536                writer.write_fmt(format_args!("{}", utils::type_color()))?;
537            }
538            writer.write_fmt(format_args!(": {}", core::any::type_name::<Self>()))?;
539            if flags.contains(DbgFlags::COLOR) {
540                writer.write_fmt(format_args!("{}", utils::reset_color()))?;
541            }
542        }
543
544        // Display reference address based on RefDisplay
545        match ref_display {
546            RefDisplay::FirstEncounter(ptr) => {
547                if flags.contains(DbgFlags::COLOR) {
548                    writer.write_fmt(format_args!("{}", utils::ref_color()))?;
549                }
550                writer.write_fmt(format_args!(
551                    " @ 0x{:0width$x}",
552                    ptr,
553                    width = 2 * core::mem::size_of::<usize>()
554                ))?;
555                if flags.contains(DbgFlags::COLOR) {
556                    writer.write_fmt(format_args!("{}", utils::reset_color()))?;
557                }
558            }
559            RefDisplay::BackReference(ptr) => {
560                if flags.contains(DbgFlags::COLOR) {
561                    writer.write_fmt(format_args!("{}", utils::backref_color()))?;
562                }
563                writer.write_fmt(format_args!(
564                    " → 0x{:0width$x}",
565                    ptr,
566                    width = 2 * core::mem::size_of::<usize>()
567                ))?;
568                if flags.contains(DbgFlags::COLOR) {
569                    writer.write_fmt(format_args!("{}", utils::reset_color()))?;
570                }
571            }
572            RefDisplay::None => {}
573        }
574
575        // Skip padding and recursion for back-references
576        if !is_backref {
577            let padding = padded_size - core::mem::size_of_val(self);
578
579            if padding != 0 {
580                writer.write_fmt(format_args!(" [{}B]", padding))?;
581            }
582        }
583
584        writer.write_char('\n')?;
585
586        // Skip recursion for back-references
587        if !is_backref {
588            if is_last {
589                prefix.push_str("  ");
590            } else {
591                prefix.push_str("│ ");
592            }
593
594            self._mem_dbg_rec_on(
595                writer, total_size, max_depth, prefix, is_last, flags, dbg_refs,
596            )?;
597
598            prefix.pop();
599            prefix.pop();
600        }
601
602        Ok(())
603    }
604}