Skip to main content

kglite_c/
alloc.rs

1//! Tracking global allocator + memory stats for the C ABI.
2//!
3//! kglite-c installs a tracking allocator (wrapping the System
4//! allocator) so a binding can observe the Rust-side heap via
5//! [`kglite_memory_stats`] — current live bytes, peak since process
6//! start, and total allocation count. Counters are process-wide.
7//!
8//! Only allocations made through the Rust global allocator are counted;
9//! the host runtime's own heap (Go, the JVM, Node, …) is separate and
10//! invisible here. The counters are maintained with `Relaxed` atomics —
11//! cheap, and exact accounting across threads isn't required for a
12//! monitoring stat.
13
14use std::alloc::{GlobalAlloc, Layout, System};
15use std::sync::atomic::{AtomicU64, Ordering};
16
17static CURRENT: AtomicU64 = AtomicU64::new(0);
18static PEAK: AtomicU64 = AtomicU64::new(0);
19static TOTAL_ALLOCS: AtomicU64 = AtomicU64::new(0);
20
21/// System allocator wrapper that tallies bytes + allocation count.
22struct TrackingAllocator;
23
24// SAFETY: every method forwards to `System` (a sound `GlobalAlloc`) and
25// only adds bookkeeping; we never hand back a pointer System didn't
26// produce. realloc is left to the default `GlobalAlloc` impl, which
27// routes through our `alloc`/`dealloc` so byte accounting stays correct.
28unsafe impl GlobalAlloc for TrackingAllocator {
29    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
30        let ptr = unsafe { System.alloc(layout) };
31        if !ptr.is_null() {
32            let size = layout.size() as u64;
33            TOTAL_ALLOCS.fetch_add(1, Ordering::Relaxed);
34            let now = CURRENT.fetch_add(size, Ordering::Relaxed) + size;
35            PEAK.fetch_max(now, Ordering::Relaxed);
36        }
37        ptr
38    }
39
40    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
41        unsafe { System.dealloc(ptr, layout) };
42        CURRENT.fetch_sub(layout.size() as u64, Ordering::Relaxed);
43    }
44}
45
46#[global_allocator]
47static GLOBAL: TrackingAllocator = TrackingAllocator;
48
49/// Rust-heap statistics from kglite's tracking allocator.
50#[repr(C)]
51#[derive(Debug, Clone, Copy)]
52pub struct KgMemStats {
53    /// Current live Rust-heap bytes (allocated minus freed).
54    pub current_bytes: u64,
55    /// Peak live Rust-heap bytes since process start.
56    pub peak_bytes: u64,
57    /// Total number of allocations since process start (monotonic).
58    pub total_allocs: u64,
59}
60
61/// Return current Rust-heap statistics from kglite's tracking allocator.
62/// Counts only allocations through the Rust global allocator — the host
63/// runtime's own heap is separate. Useful for a binding to surface
64/// kglite's memory footprint in its own metrics.
65#[no_mangle]
66pub extern "C" fn kglite_memory_stats() -> KgMemStats {
67    crate::ffi::value_boundary(
68        KgMemStats {
69            current_bytes: 0,
70            peak_bytes: 0,
71            total_allocs: 0,
72        },
73        || {
74            let current_bytes = CURRENT.load(Ordering::Relaxed);
75            // Another thread can be between CURRENT.fetch_add and PEAK.fetch_max.
76            // Fold this observation into PEAK so every returned snapshot preserves
77            // the public peak >= current invariant without serializing allocations.
78            let peak_bytes = PEAK.fetch_max(current_bytes, Ordering::Relaxed);
79            KgMemStats {
80                current_bytes,
81                peak_bytes: peak_bytes.max(current_bytes),
82                total_allocs: TOTAL_ALLOCS.load(Ordering::Relaxed),
83            }
84        },
85    )
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn memory_stats_track_allocations() {
94        let before = kglite_memory_stats();
95        // Force some heap traffic the optimizer can't elide.
96        let v: Vec<u64> = (0..10_000).collect();
97        let after = kglite_memory_stats();
98        assert!(after.total_allocs >= before.total_allocs);
99        assert!(after.peak_bytes >= after.current_bytes);
100        // Keep `v` alive across the second reading.
101        assert_eq!(v.len(), 10_000);
102    }
103}