heapster 0.7.0

A lightweight wrapper enhancing the global allocator with useful metrics.
Documentation
use core::{alloc::GlobalAlloc, ops::Sub};

use crate::Heapster;
#[cfg(feature = "histograms")]
use crate::Histogram;

/// A summary of an allocator's stats.
///
/// # Field relationships
///
/// The byte-sum fields fall into two views of the same underlying activity:
///
/// - **Fresh-only** ([`Stats::alloc_sum`], [`Stats::dealloc_sum`]) — bytes
///   from direct [`GlobalAlloc::alloc`] / [`GlobalAlloc::dealloc`] calls,
///   excluding reallocations. Useful for understanding allocation pattern
///   shape independent of realloc churn. Paired with [`Stats::alloc_count`]
///   and [`Stats::dealloc_count`], which count those same events.
///
/// - **Inclusive** ([`Stats::total_alloc_sum`], [`Stats::total_dealloc_sum`])
///   — fresh allocations plus the growth/shrink portion of in-place
///   reallocations. These reflect total bytes the heap was asked to provide
///   or reclaim over the observation window.
///
/// The following invariants hold modulo concurrent allocator activity:
///
/// ```text
/// total_alloc_sum   == alloc_sum   + realloc_growth_sum
/// total_dealloc_sum == dealloc_sum + realloc_shrink_sum
/// total_alloc_sum - total_dealloc_sum == use_curr
/// ```
///
/// Note: snapshots are not atomic across fields; counts and sums may differ
/// by at most one in-flight operation.
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Stats {
    /// The total number of allocations.
    pub alloc_count: usize,
    /// Bytes from fresh [`GlobalAlloc::alloc`] calls. See [`Stats`] for the
    /// fresh-only vs. inclusive distinction.
    pub alloc_sum: u64,
    /// The average size of allocations.
    pub alloc_avg: Option<usize>,
    /// The allocation size buckets.
    #[cfg(feature = "histograms")]
    pub alloc_histogram: Histogram,
    /// The total number of failed allocations.
    pub alloc_fail_count: usize,
    /// The total number of deallocations.
    pub dealloc_count: usize,
    /// Bytes from fresh [`GlobalAlloc::dealloc`] calls. See [`Stats`] for the
    /// fresh-only vs. inclusive distinction.
    pub dealloc_sum: u64,
    /// The average size of deallocations.
    pub dealloc_avg: Option<usize>,
    /// The total number of reallocations.
    /// The total number of reallocations caused by object growth.
    pub realloc_growth_count: usize,
    /// The sum of all growth reallocations.
    pub realloc_growth_sum: u64,
    /// The average size of reallocations caused by object growth.
    pub realloc_growth_avg: Option<usize>,
    /// The growth reallocation size buckets.
    #[cfg(feature = "histograms")]
    pub realloc_growth_histogram: Histogram,
    /// The total number of reallocations caused by object shrinkage.
    pub realloc_shrink_count: usize,
    /// The sum of all shrink reallocations.
    pub realloc_shrink_sum: u64,
    /// The average size of reallocations caused by object shrinkage.
    pub realloc_shrink_avg: Option<usize>,
    /// The shrink reallocation size buckets.
    #[cfg(feature = "histograms")]
    pub realloc_shrink_histogram: Histogram,
    /// The total number of full reallocations.
    #[cfg(feature = "realloc_moves")]
    pub realloc_move_count: usize,
    /// The sum of all full reallocations.
    #[cfg(feature = "realloc_moves")]
    pub realloc_move_sum: u64,
    /// The average size of full reallocations.
    #[cfg(feature = "realloc_moves")]
    pub realloc_move_avg: Option<usize>,
    /// The total number of failed reallocations.
    pub realloc_fail_count: usize,
    /// Total bytes the heap was asked to provide (fresh allocs + realloc growth).
    pub total_alloc_sum: u64,
    /// Total bytes the heap reclaimed (fresh deallocs + realloc shrink).
    pub total_dealloc_sum: u64,
    /// Current heap use; it always shows the current use, even
    /// after calling [`Heapster::reset`] or in the output of
    /// [`Heapster::measure`].
    pub use_curr: usize,
    /// Maximum recorded heap use; when using [`Heapster::measure`], it
    /// indicates how much maximum memory use grew during the measured
    /// operation.
    #[cfg(feature = "use_max")]
    pub use_max: usize,
}

impl<A: GlobalAlloc> Heapster<A> {
    /// Returns a summary of the allocator's stats.
    pub fn stats(&self) -> Stats {
        let alloc_count = self.alloc_count();
        let alloc_sum = self.alloc_sum();
        let alloc_avg = alloc_sum
            .checked_div(alloc_count as u64)
            .map(|avg| avg as usize);
        #[cfg(feature = "histograms")]
        let alloc_histogram = self.alloc_histogram();
        let alloc_fail_count = self.alloc_fail_count();

        let dealloc_count = self.dealloc_count();
        let dealloc_sum = self.dealloc_sum();
        let dealloc_avg = dealloc_sum
            .checked_div(dealloc_count as u64)
            .map(|avg| avg as usize);

        let realloc_growth_count = self.realloc_growth_count();
        let realloc_growth_sum = self.realloc_growth_sum();
        let realloc_growth_avg = realloc_growth_sum
            .checked_div(realloc_growth_count as u64)
            .map(|avg| avg as usize);
        #[cfg(feature = "histograms")]
        let realloc_growth_histogram = self.realloc_growth_histogram();

        let realloc_shrink_count = self.realloc_shrink_count();
        let realloc_shrink_sum = self.realloc_shrink_sum();
        let realloc_shrink_avg = realloc_shrink_sum
            .checked_div(realloc_shrink_count as u64)
            .map(|avg| avg as usize);
        #[cfg(feature = "histograms")]
        let realloc_shrink_histogram = self.realloc_shrink_histogram();

        #[cfg(feature = "realloc_moves")]
        let realloc_move_count = self.realloc_move_count();
        #[cfg(feature = "realloc_moves")]
        let realloc_move_sum = self.realloc_move_sum();
        #[cfg(feature = "realloc_moves")]
        let realloc_move_avg = realloc_move_sum
            .checked_div(realloc_move_count as u64)
            .map(|avg| avg as usize);
        let realloc_fail_count = self.realloc_fail_count();

        let total_alloc_sum = alloc_sum + realloc_growth_sum;
        let total_dealloc_sum = dealloc_sum + realloc_shrink_sum;

        let use_curr = self.use_curr();
        #[cfg(feature = "use_max")]
        let use_max = self.use_max();

        Stats {
            alloc_count,
            alloc_sum,
            alloc_avg,
            #[cfg(feature = "histograms")]
            alloc_histogram,
            alloc_fail_count,
            dealloc_count,
            dealloc_sum,
            dealloc_avg,
            realloc_growth_count,
            realloc_growth_sum,
            realloc_growth_avg,
            #[cfg(feature = "histograms")]
            realloc_growth_histogram,
            realloc_shrink_count,
            realloc_shrink_sum,
            realloc_shrink_avg,
            #[cfg(feature = "histograms")]
            realloc_shrink_histogram,
            #[cfg(feature = "realloc_moves")]
            realloc_move_count,
            #[cfg(feature = "realloc_moves")]
            realloc_move_sum,
            #[cfg(feature = "realloc_moves")]
            realloc_move_avg,
            realloc_fail_count,
            total_alloc_sum,
            total_dealloc_sum,
            use_curr,
            #[cfg(feature = "use_max")]
            use_max,
        }
    }
}

impl Sub<&Stats> for &Stats {
    type Output = Stats;

    fn sub(self, old: &Stats) -> Stats {
        let alloc_count = self.alloc_count.saturating_sub(old.alloc_count);
        let alloc_sum = self.alloc_sum.saturating_sub(old.alloc_sum);
        let alloc_avg = alloc_sum
            .checked_div(alloc_count as u64)
            .map(|avg| avg as usize);
        #[cfg(feature = "histograms")]
        let alloc_histogram = self.alloc_histogram - old.alloc_histogram;
        let alloc_fail_count = self.alloc_fail_count.saturating_sub(old.alloc_fail_count);

        let dealloc_count = self.dealloc_count.saturating_sub(old.dealloc_count);
        let dealloc_sum = self.dealloc_sum.saturating_sub(old.dealloc_sum);
        let dealloc_avg = dealloc_sum
            .checked_div(dealloc_count as u64)
            .map(|avg| avg as usize);

        let realloc_growth_count = self
            .realloc_growth_count
            .saturating_sub(old.realloc_growth_count);
        let realloc_growth_sum = self
            .realloc_growth_sum
            .saturating_sub(old.realloc_growth_sum);
        let realloc_growth_avg = realloc_growth_sum
            .checked_div(realloc_growth_count as u64)
            .map(|avg| avg as usize);
        #[cfg(feature = "histograms")]
        let realloc_growth_histogram = self.realloc_growth_histogram - old.realloc_growth_histogram;

        let realloc_shrink_count = self
            .realloc_shrink_count
            .saturating_sub(old.realloc_shrink_count);
        let realloc_shrink_sum = self
            .realloc_shrink_sum
            .saturating_sub(old.realloc_shrink_sum);
        let realloc_shrink_avg = realloc_shrink_sum
            .checked_div(realloc_shrink_count as u64)
            .map(|avg| avg as usize);
        #[cfg(feature = "histograms")]
        let realloc_shrink_histogram = self.realloc_shrink_histogram - old.realloc_shrink_histogram;

        #[cfg(feature = "realloc_moves")]
        let realloc_move_count = self
            .realloc_move_count
            .saturating_sub(old.realloc_move_count);
        #[cfg(feature = "realloc_moves")]
        let realloc_move_sum = self.realloc_move_sum.saturating_sub(old.realloc_move_sum);
        #[cfg(feature = "realloc_moves")]
        let realloc_move_avg = realloc_move_sum
            .checked_div(realloc_move_count as u64)
            .map(|avg| avg as usize);

        let realloc_fail_count = self
            .realloc_fail_count
            .saturating_sub(old.realloc_fail_count);

        let total_alloc_sum = self.total_alloc_sum.saturating_sub(old.total_alloc_sum);
        let total_dealloc_sum = self.total_dealloc_sum.saturating_sub(old.total_dealloc_sum);

        #[cfg(feature = "use_max")]
        let use_max = self.use_max.saturating_sub(old.use_max);

        Stats {
            alloc_count,
            alloc_sum,
            alloc_avg,
            #[cfg(feature = "histograms")]
            alloc_histogram,
            alloc_fail_count,
            dealloc_count,
            dealloc_sum,
            dealloc_avg,
            realloc_growth_count,
            realloc_growth_sum,
            realloc_growth_avg,
            #[cfg(feature = "histograms")]
            realloc_growth_histogram,
            realloc_shrink_count,
            realloc_shrink_sum,
            realloc_shrink_avg,
            #[cfg(feature = "histograms")]
            realloc_shrink_histogram,
            #[cfg(feature = "realloc_moves")]
            realloc_move_count,
            #[cfg(feature = "realloc_moves")]
            realloc_move_sum,
            #[cfg(feature = "realloc_moves")]
            realloc_move_avg,
            realloc_fail_count,
            total_alloc_sum,
            total_dealloc_sum,
            use_curr: self.use_curr,
            #[cfg(feature = "use_max")]
            use_max,
        }
    }
}

impl Sub<Stats> for Stats {
    type Output = Stats;

    fn sub(self, old: Stats) -> Stats {
        &self - &old
    }
}

impl Sub<&Stats> for Stats {
    type Output = Stats;

    fn sub(self, old: &Stats) -> Stats {
        &self - old
    }
}

impl Sub<Stats> for &Stats {
    type Output = Stats;

    fn sub(self, old: Stats) -> Stats {
        self - &old
    }
}