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}