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