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