Skip to main content

dev_bench/
alloc.rs

1//! Allocation tracking. Available with the `alloc-tracking` feature.
2//!
3//! Wraps `dhat` to capture total bytes, total allocation count, and
4//! peak resident bytes during a benchmark. Reports a [`CheckResult`]
5//! with regression-style verdict.
6//!
7//! ## Cost
8//!
9//! Enabling `alloc-tracking` installs `dhat::Alloc` as the global
10//! allocator. This is heavier than the default allocator and
11//! materially changes timing characteristics. **Do not combine
12//! allocation thresholds with timing thresholds in the same
13//! invocation.** Run timing benchmarks with the feature off and
14//! allocation benchmarks with it on.
15//!
16//! ## Setup
17//!
18//! Use the [`install_global_allocator!`](crate::install_global_allocator)
19//! macro at module scope in your binary or test target:
20//!
21//! ```ignore
22//! dev_bench::install_global_allocator!();
23//! ```
24//!
25//! Then start a profiler before the benchmark and snapshot stats after:
26//!
27//! ```ignore
28//! let _profiler = dhat::Profiler::new_heap();
29//! // ... run benchmarked code ...
30//! let stats = dev_bench::alloc::AllocationStats::snapshot();
31//! ```
32//!
33//! The macro expands to:
34//!
35//! ```ignore
36//! #[global_allocator]
37//! static ALLOC: dhat::Alloc = dhat::Alloc;
38//! ```
39
40use dev_report::{CheckResult, Evidence, Severity};
41
42/// Snapshot of allocation activity, captured from `dhat::HeapStats`.
43///
44/// Build via [`AllocationStats::snapshot`] inside a `dhat::Profiler`
45/// scope.
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct AllocationStats {
48    /// Total bytes allocated across the profiled scope (cumulative).
49    pub total_bytes: u64,
50    /// Total number of allocations across the profiled scope.
51    pub total_blocks: u64,
52    /// Peak bytes resident at any one time.
53    pub peak_bytes: u64,
54    /// Peak number of blocks resident at any one time.
55    pub peak_blocks: u64,
56}
57
58impl AllocationStats {
59    /// Capture the current `dhat::HeapStats` into an `AllocationStats`.
60    ///
61    /// MUST be called inside an active `dhat::Profiler::new_heap()`
62    /// scope. Outside that scope, `dhat` panics.
63    pub fn snapshot() -> Self {
64        let s = dhat::HeapStats::get();
65        Self {
66            total_bytes: s.total_bytes,
67            total_blocks: s.total_blocks,
68            peak_bytes: s.max_bytes as u64,
69            peak_blocks: s.max_blocks as u64,
70        }
71    }
72
73    /// Compare this snapshot against a baseline.
74    ///
75    /// `pct_threshold` is the maximum tolerated growth in
76    /// `total_bytes` over the baseline. A regression beyond the
77    /// threshold yields `Fail (Warning)`. No baseline yields `Skip`.
78    pub fn compare_against_baseline(
79        &self,
80        name: &str,
81        baseline: Option<AllocationStats>,
82        pct_threshold: f64,
83    ) -> CheckResult {
84        let check_name = format!("alloc::{}", name);
85        let mut evidence = vec![
86            Evidence::numeric("total_bytes", self.total_bytes as f64),
87            Evidence::numeric("total_blocks", self.total_blocks as f64),
88            Evidence::numeric("peak_bytes", self.peak_bytes as f64),
89            Evidence::numeric("peak_blocks", self.peak_blocks as f64),
90        ];
91
92        let Some(base) = baseline else {
93            let mut c = CheckResult::skip(check_name).with_detail("no allocation baseline");
94            c.tags = vec!["alloc".to_string()];
95            c.evidence = evidence;
96            return c;
97        };
98
99        evidence.push(Evidence::numeric(
100            "baseline_total_bytes",
101            base.total_bytes as f64,
102        ));
103        let allowed = base.total_bytes as f64 * (1.0 + pct_threshold / 100.0);
104        let regressed = (self.total_bytes as f64) > allowed;
105        let detail = format!(
106            "current_total_bytes={} baseline_total_bytes={} threshold_pct={}",
107            self.total_bytes, base.total_bytes, pct_threshold
108        );
109        if regressed {
110            let mut c = CheckResult::fail(check_name, Severity::Warning).with_detail(detail);
111            c.tags = vec!["alloc".to_string(), "regression".to_string()];
112            c.evidence = evidence;
113            c
114        } else {
115            let mut c = CheckResult::pass(check_name).with_detail(detail);
116            c.tags = vec!["alloc".to_string()];
117            c.evidence = evidence;
118            c
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use dev_report::Verdict;
127
128    fn synthetic(total_bytes: u64) -> AllocationStats {
129        AllocationStats {
130            total_bytes,
131            total_blocks: 10,
132            peak_bytes: total_bytes,
133            peak_blocks: 5,
134        }
135    }
136
137    #[test]
138    fn no_baseline_skips() {
139        let s = synthetic(1_000);
140        let c = s.compare_against_baseline("x", None, 10.0);
141        assert_eq!(c.verdict, Verdict::Skip);
142        assert!(c.has_tag("alloc"));
143    }
144
145    #[test]
146    fn within_threshold_passes() {
147        let curr = synthetic(105);
148        let base = synthetic(100);
149        let c = curr.compare_against_baseline("x", Some(base), 10.0);
150        assert_eq!(c.verdict, Verdict::Pass);
151    }
152
153    #[test]
154    fn over_threshold_fails() {
155        let curr = synthetic(120);
156        let base = synthetic(100);
157        let c = curr.compare_against_baseline("x", Some(base), 10.0);
158        assert_eq!(c.verdict, Verdict::Fail);
159        assert!(c.has_tag("regression"));
160    }
161
162    #[test]
163    fn evidence_includes_all_metrics() {
164        let curr = synthetic(100);
165        let c = curr.compare_against_baseline("x", None, 10.0);
166        let labels: Vec<&str> = c.evidence.iter().map(|e| e.label.as_str()).collect();
167        assert!(labels.contains(&"total_bytes"));
168        assert!(labels.contains(&"total_blocks"));
169        assert!(labels.contains(&"peak_bytes"));
170        assert!(labels.contains(&"peak_blocks"));
171    }
172}