mmtk/scheduler/
work_bucket.rs

1use super::worker_monitor::WorkerMonitor;
2use super::*;
3use crate::vm::VMBinding;
4use crossbeam::deque::{Injector, Steal, Worker};
5use enum_map::Enum;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8
9pub(super) struct BucketQueue<VM: VMBinding> {
10    flag: AtomicBool,
11    queue0: Injector<Box<dyn GCWork<VM>>>,
12    queue1: Injector<Box<dyn GCWork<VM>>>,
13}
14
15impl<VM: VMBinding> BucketQueue<VM> {
16    fn new() -> Self {
17        Self {
18            flag: AtomicBool::new(false),
19            queue0: Injector::new(),
20            queue1: Injector::new(),
21        }
22    }
23
24    fn active_queue(&self) -> &Injector<Box<dyn GCWork<VM>>> {
25        if self.flag.load(Ordering::Relaxed) {
26            &self.queue1
27        } else {
28            &self.queue0
29        }
30    }
31
32    fn inactive_queue(&self) -> &Injector<Box<dyn GCWork<VM>>> {
33        if self.flag.load(Ordering::Relaxed) {
34            &self.queue0
35        } else {
36            &self.queue1
37        }
38    }
39
40    fn is_empty(&self) -> bool {
41        self.active_queue().is_empty()
42    }
43
44    fn len(&self) -> usize {
45        self.active_queue().len()
46    }
47
48    pub(super) fn steal(&self) -> Steal<Box<dyn GCWork<VM>>> {
49        self.active_queue().steal()
50    }
51
52    fn steal_batch_and_pop(
53        &self,
54        dest: &Worker<Box<dyn GCWork<VM>>>,
55    ) -> Steal<Box<dyn GCWork<VM>>> {
56        self.active_queue().steal_batch_and_pop(dest)
57    }
58
59    fn push(&self, w: Box<dyn GCWork<VM>>) {
60        self.active_queue().push(w);
61    }
62
63    fn push_all(&self, ws: Vec<Box<dyn GCWork<VM>>>) {
64        for w in ws {
65            self.active_queue().push(w);
66        }
67    }
68
69    fn push_inactive(&self, w: Box<dyn GCWork<VM>>) {
70        self.inactive_queue().push(w);
71    }
72
73    fn push_all_inactive(&self, ws: Vec<Box<dyn GCWork<VM>>>) {
74        for w in ws {
75            self.inactive_queue().push(w);
76        }
77    }
78
79    /// Remove and return every packet currently in this queue, leaving it empty.
80    fn drain(&self) -> Vec<Box<dyn GCWork<VM>>> {
81        let mut items = Vec::new();
82        loop {
83            match self.queue0.steal() {
84                Steal::Success(w) => items.push(w),
85                Steal::Retry => continue,
86                Steal::Empty => break,
87            }
88        }
89        loop {
90            match self.queue1.steal() {
91                Steal::Success(w) => items.push(w),
92                Steal::Retry => continue,
93                Steal::Empty => break,
94            }
95        }
96        items
97    }
98
99    /// Dump all the packets in this queue for debugging purpose.
100    /// This function may dump items from the queue temporarily, thus should only be called when it is safe to do so
101    /// (e.g. when the execution has failed already and the system is going to panic).
102    pub fn debug_dump_packets(&self) -> Vec<String> {
103        let mut items = Vec::new();
104        let queue = self.active_queue();
105
106        {
107            // Drain queue by stealing until empty
108            loop {
109                match queue.steal() {
110                    crossbeam::deque::Steal::Success(work) => {
111                        items.push(work);
112                    }
113                    crossbeam::deque::Steal::Retry => continue,
114                    crossbeam::deque::Steal::Empty => break,
115                }
116            }
117        }
118
119        // Format collected items (just type names or Debug, depending on GCWork)
120        let debug_items: Vec<String> = items
121            .iter()
122            .map(|i| i.get_type_name().to_string()) // placeholder since GCWork isn’t Debug
123            .collect();
124
125        // Push items back into the queue
126        {
127            for work in items {
128                queue.push(work);
129            }
130        }
131
132        debug_items
133    }
134}
135
136pub type BucketOpenCondition<VM> = Box<dyn (Fn(&GCWorkScheduler<VM>) -> bool) + Send>;
137
138pub struct WorkBucket<VM: VMBinding> {
139    /// Whether this bucket has been opened. Work from an open bucket can be fetched by workers.
140    open: AtomicBool,
141    /// Whether this bucket is enabled.
142    /// A disabled work bucket will behave as if it does not exist in terms of scheduling,
143    /// except that users can add work to a disabled bucket, and enable it later to allow those
144    /// work to be scheduled.
145    enabled: AtomicBool,
146    /// The stage name of this bucket.
147    stage: WorkBucketStage,
148    queue: BucketQueue<VM>,
149    monitor: Arc<WorkerMonitor>,
150    /// The open condition for a bucket. If this is `Some`, the bucket will be open
151    /// when the condition is met. If this is `None`, the bucket needs to be open manually.
152    can_open: Option<BucketOpenCondition<VM>>,
153    /// After this bucket is open and all pending work packets (including the packets in this
154    /// bucket) are drained, this work packet, if exists, will be added to this bucket.  When this
155    /// happens, it will prevent opening subsequent work packets.
156    ///
157    /// The sentinel work packet may set another work packet as the new sentinel which will be
158    /// added to this bucket again after all pending work packets are drained.  This may happend
159    /// again and again, causing the GC to stay at the same stage and drain work packets in a loop.
160    ///
161    /// This is useful for handling weak references that may expand the transitive closure
162    /// recursively, such as ephemerons and Java-style SoftReference and finalizers.  Sentinels
163    /// can be used repeatedly to discover and process more such objects.
164    sentinel: Mutex<Option<Box<dyn GCWork<VM>>>>,
165}
166
167impl<VM: VMBinding> WorkBucket<VM> {
168    pub(crate) fn new(stage: WorkBucketStage, monitor: Arc<WorkerMonitor>) -> Self {
169        Self {
170            open: AtomicBool::new(stage.is_open_by_default()),
171            enabled: AtomicBool::new(stage.is_enabled_by_default()),
172            stage,
173            queue: BucketQueue::new(),
174            monitor,
175            can_open: None,
176            sentinel: Mutex::new(None),
177        }
178    }
179
180    pub fn set_enabled(&self, enabled: bool) {
181        self.enabled.store(enabled, Ordering::SeqCst)
182    }
183
184    pub fn is_enabled(&self) -> bool {
185        self.enabled.load(Ordering::Relaxed)
186    }
187
188    pub fn flip(&self) {
189        self.queue.flag.fetch_xor(true, Ordering::SeqCst);
190    }
191
192    fn notify_one_worker(&self) {
193        // If the bucket is not open, don't notify anyone.
194        if !self.is_open() || !self.is_enabled() {
195            return;
196        }
197        // Notify one if there're any parked workers.
198        self.monitor.notify_work_available(false);
199    }
200
201    pub fn notify_all_workers(&self) {
202        // If the bucket is not open, don't notify anyone.
203        if !self.is_open() || !self.is_enabled() {
204            return;
205        }
206        // Notify all if there're any parked workers.
207        self.monitor.notify_work_available(true);
208    }
209
210    pub fn is_open(&self) -> bool {
211        self.open.load(Ordering::SeqCst)
212    }
213
214    /// Open the bucket
215    pub fn open(&self) {
216        self.open.store(true, Ordering::SeqCst);
217    }
218
219    /// Test if the bucket is drained
220    pub fn is_empty(&self) -> bool {
221        self.queue.is_empty()
222    }
223
224    pub fn len(&self) -> usize {
225        self.queue.len()
226    }
227
228    pub fn is_drained(&self) -> bool {
229        !self.is_enabled() || (self.is_open() && self.is_empty())
230    }
231
232    /// Remove and return every packet currently queued in this bucket (including the
233    /// prioritized queue, if any), leaving it empty.
234    ///
235    /// This does not synchronize with producers or consumers of this bucket in any way: the
236    /// caller must independently ensure that nothing can be concurrently adding to or polling
237    /// this bucket (e.g. by only calling this once all GC workers are known to be parked, and
238    /// after disabling the bucket so no new packets can be routed to it, as
239    /// `ConcurrentImmix::schedule_concurrent_marking_final_pause` does for `Concurrent`).
240    pub(crate) fn drain_all_packets(&self) -> Vec<Box<dyn GCWork<VM>>> {
241        self.queue.drain()
242    }
243
244    /// Close the bucket
245    pub fn close(&self) {
246        debug_assert!(
247            self.queue.is_empty(),
248            "Bucket {:?} not drained before close",
249            self.stage
250        );
251        self.open.store(false, Ordering::Relaxed);
252    }
253
254    fn warn_notify_add_if_disabled(&self) {
255        #[cfg(debug_assertions)]
256        if !self.is_enabled() {
257            // This is usually benign if it happens occasionally. But if we keep adding work to a disabled bucket,
258            // we keep waking up workers for new work that they can't work on.
259            warn!(
260                "Add a work to a disabled bucket with notifying one worker {:?}",
261                self.stage
262            );
263        }
264    }
265
266    /// Add a work packet to this bucket
267    pub fn add<W: GCWork<VM>>(&self, work: W) {
268        self.warn_notify_add_if_disabled();
269        self.queue.push(Box::new(work));
270        self.notify_one_worker();
271    }
272
273    /// Add a work packet to this bucket
274    pub fn add_boxed(&self, work: Box<dyn GCWork<VM>>) {
275        self.warn_notify_add_if_disabled();
276        self.queue.push(work);
277        self.notify_one_worker();
278    }
279
280    pub fn add_deferred(&self, work: Box<dyn GCWork<VM>>) {
281        self.queue.push_inactive(work);
282    }
283
284    pub fn bulk_add_deferred(&self, work_vec: Vec<Box<dyn GCWork<VM>>>) {
285        self.queue.push_all_inactive(work_vec);
286    }
287
288    /// Add a work packet to this bucket, but do not notify any workers.
289    /// This is useful when the current thread is holding the mutex of `WorkerMonitor` which is
290    /// used for notifying workers.  This usually happens if the current thread is the last worker
291    /// parked.
292    pub(crate) fn add_no_notify<W: GCWork<VM>>(&self, work: W) {
293        self.queue.push(Box::new(work));
294    }
295
296    /// Like [`WorkBucket::add_no_notify`], but the work is boxed.
297    pub(crate) fn add_boxed_no_notify(&self, work: Box<dyn GCWork<VM>>) {
298        self.queue.push(work);
299    }
300
301    /// Add multiple packets
302    pub fn bulk_add(&self, work_vec: Vec<Box<dyn GCWork<VM>>>) {
303        debug_assert!(self.is_enabled());
304        if work_vec.is_empty() {
305            return;
306        }
307        let len = work_vec.len();
308        self.queue.push_all(work_vec);
309        if self.is_open() {
310            if len == 1 {
311                self.notify_one_worker();
312            } else {
313                self.notify_all_workers();
314            }
315        }
316    }
317
318    /// Get a work packet from this bucket
319    pub fn poll(&self, worker: &Worker<Box<dyn GCWork<VM>>>) -> Steal<Box<dyn GCWork<VM>>> {
320        if !self.is_enabled() || !self.is_open() || self.is_empty() {
321            return Steal::Empty;
322        }
323        self.queue.steal_batch_and_pop(worker)
324    }
325
326    pub fn set_open_condition(
327        &mut self,
328        pred: impl Fn(&GCWorkScheduler<VM>) -> bool + Send + 'static,
329    ) {
330        self.can_open = Some(Box::new(pred));
331    }
332
333    pub fn set_sentinel(&self, new_sentinel: Box<dyn GCWork<VM>>) {
334        let mut sentinel = self.sentinel.lock().unwrap();
335        *sentinel = Some(new_sentinel);
336    }
337
338    pub fn has_sentinel(&self) -> bool {
339        let sentinel = self.sentinel.lock().unwrap();
340        sentinel.is_some()
341    }
342
343    pub fn update(&self, scheduler: &GCWorkScheduler<VM>) -> bool {
344        if let Some(can_open) = self.can_open.as_ref() {
345            if !self.is_open() && can_open(scheduler) {
346                debug!("Opening work bucket: {:?}", self.stage);
347                self.open();
348                return true;
349            }
350        }
351        false
352    }
353
354    pub fn maybe_schedule_sentinel(&self) -> bool {
355        debug_assert!(
356            self.is_open(),
357            "Attempted to schedule sentinel work while bucket is not open"
358        );
359        let maybe_sentinel = {
360            let mut sentinel = self.sentinel.lock().unwrap();
361            sentinel.take()
362        };
363        if let Some(work) = maybe_sentinel {
364            // We don't need to notify other workers because this function is called by the last
365            // parked worker.  After this function returns, the caller will notify workers because
366            // more work packets become available.
367            self.add_boxed_no_notify(work);
368            true
369        } else {
370            false
371        }
372    }
373
374    pub(super) fn get_queue(&self) -> &BucketQueue<VM> {
375        &self.queue
376    }
377
378    pub(super) fn get_stage(&self) -> WorkBucketStage {
379        self.stage
380    }
381}
382
383/// This enum defines all the work bucket types. The scheduler
384/// will instantiate a work bucket for each stage defined here.
385#[derive(Debug, Enum, Copy, Clone, Eq, PartialEq, Hash)]
386pub enum WorkBucketStage {
387    /// This bucket is always open.
388    Unconstrained,
389    /// This bucket is intended for concurrent work. Though some concurrent work may be put and executed in the unconstrained bucket,
390    /// work in the unconstrained bucket will always be consumed during STW. Users can disable this bucket
391    /// and cache some concurrent work during STW, and only enable this bucket and allow concurrent execution once a STW is done.
392    Concurrent,
393    /// Concurrent work that may be resumed across a stop-the-world pause (LXR-specific), such as
394    /// concurrent marking packets discovered while processing reference-count increments during
395    /// `InitialMark`.  Unlike other stages, this bucket is allowed to remain non-empty when a STW
396    /// pause ends.
397    ConcurrentResumable,
398    /// The first stop-the-world stage (see [`WorkBucketStage::FIRST_STW_STAGE`]).  Used to join
399    /// outstanding concurrent work, e.g. flushing SATB mod-buffer packets recorded by the LXR
400    /// barrier, before the rest of the STW stages proceed.
401    FinishConcurrentWork,
402    /// Process the reference-count increments that must not move their objects.  LXR also scans
403    /// roots here (see `root_scanning_stage`); the roots a binding reports as objects rather than
404    /// as slots are counted in this stage because there is no slot to write a forwarding pointer
405    /// back into.  Counting them before `RCProcessIncs` opens is what keeps them in place.
406    RCProcessIncsNonMoving,
407    /// Process reference-count increments from the LXR barrier and from root slots.  May
408    /// evacuate nursery objects, so it must run after `RCProcessIncsNonMoving`.
409    RCProcessIncs,
410    /// Preparation work.  Plans, spaces, GC workers, mutators, etc. should be prepared for GC at
411    /// this stage.
412    Prepare,
413    /// Clear the VO bit metadata.  Mainly used by ImmixSpace.
414    #[cfg(feature = "vo_bit")]
415    ClearVOBits,
416    /// Compute the transtive closure starting from transitively pinning (TP) roots following only strong references.
417    /// No objects in this closure are allow to move.
418    TPinningClosure,
419    /// Trace (non-transitively) pinning roots. Objects pointed by those roots must not move, but their children may. To ensure correctness, these must be processed after TPinningClosure
420    PinningRootsTrace,
421    /// Compute the transtive closure following only strong references.
422    Closure,
423    /// Handle Java-style soft references, and potentially expand the transitive closure.
424    SoftRefClosure,
425    /// Handle Java-style weak references.
426    WeakRefClosure,
427    /// Resurrect Java-style finalizable objects, and potentially expand the transitive closure.
428    FinalRefClosure,
429    /// Handle Java-style phantom references.
430    PhantomRefClosure,
431    /// Let the VM handle VM-specific weak data structures, including weak references, weak
432    /// collections, table of finalizable objects, ephemerons, etc.  Potentially expand the
433    /// transitive closure.
434    ///
435    /// NOTE: This stage is intended to replace the Java-specific weak reference handling stages
436    /// above.
437    VMRefClosure,
438    /// Compute the forwarding addresses of objects (mark-compact-only).
439    CalculateForwarding,
440    /// Scan roots again to initiate another transitive closure to update roots and reference
441    /// after computing the forwarding addresses (mark-compact-only).
442    SecondRoots,
443    /// Update Java-style weak references after computing forwarding addresses (mark-compact-only).
444    ///
445    /// NOTE: This stage should be updated to adapt to the VM-side reference handling.  It shall
446    /// be kept after removing `{Soft,Weak,Final,Phantom}RefClosure`.
447    RefForwarding,
448    /// Update the list of Java-style finalization cadidates and finalizable objects after
449    /// computing forwarding addresses (mark-compact-only).
450    FinalizableForwarding,
451    /// Let the VM handle the forwarding of reference fields in any VM-specific weak data
452    /// structures, including weak references, weak collections, table of finalizable objects,
453    /// ephemerons, etc., after computing forwarding addresses (mark-compact-only).
454    ///
455    /// NOTE: This stage is intended to replace Java-specific forwarding phases above.
456    VMRefForwarding,
457    /// Compact objects (mark-compact-only).
458    Compact,
459    /// Work packets that should be done just before GC shall go here.  This includes releasing
460    /// resources and setting states in plans, spaces, GC workers, mutators, etc.
461    Release,
462    /// Process reference-count decrements recorded by the LXR barrier, and sweep objects whose
463    /// reference count has dropped to zero, during a stop-the-world pause.  This is used when
464    /// lazy (concurrent) decrements are disabled for the current GC.
465    STWRCDecsAndSweep,
466    /// Resume mutators and end GC.
467    Final,
468}
469
470// Alias
471#[allow(non_upper_case_globals)]
472impl WorkBucketStage {
473    /// The first stop-the-world stage. This stage has no open condition, and will be opened manually
474    /// once all the mutators threads are stopped.
475    pub const FIRST_STW_STAGE: Self = WorkBucketStage::FinishConcurrentWork;
476
477    /// Is this the first stop-the-world stage? See [`Self::FIRST_STW_STAGE`].
478    pub const fn is_first_stw_stage(&self) -> bool {
479        matches!(self, &WorkBucketStage::FIRST_STW_STAGE)
480    }
481
482    /// Is this stage always open?
483    pub const fn is_always_open(&self) -> bool {
484        matches!(self, WorkBucketStage::Unconstrained)
485    }
486
487    /// Is this stage open by default?
488    pub const fn is_open_by_default(&self) -> bool {
489        matches!(
490            self,
491            WorkBucketStage::Unconstrained
492                | WorkBucketStage::Concurrent
493                | WorkBucketStage::ConcurrentResumable
494        )
495    }
496
497    /// Is this stage enabled by default?
498    pub const fn is_enabled_by_default(&self) -> bool {
499        !matches!(self, WorkBucketStage::Concurrent)
500            && !matches!(self, WorkBucketStage::ConcurrentResumable)
501    }
502
503    /// Is this stage sequentially opened? All the stop-the-world stages, except the first one, are sequentially opened.
504    pub const fn is_sequentially_opened(&self) -> bool {
505        self.is_stw() && !self.is_first_stw_stage()
506    }
507
508    /// Is this stage a stop-the-world stage?
509    pub const fn is_stw(&self) -> bool {
510        !self.is_concurrent()
511    }
512
513    /// Is this stage concurrent (which may be executed during mutator time)?
514    pub const fn is_concurrent(&self) -> bool {
515        matches!(
516            self,
517            WorkBucketStage::Unconstrained
518                | WorkBucketStage::Concurrent
519                | WorkBucketStage::ConcurrentResumable
520        )
521    }
522
523    /// Alias for [`WorkBucketStage::Closure`], used by LXR when scheduling mature-space
524    /// evacuation remset packets so that they are processed as part of the transitive closure
525    /// stage.
526    pub const RCEvacuateMature: Self = Self::Closure;
527}