1use std::cell::{Cell, RefCell};
41use std::collections::HashMap;
42use std::marker::PhantomData;
43use std::ptr::NonNull;
44
45thread_local! {
59 static CURRENT_HEAP_STACK: RefCell<Vec<NonNull<Heap>>> = const { RefCell::new(Vec::new()) };
60}
61
62pub struct HeapGuard {
66 _private: (),
69}
70
71impl HeapGuard {
72 pub fn push(heap: &Heap) -> Self {
81 let ptr = NonNull::from(heap);
82 CURRENT_HEAP_STACK.with(|stack| stack.borrow_mut().push(ptr));
83 HeapGuard { _private: () }
84 }
85}
86
87impl Drop for HeapGuard {
88 fn drop(&mut self) {
89 CURRENT_HEAP_STACK.with(|stack| {
90 let popped = stack.borrow_mut().pop();
91 debug_assert!(popped.is_some(), "HeapGuard::drop with empty stack");
92 });
93 }
94}
95
96pub fn with_current_heap<R>(f: impl for<'a> FnOnce(Option<&'a Heap>) -> R) -> R {
103 CURRENT_HEAP_STACK.with(|stack| {
104 let ptr = stack.borrow().last().copied();
105 let heap = ptr.map(|ptr| unsafe { &*ptr.as_ptr() });
109 f(heap)
110 })
111}
112
113#[derive(Copy, Clone, Debug)]
114pub struct HeapRef {
115 ptr: NonNull<Heap>,
116}
117
118impl HeapRef {
119 pub fn from_heap(heap: &Heap) -> Self {
120 HeapRef {
121 ptr: NonNull::from(heap),
122 }
123 }
124
125 pub fn contains_allocation(self, identity: usize, token: usize) -> bool {
126 unsafe { self.ptr.as_ref() }.contains_allocation(identity, token)
131 }
132}
133
134#[derive(Copy, Clone, PartialEq, Eq, Debug)]
136pub enum Color {
137 White0,
141 White1,
143 Gray,
145 Black,
147}
148
149impl Color {
150 pub fn is_white(self) -> bool {
151 matches!(self, Color::White0 | Color::White1)
152 }
153
154 fn other_white(self) -> Self {
155 match self {
156 Color::White0 => Color::White1,
157 Color::White1 => Color::White0,
158 Color::Gray | Color::Black => self,
159 }
160 }
161}
162
163#[derive(Copy, Clone, PartialEq, Eq, Debug)]
167pub enum GcAge {
168 New,
169 Survival,
170 Old0,
171 Old1,
172 Old,
173 Touched1,
174 Touched2,
175}
176
177impl GcAge {
178 pub fn is_old(self) -> bool {
179 !matches!(self, GcAge::New | GcAge::Survival)
180 }
181
182 fn next_after_minor(self) -> Self {
183 match self {
184 GcAge::New => GcAge::Survival,
185 GcAge::Survival | GcAge::Old0 => GcAge::Old1,
186 GcAge::Old1 | GcAge::Old | GcAge::Touched2 => GcAge::Old,
187 GcAge::Touched1 => GcAge::Touched2,
188 }
189 }
190}
191
192#[repr(C)]
194pub struct GcHeader {
195 color: Cell<Color>,
196 age: Cell<GcAge>,
197 finalized: Cell<bool>,
200 collected: Cell<bool>,
209 next: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
211 size: Cell<usize>,
217 type_name: &'static str,
220}
221
222impl GcHeader {
223 fn new_white(size: usize, color: Color, type_name: &'static str) -> Self {
224 Self {
225 color: Cell::new(color),
226 age: Cell::new(GcAge::New),
227 finalized: Cell::new(false),
228 collected: Cell::new(false),
229 next: Cell::new(None),
230 size: Cell::new(size),
231 type_name,
232 }
233 }
234}
235
236#[repr(C)]
238pub struct GcBox<T: ?Sized> {
239 header: GcHeader,
240 value: T,
241}
242
243impl<T: ?Sized> GcBox<T> {
244 pub fn header(&self) -> &GcHeader {
245 &self.header
246 }
247 pub fn value(&self) -> &T {
248 &self.value
249 }
250}
251
252pub struct Gc<T: ?Sized> {
254 ptr: NonNull<GcBox<T>>,
255 _marker: PhantomData<T>,
257}
258
259impl<T: ?Sized> Copy for Gc<T> {}
263impl<T: ?Sized> Clone for Gc<T> {
264 fn clone(&self) -> Self {
265 *self
266 }
267}
268
269impl<T: ?Sized> PartialEq for Gc<T> {
270 fn eq(&self, other: &Self) -> bool {
271 std::ptr::addr_eq(self.ptr.as_ptr(), other.ptr.as_ptr())
272 }
273}
274impl<T: ?Sized> Eq for Gc<T> {}
275
276impl<T: ?Sized> std::hash::Hash for Gc<T> {
277 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
278 self.ptr.as_ptr().hash(state)
279 }
280}
281
282impl<T: ?Sized> std::fmt::Debug for Gc<T> {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 write!(f, "Gc({:p})", self.ptr.as_ptr())
285 }
286}
287
288impl<T: Trace + 'static> Gc<T> {
289 pub fn new_uncollected(value: T) -> Self {
295 let size = std::mem::size_of::<T>();
296 let boxed = Box::new(GcBox {
297 header: GcHeader::new_white(size, Color::White0, std::any::type_name::<T>()),
298 value,
299 });
300 Gc {
301 ptr: NonNull::new(Box::into_raw(boxed)).expect("Box::into_raw is non-null"),
302 _marker: PhantomData,
303 }
304 }
305}
306
307impl<T: ?Sized> Gc<T> {
308 pub fn ptr_eq(a: Self, b: Self) -> bool {
310 std::ptr::addr_eq(a.ptr.as_ptr(), b.ptr.as_ptr())
311 }
312
313 pub fn identity(self) -> usize {
316 self.ptr.as_ptr() as *const () as usize
317 }
318
319 fn as_box(&self) -> &GcBox<T> {
323 unsafe { self.ptr.as_ref() }
329 }
330
331 fn header(&self) -> &GcHeader {
332 &self.as_box().header
333 }
334
335 pub fn color(self) -> Color {
336 self.header().color.get()
337 }
338
339 pub fn set_color(self, color: Color) {
340 self.header().color.set(color);
341 }
342
343 pub fn age(self) -> GcAge {
344 self.header().age.get()
345 }
346
347 pub fn set_age(self, age: GcAge) {
348 self.header().age.set(age);
349 }
350
351 pub fn account_buffer(&self, heap: &Heap, delta: isize) {
361 if delta == 0 {
362 return;
363 }
364 let header = self.header();
365 if !header.collected.get() {
366 return;
367 }
368 if delta >= 0 {
369 header.size.set(header.size.get().saturating_add(delta as usize));
370 } else {
371 header
372 .size
373 .set(header.size.get().saturating_sub((-delta) as usize));
374 }
375 heap.adjust_bytes(delta);
376 }
377}
378
379impl<T: ?Sized> std::ops::Deref for Gc<T> {
380 type Target = T;
381 fn deref(&self) -> &T {
382 &self.as_box().value
383 }
384}
385
386impl<T: ?Sized> AsRef<T> for Gc<T> {
387 fn as_ref(&self) -> &T {
388 &self.as_box().value
389 }
390}
391
392pub trait Trace {
409 fn trace(&self, m: &mut Marker);
410}
411
412impl<T: Trace> Trace for Vec<T> {
414 fn trace(&self, m: &mut Marker) {
415 for item in self.iter() {
416 item.trace(m);
417 }
418 }
419}
420
421impl<T: Trace> Trace for Option<T> {
422 fn trace(&self, m: &mut Marker) {
423 if let Some(v) = self {
424 v.trace(m);
425 }
426 }
427}
428
429impl<T: Trace + ?Sized> Trace for Box<T> {
430 fn trace(&self, m: &mut Marker) {
431 (**self).trace(m);
432 }
433}
434
435impl<T: Trace + ?Sized> Trace for std::rc::Rc<T> {
436 fn trace(&self, m: &mut Marker) {
437 (**self).trace(m);
438 }
439}
440
441impl<T: Trace> Trace for RefCell<T> {
442 fn trace(&self, m: &mut Marker) {
443 self.borrow().trace(m);
444 }
445}
446
447impl<T: Trace + 'static> Trace for Gc<T> {
449 fn trace(&self, m: &mut Marker) {
450 m.mark(*self);
451 }
452}
453
454macro_rules! trace_noop {
456 ($($t:ty),*) => {
457 $(impl Trace for $t {
458 fn trace(&self, _m: &mut Marker) {}
459 })*
460 };
461}
462trace_noop!(
463 bool, u8, u16, u32, u64, u128, usize,
464 i8, i16, i32, i64, i128, isize,
465 f32, f64, char, String, str
466);
467
468impl<T> Trace for std::marker::PhantomData<T> {
469 fn trace(&self, _m: &mut Marker) {}
470}
471
472pub struct Marker {
474 gray_queue: Vec<NonNull<GcBox<dyn Trace>>>,
475 visited: std::collections::HashSet<usize>,
476}
477
478impl Marker {
479 fn new() -> Self {
480 Self {
481 gray_queue: Vec::with_capacity(256),
482 visited: std::collections::HashSet::new(),
483 }
484 }
485
486 pub fn mark<T: Trace + 'static>(&mut self, gc: Gc<T>) {
499 let id = gc.identity();
500 if self.visited.insert(id) {
501 let header = gc.header();
502 header.color.set(Color::Gray);
503 let ptr: NonNull<GcBox<dyn Trace>> = gc.ptr;
504 self.gray_queue.push(ptr);
505 }
506 }
507
508 pub fn try_visit(&mut self, addr: usize) -> bool {
513 self.visited.insert(addr)
514 }
515
516 pub fn is_visited(&self, id: usize) -> bool {
521 self.visited.contains(&id)
522 }
523
524 pub fn visited_count(&self) -> usize {
528 self.visited.len()
529 }
530
531 pub fn drain_gray_queue(&mut self) {
540 while let Some(gray_ptr) = self.gray_queue.pop() {
541 unsafe {
542 let bx = gray_ptr.as_ref();
543 bx.header.color.set(Color::Black);
544 bx.value.trace(self);
545 }
546 }
547 }
548}
549
550#[derive(Copy, Clone, PartialEq, Eq, Debug)]
565pub enum GcState {
566 Pause,
567 Propagate,
568 Atomic,
569 Sweep,
570 Finalize,
571}
572
573impl GcState {
574 pub fn is_pause(self) -> bool {
575 matches!(self, GcState::Pause)
576 }
577 pub fn is_propagate(self) -> bool {
578 matches!(self, GcState::Propagate)
579 }
580 pub fn is_sweep(self) -> bool {
581 matches!(self, GcState::Sweep)
582 }
583}
584
585#[derive(Copy, Clone, PartialEq, Eq, Debug)]
587pub enum StepOutcome {
588 Paused,
590 InProgress,
593 SkippedStopped,
596}
597
598#[derive(Copy, Clone, Debug)]
606pub struct StepBudget {
607 pub remaining_work: isize,
608 pub max_credit: isize,
609}
610
611impl StepBudget {
612 pub fn from_work(work: isize) -> Self {
614 Self { remaining_work: work.max(1), max_credit: work.max(1) }
615 }
616}
617
618const GC_MIN_THRESHOLD: usize = 1024 * 1024;
633
634pub struct Heap {
635 head: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
637 bytes: Cell<usize>,
639 current_white: Cell<Color>,
641 allocation_tokens: RefCell<HashMap<usize, usize>>,
645 next_allocation_token: Cell<usize>,
647 threshold: Cell<usize>,
649 pause_multiplier: Cell<usize>,
651 state: Cell<GcState>,
653 paused: Cell<bool>,
656 collections: Cell<usize>,
658 marker: RefCell<Option<Marker>>,
661 sweep_prev_next: Cell<Option<NonNull<Cell<Option<NonNull<GcBox<dyn Trace>>>>>>>,
666}
667
668impl Default for Heap {
669 fn default() -> Self {
670 Self::new()
671 }
672}
673
674impl Heap {
675 pub fn new() -> Self {
676 Self {
677 head: Cell::new(None),
678 bytes: Cell::new(0),
679 current_white: Cell::new(Color::White0),
680 allocation_tokens: RefCell::new(HashMap::new()),
681 next_allocation_token: Cell::new(1),
682 threshold: Cell::new(64 * 1024), pause_multiplier: Cell::new(200), state: Cell::new(GcState::Pause),
685 paused: Cell::new(true), collections: Cell::new(0),
687 marker: RefCell::new(None),
688 sweep_prev_next: Cell::new(None),
689 }
690 }
691
692 pub fn unpause(&self) {
695 self.paused.set(false);
696 }
697
698 pub fn is_paused(&self) -> bool {
699 self.paused.get()
700 }
701
702 pub fn allocate<T: Trace + 'static>(&self, value: T) -> Gc<T> {
704 let size = std::mem::size_of::<GcBox<T>>();
705 let boxed = Box::new(GcBox {
706 header: GcHeader::new_white(
707 size,
708 self.current_white.get(),
709 std::any::type_name::<T>(),
710 ),
711 value,
712 });
713 boxed.header.next.set(self.head.get());
714 boxed.header.collected.set(true);
715 let raw: *mut GcBox<T> = Box::into_raw(boxed);
716 let ptr: NonNull<GcBox<T>> =
717 NonNull::new(raw).expect("Box::into_raw is non-null");
718 let dyn_ptr: NonNull<GcBox<dyn Trace>> = ptr;
719 let identity = ptr.as_ptr() as *const () as usize;
720 let token = self.next_token();
721 self.allocation_tokens
722 .borrow_mut()
723 .insert(identity, token);
724 self.head.set(Some(dyn_ptr));
725 self.bytes.set(self.bytes.get() + size);
726 Gc {
727 ptr,
728 _marker: PhantomData,
729 }
730 }
731
732 pub fn bytes_used(&self) -> usize {
734 self.bytes.get()
735 }
736
737 pub fn adjust_bytes(&self, delta: isize) {
742 if delta >= 0 {
743 self.bytes.set(self.bytes.get().saturating_add(delta as usize));
744 } else {
745 self.bytes
746 .set(self.bytes.get().saturating_sub((-delta) as usize));
747 }
748 }
749
750 pub fn threshold_bytes(&self) -> usize {
755 self.threshold.get()
756 }
757
758 pub fn set_threshold_bytes(&self, threshold: usize) {
764 self.threshold.set(threshold.max(1));
765 }
766
767 pub fn would_collect(&self) -> bool {
771 !self.paused.get() && self.bytes.get() >= self.threshold.get()
772 }
773
774 pub fn collections(&self) -> usize {
775 self.collections.get()
776 }
777
778 fn next_token(&self) -> usize {
779 let token = self.next_allocation_token.get().max(1);
780 let next = token.checked_add(1).unwrap_or(1).max(1);
781 self.next_allocation_token.set(next);
782 token
783 }
784
785 fn current_white(&self) -> Color {
786 self.current_white.get()
787 }
788
789 fn other_white(&self) -> Color {
790 self.current_white.get().other_white()
791 }
792
793 fn flip_current_white(&self) {
794 self.current_white.set(self.other_white());
795 }
796
797 fn for_each_header(&self, mut f: impl FnMut(&GcHeader)) {
798 let mut cursor = self.head.get();
799 while let Some(ptr) = cursor {
800 let header = self.header_from_ptr(ptr);
801 cursor = header.next.get();
802 f(header);
803 }
804 }
805
806 fn header_from_ptr<'a>(&'a self, ptr: NonNull<GcBox<dyn Trace>>) -> &'a GcHeader {
807 unsafe { &(*ptr.as_ptr()).header }
808 }
809
810 pub fn allocation_token(&self, identity: usize) -> Option<usize> {
815 self.allocation_tokens.borrow().get(&identity).copied()
816 }
817
818 pub fn contains_allocation(&self, identity: usize, token: usize) -> bool {
823 self.allocation_token(identity) == Some(token)
824 }
825
826 pub fn barrier<P, C>(&self, parent: Gc<P>, child: Gc<C>)
836 where
837 P: Trace + 'static,
838 C: Trace + 'static,
839 {
840 if self.paused.get() || self.state.get().is_pause() {
841 return;
842 }
843 if parent.header().color.get() != Color::Black {
844 return;
845 }
846 if !child.header().color.get().is_white() {
847 return;
848 }
849 child.header().color.set(Color::Gray);
850 if let Ok(mut m_opt) = self.marker.try_borrow_mut() {
851 if let Some(m) = m_opt.as_mut() {
852 let ptr: NonNull<GcBox<dyn Trace>> = child.ptr;
853 m.gray_queue.push(ptr);
854 m.visited.insert(child.identity());
855 }
856 }
857 }
858
859 pub fn generational_forward_barrier<P, C>(&self, parent: Gc<P>, child: Gc<C>)
864 where
865 P: Trace + 'static,
866 C: Trace + 'static,
867 {
868 if parent.age().is_old() && !child.age().is_old() {
869 child.set_age(GcAge::Old0);
870 }
871 self.barrier(parent, child);
872 }
873
874 pub fn generational_backward_barrier<P>(&self, parent: Gc<P>)
878 where
879 P: Trace + 'static,
880 {
881 if parent.age().is_old() {
882 parent.set_color(Color::Gray);
883 parent.set_age(GcAge::Touched1);
884 }
885 }
886
887 pub fn step(&self, roots: &dyn Trace) {
891 self.step_with_post_mark(roots, |_: &mut Marker| {});
892 }
893
894 pub fn step_with_post_mark<F: FnMut(&mut Marker)>(
899 &self,
900 roots: &dyn Trace,
901 post_mark: F,
902 ) {
903 if self.paused.get() {
904 return;
905 }
906 if self.bytes.get() < self.threshold.get() {
907 return;
908 }
909 self.full_collect_with_post_mark(roots, post_mark);
910 }
911
912 pub fn full_collect(&self, roots: &dyn Trace) {
915 self.full_collect_with_post_mark(roots, |_: &mut Marker| {});
916 }
917
918 pub fn mark_only_with_post_mark<F: FnMut(&mut Marker)>(
923 &self,
924 roots: &dyn Trace,
925 mut post_mark: F,
926 ) {
927 if self.paused.get() {
928 return;
929 }
930 let mut marker = Marker::new();
931 roots.trace(&mut marker);
932 marker.drain_gray_queue();
933 post_mark(&mut marker);
934 marker.drain_gray_queue();
935 }
936
937 pub fn promote_all_to_old(&self) {
940 self.for_each_header(|header| {
941 header.age.set(GcAge::Old);
942 header.color.set(Color::Black);
943 });
944 }
945
946 pub fn reset_all_ages(&self) {
949 let current_white = self.current_white();
950 self.for_each_header(|header| {
951 header.age.set(GcAge::New);
952 header.color.set(current_white);
953 });
954 }
955
956 pub fn minor_collect_with_post_mark<F: FnMut(&mut Marker)>(
963 &self,
964 roots: &dyn Trace,
965 mut post_mark: F,
966 ) {
967 if self.paused.get() {
968 return;
969 }
970 if !self.state.get().is_pause() {
971 self.abort_cycle();
972 }
973
974 self.state.set(GcState::Propagate);
975 let mut marker = Marker::new();
976 roots.trace(&mut marker);
977 marker.drain_gray_queue();
978
979 self.state.set(GcState::Atomic);
980 post_mark(&mut marker);
981 marker.drain_gray_queue();
982
983 self.state.set(GcState::Sweep);
984 self.sweep_young();
985 *self.marker.borrow_mut() = None;
986 self.sweep_prev_next.set(None);
987 self.state.set(GcState::Pause);
988 self.collections.set(self.collections.get() + 1);
989 }
990
991 pub fn full_collect_with_post_mark<F: FnMut(&mut Marker)>(
998 &self,
999 roots: &dyn Trace,
1000 mut post_mark: F,
1001 ) {
1002 if self.paused.get() {
1003 return;
1004 }
1005 if !self.state.get().is_pause() {
1006 self.abort_cycle();
1007 }
1008 let unlimited = StepBudget { remaining_work: isize::MAX, max_credit: isize::MAX };
1009 loop {
1010 let outcome = self.incremental_step_with_post_mark(roots, unlimited, &mut post_mark);
1011 if matches!(outcome, StepOutcome::Paused | StepOutcome::SkippedStopped) {
1012 break;
1013 }
1014 }
1015 }
1016
1017 pub fn incremental_step_with_post_mark<F: FnMut(&mut Marker)>(
1031 &self,
1032 roots: &dyn Trace,
1033 mut budget: StepBudget,
1034 mut post_mark: F,
1035 ) -> StepOutcome {
1036 if self.paused.get() {
1037 return StepOutcome::SkippedStopped;
1038 }
1039 self.run_budgeted(roots, &mut budget, &mut post_mark);
1040 if self.state.get().is_pause() {
1041 StepOutcome::Paused
1042 } else {
1043 StepOutcome::InProgress
1044 }
1045 }
1046
1047 fn run_budgeted(
1048 &self,
1049 roots: &dyn Trace,
1050 budget: &mut StepBudget,
1051 post_mark: &mut dyn FnMut(&mut Marker),
1052 ) -> bool {
1053 self.run_budgeted_until(roots, budget, post_mark, None)
1054 }
1055
1056 fn run_budgeted_until(
1057 &self,
1058 roots: &dyn Trace,
1059 budget: &mut StepBudget,
1060 post_mark: &mut dyn FnMut(&mut Marker),
1061 stop_at: Option<GcState>,
1062 ) -> bool {
1063 let mut did_work = false;
1064 loop {
1065 if stop_at == Some(self.state.get()) {
1066 return did_work;
1067 }
1068 if budget.remaining_work <= -budget.max_credit {
1069 return did_work;
1070 }
1071 match self.state.get() {
1072 GcState::Pause => {
1073 self.start_cycle(roots);
1074 self.state.set(GcState::Propagate);
1075 budget.remaining_work -= 1;
1076 did_work = true;
1077 if stop_at == Some(GcState::Propagate) {
1078 return did_work;
1079 }
1080 }
1081 GcState::Propagate => {
1082 let work = self.drain_gray_budgeted(budget.remaining_work.max(1));
1083 budget.remaining_work -= work as isize;
1084 did_work = did_work || work > 0;
1085 let empty = {
1086 let m = self.marker.borrow();
1087 m.as_ref().map(|m| m.gray_queue.is_empty()).unwrap_or(true)
1088 };
1089 if empty {
1090 self.state.set(GcState::Atomic);
1091 if stop_at == Some(GcState::Atomic) {
1092 return did_work;
1093 }
1094 } else if budget.remaining_work <= 0 {
1095 return did_work;
1096 }
1097 }
1098 GcState::Atomic => {
1099 self.run_atomic(post_mark);
1100 self.state.set(GcState::Sweep);
1101 budget.remaining_work -= 1;
1102 did_work = true;
1103 if stop_at == Some(GcState::Sweep) {
1104 return did_work;
1105 }
1106 }
1107 GcState::Sweep => {
1108 let work = self.sweep_budgeted(budget.remaining_work.max(1));
1109 budget.remaining_work -= work as isize;
1110 did_work = did_work || work > 0;
1111 if self.sweep_prev_next.get().is_none() {
1112 self.state.set(GcState::Finalize);
1113 if stop_at == Some(GcState::Finalize) {
1114 return did_work;
1115 }
1116 } else if budget.remaining_work <= 0 {
1117 return did_work;
1118 }
1119 }
1120 GcState::Finalize => {
1121 self.finish_cycle();
1122 self.state.set(GcState::Pause);
1123 if stop_at == Some(GcState::Pause) {
1124 return did_work;
1125 }
1126 return did_work;
1127 }
1128 }
1129 }
1130 }
1131
1132 pub fn incremental_run_until_state_with_post_mark<F: FnMut(&mut Marker)>(
1137 &self,
1138 roots: &dyn Trace,
1139 target: GcState,
1140 max_work: isize,
1141 mut post_mark: F,
1142 ) -> StepOutcome {
1143 if self.paused.get() {
1144 return StepOutcome::SkippedStopped;
1145 }
1146 let work = max_work.max(1);
1147 let mut budget = StepBudget {
1148 remaining_work: work,
1149 max_credit: work,
1150 };
1151 self.run_budgeted_until(roots, &mut budget, &mut post_mark, Some(target));
1152 if self.state.get().is_pause() {
1153 StepOutcome::Paused
1154 } else {
1155 StepOutcome::InProgress
1156 }
1157 }
1158
1159 fn start_cycle(&self, roots: &dyn Trace) {
1160 self.flip_current_white();
1161 let mut marker = Marker::new();
1162 roots.trace(&mut marker);
1163 *self.marker.borrow_mut() = Some(marker);
1164 self.sweep_prev_next.set(None);
1165 }
1166
1167 fn drain_gray_budgeted(&self, max_units: isize) -> usize {
1168 let mut m_opt = self.marker.borrow_mut();
1169 let marker = match m_opt.as_mut() {
1170 Some(m) => m,
1171 None => return 0,
1172 };
1173 let mut work = 0usize;
1174 let mut budget = max_units;
1175 while budget > 0 {
1176 let next = match marker.gray_queue.pop() {
1177 Some(p) => p,
1178 None => break,
1179 };
1180 unsafe {
1181 let bx = next.as_ref();
1182 bx.header.color.set(Color::Black);
1183 bx.value.trace(marker);
1184 }
1185 work += 1;
1186 budget -= 1;
1187 }
1188 work
1189 }
1190
1191 fn run_atomic(&self, post_mark: &mut dyn FnMut(&mut Marker)) {
1192 let mut m_opt = self.marker.borrow_mut();
1193 if let Some(marker) = m_opt.as_mut() {
1194 marker.drain_gray_queue();
1195 post_mark(marker);
1196 marker.drain_gray_queue();
1197 }
1198 self.sweep_prev_next.set(Some(NonNull::from(&self.head)));
1199 }
1200
1201 fn sweep_budgeted(&self, max_units: isize) -> usize {
1202 let mut work = 0usize;
1203 let mut budget = max_units;
1204 let mut freed_bytes = 0usize;
1205 let current_white = self.current_white();
1206 let dead_white = self.other_white();
1207 let mut prev_next_ptr = match self.sweep_prev_next.get() {
1208 Some(p) => p,
1209 None => return 0,
1210 };
1211 while budget > 0 {
1212 let prev_cell = unsafe { prev_next_ptr.as_ref() };
1213 let cursor = prev_cell.get();
1214 let ptr = match cursor {
1215 Some(p) => p,
1216 None => {
1217 self.sweep_prev_next.set(None);
1218 break;
1219 }
1220 };
1221 let header = self.header_from_ptr(ptr);
1222 let next = header.next.get();
1223 let color = header.color.get();
1224 if color == dead_white {
1225 prev_cell.set(next);
1226 freed_bytes += header.size.get();
1227 self.allocation_tokens
1228 .borrow_mut()
1229 .remove(&(ptr.as_ptr() as *const () as usize));
1230 unsafe {
1231 let _ = Box::from_raw(ptr.as_ptr());
1232 }
1233 } else {
1234 if matches!(color, Color::Black | Color::Gray) {
1235 header.color.set(current_white);
1236 }
1237 prev_next_ptr = unsafe { NonNull::from(&(*ptr.as_ptr()).header.next) };
1238 self.sweep_prev_next.set(Some(prev_next_ptr));
1239 }
1240 work += 1;
1241 budget -= 1;
1242 }
1243 if freed_bytes > 0 {
1244 self.bytes.set(self.bytes.get().saturating_sub(freed_bytes));
1245 }
1246 work
1247 }
1248
1249 fn sweep_young(&self) {
1250 let mut freed_bytes = 0usize;
1251 let current_white = self.current_white();
1252 let mut prev_next_ptr = NonNull::from(&self.head);
1253 loop {
1254 let prev_cell = unsafe { prev_next_ptr.as_ref() };
1255 let Some(ptr) = prev_cell.get() else { break; };
1256 let header = self.header_from_ptr(ptr);
1257 let next = header.next.get();
1258 if header.color.get().is_white() && !header.age.get().is_old() {
1259 prev_cell.set(next);
1260 freed_bytes += header.size.get();
1261 self.allocation_tokens
1262 .borrow_mut()
1263 .remove(&(ptr.as_ptr() as *const () as usize));
1264 unsafe {
1265 let _ = Box::from_raw(ptr.as_ptr());
1266 }
1267 continue;
1268 }
1269
1270 if !header.color.get().is_white() {
1271 let age = header.age.get();
1272 header.age.set(age.next_after_minor());
1273 match age {
1274 GcAge::New => header.color.set(current_white),
1275 GcAge::Touched1 | GcAge::Touched2 => header.color.set(Color::Black),
1276 _ => {}
1277 }
1278 }
1279 prev_next_ptr = unsafe { NonNull::from(&(*ptr.as_ptr()).header.next) };
1280 }
1281 if freed_bytes > 0 {
1282 self.bytes.set(self.bytes.get().saturating_sub(freed_bytes));
1283 }
1284 }
1285
1286 fn finish_cycle(&self) {
1287 *self.marker.borrow_mut() = None;
1288 self.sweep_prev_next.set(None);
1289 let next = self
1290 .bytes
1291 .get()
1292 .saturating_mul(self.pause_multiplier.get())
1293 / 100;
1294 self.threshold.set(next.max(GC_MIN_THRESHOLD));
1295 self.collections.set(self.collections.get() + 1);
1296 }
1297
1298 fn abort_cycle(&self) {
1299 if !self.state.get().is_pause() {
1300 *self.marker.borrow_mut() = None;
1301 self.sweep_prev_next.set(None);
1302 let current_white = self.current_white();
1303 self.for_each_header(|header| {
1304 header.color.set(current_white);
1305 });
1306 self.state.set(GcState::Pause);
1307 }
1308 }
1309
1310 pub fn gc_state(&self) -> GcState {
1312 self.state.get()
1313 }
1314
1315 pub fn allgc_count(&self) -> usize {
1318 let mut count = 0usize;
1319 self.for_each_header(|_| {
1320 count += 1;
1321 });
1322 count
1323 }
1324
1325 pub fn type_name_count(&self, mut predicate: impl FnMut(&'static str) -> bool) -> usize {
1329 let mut count = 0usize;
1330 self.for_each_header(|header| {
1331 if predicate(header.type_name) {
1332 count += 1;
1333 }
1334 });
1335 count
1336 }
1337
1338 pub fn drop_all(&self) {
1342 *self.marker.borrow_mut() = None;
1343 self.sweep_prev_next.set(None);
1344 self.state.set(GcState::Pause);
1345 let mut cursor = self.head.get();
1346 self.head.set(None);
1347 self.allocation_tokens.borrow_mut().clear();
1348 while let Some(ptr) = cursor {
1349 let next = unsafe {
1351 let next = (*ptr.as_ptr()).header.next.get();
1352 let _ = Box::from_raw(ptr.as_ptr());
1353 next
1354 };
1355 cursor = next;
1356 }
1357 self.bytes.set(0);
1358 }
1359}
1360
1361impl Drop for Heap {
1362 fn drop(&mut self) {
1363 self.drop_all();
1364 }
1365}
1366
1367#[cfg(test)]
1372mod tests {
1373 use super::*;
1374
1375 struct Cell0 {
1377 next: Cell<Option<Gc<Cell0>>>,
1378 marker_calls: Cell<usize>,
1379 }
1380
1381 impl Trace for Cell0 {
1382 fn trace(&self, m: &mut Marker) {
1383 self.marker_calls.set(self.marker_calls.get() + 1);
1384 if let Some(n) = self.next.get() {
1385 m.mark(n);
1386 }
1387 }
1388 }
1389
1390 struct OneRoot(Option<Gc<Cell0>>);
1392 impl Trace for OneRoot {
1393 fn trace(&self, m: &mut Marker) {
1394 if let Some(g) = self.0 {
1395 m.mark(g);
1396 }
1397 }
1398 }
1399
1400 #[test]
1401 fn alloc_and_drop_all() {
1402 let heap = Heap::new();
1403 heap.unpause();
1404 let _a = heap.allocate(Cell0 {
1405 next: Cell::new(None),
1406 marker_calls: Cell::new(0),
1407 });
1408 let _b = heap.allocate(Cell0 {
1409 next: Cell::new(None),
1410 marker_calls: Cell::new(0),
1411 });
1412 assert!(heap.bytes_used() > 0);
1413 heap.drop_all();
1414 assert_eq!(heap.bytes_used(), 0);
1415 }
1416
1417 #[test]
1418 fn account_buffer_refunded_on_sweep() {
1419 let heap = Heap::new();
1420 heap.unpause();
1421 let baseline = heap.bytes_used();
1422 {
1423 let a = heap.allocate(Cell0 {
1424 next: Cell::new(None),
1425 marker_calls: Cell::new(0),
1426 });
1427 assert!(heap.bytes_used() > baseline);
1428 a.account_buffer(&heap, 4096);
1429 assert_eq!(a.header().size.get(), std::mem::size_of::<GcBox<Cell0>>() + 4096);
1430 }
1431 heap.full_collect(&OneRoot(None));
1434 assert_eq!(
1435 heap.bytes_used(),
1436 baseline,
1437 "account_buffer charge must be fully refunded by sweep"
1438 );
1439 }
1440
1441 #[test]
1442 fn account_buffer_noop_on_uncollected() {
1443 let heap = Heap::new();
1444 heap.unpause();
1445 let g = Gc::new_uncollected(Cell0 {
1446 next: Cell::new(None),
1447 marker_calls: Cell::new(0),
1448 });
1449 let before = heap.bytes_used();
1450 g.account_buffer(&heap, 8192);
1451 assert_eq!(heap.bytes_used(), before, "uncollected box must not charge the pacer");
1452 }
1453
1454 #[test]
1455 fn collect_unreachable_frees_bytes() {
1456 let heap = Heap::new();
1457 heap.unpause();
1458 let _a = heap.allocate(Cell0 {
1459 next: Cell::new(None),
1460 marker_calls: Cell::new(0),
1461 });
1462 let bytes_before = heap.bytes_used();
1463 assert!(bytes_before > 0);
1464 heap.full_collect(&OneRoot(None));
1466 assert_eq!(heap.bytes_used(), 0);
1467 assert_eq!(heap.collections(), 1);
1468 }
1469
1470 #[test]
1471 fn collect_keeps_reachable() {
1472 let heap = Heap::new();
1473 heap.unpause();
1474 let root = heap.allocate(Cell0 {
1475 next: Cell::new(None),
1476 marker_calls: Cell::new(0),
1477 });
1478 let bytes_before = heap.bytes_used();
1479 heap.full_collect(&OneRoot(Some(root)));
1480 assert_eq!(heap.bytes_used(), bytes_before);
1481 assert_eq!(root.marker_calls.get(), 1);
1482 }
1483
1484 #[test]
1485 fn allocations_start_new_and_white() {
1486 let heap = Heap::new();
1487 heap.unpause();
1488 let obj = heap.allocate(Cell0 {
1489 next: Cell::new(None),
1490 marker_calls: Cell::new(0),
1491 });
1492 assert_eq!(obj.age(), GcAge::New);
1493 assert!(obj.color().is_white());
1494 }
1495
1496 #[test]
1497 fn allocation_tokens_track_exact_live_box() {
1498 let heap = Heap::new();
1499 heap.unpause();
1500 let obj = heap.allocate(Cell0 {
1501 next: Cell::new(None),
1502 marker_calls: Cell::new(0),
1503 });
1504 let id = obj.identity();
1505 let token = heap
1506 .allocation_token(id)
1507 .expect("heap allocation should get a token");
1508
1509 assert!(heap.contains_allocation(id, token));
1510 assert!(!heap.contains_allocation(id, token + 1));
1511
1512 heap.full_collect(&OneRoot(None));
1513 assert_eq!(heap.allocation_token(id), None);
1514 assert!(!heap.contains_allocation(id, token));
1515 }
1516
1517 #[test]
1518 fn allocation_during_incremental_sweep_survives_current_cycle() {
1519 let heap = Heap::new();
1520 heap.unpause();
1521 let old_dead = heap.allocate(Cell0 {
1522 next: Cell::new(None),
1523 marker_calls: Cell::new(0),
1524 });
1525 let old_id = old_dead.identity();
1526
1527 let outcome = heap.incremental_step_with_post_mark(
1528 &OneRoot(None),
1529 StepBudget::from_work(1),
1530 |_| {},
1531 );
1532 assert_eq!(outcome, StepOutcome::InProgress);
1533 assert_eq!(heap.gc_state(), GcState::Sweep);
1534
1535 let new_during_sweep = heap.allocate(Cell0 {
1536 next: Cell::new(None),
1537 marker_calls: Cell::new(0),
1538 });
1539 let new_id = new_during_sweep.identity();
1540
1541 loop {
1542 let outcome = heap.incremental_step_with_post_mark(
1543 &OneRoot(None),
1544 StepBudget::from_work(64),
1545 |_| {},
1546 );
1547 if matches!(outcome, StepOutcome::Paused) {
1548 break;
1549 }
1550 }
1551
1552 assert_eq!(heap.allocation_token(old_id), None);
1553 assert!(heap.allocation_token(new_id).is_some());
1554 assert_eq!(heap.allgc_count(), 1);
1555
1556 heap.full_collect(&OneRoot(None));
1557 assert_eq!(heap.allocation_token(new_id), None);
1558 assert_eq!(heap.allgc_count(), 0);
1559 }
1560
1561 #[test]
1562 fn promote_and_reset_all_ages() {
1563 let heap = Heap::new();
1564 heap.unpause();
1565 let a = heap.allocate(Cell0 {
1566 next: Cell::new(None),
1567 marker_calls: Cell::new(0),
1568 });
1569 let b = heap.allocate(Cell0 {
1570 next: Cell::new(None),
1571 marker_calls: Cell::new(0),
1572 });
1573
1574 heap.promote_all_to_old();
1575 assert_eq!(a.age(), GcAge::Old);
1576 assert_eq!(b.age(), GcAge::Old);
1577 assert_eq!(a.color(), Color::Black);
1578 assert_eq!(b.color(), Color::Black);
1579
1580 heap.reset_all_ages();
1581 assert_eq!(a.age(), GcAge::New);
1582 assert_eq!(b.age(), GcAge::New);
1583 assert!(a.color().is_white());
1584 assert!(b.color().is_white());
1585 }
1586
1587 #[test]
1588 fn generational_barriers_update_ages() {
1589 let heap = Heap::new();
1590 heap.unpause();
1591 let parent = heap.allocate(Cell0 {
1592 next: Cell::new(None),
1593 marker_calls: Cell::new(0),
1594 });
1595 let child = heap.allocate(Cell0 {
1596 next: Cell::new(None),
1597 marker_calls: Cell::new(0),
1598 });
1599
1600 parent.set_age(GcAge::Old);
1601 parent.set_color(Color::Black);
1602 heap.generational_backward_barrier(parent);
1603 assert_eq!(parent.age(), GcAge::Touched1);
1604 assert_eq!(parent.color(), Color::Gray);
1605
1606 heap.generational_forward_barrier(parent, child);
1607 assert_eq!(child.age(), GcAge::Old0);
1608 }
1609
1610 #[test]
1611 fn minor_collect_frees_young_and_keeps_old() {
1612 let heap = Heap::new();
1613 heap.unpause();
1614 let old_unreachable = heap.allocate(Cell0 {
1615 next: Cell::new(None),
1616 marker_calls: Cell::new(0),
1617 });
1618 old_unreachable.set_age(GcAge::Old);
1619 old_unreachable.set_color(Color::Black);
1620 let _young_unreachable = heap.allocate(Cell0 {
1621 next: Cell::new(None),
1622 marker_calls: Cell::new(0),
1623 });
1624 let young_survivor = heap.allocate(Cell0 {
1625 next: Cell::new(None),
1626 marker_calls: Cell::new(0),
1627 });
1628
1629 heap.minor_collect_with_post_mark(&OneRoot(Some(young_survivor)), |_| {});
1630
1631 assert_eq!(heap.allgc_count(), 2);
1632 assert_eq!(old_unreachable.age(), GcAge::Old);
1633 assert_eq!(young_survivor.age(), GcAge::Survival);
1634 assert!(young_survivor.color().is_white());
1635 }
1636
1637 #[test]
1638 fn collect_traverses_cycles() {
1639 let heap = Heap::new();
1640 heap.unpause();
1641 let a = heap.allocate(Cell0 {
1642 next: Cell::new(None),
1643 marker_calls: Cell::new(0),
1644 });
1645 let b = heap.allocate(Cell0 {
1646 next: Cell::new(Some(a)),
1647 marker_calls: Cell::new(0),
1648 });
1649 a.next.set(Some(b)); heap.full_collect(&OneRoot(Some(a)));
1652 assert_eq!(a.marker_calls.get(), 1);
1653 assert_eq!(b.marker_calls.get(), 1);
1654 heap.full_collect(&OneRoot(None));
1658 assert_eq!(heap.bytes_used(), 0);
1659 }
1660
1661 #[test]
1662 fn heap_guard_stacks() {
1663 assert!(with_current_heap(|heap| heap.is_none()), "no guard initially");
1664 let h1 = Heap::new();
1665 h1.unpause();
1666 {
1667 let _g1 = HeapGuard::push(&h1);
1668 assert!(with_current_heap(|heap| heap.is_some()));
1669 let h2 = Heap::new();
1670 h2.unpause();
1671 {
1672 let _g2 = HeapGuard::push(&h2);
1673 with_current_heap(|heap| {
1675 assert!(std::ptr::addr_eq(
1676 heap.unwrap() as *const _,
1677 &h2 as *const _,
1678 ));
1679 });
1680 }
1681 with_current_heap(|heap| {
1683 assert!(std::ptr::addr_eq(
1684 heap.unwrap() as *const _,
1685 &h1 as *const _,
1686 ));
1687 });
1688 }
1689 assert!(
1690 with_current_heap(|heap| heap.is_none()),
1691 "stack empty after all guards drop"
1692 );
1693 }
1694
1695 #[test]
1696 fn step_threshold_triggers_collect() {
1697 let heap = Heap::new();
1698 heap.unpause();
1699 let mut keeps = Vec::new();
1701 for _ in 0..64 {
1702 keeps.push(heap.allocate(Cell0 {
1706 next: Cell::new(None),
1707 marker_calls: Cell::new(0),
1708 }));
1709 }
1710 struct ManyRoots<'a>(&'a [Gc<Cell0>]);
1712 impl<'a> Trace for ManyRoots<'a> {
1713 fn trace(&self, m: &mut Marker) {
1714 for g in self.0.iter() {
1715 m.mark(*g);
1716 }
1717 }
1718 }
1719 heap.step(&ManyRoots(&keeps));
1720 assert!(heap.bytes_used() > 0);
1722 }
1723
1724 #[test]
1725 fn threshold_floored_after_collecting_tiny_heap() {
1726 let heap = Heap::new();
1727 heap.unpause();
1728 struct NoRoots;
1729 impl Trace for NoRoots {
1730 fn trace(&self, _m: &mut Marker) {}
1731 }
1732 for _ in 0..200 {
1733 heap.allocate(Cell0 {
1734 next: Cell::new(None),
1735 marker_calls: Cell::new(0),
1736 });
1737 }
1738 heap.full_collect(&NoRoots);
1739 assert!(
1740 heap.threshold_bytes() >= GC_MIN_THRESHOLD,
1741 "threshold {} collapsed below floor {}; a churning program would full-collect per allocation",
1742 heap.threshold_bytes(),
1743 GC_MIN_THRESHOLD
1744 );
1745 }
1746
1747 fn build_chain(heap: &Heap, len: usize) -> Gc<Cell0> {
1748 let head = heap.allocate(Cell0 {
1749 next: Cell::new(None),
1750 marker_calls: Cell::new(0),
1751 });
1752 let mut cur = head;
1753 for _ in 1..len {
1754 let n = heap.allocate(Cell0 {
1755 next: Cell::new(None),
1756 marker_calls: Cell::new(0),
1757 });
1758 cur.next.set(Some(n));
1759 cur = n;
1760 }
1761 head
1762 }
1763
1764 #[test]
1765 fn budget_zero_does_some_work() {
1766 let heap = Heap::new();
1767 heap.unpause();
1768 let head = build_chain(&heap, 8);
1769 let roots = OneRoot(Some(head));
1770 let budget = StepBudget::from_work(0);
1771 let outcome = heap.incremental_step_with_post_mark(&roots, budget, |_| {});
1772 assert_ne!(outcome, StepOutcome::SkippedStopped);
1773 assert_ne!(heap.gc_state(), GcState::Pause);
1774 }
1775
1776 #[test]
1777 fn run_until_state_stops_before_next_phase_work() {
1778 let heap = Heap::new();
1779 heap.unpause();
1780 let head = build_chain(&heap, 8);
1781 let roots = OneRoot(Some(head));
1782 let atomic_calls = Cell::new(0);
1783
1784 let outcome = heap.incremental_run_until_state_with_post_mark(
1785 &roots,
1786 GcState::Atomic,
1787 1024,
1788 |_| atomic_calls.set(atomic_calls.get() + 1),
1789 );
1790 assert_eq!(outcome, StepOutcome::InProgress);
1791 assert_eq!(heap.gc_state(), GcState::Atomic);
1792 assert_eq!(atomic_calls.get(), 0, "atomic hook must not run before inspection");
1793
1794 let outcome = heap.incremental_run_until_state_with_post_mark(
1795 &roots,
1796 GcState::Sweep,
1797 1024,
1798 |_| atomic_calls.set(atomic_calls.get() + 1),
1799 );
1800 assert_eq!(outcome, StepOutcome::InProgress);
1801 assert_eq!(heap.gc_state(), GcState::Sweep);
1802 assert_eq!(atomic_calls.get(), 1, "entering sweep must run the atomic hook once");
1803 }
1804
1805 #[test]
1806 fn larger_budget_drains_more_gray_than_smaller() {
1807 let small_heap = Heap::new();
1808 small_heap.unpause();
1809 let h1 = build_chain(&small_heap, 64);
1810 let r1 = OneRoot(Some(h1));
1811 let mut small_calls = 0;
1812 loop {
1813 small_calls += 1;
1814 let outcome = small_heap.incremental_step_with_post_mark(
1815 &r1,
1816 StepBudget::from_work(2),
1817 |_| {},
1818 );
1819 if outcome == StepOutcome::Paused {
1820 break;
1821 }
1822 assert!(small_calls < 10_000, "did not converge");
1823 }
1824
1825 let big_heap = Heap::new();
1826 big_heap.unpause();
1827 let h2 = build_chain(&big_heap, 64);
1828 let r2 = OneRoot(Some(h2));
1829 let mut big_calls = 0;
1830 loop {
1831 big_calls += 1;
1832 let outcome = big_heap.incremental_step_with_post_mark(
1833 &r2,
1834 StepBudget::from_work(64),
1835 |_| {},
1836 );
1837 if outcome == StepOutcome::Paused {
1838 break;
1839 }
1840 assert!(big_calls < 10_000, "did not converge");
1841 }
1842
1843 assert!(
1844 big_calls < small_calls,
1845 "expected big_calls={} < small_calls={}",
1846 big_calls,
1847 small_calls
1848 );
1849 }
1850
1851 #[test]
1852 fn sweep_can_pause_and_resume() {
1853 let heap = Heap::new();
1854 heap.unpause();
1855 for _ in 0..16 {
1856 let _ = heap.allocate(Cell0 {
1857 next: Cell::new(None),
1858 marker_calls: Cell::new(0),
1859 });
1860 }
1861 let roots = OneRoot(None);
1862 let bytes_before = heap.bytes_used();
1863 assert!(bytes_before > 0);
1864 let mut step_count = 0;
1865 let mut saw_in_progress_during_sweep = false;
1866 loop {
1867 step_count += 1;
1868 let outcome = heap.incremental_step_with_post_mark(
1869 &roots,
1870 StepBudget::from_work(2),
1871 |_| {},
1872 );
1873 if heap.gc_state() == GcState::Sweep && outcome == StepOutcome::InProgress {
1874 saw_in_progress_during_sweep = true;
1875 }
1876 if outcome == StepOutcome::Paused {
1877 break;
1878 }
1879 assert!(step_count < 10_000, "did not converge");
1880 }
1881 assert!(saw_in_progress_during_sweep, "sweep never paused mid-list");
1882 assert_eq!(heap.bytes_used(), 0);
1883 }
1884
1885 #[test]
1886 fn post_mark_runs_once_per_atomic() {
1887 let heap = Heap::new();
1888 heap.unpause();
1889 for _ in 0..32 {
1890 let _ = heap.allocate(Cell0 {
1891 next: Cell::new(None),
1892 marker_calls: Cell::new(0),
1893 });
1894 }
1895 let roots = OneRoot(None);
1896 let call_count = std::cell::Cell::new(0);
1897 let mut step_count = 0;
1898 loop {
1899 step_count += 1;
1900 let outcome = heap.incremental_step_with_post_mark(
1901 &roots,
1902 StepBudget::from_work(2),
1903 |_| {
1904 call_count.set(call_count.get() + 1);
1905 },
1906 );
1907 if outcome == StepOutcome::Paused {
1908 break;
1909 }
1910 assert!(step_count < 10_000, "did not converge");
1911 }
1912 assert_eq!(call_count.get(), 1, "post_mark must run exactly once per cycle");
1913 }
1914
1915 #[test]
1916 fn full_collect_equivalent_to_incremental_to_pause() {
1917 let h1 = Heap::new();
1918 h1.unpause();
1919 let head1 = h1.allocate(Cell0 {
1920 next: Cell::new(None),
1921 marker_calls: Cell::new(0),
1922 });
1923 let _orphan1 = h1.allocate(Cell0 {
1924 next: Cell::new(None),
1925 marker_calls: Cell::new(0),
1926 });
1927 let _orphan2 = h1.allocate(Cell0 {
1928 next: Cell::new(None),
1929 marker_calls: Cell::new(0),
1930 });
1931 let roots1 = OneRoot(Some(head1));
1932 h1.full_collect(&roots1);
1933 let bytes_full = h1.bytes_used();
1934
1935 let h2 = Heap::new();
1936 h2.unpause();
1937 let head2 = h2.allocate(Cell0 {
1938 next: Cell::new(None),
1939 marker_calls: Cell::new(0),
1940 });
1941 let _orphan3 = h2.allocate(Cell0 {
1942 next: Cell::new(None),
1943 marker_calls: Cell::new(0),
1944 });
1945 let _orphan4 = h2.allocate(Cell0 {
1946 next: Cell::new(None),
1947 marker_calls: Cell::new(0),
1948 });
1949 let roots2 = OneRoot(Some(head2));
1950 loop {
1951 let outcome = h2.incremental_step_with_post_mark(
1952 &roots2,
1953 StepBudget::from_work(1),
1954 |_| {},
1955 );
1956 if outcome == StepOutcome::Paused {
1957 break;
1958 }
1959 }
1960 assert_eq!(h2.bytes_used(), bytes_full);
1961 }
1962}
1963
1964