Skip to main content

shuyi_alloc_count/
global.rs

1//! The one counting allocator of a debug build (the `global` feature):
2//! a `#[global_allocator]` [`crate::CountingAlloc::with_tags`] over
3//! [`std::alloc::System`], and [`reading`] and [`rows`], which read it.
4//! What it counts goes under the tags the program enters
5//! ([`crate::tags`]): `shuyi-app` and `shuyi-cli` enter the frame's
6//! phases, the game's spans and each asset kind's decode.
7//!
8//! **Why it is here.** A binary has one global allocator: two
9//! `#[global_allocator]` statics in it don't link. `shuyi-app` (a game's
10//! window) and `shuyi-cli` (its tool program) both count the heap, and a
11//! tool program links both. So neither declares the static: both turn on
12//! this feature, Cargo unifies it, and the binary gets this one static.
13//!
14//! **When it counts.** In a debug build (`debug_assertions`), unless the
15//! game builds with `--cfg shuyi_no_heap_count`, which a game with a
16//! global allocator of its own sets in its `.cargo/config.toml`'s
17//! `rustflags` (the crate's docs show how). A release build has no static
18//! at all: the program allocates with [`std::alloc::System`] directly,
19//! as if this crate weren't there.
20//!
21//! It counts the whole process: every thread's allocations, not only the
22//! game's.
23
24#[cfg(all(debug_assertions, not(shuyi_no_heap_count)))]
25use std::alloc::System;
26
27#[cfg(all(debug_assertions, not(shuyi_no_heap_count)))]
28use crate::{CountingAlloc, Tagged};
29use crate::{Counts, TagCounts};
30
31/// Whether this build counts the heap: a debug build without `--cfg
32/// shuyi_no_heap_count`.
33pub const COUNTING: bool = cfg!(all(debug_assertions, not(shuyi_no_heap_count)));
34
35#[cfg(all(debug_assertions, not(shuyi_no_heap_count)))]
36#[global_allocator]
37static HEAP: CountingAlloc<System, Tagged> = CountingAlloc::with_tags(System);
38
39/// The heap's counts now, the calls since the last reading taken; `None`
40/// in a build that doesn't count ([`COUNTING`]).
41pub fn reading() -> Option<Counts> {
42    COUNTING.then(read)
43}
44
45/// The heap's counts by tag now ([`CountingAlloc::rows`](crate::CountingAlloc::rows));
46/// `None` in a build that doesn't count ([`COUNTING`]).
47pub fn rows() -> Option<Vec<TagCounts>> {
48    COUNTING.then(read_rows)
49}
50
51// What `reading` and `rows` read, by build: two names each, so mutation
52// testing (which runs a debug build) can leave out the ones it never
53// compiles.
54#[cfg(all(debug_assertions, not(shuyi_no_heap_count)))]
55use {counted as read, counted_rows as read_rows};
56#[cfg(not(all(debug_assertions, not(shuyi_no_heap_count))))]
57use {uncounted as read, uncounted_rows as read_rows};
58
59/// The global counter's counts.
60#[cfg(all(debug_assertions, not(shuyi_no_heap_count)))]
61fn counted() -> Counts {
62    HEAP.counts()
63}
64
65/// The global counter's counts by tag.
66#[cfg(all(debug_assertions, not(shuyi_no_heap_count)))]
67fn counted_rows() -> Vec<TagCounts> {
68    HEAP.rows()
69}
70
71/// Never called: this build has no counter, so [`COUNTING`] is false.
72#[cfg(not(all(debug_assertions, not(shuyi_no_heap_count))))]
73fn uncounted() -> Counts {
74    Counts::default()
75}
76
77/// Never called, as [`uncounted`].
78#[cfg(not(all(debug_assertions, not(shuyi_no_heap_count))))]
79fn uncounted_rows() -> Vec<TagCounts> {
80    Vec::new()
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn a_debug_test_build_counts_unless_it_opted_out() {
89        let counting = cfg!(all(debug_assertions, not(shuyi_no_heap_count)));
90        assert_eq!(COUNTING, counting);
91        assert_eq!(reading().is_some(), counting);
92        assert_eq!(rows().is_some(), counting);
93    }
94
95    /// Other test threads allocate too, so only what holds whatever they
96    /// do: a block kept alive is in the live bytes and the peak.
97    #[cfg(all(debug_assertions, not(shuyi_no_heap_count)))]
98    #[test]
99    fn the_global_counter_sees_a_block_this_thread_keeps() {
100        let block = vec![1u8; 1 << 20];
101        let counts = reading().unwrap();
102        // The peak only rises (nothing resets the global one), and the
103        // block raised it when it was made.
104        assert!(counts.live_bytes >= 1 << 20, "{counts:?}");
105        assert!(counts.peak_bytes >= 1 << 20, "{counts:?}");
106        drop(block);
107    }
108
109    /// The tag of `name`, a span's, unique to the test that asks.
110    #[cfg(all(debug_assertions, not(shuyi_no_heap_count)))]
111    fn span(name: &'static str) -> u16 {
112        crate::tags::tag(2, name)
113    }
114
115    /// The global counter's row of `tag`, if it has one.
116    #[cfg(all(debug_assertions, not(shuyi_no_heap_count)))]
117    fn row(tag: u16) -> Option<TagCounts> {
118        rows().unwrap().into_iter().find(|r| r.tag == tag)
119    }
120
121    /// A list made under one tag and grown under another counts under the
122    /// second. Its tags are this test's, so other threads never add to
123    /// them.
124    #[cfg(all(debug_assertions, not(shuyi_no_heap_count)))]
125    #[test]
126    fn a_list_grown_under_another_tag_counts_under_it() {
127        use crate::tags::{enter, leave};
128        let (made, grown) = (span("global-test-made"), span("global-test-grown"));
129        let before = enter(made);
130        let mut list: Vec<u8> = Vec::with_capacity(1000);
131        enter(grown);
132        list.reserve_exact(4000);
133        leave(before);
134        let capacity = u64::try_from(list.capacity()).unwrap();
135        let made_row = row(made).unwrap();
136        assert_eq!((made_row.live_bytes, made_row.live_blocks), (0, 0));
137        assert_eq!(made_row.allocated_bytes, 1000);
138        assert_eq!(made_row.peak_bytes, 1000);
139        let grown_row = row(grown).unwrap();
140        assert_eq!((grown_row.live_bytes, grown_row.live_blocks), (capacity, 1));
141        assert_eq!(grown_row.name, "global-test-grown");
142        assert_eq!(grown_row.class, 2);
143        drop(list);
144        assert_eq!(row(grown).unwrap().live_bytes, 0);
145    }
146}