gc_arena/
context.rs

1use alloc::{boxed::Box, vec::Vec};
2use core::{
3    cell::{Cell, RefCell},
4    mem,
5    ops::Deref,
6    ptr::NonNull,
7};
8
9use crate::{
10    collect::Collect,
11    metrics::Metrics,
12    types::{GcBox, GcBoxHeader, GcBoxInner, GcColor, Invariant},
13};
14
15/// Handle value given by arena callbacks during construction and mutation. Allows allocating new
16/// `Gc` pointers and internally mutating values held by `Gc` pointers.
17#[repr(transparent)]
18pub struct Mutation<'gc> {
19    context: Context,
20    _invariant: Invariant<'gc>,
21}
22
23impl<'gc> Mutation<'gc> {
24    #[inline]
25    pub fn metrics(&self) -> &Metrics {
26        self.context.metrics()
27    }
28
29    #[inline]
30    pub(crate) fn allocate<T: 'gc + Collect>(&self, t: T) -> NonNull<GcBoxInner<T>> {
31        self.context.allocate(t)
32    }
33
34    #[inline]
35    pub(crate) fn write_barrier(&self, gc_box: GcBox) {
36        self.context.write_barrier(gc_box)
37    }
38
39    #[inline]
40    pub(crate) fn upgrade(&self, gc_box: GcBox) -> bool {
41        self.context.upgrade(gc_box)
42    }
43}
44
45/// Handle value given to finalization callbacks in `MarkedArena`.
46///
47/// Derefs to `Mutation<'gc>` to allow for arbitrary mutation, but adds additional powers to examine
48/// the state of the fully marked arena.
49#[repr(transparent)]
50pub struct Finalization<'gc> {
51    context: Context,
52    _invariant: Invariant<'gc>,
53}
54
55impl<'gc> Deref for Finalization<'gc> {
56    type Target = Mutation<'gc>;
57
58    fn deref(&self) -> &Self::Target {
59        // SAFETY: Finalization and Mutation are #[repr(transparent)]
60        unsafe { mem::transmute::<&Self, &Mutation>(&self) }
61    }
62}
63
64impl<'gc> Finalization<'gc> {
65    #[inline]
66    pub(crate) fn resurrect(&self, gc_box: GcBox) {
67        self.context.resurrect(gc_box)
68    }
69}
70
71/// Handle value given by arena callbacks during garbage collection, which must be passed through
72/// `Collect::trace` implementations.
73#[repr(transparent)]
74pub struct Collection {
75    context: Context,
76}
77
78impl Collection {
79    #[inline]
80    pub fn metrics(&self) -> &Metrics {
81        self.context.metrics()
82    }
83
84    #[inline]
85    pub(crate) fn trace(&self, gc_box: GcBox) {
86        self.context.trace(gc_box)
87    }
88
89    #[inline]
90    pub(crate) fn trace_weak(&self, gc_box: GcBox) {
91        self.context.trace_weak(gc_box)
92    }
93}
94
95#[derive(Debug, Copy, Clone, Eq, PartialEq)]
96pub(crate) enum Phase {
97    Mark,
98    Sweep,
99    Sleep,
100    Drop,
101}
102
103#[derive(Debug, Copy, Clone, Eq, PartialEq)]
104pub(crate) enum EarlyStop {
105    BeforeSweep,
106    AfterSweep,
107}
108
109pub(crate) struct Context {
110    metrics: Metrics,
111    phase: Cell<Phase>,
112    #[cfg(feature = "tracing")]
113    phase_span: Cell<tracing::Span>,
114
115    root_needs_trace: Cell<bool>,
116
117    // A linked list of all allocated `GcBox`es.
118    all: Cell<Option<GcBox>>,
119
120    // A copy of the head of `all` at the end of `Phase::Mark`.
121    // During `Phase::Sweep`, we free all white allocations on this list.
122    // Any allocations created *during* `Phase::Sweep` will be added to `all`,
123    // but `sweep` will *not* be updated. This ensures that we keep allocations
124    // alive until we've had a chance to trace them.
125    sweep: Cell<Option<GcBox>>,
126
127    // The most recent black object that we encountered during `Phase::Sweep`.
128    // When we free objects, we update this `GcBox.next` to remove them from
129    // the linked list.
130    sweep_prev: Cell<Option<GcBox>>,
131
132    /// A queue of gray objects, used during `Phase::Mark`.
133    /// This holds traceable objects that have yet to be traced.
134    /// When we enter `Phase::Mark`, we push `root` to this queue.
135    gray: RefCell<Vec<GcBox>>,
136
137    // A queue of gray objects that became gray as a result
138    // of a `write_barrier` call.
139    gray_again: RefCell<Vec<GcBox>>,
140}
141
142impl Drop for Context {
143    fn drop(&mut self) {
144        struct DropAll(Option<GcBox>);
145
146        impl Drop for DropAll {
147            fn drop(&mut self) {
148                if let Some(gc_box) = self.0.take() {
149                    let mut drop_resume = DropAll(Some(gc_box));
150                    while let Some(gc_box) = drop_resume.0.take() {
151                        drop_resume.0 = gc_box.header().next();
152                        // SAFETY: the context owns its GC'd objects
153                        unsafe { free_gc_box(gc_box) }
154                    }
155                }
156            }
157        }
158
159        let _guard = PhaseGuard::enter(&self, Some(Phase::Drop));
160        DropAll(self.all.get());
161    }
162}
163
164impl Context {
165    pub(crate) unsafe fn new() -> Context {
166        let metrics = Metrics::new();
167        Context {
168            phase: Cell::new(Phase::Sleep),
169            #[cfg(feature = "tracing")]
170            phase_span: Cell::new(PhaseGuard::span_for(&metrics, Phase::Sleep)),
171            metrics,
172            root_needs_trace: Cell::new(true),
173            all: Cell::new(None),
174            sweep: Cell::new(None),
175            sweep_prev: Cell::new(None),
176            gray: RefCell::new(Vec::new()),
177            gray_again: RefCell::new(Vec::new()),
178        }
179    }
180
181    #[inline]
182    pub(crate) unsafe fn mutation_context<'gc>(&self) -> &Mutation<'gc> {
183        mem::transmute::<&Self, &Mutation>(&self)
184    }
185
186    #[inline]
187    fn collection_context(&self) -> &Collection {
188        // SAFETY: `Collection` is `repr(transparent)`
189        unsafe { mem::transmute::<&Self, &Collection>(self) }
190    }
191
192    #[inline]
193    pub(crate) unsafe fn finalization_context<'gc>(&self) -> &Finalization<'gc> {
194        mem::transmute::<&Self, &Finalization>(&self)
195    }
196
197    #[inline]
198    pub(crate) fn metrics(&self) -> &Metrics {
199        &self.metrics
200    }
201
202    #[inline]
203    pub(crate) fn root_barrier(&self) {
204        if self.phase.get() == Phase::Mark {
205            self.root_needs_trace.set(true);
206        }
207    }
208
209    #[inline]
210    pub(crate) fn phase(&self) -> Phase {
211        self.phase.get()
212    }
213
214    #[inline]
215    pub(crate) fn gray_remaining(&self) -> bool {
216        !self.gray.borrow().is_empty()
217            || !self.gray_again.borrow().is_empty()
218            || self.root_needs_trace.get()
219    }
220
221    // Do some collection work until either the debt goes down below the target amount or we have
222    // finished the gc sweep phase. The unit of "work" here is a byte count of objects either turned
223    // black or freed, so to completely collect a heap with 1000 bytes of objects should take 1000
224    // units of work, whatever percentage of them are live or not.
225    //
226    // In order for this to be safe, at the time of call no `Gc` pointers can be live that are not
227    // reachable from the given root object.
228    //
229    // If we are currently in `Phase::Sleep`, this will transition the collector to `Phase::Mark`.
230    pub(crate) unsafe fn do_collection<R: Collect>(
231        &self,
232        root: &R,
233        target_debt: f64,
234        early_stop: Option<EarlyStop>,
235    ) {
236        self.do_collection_inner(root, target_debt, early_stop)
237    }
238
239    fn do_collection_inner<R: Collect>(
240        &self,
241        root: &R,
242        mut target_debt: f64,
243        early_stop: Option<EarlyStop>,
244    ) {
245        let mut entered = PhaseGuard::enter(self, None);
246
247        if !(self.metrics.allocation_debt() > target_debt) {
248            entered.log_progress("GC: paused");
249            return;
250        }
251
252        loop {
253            match self.phase.get() {
254                Phase::Sleep => {
255                    // Immediately enter the mark phase; no need to update metrics here.
256                    entered.switch(Phase::Mark);
257                    continue;
258                }
259                Phase::Mark => {
260                    // We look for an object first in the normal gray queue, then the "gray again"
261                    // queue. Objects from the normal gray queue count as regular work, but objects
262                    // which are gray a second time have already been counted as work, so we don't
263                    // double count them. Processing "gray again" objects later also gives them more
264                    // time to be mutated again without triggering another write barrier.
265                    let next_gray = if let Some(gc_box) = self.gray.borrow_mut().pop() {
266                        self.metrics.mark_gc_traced(gc_box.header().size_of_box());
267                        Some(gc_box)
268                    } else if let Some(gc_box) = self.gray_again.borrow_mut().pop() {
269                        Some(gc_box)
270                    } else {
271                        None
272                    };
273
274                    if let Some(gc_box) = next_gray {
275                        // If we have an object in the gray queue, take one, trace it, and turn it
276                        // black.
277
278                        // Our `Collect::trace` call may panic, and if it does the object will be
279                        // lost from the gray queue but potentially incompletely traced. By catching
280                        // a panic during `Arena::collect()`, this could lead to memory unsafety.
281                        //
282                        // So, if the `Collect::trace` call panics, we need to add the popped object
283                        // back to the `gray_again` queue. If the panic is caught, this will maybe
284                        // give it some time to not panic before attempting to collect it again, and
285                        // also this doesn't invalidate the collection debt math.
286                        struct DropGuard<'a> {
287                            cx: &'a Context,
288                            gc_box: GcBox,
289                        }
290
291                        impl<'a> Drop for DropGuard<'a> {
292                            fn drop(&mut self) {
293                                self.cx.gray_again.borrow_mut().push(self.gc_box);
294                            }
295                        }
296
297                        let guard = DropGuard { cx: self, gc_box };
298                        debug_assert!(gc_box.header().is_live());
299                        unsafe { gc_box.trace_value(self.collection_context()) }
300                        gc_box.header().set_color(GcColor::Black);
301                        mem::forget(guard);
302                    } else if self.root_needs_trace.get() {
303                        // We treat the root object as gray if `root_needs_trace` is set, and we
304                        // process it at the end of the gray queue for the same reason as the "gray
305                        // again" objects.
306                        root.trace(self.collection_context());
307                        self.root_needs_trace.set(false);
308                    } else {
309                        if early_stop == Some(EarlyStop::BeforeSweep) {
310                            target_debt = f64::INFINITY;
311                        } else {
312                            // If we have no gray objects left, we enter the sweep phase.
313                            entered.switch(Phase::Sweep);
314
315                            // Set `sweep to the current head of our `all` linked list. Any new
316                            // allocations during the newly-entered `Phase:Sweep` will update `all`,
317                            // but will *not* be reachable from `this.sweep`.
318                            self.sweep.set(self.all.get());
319
320                            // No need to update metrics here.
321                            continue;
322                        }
323                    }
324                }
325                Phase::Sweep => {
326                    if early_stop == Some(EarlyStop::AfterSweep) {
327                        target_debt = f64::INFINITY;
328                    } else if let Some(mut sweep) = self.sweep.get() {
329                        let sweep_header = sweep.header();
330
331                        let next_box = sweep_header.next();
332                        self.sweep.set(next_box);
333
334                        match sweep_header.color() {
335                            // If the next object in the sweep portion of the main list is white, we
336                            // need to remove it from the main object list and destruct it.
337                            GcColor::White => {
338                                if let Some(sweep_prev) = self.sweep_prev.get() {
339                                    sweep_prev.header().set_next(next_box);
340                                } else {
341                                    // If `sweep_prev` is None, then the sweep pointer is also the
342                                    // beginning of the main object list, so we need to adjust it.
343                                    debug_assert_eq!(self.all.get(), Some(sweep));
344                                    self.all.set(next_box);
345                                }
346                                self.metrics.mark_gc_deallocated(sweep_header.size_of_box());
347
348                                // SAFETY: this object is white, and wasn't traced by a `GcWeak`
349                                // during this cycle, meaning it cannot have either strong or weak
350                                // pointers, so we can drop the whole object.
351                                unsafe { free_gc_box(sweep) }
352                            }
353                            // Keep the `GcBox` as part of the linked list if we traced a weak
354                            // pointer to it. The weak pointer still needs access to the `GcBox` to
355                            // be able to check if the object is still alive. We can only deallocate
356                            // the `GcBox`, once there are no weak pointers left.
357                            GcColor::WhiteWeak => {
358                                self.sweep_prev.set(Some(sweep));
359                                sweep_header.set_color(GcColor::White);
360                                if sweep_header.is_live() {
361                                    sweep_header.set_live(false);
362                                    // SAFETY: Since this object is white, that means there are no
363                                    // more strong pointers to this object, only weak pointers, so
364                                    // we can safely drop its contents.
365                                    unsafe { sweep.drop_in_place() }
366                                }
367                            }
368                            // If the next object in the sweep portion of the main list is black, we
369                            // need to keep it but turn it back white.
370                            GcColor::Black => {
371                                self.sweep_prev.set(Some(sweep));
372                                self.metrics.mark_gc_remembered(sweep_header.size_of_box());
373                                sweep_header.set_color(GcColor::White);
374                            }
375                            // No gray objects should be in this part of the main list, they should
376                            // be added to the beginning of the list before the sweep pointer, so it
377                            // should not be possible for us to encounter them here.
378                            GcColor::Gray => {
379                                debug_assert!(false, "unexpected gray object in sweep list")
380                            }
381                        }
382                    } else {
383                        self.sweep_prev.set(None);
384                        self.root_needs_trace.set(true);
385                        entered.switch(Phase::Sleep);
386                        self.metrics.start_cycle();
387                        // Collection is done, forcibly exit the loop.
388                        target_debt = f64::INFINITY;
389                    }
390                }
391                Phase::Drop => unreachable!(),
392            }
393
394            if !(self.metrics.allocation_debt() > target_debt) {
395                entered.log_progress("GC: yielding...");
396                return;
397            }
398        }
399    }
400
401    fn allocate<T: Collect>(&self, t: T) -> NonNull<GcBoxInner<T>> {
402        let header = GcBoxHeader::new::<T>();
403        header.set_next(self.all.get());
404        header.set_live(true);
405        header.set_needs_trace(T::needs_trace());
406
407        let alloc_size = header.size_of_box();
408        self.metrics.mark_gc_allocated(alloc_size);
409
410        // Make the generated code easier to optimize into `T` being constructed in place or at the
411        // very least only memcpy'd once.
412        // For more information, see: https://github.com/kyren/gc-arena/pull/14
413        let (gc_box, ptr) = unsafe {
414            let mut uninitialized = Box::new(mem::MaybeUninit::<GcBoxInner<T>>::uninit());
415            core::ptr::write(uninitialized.as_mut_ptr(), GcBoxInner::new(header, t));
416            let ptr = NonNull::new_unchecked(Box::into_raw(uninitialized) as *mut GcBoxInner<T>);
417            (GcBox::erase(ptr), ptr)
418        };
419
420        self.all.set(Some(gc_box));
421        if self.phase.get() == Phase::Sweep && self.sweep_prev.get().is_none() {
422            self.sweep_prev.set(self.all.get());
423        }
424
425        ptr
426    }
427
428    #[inline]
429    fn write_barrier(&self, gc_box: GcBox) {
430        // During the propagating phase, if we are mutating a black object, we may add a white
431        // object to it and invalidate the invariant that black objects may not point to white
432        // objects. Turn black obejcts to gray to prevent this.
433        let header = gc_box.header();
434        if self.phase.get() == Phase::Mark && header.color() == GcColor::Black {
435            header.set_color(GcColor::Gray);
436
437            // Outline the actual enqueueing code (which is somewhat expensive and won't be
438            // executed often) to promote the inlining of the write barrier.
439            #[cold]
440            fn enqueue(this: &Context, gc_box: GcBox) {
441                this.gray_again.borrow_mut().push(gc_box);
442            }
443
444            enqueue(&self, gc_box);
445        }
446    }
447
448    #[inline]
449    fn trace(&self, gc_box: GcBox) {
450        let header = gc_box.header();
451        match header.color() {
452            GcColor::Black | GcColor::Gray => {}
453            GcColor::White | GcColor::WhiteWeak => {
454                if header.needs_trace() {
455                    // A white traceable object is not in the gray queue, becomes gray and enters
456                    // the normal gray queue.
457                    header.set_color(GcColor::Gray);
458                    debug_assert!(header.is_live());
459                    self.gray.borrow_mut().push(gc_box);
460                } else {
461                    // A white object that doesn't need tracing simply becomes black.
462                    header.set_color(GcColor::Black);
463                }
464            }
465        }
466    }
467
468    #[inline]
469    fn trace_weak(&self, gc_box: GcBox) {
470        let header = gc_box.header();
471        if header.color() == GcColor::White {
472            header.set_color(GcColor::WhiteWeak);
473        }
474    }
475
476    /// Determines whether or not a Gc pointer is safe to be upgraded.
477    /// This is used by weak pointers to determine if it can safely upgrade to a strong pointer.
478    #[inline]
479    fn upgrade(&self, gc_box: GcBox) -> bool {
480        let header = gc_box.header();
481
482        // This object has already been freed, definitely not safe to upgrade.
483        if !header.is_live() {
484            return false;
485        }
486
487        // Consider the different possible phases of the GC:
488        // * In `Phase::Sleep`, the GC is not running, so we can upgrade.
489        //   If the newly-created `Gc` or `GcCell` survives the current `arena.mutate`
490        //   call, then the situtation is equivalent to having copied an existing `Gc`/`GcCell`,
491        //   or having created a new allocation.
492        //
493        // * In `Phase::Mark`:
494        //   If the newly-created `Gc` or `GcCell` survives the current `arena.mutate`
495        //   call, then it must have been stored somewhere, triggering a write barrier.
496        //   This will ensure that the new `Gc`/`GcCell` gets traced (if it's now reachable)
497        //   before we transition to `Phase::Sweep`.
498        //
499        // * In `Phase::Sweep`:
500        //   If the allocation is `WhiteWeak`, then it's impossible for it to have been freshly-
501        //   created during this `Phase::Sweep`. `WhiteWeak` is only  set when a white `GcWeak/
502        //   GcWeakCell` is traced. A `GcWeak/GcWeakCell` must be created from an existing `Gc/
503        //   GcCell` via `downgrade()`, so `WhiteWeak` means that a `GcWeak` / `GcWeakCell` existed
504        //   during the last `Phase::Mark.`
505        //
506        //   Therefore, a `WhiteWeak` object is guaranteed to be deallocated during this
507        //   `Phase::Sweep`, and we must not upgrade it.
508        //
509        //   Conversely, it's always safe to upgrade a white object that is not `WhiteWeak`.
510        //   In order to call `upgrade`, you must have a `GcWeak/GcWeakCell`. Since it is
511        //   not `WhiteWeak` there cannot have been any `GcWeak/GcWeakCell`s during the
512        //   last `Phase::Mark`, so the weak pointer must have been created during this
513        //   `Phase::Sweep`. This is only possible if the underlying allocation was freshly-created
514        //   - if the allocation existed during `Phase::Mark` but was not traced, then it
515        //   must have been unreachable, which means that the user wouldn't have been able to call
516        //   `downgrade`. Therefore, we can safely upgrade, knowing that the object will not be
517        //   freed during this phase, despite being white.
518        if self.phase.get() == Phase::Sweep && header.color() == GcColor::WhiteWeak {
519            return false;
520        }
521        true
522    }
523
524    #[inline]
525    fn resurrect(&self, gc_box: GcBox) {
526        let header = gc_box.header();
527        debug_assert_eq!(self.phase.get(), Phase::Mark);
528        debug_assert!(header.is_live());
529        if matches!(header.color(), GcColor::White | GcColor::WhiteWeak) {
530            header.set_color(GcColor::Gray);
531            self.gray.borrow_mut().push(gc_box);
532        }
533    }
534}
535
536// SAFETY: the gc_box must never be accessed after calling this function.
537unsafe fn free_gc_box<'gc>(mut gc_box: GcBox) {
538    if gc_box.header().is_live() {
539        // If the alive flag is set, that means we haven't dropped the inner value of this object,
540        gc_box.drop_in_place();
541    }
542    gc_box.dealloc();
543}
544
545/// Helper type for managing phase transitions.
546struct PhaseGuard<'a> {
547    cx: &'a Context,
548    #[cfg(feature = "tracing")]
549    span: tracing::span::EnteredSpan,
550}
551
552impl<'a> Drop for PhaseGuard<'a> {
553    fn drop(&mut self) {
554        #[cfg(feature = "tracing")]
555        {
556            let span = mem::replace(&mut self.span, tracing::Span::none().entered());
557            self.cx.phase_span.set(span.exit());
558        }
559    }
560}
561
562impl<'a> PhaseGuard<'a> {
563    fn enter(cx: &'a Context, phase: Option<Phase>) -> Self {
564        if let Some(phase) = phase {
565            cx.phase.set(phase);
566        }
567
568        Self {
569            cx,
570            #[cfg(feature = "tracing")]
571            span: {
572                let mut span = cx.phase_span.replace(tracing::Span::none());
573                if let Some(phase) = phase {
574                    span = Self::span_for(&cx.metrics, phase);
575                }
576                span.entered()
577            },
578        }
579    }
580
581    fn switch(&mut self, phase: Phase) {
582        self.cx.phase.set(phase);
583
584        #[cfg(feature = "tracing")]
585        {
586            let _ = mem::replace(&mut self.span, tracing::Span::none().entered());
587            self.span = Self::span_for(&self.cx.metrics, phase).entered();
588        }
589    }
590
591    fn log_progress(&mut self, #[allow(unused)] message: &str) {
592        // TODO: add more infos here
593        #[cfg(feature = "tracing")]
594        tracing::debug!(
595            target: "gc_arena",
596            parent: &self.span,
597            message,
598            phase = tracing::field::debug(self.cx.phase.get()),
599            allocated = self.cx.metrics.total_allocation(),
600        );
601    }
602
603    #[cfg(feature = "tracing")]
604    fn span_for(metrics: &Metrics, phase: Phase) -> tracing::Span {
605        tracing::debug_span!(
606            target: "gc_arena",
607            "gc_arena",
608            id = metrics.arena_id(),
609            ?phase,
610        )
611    }
612}