Skip to main content

blue_lang_runtime/
alloc.rs

1//! How many allocations a stretch of a blue program made, and how many bytes.
2//!
3//! `blue profile` reports time per definition; this is the other half of what
4//! a definition costs (`theory/BLUE-MEMORY.md` §VI M0: you cannot configure
5//! what you cannot account for). One counter, read at two points: a caller
6//! snapshots [`allocations`] when an activation enters and again when it
7//! leaves, and the difference is what that activation allocated, everything it
8//! called included. Subtracting what its callees allocated gives the
9//! exclusive figure, exactly as the profiler does with time.
10//!
11//! [`Counting`] is a `GlobalAlloc` over `System` that counts each allocation
12//! on the thread that made it. A binary opts in with one line:
13//!
14//! ```ignore
15//! #[global_allocator]
16//! static ALLOC: blue_lang_runtime::alloc::Counting = blue_lang_runtime::alloc::Counting;
17//! ```
18//!
19//! The count is per thread so a profile of one interpreter is not charged for
20//! another thread's work, and it costs two thread-local additions per
21//! allocation and one per free (so a room can say what it left live). Until a binary installs [`Counting`],
22//! [`allocations`] answers `None`: a program that was never counted reads as
23//! unmeasured, never as having allocated nothing.
24
25use std::alloc::{GlobalAlloc, Layout, System};
26use std::cell::Cell;
27use std::sync::atomic::{AtomicBool, Ordering};
28
29/// Allocations made and bytes requested on one thread, from its start.
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub struct Allocations {
32    /// Allocation calls. A `realloc` that grows counts as one.
33    pub count: u64,
34    /// Bytes requested. A growing `realloc` counts the bytes it added.
35    pub bytes: u64,
36    /// Bytes returned. A shrinking `realloc` counts the bytes it gave back.
37    /// Charged to the thread that frees, which for blue's single-threaded
38    /// interpreters is the thread that allocated.
39    pub freed: u64,
40}
41
42impl Allocations {
43    /// What was allocated between `earlier` and `self`, on one thread.
44    #[must_use]
45    pub fn since(self, earlier: Self) -> Self {
46        Self {
47            count: self.count.saturating_sub(earlier.count),
48            bytes: self.bytes.saturating_sub(earlier.bytes),
49            freed: self.freed.saturating_sub(earlier.freed),
50        }
51    }
52
53    /// Bytes allocated and not yet returned over the stretch this measures:
54    /// what it left live. Negative when it freed more than it allocated
55    /// (memory from before the stretch was released inside it).
56    #[must_use]
57    pub fn retained(self) -> i64 {
58        i64::try_from(self.bytes).unwrap_or(i64::MAX)
59            - i64::try_from(self.freed).unwrap_or(i64::MAX)
60    }
61}
62
63impl std::ops::AddAssign for Allocations {
64    fn add_assign(&mut self, other: Self) {
65        self.count += other.count;
66        self.bytes += other.bytes;
67        self.freed += other.freed;
68    }
69}
70
71/// Set by the first allocation [`Counting`] serves, so [`allocations`] can tell
72/// "not counted" from "counted zero".
73static INSTALLED: AtomicBool = AtomicBool::new(false);
74thread_local! {
75    // `const` initialisation: no lazy init, so no allocation from inside the
76    // allocator, and no destructor to run during thread teardown.
77    static COUNT: Cell<u64> = const { Cell::new(0) };
78    static BYTES: Cell<u64> = const { Cell::new(0) };
79    static FREED: Cell<u64> = const { Cell::new(0) };
80}
81
82/// This thread's running totals, or `None` when [`Counting`] is not the global
83/// allocator of this binary.
84#[must_use]
85pub fn allocations() -> Option<Allocations> {
86    if !INSTALLED.load(Ordering::Relaxed) {
87        return None;
88    }
89    Some(Allocations {
90        count: COUNT.try_with(Cell::get).unwrap_or(0),
91        bytes: BYTES.try_with(Cell::get).unwrap_or(0),
92        freed: FREED.try_with(Cell::get).unwrap_or(0),
93    })
94}
95
96fn charge(bytes: usize) {
97    // `try_with`: an allocation made while this thread's locals are being torn
98    // down is served and simply not counted.
99    let _ = COUNT.try_with(|c| c.set(c.get() + 1));
100    let _ = BYTES.try_with(|b| b.set(b.get() + bytes as u64));
101}
102
103fn credit(bytes: usize) {
104    let _ = FREED.try_with(|f| f.set(f.get() + bytes as u64));
105}
106
107/// The system allocator, counting what each thread allocates. See the module
108/// documentation for how a binary installs it.
109pub struct Counting;
110
111// SAFETY: every method forwards to `System` with the caller's arguments
112// unchanged; the counting touches only `Cell`s and an atomic flag, neither of
113// which allocates.
114unsafe impl GlobalAlloc for Counting {
115    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
116        INSTALLED.store(true, Ordering::Relaxed);
117        charge(layout.size());
118        unsafe { System.alloc(layout) }
119    }
120
121    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
122        INSTALLED.store(true, Ordering::Relaxed);
123        charge(layout.size());
124        unsafe { System.alloc_zeroed(layout) }
125    }
126
127    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
128        credit(layout.size());
129        unsafe { System.dealloc(ptr, layout) }
130    }
131
132    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
133        if new_size > layout.size() {
134            charge(new_size - layout.size());
135        } else {
136            credit(layout.size() - new_size);
137        }
138        unsafe { System.realloc(ptr, layout, new_size) }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    /// This crate's own test binary does not install [`Counting`], so the
147    /// answer must be "unmeasured", not a zero that reads as "allocated
148    /// nothing". `tests/alloc_count.rs` installs it and measures.
149    #[test]
150    fn an_uncounted_binary_reads_as_unmeasured() {
151        let _ = vec![1u8; 64];
152        assert_eq!(allocations(), None);
153    }
154
155    #[test]
156    fn since_is_the_difference_and_never_underflows() {
157        let a = Allocations {
158            count: 3,
159            bytes: 100,
160            freed: 40,
161        };
162        let b = Allocations {
163            count: 5,
164            bytes: 160,
165            freed: 50,
166        };
167        assert_eq!(
168            b.since(a),
169            Allocations {
170                count: 2,
171                bytes: 60,
172                freed: 10
173            }
174        );
175        assert_eq!(a.since(b), Allocations::default());
176    }
177}