From 6a5417ed3d4f26811f12e4f121a42a16458e260c Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 31 Aug 2026 23:52:21 +0000 Subject: [PATCH] Report an arena's occupancy and retained heap --- node-graph/libraries/core-types/src/arena.rs | 88 +++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/node-graph/libraries/core-types/src/arena.rs b/node-graph/libraries/core-types/src/arena.rs index eedf287ae9..fd7c86349c 100644 --- a/node-graph/libraries/core-types/src/arena.rs +++ b/node-graph/libraries/core-types/src/arena.rs @@ -21,6 +21,9 @@ pub struct Arena { /// Set by a refused reservation and cleared by [`Arena::reset`], so a region /// no evaluation resets can be seen to need one. exhausted: AtomicBool, + /// Heap the parked payloads keep alive, which occupancy does not measure: + /// a park costs one pointer in the arena and owns its content outside it. + retained_heap: AtomicUsize, } impl std::fmt::Debug for Arena { @@ -32,6 +35,9 @@ impl std::fmt::Debug for Arena { struct DropEntry { offset: usize, drop_fn: unsafe fn(*mut u8), + /// The park glue's estimate of the heap this payload owns, 0 where the + /// glue cannot measure it, so the counter is a lower bound. + retained: usize, } // SAFETY: disjoint regions are handed out by an atomic bump; a region is written @@ -85,6 +91,7 @@ impl Arena { buf, drops: Mutex::new(Vec::new()), exhausted: AtomicBool::new(false), + retained_heap: AtomicUsize::new(0), }) } @@ -98,6 +105,7 @@ impl Arena { buf: Box::new([]), drops: Mutex::new(Vec::new()), exhausted: AtomicBool::new(false), + retained_heap: AtomicUsize::new(0), } } @@ -110,6 +118,29 @@ impl Arena { self.generation.load(Ordering::Acquire) } + /// Bytes handed out since the last [`Arena::reset`], including alignment + /// padding, so a caller can flush at a boundary before a refusal. + pub fn occupancy(&self) -> usize { + self.offset.load(Ordering::Relaxed) + } + + pub fn capacity(&self) -> usize { + self.buf.len() + } + + /// The heap the parked payloads own, summed from the park glue's hints. + /// A lower bound: glue that cannot measure its payload contributes 0. + pub fn retained_heap(&self) -> usize { + self.retained_heap.load(Ordering::Relaxed) + } + + /// Whether `ptr` addresses this arena's backbone, which is the provenance + /// question a promote asks of every parked reference. + pub fn contains(&self, ptr: *const u8) -> bool { + let base = self.base() as usize; + (ptr as usize).wrapping_sub(base) < self.buf.len() + } + fn base(&self) -> *mut u8 { self.buf.as_ptr() as *mut u8 } @@ -150,6 +181,12 @@ impl Arena { } pub fn alloc(&self, value: T) -> Option<(&T, ArenaWeak)> { + self.alloc_sized(value, 0) + } + + /// [`Arena::alloc`] with the park glue's estimate of the heap `value` owns, + /// which the region's own occupancy cannot see. + pub fn alloc_sized(&self, value: T, retained: usize) -> Option<(&T, ArenaWeak)> { let offset = self.reserve(size_of::(), align_of::())?; // Built before the write so an unencodable offset drops `value` here // rather than stranding it in the arena without drop glue. @@ -161,7 +198,8 @@ impl Arena { unsafe fn glue(p: *mut u8) { unsafe { p.cast::().drop_in_place() } } - self.drops.lock().unwrap().push(DropEntry { offset, drop_fn: glue:: }); + self.drops.lock().unwrap().push(DropEntry { offset, drop_fn: glue::, retained }); + self.retained_heap.fetch_add(retained, Ordering::Relaxed); } // SAFETY: initialized above; insert-only, so no `&mut` to it can exist. Some((unsafe { &*ptr }, weak)) @@ -207,6 +245,10 @@ impl Arena { // SAFETY: registered at alloc time; insert-only means the region was // never overwritten within this generation. unsafe { (entry.drop_fn)(base.add(entry.offset)) } + // Decremented as each payload's heap is freed, so an unwinding reset + // leaves the counter matching what is still parked. + let retained = self.retained_heap.get_mut(); + *retained = retained.saturating_sub(entry.retained); } *self.offset.get_mut() = 0; *self.exhausted.get_mut() = false; @@ -444,6 +486,50 @@ mod tests { assert_eq!(*ORDER.lock().unwrap(), vec![2, 1, 0], "later allocations may borrow earlier ones, so they drop first"); } + #[test] + fn sized_parks_account_for_their_retained_heap() { + let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner); + let mut arena = Arena::new(1024).unwrap(); + assert_eq!(arena.retained_heap(), 0); + + let owned = String::from("retained by the park"); + let length = owned.len(); + arena.alloc_sized(owned, length).unwrap(); + assert_eq!(arena.retained_heap(), length, "the park's hint reaches the counter"); + + arena.alloc(String::from("unmeasured")).unwrap(); + assert_eq!(arena.retained_heap(), length, "an unmeasured park contributes nothing"); + + arena.reset(); + assert_eq!(arena.retained_heap(), 0, "the flush frees every parked payload"); + } + + #[test] + fn occupancy_tracks_the_bump_and_clears_on_reset() { + let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner); + let mut arena = Arena::new(1024).unwrap(); + assert_eq!(arena.occupancy(), 0); + assert_eq!(arena.capacity(), 1024); + arena.alloc(0u64).unwrap(); + assert_eq!(arena.occupancy(), 8); + arena.reset(); + assert_eq!(arena.occupancy(), 0); + } + + #[test] + fn containment_answers_only_for_this_arena() { + let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner); + let first = Arena::new(1024).unwrap(); + let second = Arena::new(1024).unwrap(); + let (value, _) = first.alloc(41u32).unwrap(); + let ptr = std::ptr::from_ref(value).cast::(); + assert!(first.contains(ptr)); + assert!(!second.contains(ptr)); + assert!(!first.contains(std::ptr::null())); + let heap = Box::new(7u32); + assert!(!first.contains(std::ptr::from_ref(&*heap).cast::()), "heap the arena does not back is outside it"); + } + #[test] fn drop_glue_runs_on_reset() { let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner);