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}