mmtk/plan/concurrent/immix/
global.rs

1use crate::plan::concurrent::global::ConcurrentPlan;
2use crate::plan::concurrent::immix::gc_work::ConcurrentImmixGCWorkContext;
3use crate::plan::concurrent::immix::gc_work::ConcurrentImmixSTWGCWorkContext;
4use crate::plan::concurrent::Pause;
5use crate::plan::global::BasePlan;
6use crate::plan::global::CommonPlan;
7use crate::plan::global::CreateGeneralPlanArgs;
8use crate::plan::global::CreateSpecificPlanArgs;
9use crate::plan::immix::mutator::ALLOCATOR_MAPPING;
10use crate::plan::tracing::gc_work::weakref::VMProcessWeakRefs;
11use crate::plan::AllocationSemantics;
12use crate::plan::Plan;
13use crate::plan::PlanConstraints;
14use crate::policy::immix::defrag::StatsForDefrag;
15use crate::policy::immix::ImmixSpaceArgs;
16use crate::policy::immix::TRACE_KIND_DEFRAG;
17use crate::policy::immix::TRACE_KIND_FAST;
18use crate::policy::space::Space;
19use crate::scheduler::gc_work::Release;
20use crate::scheduler::gc_work::StopMutators;
21use crate::scheduler::*;
22use crate::util::alloc::allocators::AllocatorSelector;
23use crate::util::copy::*;
24use crate::util::heap::gc_trigger::SpaceStats;
25use crate::util::heap::VMRequest;
26use crate::util::metadata::log_bit::UnlogBitsOperation;
27use crate::util::metadata::side_metadata::SideMetadataContext;
28use crate::vm::ObjectModel;
29use crate::vm::VMBinding;
30use crate::MMTK;
31use crate::{policy::immix::ImmixSpace, util::opaque_pointer::VMWorkerThread};
32use std::sync::atomic::AtomicBool;
33
34use atomic::Atomic;
35use atomic::Ordering;
36use enum_map::EnumMap;
37
38use mmtk_macros::{HasSpaces, PlanTraceObject};
39
40/// A concurrent Immix plan. The plan supports concurrent collection (strictly non-moving) and STW full heap collection (which may do defrag).
41/// The concurrent GC consists of two STW pauses (initial mark and final mark) with concurrent marking in between.
42#[derive(HasSpaces, PlanTraceObject)]
43pub struct ConcurrentImmix<VM: VMBinding> {
44    #[post_scan]
45    #[space]
46    #[copy_semantics(CopySemantics::DefaultCopy)]
47    pub immix_space: ImmixSpace<VM>,
48    #[parent]
49    pub common: CommonPlan<VM>,
50    last_gc_was_defrag: AtomicBool,
51    current_pause: Atomic<Option<Pause>>,
52    previous_pause: Atomic<Option<Pause>>,
53    should_do_full_gc: AtomicBool,
54    concurrent_marking_active: AtomicBool,
55}
56
57/// The plan constraints for the concurrent immix plan.
58pub const CONCURRENT_IMMIX_CONSTRAINTS: PlanConstraints = PlanConstraints {
59    // If we disable moving in Immix, this is a non-moving plan.
60    moves_objects: !cfg!(feature = "immix_non_moving"),
61    // Max immix object size is half of a block.
62    max_non_los_default_alloc_bytes: crate::policy::immix::MAX_IMMIX_OBJECT_SIZE,
63    needs_prepare_mutator: true,
64    barrier: crate::BarrierSelector::SATBBarrier,
65    needs_log_bit: true,
66    ..PlanConstraints::default()
67};
68
69impl<VM: VMBinding> Plan for ConcurrentImmix<VM> {
70    fn collection_required(&self, space_full: bool, _space: Option<SpaceStats<Self::VM>>) -> bool {
71        if self.base().collection_required(self, space_full) {
72            self.should_do_full_gc.store(true, Ordering::Release);
73            info!("Triggering full GC");
74            return true;
75        }
76
77        // Check stw for final mark
78        let concurrent_marking_in_progress = self.concurrent_marking_in_progress();
79        if concurrent_marking_in_progress
80            && self.common.base.scheduler.work_buckets[WorkBucketStage::Concurrent].is_drained()
81        {
82            // After the Concurrent bucket is drained during concurrent marking,
83            // we trigger the FinalMark pause at the next poll() site (here).
84            // FIXME: Immediately trigger FinalMark when the Concurrent bucket is drained.
85            return true;
86        }
87
88        // Check stw for initial mark
89
90        // If concurrent marking is disbled, no need to check further.
91        if self.concurrent_marking_is_disabled() {
92            return false;
93        }
94
95        let threshold = self.get_total_pages() >> 1;
96        let used_pages_after_last_gc = self.common.base.global_state.get_used_pages_after_last_gc();
97        let used_pages_now = self.get_used_pages();
98        let allocated = used_pages_now.saturating_sub(used_pages_after_last_gc);
99        if !concurrent_marking_in_progress && allocated > threshold {
100            info!("Allocated {allocated} pages since last GC ({used_pages_now} - {used_pages_after_last_gc} > {threshold}): Do concurrent marking");
101            debug_assert!(
102                self.common.base.scheduler.work_buckets[WorkBucketStage::Concurrent].is_empty()
103            );
104            debug_assert!(!self.concurrent_marking_in_progress());
105            debug_assert_ne!(self.previous_pause(), Some(Pause::InitialMark));
106            return true;
107        }
108
109        false
110    }
111
112    fn last_collection_was_exhaustive(&self) -> bool {
113        self.immix_space
114            .is_last_gc_exhaustive(self.last_gc_was_defrag.load(Ordering::Relaxed))
115    }
116
117    fn constraints(&self) -> &'static PlanConstraints {
118        &CONCURRENT_IMMIX_CONSTRAINTS
119    }
120
121    fn create_copy_config(&'static self) -> CopyConfig<Self::VM> {
122        use enum_map::enum_map;
123        CopyConfig {
124            copy_mapping: enum_map! {
125                CopySemantics::DefaultCopy => CopySelector::Immix(0),
126                _ => CopySelector::Unused,
127            },
128            space_mapping: vec![(CopySelector::Immix(0), &self.immix_space)],
129            constraints: &CONCURRENT_IMMIX_CONSTRAINTS,
130        }
131    }
132
133    fn schedule_collection(&'static self, scheduler: &GCWorkScheduler<VM>) {
134        // If concurrent marking is disabled, force a full GC.
135        // Though we have checked in collection_required to not trigger a concurrent GC, it is still possible
136        // that a GC is triggered without going through collection_required, e.g. a user triggered GC, or a GC trigger
137        // implemented at the binding side without calling collection_required.
138        // In those cases, we also want to force a full GC.
139        if self.concurrent_marking_is_disabled() {
140            self.should_do_full_gc.store(true, Ordering::SeqCst);
141        }
142
143        let pause = if self.concurrent_marking_in_progress() {
144            // FIXME: Currently it is unsafe to bypass `FinalMark` and go directly from `InitialMark` to `Full`.
145            // It is related to defragmentation.  See https://github.com/mmtk/mmtk-core/issues/1357 for more details.
146            // We currently force `FinalMark` to happen if the last pause is `InitialMark`.
147            Pause::FinalMark
148        } else if self.should_do_full_gc.load(Ordering::SeqCst)
149            // For user-triggered GCs, we don't want a simple initial pause which reclaims nothing.
150            // We do a full STW collection for user triggered collection instead.
151            || self.base().global_state.is_user_triggered_collection()
152        {
153            Pause::Full
154        } else {
155            Pause::InitialMark
156        };
157
158        self.current_pause.store(Some(pause), Ordering::SeqCst);
159
160        probe!(mmtk, concurrent_pause_determined, pause as usize);
161
162        match pause {
163            Pause::Full => {
164                // Ref closure buckets is disabled by initial mark, and needs to be re-enabled for full GC before
165                // we reuse the normal Immix scheduling.
166                self.set_ref_closure_buckets_enabled(true);
167                crate::plan::immix::global::Immix::schedule_immix_full_heap_collection::<
168                    ConcurrentImmix<VM>,
169                    ConcurrentImmixSTWGCWorkContext<VM, TRACE_KIND_FAST>,
170                    ConcurrentImmixSTWGCWorkContext<VM, TRACE_KIND_DEFRAG>,
171                >(self, &self.immix_space, scheduler);
172            }
173            Pause::InitialMark => self.schedule_concurrent_marking_initial_pause(scheduler),
174            Pause::FinalMark => self.schedule_concurrent_marking_final_pause(scheduler),
175        }
176    }
177
178    fn get_allocator_mapping(&self) -> &'static EnumMap<AllocationSemantics, AllocatorSelector> {
179        &ALLOCATOR_MAPPING
180    }
181
182    fn prepare(&mut self, tls: VMWorkerThread) {
183        let pause = self.current_pause().unwrap();
184        match pause {
185            Pause::Full => {
186                self.common.prepare(tls, true);
187                self.immix_space.prepare(
188                    true,
189                    Some(StatsForDefrag::new(self)),
190                    // Ignore unlog bits in full GCs because unlog bits should be all 0.
191                    UnlogBitsOperation::NoOp,
192                );
193            }
194            Pause::InitialMark => {
195                self.immix_space.prepare(
196                    true,
197                    Some(StatsForDefrag::new(self)),
198                    // Bulk set log bits so SATB barrier will be triggered on the existing objects.
199                    UnlogBitsOperation::BulkSet,
200                );
201
202                self.common.prepare(tls, true);
203                // Bulk set log bits so SATB barrier will be triggered on the existing objects.
204                self.common
205                    .schedule_unlog_bits_op(UnlogBitsOperation::BulkSet);
206            }
207            Pause::FinalMark => (),
208        }
209    }
210
211    fn release(&mut self, tls: VMWorkerThread) {
212        let pause = self.current_pause().unwrap();
213        match pause {
214            Pause::InitialMark => (),
215            Pause::Full | Pause::FinalMark => {
216                self.immix_space.release(
217                    true,
218                    // Bulk clear log bits so SATB barrier will not be triggered.
219                    UnlogBitsOperation::BulkClear,
220                );
221
222                self.common.release(tls, true);
223
224                if pause == Pause::FinalMark {
225                    // Bulk clear log bits so SATB barrier will not be triggered.
226                    self.common
227                        .schedule_unlog_bits_op(UnlogBitsOperation::BulkClear);
228                } else {
229                    // Full pauses didn't set unlog bits in the first place,
230                    // so there is no need to clear them.
231                    // TODO: Currently InitialMark must be followed by a FinalMark.
232                    // If we allow upgrading a concurrent GC to a full STW GC,
233                    // we will need to clear the unlog bits at an appropriate place.
234                }
235            }
236        }
237    }
238
239    fn end_of_pause(&mut self, mmtk: &'static MMTK<VM>, _tls: VMWorkerThread) {
240        self.last_gc_was_defrag
241            .store(self.immix_space.end_of_gc(), Ordering::Relaxed);
242
243        let pause = self.current_pause().unwrap();
244        if pause == Pause::InitialMark {
245            self.set_concurrent_marking_state(true);
246        }
247        self.previous_pause.store(Some(pause), Ordering::SeqCst);
248        self.current_pause.store(None, Ordering::SeqCst);
249        if pause != Pause::FinalMark {
250            self.should_do_full_gc.store(false, Ordering::SeqCst);
251        } else {
252            // FIXME: Currently it is unsafe to trigger full GC during concurrent marking.
253            // See `Self::schedule_collection`.
254            // We keep the value of `self.should_do_full_gc` so that if full GC is triggered,
255            // the next GC will be full GC.
256        }
257
258        // Every pause ends a GC cycle, except `InitialMark`, which is followed by concurrent
259        // marking and a `FinalMark` pause before the cycle ends.
260        if pause != Pause::InitialMark {
261            mmtk.gc_trigger.policy.on_gc_end(mmtk);
262        }
263
264        info!("{:?} end", pause);
265    }
266
267    fn current_gc_may_move_object(&self) -> bool {
268        self.immix_space.in_defrag()
269    }
270
271    fn get_collection_reserved_pages(&self) -> usize {
272        self.immix_space.defrag_headroom_pages()
273    }
274
275    fn get_used_pages(&self) -> usize {
276        self.immix_space.reserved_pages() + self.common.get_used_pages()
277    }
278
279    fn base(&self) -> &BasePlan<VM> {
280        &self.common.base
281    }
282
283    fn base_mut(&mut self) -> &mut BasePlan<Self::VM> {
284        &mut self.common.base
285    }
286
287    fn common(&self) -> &CommonPlan<VM> {
288        &self.common
289    }
290
291    fn notify_mutators_paused(&self, mmtk: &'static MMTK<VM>) {
292        use crate::vm::ActivePlan;
293        let pause = self.current_pause().unwrap();
294        match pause {
295            Pause::Full => {
296                self.set_concurrent_marking_state(false);
297            }
298            Pause::InitialMark => {
299                debug_assert!(
300                    !self.concurrent_marking_in_progress(),
301                    "prev pause: {:?}",
302                    self.previous_pause().unwrap()
303                );
304            }
305            Pause::FinalMark => {
306                debug_assert!(self.concurrent_marking_in_progress());
307                // Flush barrier buffers
308                for mutator in <VM as VMBinding>::VMActivePlan::mutators() {
309                    mutator.barrier.flush();
310                }
311                self.set_concurrent_marking_state(false);
312            }
313        }
314
315        // Every pause starts a new GC cycle, except `FinalMark`, which continues the cycle
316        // started by the preceding `InitialMark` pause.
317        if pause != Pause::FinalMark {
318            mmtk.gc_trigger.policy.on_gc_start(mmtk);
319        }
320
321        info!("{:?} start", pause);
322    }
323
324    fn concurrent(&self) -> Option<&dyn ConcurrentPlan<VM = VM>> {
325        Some(self)
326    }
327}
328
329impl<VM: VMBinding> ConcurrentImmix<VM> {
330    pub fn new(args: CreateGeneralPlanArgs<VM>) -> Self {
331        if *args.options.concurrent_immix_disable_concurrent_marking {
332            warn!("Option 'concurrent_immix_disable_concurrent_marking' is set to true. Concurrent marking is disabled for ConcurrentImmix. This will make ConcurrentImmix behave exactly like full heap Immix.");
333        }
334
335        let spec = crate::util::metadata::extract_side_metadata(&[
336            *VM::VMObjectModel::GLOBAL_LOG_BIT_SPEC,
337        ]);
338
339        let mut plan_args = CreateSpecificPlanArgs {
340            global_args: args,
341            constraints: &CONCURRENT_IMMIX_CONSTRAINTS,
342            global_side_metadata_specs: SideMetadataContext::new_global_specs(&spec),
343        };
344
345        let immix_args = ImmixSpaceArgs {
346            mixed_age: false,
347            never_move_objects: false,
348        };
349
350        // These buckets are not used in an Immix plan. We can simply disable them.
351        // TODO: We should be more systmatic on this, and disable unnecessary buckets for other plans as well.
352        let scheduler = &plan_args.global_args.scheduler;
353        scheduler.work_buckets[WorkBucketStage::VMRefForwarding].set_enabled(false);
354        scheduler.work_buckets[WorkBucketStage::CalculateForwarding].set_enabled(false);
355        scheduler.work_buckets[WorkBucketStage::SecondRoots].set_enabled(false);
356        scheduler.work_buckets[WorkBucketStage::RefForwarding].set_enabled(false);
357        scheduler.work_buckets[WorkBucketStage::FinalizableForwarding].set_enabled(false);
358        scheduler.work_buckets[WorkBucketStage::Compact].set_enabled(false);
359
360        ConcurrentImmix {
361            immix_space: ImmixSpace::new(
362                plan_args.get_normal_space_args("immix", true, false, VMRequest::discontiguous()),
363                immix_args,
364            ),
365            common: CommonPlan::new(plan_args),
366            last_gc_was_defrag: AtomicBool::new(false),
367            current_pause: Atomic::new(None),
368            previous_pause: Atomic::new(None),
369            should_do_full_gc: AtomicBool::new(false),
370            concurrent_marking_active: AtomicBool::new(false),
371        }
372    }
373
374    fn set_ref_closure_buckets_enabled(&self, do_closure: bool) {
375        let scheduler = &self.common.base.scheduler;
376        scheduler.work_buckets[WorkBucketStage::VMRefClosure].set_enabled(do_closure);
377        scheduler.work_buckets[WorkBucketStage::WeakRefClosure].set_enabled(do_closure);
378        scheduler.work_buckets[WorkBucketStage::FinalRefClosure].set_enabled(do_closure);
379        scheduler.work_buckets[WorkBucketStage::SoftRefClosure].set_enabled(do_closure);
380        scheduler.work_buckets[WorkBucketStage::PhantomRefClosure].set_enabled(do_closure);
381    }
382
383    pub(crate) fn schedule_concurrent_marking_initial_pause(
384        &'static self,
385        scheduler: &GCWorkScheduler<VM>,
386    ) {
387        use crate::scheduler::gc_work::Prepare;
388
389        self.set_ref_closure_buckets_enabled(false);
390
391        scheduler.work_buckets[WorkBucketStage::Unconstrained]
392            .add(StopMutators::<ConcurrentImmixGCWorkContext<VM>>::new());
393        scheduler.work_buckets[WorkBucketStage::Prepare]
394            .add(Prepare::<ConcurrentImmixGCWorkContext<VM>>::new(self));
395    }
396
397    fn schedule_concurrent_marking_final_pause(&'static self, scheduler: &GCWorkScheduler<VM>) {
398        self.set_ref_closure_buckets_enabled(true);
399
400        // Skip root scanning in the final mark
401        scheduler.work_buckets[WorkBucketStage::Unconstrained]
402            .add(StopMutators::<ConcurrentImmixGCWorkContext<VM>>::new_no_scan_roots());
403
404        scheduler.work_buckets[WorkBucketStage::Release]
405            .add(Release::<ConcurrentImmixGCWorkContext<VM>>::new(self));
406
407        // Sanity
408        #[cfg(feature = "sanity")]
409        {
410            use crate::util::sanity::sanity_checker::ScheduleSanityGC;
411            scheduler.work_buckets[WorkBucketStage::Final].add(ScheduleSanityGC::<Self>::new(self));
412        }
413
414        // Deal with weak ref and finalizers
415        // TODO: Check against schedule_common_work and see if we are still missing any work packet
416        type RefTracePolicy<VM> =
417            crate::plan::tracing::PlanTrace<ConcurrentImmix<VM>, TRACE_KIND_FAST>;
418        // Reference processing
419        if !*self.base().options.no_reference_types {
420            use crate::util::reference_processor::{
421                PhantomRefProcessing, SoftRefProcessing, WeakRefProcessing,
422            };
423            scheduler.work_buckets[WorkBucketStage::SoftRefClosure]
424                .add(SoftRefProcessing::<RefTracePolicy<VM>>::new());
425            scheduler.work_buckets[WorkBucketStage::WeakRefClosure]
426                .add(WeakRefProcessing::<VM>::new());
427            scheduler.work_buckets[WorkBucketStage::PhantomRefClosure]
428                .add(PhantomRefProcessing::<VM>::new());
429
430            use crate::util::reference_processor::RefEnqueue;
431            scheduler.work_buckets[WorkBucketStage::Release].add(RefEnqueue::<VM>::new());
432        }
433
434        // Finalization
435        if !*self.base().options.no_finalizer {
436            use crate::util::finalizable_processor::Finalization;
437            // finalization
438            scheduler.work_buckets[WorkBucketStage::FinalRefClosure]
439                .add(Finalization::<RefTracePolicy<VM>>::new());
440        }
441
442        // VM-specific weak ref processing
443        // Note that ConcurrentImmix does not have a separate forwarding stage,
444        // so we don't schedule the `VMForwardWeakRefs` work packet.
445        scheduler.work_buckets[WorkBucketStage::VMRefClosure]
446            .set_sentinel(Box::new(VMProcessWeakRefs::<RefTracePolicy<VM>>::new()));
447    }
448
449    pub fn concurrent_marking_in_progress(&self) -> bool {
450        self.concurrent_marking_active.load(Ordering::Acquire)
451    }
452
453    fn set_concurrent_marking_state(&self, active: bool) {
454        use crate::plan::global::HasSpaces;
455
456        // Tell the spaces to allocate new objects as live
457        let allocate_object_as_live = active;
458        self.for_each_space(&mut |space: &dyn Space<VM>| {
459            space.set_allocate_as_live(allocate_object_as_live);
460        });
461
462        // Store the state.
463        self.concurrent_marking_active
464            .store(active, Ordering::SeqCst);
465
466        // We also set SATB barrier as active -- this is done in Mutator prepare/release.
467    }
468
469    pub(super) fn is_concurrent_marking_active(&self) -> bool {
470        self.concurrent_marking_active.load(Ordering::SeqCst)
471    }
472
473    fn previous_pause(&self) -> Option<Pause> {
474        self.previous_pause.load(Ordering::SeqCst)
475    }
476
477    fn concurrent_marking_is_disabled(&self) -> bool {
478        *self
479            .base()
480            .options
481            .concurrent_immix_disable_concurrent_marking
482    }
483}
484
485impl<VM: VMBinding> ConcurrentPlan for ConcurrentImmix<VM> {
486    fn current_pause(&self) -> Option<Pause> {
487        self.current_pause.load(Ordering::SeqCst)
488    }
489
490    fn concurrent_work_in_progress(&self) -> bool {
491        self.concurrent_marking_in_progress()
492    }
493}