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