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