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}