mmtk/
global_state.rs

1use atomic_refcell::AtomicRefCell;
2use std::collections::HashMap;
3use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
4use std::time::{Duration, Instant};
5
6/// This stores some global states for an MMTK instance.
7/// Some MMTK components like plans and allocators may keep an reference to the struct, and can access it.
8// This used to be a part of the `BasePlan`. In that case, any component that accesses
9// the states needs a reference to the plan. It makes it harder for us to reason about the access pattern
10// for the plan, as many components hold references to the plan. Besides, the states
11// actually are not related with a plan, they are just global states for MMTK. So we refactored
12// those fields to this separate struct. For components that access the state, they just need
13// a reference to the struct, and are no longer dependent on the plan.
14// We may consider further break down the fields into smaller structs.
15pub struct GlobalState {
16    /// The current GC status.
17    pub(crate) gc_status: GcStatusWord,
18    /// The time when a GC pause is requested. Used to calculate the time-to-yield metric.
19    pub(crate) pause_requested_time: AtomicRefCell<Option<Instant>>,
20    /// When did the current GC pause begin, i.e. when did all mutators finish stopping (see
21    /// `pause_requested_time`)? Consumed once the pause ends (mutators are about to resume) to
22    /// compute the "pause time" statistic: the duration mutators spend stopped for the pause.
23    pub(crate) pause_start_time: AtomicRefCell<Option<Instant>>,
24    /// Is the current GC an emergency collection? Emergency means we may run out of memory soon, and we should
25    /// attempt to collect as much as we can.
26    pub(crate) emergency_collection: AtomicBool,
27    /// Is the current GC triggered by the user?
28    pub(crate) user_triggered_collection: AtomicBool,
29    /// Is the current GC triggered internally by MMTK? This is unused for now. We may have internally triggered GC
30    /// for a concurrent plan.
31    pub(crate) internal_triggered_collection: AtomicBool,
32    /// Is the last GC internally triggered?
33    pub(crate) last_internal_triggered_collection: AtomicBool,
34    // Has an allocation succeeded since the emergency collection?
35    pub(crate) allocation_success: AtomicBool,
36    // Maximum number of failed attempts by a single thread
37    pub(crate) max_collection_attempts: AtomicUsize,
38    // Current collection attempt
39    pub(crate) cur_collection_attempts: AtomicUsize,
40    /// A counter for per-mutator stack scanning
41    pub(crate) scanned_stacks: AtomicUsize,
42    /// Have we scanned all the stacks?
43    pub(crate) stacks_prepared: AtomicBool,
44    /// A counter that keeps tracks of the number of bytes allocated since last stress test
45    pub(crate) allocation_bytes: AtomicUsize,
46    /// Are we inside the benchmark harness?
47    pub(crate) inside_harness: AtomicBool,
48    /// A counteer that keeps tracks of the number of bytes allocated by malloc
49    #[cfg(feature = "malloc_counted_size")]
50    pub(crate) malloc_bytes: AtomicUsize,
51    /// This stores the live bytes and the used bytes (by pages) for each space in last GC. This counter is only updated in the GC release phase.
52    pub(crate) live_bytes_in_last_gc: AtomicRefCell<HashMap<&'static str, LiveBytesStats>>,
53    /// The number of used pages at the end of the last GC. This can be used to estimate how many pages we have allocated since last GC.
54    pub(crate) used_pages_after_last_gc: AtomicUsize,
55}
56
57impl GlobalState {
58    /// Is MMTk initialized?
59    pub fn is_initialized(&self) -> bool {
60        self.gc_status.is_initialized()
61    }
62
63    /// Set the collection kind for the current GC. This is called before
64    /// scheduling collection to determin what kind of collection it will be.
65    pub fn set_collection_kind(
66        &self,
67        last_collection_was_exhaustive: bool,
68        heap_can_grow: bool,
69    ) -> bool {
70        self.cur_collection_attempts.store(
71            if self.user_triggered_collection.load(Ordering::Relaxed) {
72                1
73            } else {
74                self.determine_collection_attempts()
75            },
76            Ordering::Relaxed,
77        );
78
79        let emergency_collection = !self.is_internal_triggered_collection()
80            && last_collection_was_exhaustive
81            && self.cur_collection_attempts.load(Ordering::Relaxed) > 1
82            && !heap_can_grow;
83        self.emergency_collection
84            .store(emergency_collection, Ordering::Relaxed);
85
86        emergency_collection
87    }
88
89    fn determine_collection_attempts(&self) -> usize {
90        if !self.allocation_success.load(Ordering::Relaxed) {
91            self.max_collection_attempts.fetch_add(1, Ordering::Relaxed);
92        } else {
93            self.allocation_success.store(false, Ordering::Relaxed);
94            self.max_collection_attempts.store(1, Ordering::Relaxed);
95        }
96
97        self.max_collection_attempts.load(Ordering::Relaxed)
98    }
99
100    fn is_internal_triggered_collection(&self) -> bool {
101        let is_internal_triggered = self
102            .last_internal_triggered_collection
103            .load(Ordering::SeqCst);
104        // Remove this assertion when we have concurrent GC.
105        assert!(
106            !is_internal_triggered,
107            "We have no concurrent GC implemented. We should not have internally triggered GC"
108        );
109        is_internal_triggered
110    }
111
112    pub fn is_emergency_collection(&self) -> bool {
113        self.emergency_collection.load(Ordering::Relaxed)
114    }
115
116    /// Return true if this collection was triggered by application code.
117    pub fn is_user_triggered_collection(&self) -> bool {
118        self.user_triggered_collection.load(Ordering::Relaxed)
119    }
120
121    /// Reset collection state information.
122    pub fn reset_collection_trigger(&self) {
123        self.last_internal_triggered_collection.store(
124            self.internal_triggered_collection.load(Ordering::SeqCst),
125            Ordering::Relaxed,
126        );
127        self.internal_triggered_collection
128            .store(false, Ordering::SeqCst);
129        self.user_triggered_collection
130            .store(false, Ordering::Relaxed);
131    }
132
133    /// Are the stacks scanned?
134    pub fn stacks_prepared(&self) -> bool {
135        self.stacks_prepared.load(Ordering::SeqCst)
136    }
137
138    /// Prepare for stack scanning. This is usually used with `inform_stack_scanned()`.
139    /// This should be called before doing stack scanning.
140    pub fn prepare_for_stack_scanning(&self) {
141        self.scanned_stacks.store(0, Ordering::SeqCst);
142        self.stacks_prepared.store(false, Ordering::SeqCst);
143    }
144
145    /// Inform that 1 stack has been scanned. The argument `n_mutators` indicates the
146    /// total stacks we should scan. This method returns true if the number of scanned
147    /// stacks equals the total mutator count. Otherwise it returns false. This method
148    /// is thread safe and we guarantee only one thread will return true.
149    pub fn inform_stack_scanned(&self, n_mutators: usize) -> bool {
150        let old = self.scanned_stacks.fetch_add(1, Ordering::SeqCst);
151        debug_assert!(
152            old < n_mutators,
153            "The number of scanned stacks ({}) is more than the number of mutators ({})",
154            old,
155            n_mutators
156        );
157        let scanning_done = old + 1 == n_mutators;
158        if scanning_done {
159            self.stacks_prepared.store(true, Ordering::SeqCst);
160        }
161        scanning_done
162    }
163
164    /// Increase the allocation bytes and return the current allocation bytes after increasing
165    pub fn increase_allocation_bytes_by(&self, size: usize) -> usize {
166        let old_allocation_bytes = self.allocation_bytes.fetch_add(size, Ordering::SeqCst);
167        trace!(
168            "Stress GC: old_allocation_bytes = {}, size = {}, allocation_bytes = {}",
169            old_allocation_bytes,
170            size,
171            self.allocation_bytes.load(Ordering::Relaxed),
172        );
173        old_allocation_bytes + size
174    }
175
176    #[cfg(feature = "malloc_counted_size")]
177    pub fn get_malloc_bytes_in_pages(&self) -> usize {
178        crate::util::conversions::bytes_to_pages_up(self.malloc_bytes.load(Ordering::Relaxed))
179    }
180
181    #[cfg(feature = "malloc_counted_size")]
182    pub(crate) fn increase_malloc_bytes_by(&self, size: usize) {
183        self.malloc_bytes.fetch_add(size, Ordering::SeqCst);
184    }
185
186    #[cfg(feature = "malloc_counted_size")]
187    pub(crate) fn decrease_malloc_bytes_by(&self, size: usize) {
188        self.malloc_bytes.fetch_sub(size, Ordering::SeqCst);
189    }
190
191    pub(crate) fn set_used_pages_after_last_gc(&self, pages: usize) {
192        self.used_pages_after_last_gc
193            .store(pages, Ordering::Relaxed);
194    }
195
196    pub(crate) fn get_used_pages_after_last_gc(&self) -> usize {
197        self.used_pages_after_last_gc.load(Ordering::Relaxed)
198    }
199
200    /// Record that a GC pause has just been successfully requested. Called by `GCTrigger::request()`
201    /// on the thread that won the race to move the GC status to `PauseRequested`.
202    pub(crate) fn record_pause_requested_time(&self) {
203        let mut pause_requested_time = self.pause_requested_time.borrow_mut();
204        assert!(
205            pause_requested_time.is_none(),
206            "A pause was requested while a previous pause request time is still pending"
207        );
208        *pause_requested_time = Some(Instant::now());
209    }
210
211    /// Take the time-to-yield duration: the time elapsed since the pause was requested (i.e.
212    /// since `record_pause_requested_time` was last called). This should be called exactly once
213    /// all mutators have stopped for the pause.
214    pub(crate) fn take_time_to_yield(&self) -> Duration {
215        self.pause_requested_time
216            .borrow_mut()
217            .take()
218            .expect("Pause requested time was not recorded")
219            .elapsed()
220    }
221
222    /// Record that a GC pause has just begun, i.e. all mutators have just finished stopping.
223    pub(crate) fn record_pause_start_time(&self) {
224        let mut pause_start_time = self.pause_start_time.borrow_mut();
225        assert!(
226            pause_start_time.is_none(),
227            "A pause was started while a previous pause start time is still pending"
228        );
229        *pause_start_time = Some(Instant::now());
230    }
231
232    /// Take the pause time duration: the time elapsed since the pause began (i.e. since
233    /// `record_pause_start_time` was last called). This should be called exactly once the pause
234    /// is about to end, i.e. right before mutators are resumed.
235    pub(crate) fn take_pause_time(&self) -> Duration {
236        self.pause_start_time
237            .borrow_mut()
238            .take()
239            .expect("Pause start time was not recorded")
240            .elapsed()
241    }
242}
243
244impl Default for GlobalState {
245    fn default() -> Self {
246        Self {
247            gc_status: GcStatusWord::new(GcStatus::Uninitialized),
248            pause_requested_time: AtomicRefCell::new(None),
249            pause_start_time: AtomicRefCell::new(None),
250            stacks_prepared: AtomicBool::new(false),
251            emergency_collection: AtomicBool::new(false),
252            user_triggered_collection: AtomicBool::new(false),
253            internal_triggered_collection: AtomicBool::new(false),
254            last_internal_triggered_collection: AtomicBool::new(false),
255            allocation_success: AtomicBool::new(false),
256            max_collection_attempts: AtomicUsize::new(0),
257            cur_collection_attempts: AtomicUsize::new(0),
258            scanned_stacks: AtomicUsize::new(0),
259            allocation_bytes: AtomicUsize::new(0),
260            inside_harness: AtomicBool::new(false),
261            #[cfg(feature = "malloc_counted_size")]
262            malloc_bytes: AtomicUsize::new(0),
263            live_bytes_in_last_gc: AtomicRefCell::new(HashMap::new()),
264            used_pages_after_last_gc: AtomicUsize::new(0),
265        }
266    }
267}
268
269/// The status of MMTk's GC subsystem. This doubles as the "is MMTk initialized" flag (via
270/// [`GcStatus::Uninitialized`]) and as the state that tracks whether a GC is running and, if so,
271/// what phase it is in. See `GcStatusWord` (the internal atomic encoding of this type) for how
272/// this is stored atomically and which transitions between variants are legal.
273#[derive(PartialEq, Copy, Clone, Debug)]
274pub enum GcStatus {
275    /// MMTk has not been initialized yet, i.e. `initialize_collection()` has not been called, so
276    /// there are no GC worker threads available to run a collection.
277    Uninitialized,
278    /// MMTk is initialized, and no GC is running, pending, or requested.
279    NotInGC,
280    /// A concurrent GC's background work (e.g. concurrent marking) is running while mutators
281    /// continue to run normally. See `ConcurrentPlan::concurrent_work_in_progress`.
282    InConcurrentGC,
283    /// A stop-the-world pause is active: mutators are stopped and GC workers are doing pause
284    /// work (e.g. tracing).
285    InPause,
286    /// A GC pause has been requested (by `GcStatusWord::try_request_pause`) but mutators have
287    /// not all stopped yet.
288    PauseRequested,
289    /// Collection is currently disabled (e.g. by a mutator calling `disable_collection()`).
290    /// The usize payload is the non-zero nesting depth of disable calls:
291    /// each call to `disable_collection()` increments the depth, and each call to `enable_collection()`
292    /// decrements it. When the depth is about to reach zero, the status is transitioned to `NoInGC`.
293    Disabled(usize),
294}
295
296/// A lock-free, atomic encoding of [`GcStatus`]. This packs the variant tag into the low bits
297/// of a `usize` and, for `GcStatus::Disabled`, the nesting depth into the remaining high bits,
298/// so the whole status fits in a single machine word and can be updated with atomic operations
299/// instead of behind a `Mutex<GcStatus>`.
300///
301/// `GcStatus` is a state machine: only a handful of transitions between its variants are legal.
302/// Every legal transition is exposed here as its own method, each performing its own
303/// compare-and-swap retry loop and asserting that the transition is legal for the status it
304/// finds. Do not add a generic "set the status to X" method: doing so would make it possible to
305/// bypass the state machine's invariants.
306pub(crate) struct GcStatusWord(AtomicUsize);
307
308impl GcStatusWord {
309    /// Number of bits used to encode the variant tag. 3 bits is enough to distinguish the 6
310    /// variants, leaving the rest of the word for `Disabled`'s nesting depth.
311    const TAG_BITS: u32 = 3;
312    const TAG_MASK: usize = (1 << Self::TAG_BITS) - 1;
313
314    fn encode(status: GcStatus) -> usize {
315        match status {
316            GcStatus::Uninitialized => 0,
317            GcStatus::NotInGC => 1,
318            GcStatus::InConcurrentGC => 2,
319            GcStatus::InPause => 3,
320            GcStatus::PauseRequested => 4,
321            GcStatus::Disabled(depth) => {
322                debug_assert!(
323                    depth < (1 << (usize::BITS - Self::TAG_BITS)),
324                    "GC-disable nesting depth overflows the bits reserved for it"
325                );
326                5 | (depth << Self::TAG_BITS)
327            }
328        }
329    }
330
331    fn decode(bits: usize) -> GcStatus {
332        match bits & Self::TAG_MASK {
333            0 => GcStatus::Uninitialized,
334            1 => GcStatus::NotInGC,
335            2 => GcStatus::InConcurrentGC,
336            3 => GcStatus::InPause,
337            4 => GcStatus::PauseRequested,
338            5 => GcStatus::Disabled(bits >> Self::TAG_BITS),
339            _ => unreachable!("invalid encoded GcStatus tag"),
340        }
341    }
342
343    pub(crate) fn new(status: GcStatus) -> Self {
344        GcStatusWord(AtomicUsize::new(Self::encode(status)))
345    }
346
347    /// Read the current status.
348    pub(crate) fn load(&self) -> GcStatus {
349        Self::decode(self.0.load(Ordering::SeqCst))
350    }
351
352    /// Inner implementation of [`Self::transition`], handling encoding, decoding, and atomic RMW
353    /// operation.
354    fn transition_inner<F: FnMut(GcStatus) -> GcStatus>(&self, mut f: F) -> GcStatus {
355        let old_bits = self
356            .0
357            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |bits| {
358                Some(Self::encode(f(Self::decode(bits))))
359            })
360            .unwrap(); // `f` always returns a status to move to, so this never returns `Err`.
361        Self::decode(old_bits)
362    }
363
364    /// Attempt to atomically transition the GC status using function `f`.  Return the status
365    /// atomically transitioned *from* (not the new status).  It will retry `f` if the status is
366    /// modified concurrently.
367    ///
368    /// Note: Returning the old status (rather than the new one) lets a caller tell whether it "won"
369    /// the race when multiple threads concurrently drive the same transition: only the thread whose
370    /// CAS actually moved the status away from a given old value can be sure it is the one
371    /// responsible for that transition, so it is the one that should perform any side effect that
372    /// must happen exactly once (e.g. notifying the scheduler). If `transition` returned the new
373    /// status instead, every racing thread would observe the same new status and none could tell
374    /// which of them caused it. `f` may be invoked more than once under contention.
375    fn transition<F: FnMut(GcStatus) -> GcStatus>(&self, mut f: F) -> GcStatus {
376        let mut maybe_new_state = None;
377        let old_state = self.transition_inner(|old_state| {
378            let new_state = f(old_state);
379            maybe_new_state = Some(new_state);
380            new_state
381        });
382        let new_state = maybe_new_state.unwrap();
383        log::trace!("GC status transitioned from {old_state:?} to {new_state:?}");
384        old_state
385    }
386
387    /// Inner implementation of [`Self::try_transition`], handling encoding, decoding, and atomic RMW
388    /// operation.
389    fn try_transition_inner<F: FnMut(GcStatus) -> Option<GcStatus>>(
390        &self,
391        mut f: F,
392    ) -> Result<GcStatus, GcStatus> {
393        self.0
394            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |bits| {
395                f(Self::decode(bits)).map(Self::encode)
396            })
397            .map(Self::decode)
398            .map_err(Self::decode)
399    }
400
401    /// Attempt to atomically transition the GC status using function `f`.  Return `Ok(old_status)`
402    /// if `f` returns `Some(new_status)`, in which case it has atomically transitioned the state
403    /// from `old_state` to `new_state`.  Return `Err(old_status)` if `f` returns `None`, in which
404    /// case `old_status` is the status passed to the last invocation of `f`.  It will retry `f` if
405    /// `f` returns `Some` but the underlying status is modified concurrently.
406    fn try_transition<F: FnMut(GcStatus) -> Option<GcStatus>>(
407        &self,
408        mut f: F,
409    ) -> Result<GcStatus, GcStatus> {
410        let mut maybe_new_state = None;
411        let result = self.try_transition_inner(|old_state| {
412            maybe_new_state = f(old_state);
413            maybe_new_state
414        });
415
416        match result {
417            Ok(old_state) => {
418                let new_state = maybe_new_state.unwrap();
419                log::trace!("GC status transitioned from {old_state:?} to {new_state:?}")
420            }
421            Err(old_state) => {
422                log::trace!("GC status transition attempted, but remains {old_state:?}")
423            }
424        }
425
426        result
427    }
428
429    pub(crate) fn is_initialized(&self) -> bool {
430        self.load() != GcStatus::Uninitialized
431    }
432
433    pub(crate) fn is_disabled(&self) -> bool {
434        matches!(self.load(), GcStatus::Disabled(_))
435    }
436
437    /// `Uninitialized` -> `NotInGC`.
438    pub(crate) fn set_initialized(&self) {
439        self.transition(|status| {
440            assert!(
441                status == GcStatus::Uninitialized,
442                "Trying to set initialized GC status when it is not uninitialized"
443            );
444            GcStatus::NotInGC
445        });
446    }
447
448    /// Any status other than `Uninitialized` -> `Uninitialized`.
449    pub(crate) fn set_uninitialized(&self) {
450        self.transition(|status| {
451            assert!(
452                status != GcStatus::Uninitialized,
453                "Trying to set uninitialized GC status when it is already uninitialized"
454            );
455            GcStatus::Uninitialized
456        });
457    }
458
459    /// `PauseRequested` -> `InPause`.
460    pub(crate) fn set_in_pause(&self) {
461        self.transition(|status| {
462            assert!(
463                status == GcStatus::PauseRequested,
464                "Trying to set in-pause GC status in invalid status: {:?}",
465                status
466            );
467            GcStatus::InPause
468        });
469    }
470
471    /// `InPause` -> `InConcurrentGC`, e.g. once a GC pause has finished but concurrent work (such
472    /// as concurrent marking) was scheduled to continue after mutators resume.
473    pub(crate) fn set_in_concurrent_gc(&self) {
474        self.transition(|status| {
475            assert!(
476                status == GcStatus::InPause,
477                "Trying to set in-concurrent-gc GC status in invalid status: {:?}",
478                status
479            );
480            GcStatus::InConcurrentGC
481        });
482    }
483
484    /// `InPause` -> `NotInGC`, e.g. once a GC pause has finished and no concurrent work remains.
485    pub(crate) fn set_not_in_gc(&self) {
486        self.transition(|status| {
487            assert!(
488                status == GcStatus::InPause,
489                "Trying to set not-in-gc GC status in invalid status: {:?}",
490                status
491            );
492            GcStatus::NotInGC
493        });
494    }
495
496    /// `NotInGC`/`Disabled(depth)` -> `Disabled(depth + 1)`. Leaves the status unchanged if
497    /// collection cannot be disabled from the current status (e.g. a GC is in progress or has
498    /// been requested), and returns `Err` with the status that blocked the transition.
499    ///
500    /// On success, returns `Ok(true)` if this call actually switched collection from enabled to
501    /// disabled (i.e. it was the outermost `NotInGC` -> `Disabled(1)` transition), `Ok(false)` if
502    /// it only increased the nesting depth of an already-disabled status. Mirrors the meaning of
503    /// [`GcStatusWord::set_enabled`]'s return value.
504    pub(crate) fn set_disabled(&self) -> Result<bool, GcStatus> {
505        self.try_transition(|status| match status {
506            GcStatus::Disabled(depth) => Some(GcStatus::Disabled(depth + 1)),
507            GcStatus::NotInGC => Some(GcStatus::Disabled(1)),
508            _ => None,
509        })
510        .map(|old_status| old_status == GcStatus::NotInGC)
511    }
512
513    /// `Disabled(depth)` -> `Disabled(depth - 1)`, or `Disabled(1)` -> `NotInGC`. If collection is
514    /// not currently disabled, this is a no-op (the status is left unchanged). Returns `true` if
515    /// this call actually re-enabled collection (i.e. it was the outermost `Disabled(1)` ->
516    /// `NotInGC` transition), `false` if it only decremented the nesting depth, or if collection
517    /// was already enabled.
518    pub(crate) fn set_enabled(&self) -> bool {
519        let old = self.transition(|status| match status {
520            GcStatus::Disabled(1) => GcStatus::NotInGC,
521            GcStatus::Disabled(depth) => GcStatus::Disabled(depth - 1),
522            other => other,
523        });
524        old == GcStatus::Disabled(1)
525    }
526
527    /// `NotInGC`/`InConcurrentGC` -> `PauseRequested`, unless collection is disabled, MMTk is not
528    /// yet initialized, or a pause has already been requested, in which case `Err` is returned
529    /// with the status that prevented the transition (`Disabled(_)`, `Uninitialized`, or
530    /// `PauseRequested` respectively). On success, returns `Ok` with the status transitioned from.
531    pub(crate) fn try_request_pause(&self) -> Result<GcStatus, GcStatus> {
532        self.try_transition(|status| match status {
533            GcStatus::Disabled(_) | GcStatus::Uninitialized | GcStatus::PauseRequested => None,
534            GcStatus::NotInGC | GcStatus::InConcurrentGC => Some(GcStatus::PauseRequested),
535            _ => panic!("Trying to request a GC pause in invalid status: {status:?}"),
536        })
537    }
538}
539
540#[cfg(test)]
541mod gc_status_tests {
542    use super::{GcStatus, GcStatusWord};
543
544    #[test]
545    fn encode_decode_roundtrip() {
546        let statuses = [
547            GcStatus::Uninitialized,
548            GcStatus::NotInGC,
549            GcStatus::InConcurrentGC,
550            GcStatus::InPause,
551            GcStatus::PauseRequested,
552            GcStatus::Disabled(1),
553            GcStatus::Disabled(42),
554        ];
555        for status in statuses {
556            assert_eq!(GcStatusWord::decode(GcStatusWord::encode(status)), status);
557        }
558    }
559
560    #[test]
561    fn new_and_load_roundtrip() {
562        let statuses = [
563            GcStatus::Uninitialized,
564            GcStatus::NotInGC,
565            GcStatus::InConcurrentGC,
566            GcStatus::InPause,
567            GcStatus::PauseRequested,
568            GcStatus::Disabled(1),
569            GcStatus::Disabled(42),
570        ];
571        for status in statuses {
572            assert_eq!(GcStatusWord::new(status).load(), status);
573        }
574    }
575
576    #[test]
577    fn set_initialized_from_uninitialized() {
578        let word = GcStatusWord::new(GcStatus::Uninitialized);
579        assert!(!word.is_initialized());
580        word.set_initialized();
581        assert_eq!(word.load(), GcStatus::NotInGC);
582        assert!(word.is_initialized());
583    }
584
585    #[test]
586    #[should_panic(expected = "not uninitialized")]
587    fn set_initialized_panics_if_already_initialized() {
588        GcStatusWord::new(GcStatus::NotInGC).set_initialized();
589    }
590
591    #[test]
592    fn set_uninitialized_from_not_in_gc() {
593        let word = GcStatusWord::new(GcStatus::NotInGC);
594        word.set_uninitialized();
595        assert_eq!(word.load(), GcStatus::Uninitialized);
596    }
597
598    #[test]
599    #[should_panic(expected = "already uninitialized")]
600    fn set_uninitialized_panics_if_already_uninitialized() {
601        GcStatusWord::new(GcStatus::Uninitialized).set_uninitialized();
602    }
603
604    #[test]
605    fn try_request_pause_from_not_in_gc() {
606        let word = GcStatusWord::new(GcStatus::NotInGC);
607        assert_eq!(word.try_request_pause(), Ok(GcStatus::NotInGC));
608        assert_eq!(word.load(), GcStatus::PauseRequested);
609    }
610
611    #[test]
612    fn try_request_pause_from_in_concurrent_gc() {
613        let word = GcStatusWord::new(GcStatus::InConcurrentGC);
614        assert_eq!(word.try_request_pause(), Ok(GcStatus::InConcurrentGC));
615        assert_eq!(word.load(), GcStatus::PauseRequested);
616    }
617
618    #[test]
619    fn try_request_pause_when_already_requested() {
620        let word = GcStatusWord::new(GcStatus::PauseRequested);
621        assert_eq!(word.try_request_pause(), Err(GcStatus::PauseRequested));
622        // Idempotent: the status is unchanged, not "double requested".
623        assert_eq!(word.load(), GcStatus::PauseRequested);
624    }
625
626    /// A GC pause should never be requested while one is already underway: by the time the
627    /// status reaches `InPause`, all mutators must already be stopped, so no mutator should be
628    /// calling `try_request_pause` at all. Observing `InPause` here indicates a state-machine
629    /// violation elsewhere, so it must panic rather than being silently treated as a no-op.
630    #[test]
631    #[should_panic(expected = "invalid status")]
632    fn try_request_pause_panics_when_already_in_pause() {
633        let _ = GcStatusWord::new(GcStatus::InPause).try_request_pause();
634    }
635
636    #[test]
637    fn try_request_pause_when_disabled() {
638        let word = GcStatusWord::new(GcStatus::Disabled(1));
639        assert_eq!(word.try_request_pause(), Err(GcStatus::Disabled(1)));
640        // Unchanged: disabling is not overridden by a pause request.
641        assert_eq!(word.load(), GcStatus::Disabled(1));
642    }
643
644    /// Allocation can call `poll()` (and thus `try_request_pause`) before
645    /// `initialize_collection()` has been called, e.g. if the heap fills up before the VM
646    /// binding initializes MMTk's GC worker threads. This must not panic here: the caller (e.g.
647    /// `Space::not_acquiring`) is responsible for producing a clear "GC is not allowed here"
648    /// error once it knows allocation has genuinely failed.
649    #[test]
650    fn try_request_pause_when_uninitialized() {
651        let word = GcStatusWord::new(GcStatus::Uninitialized);
652        assert_eq!(word.try_request_pause(), Err(GcStatus::Uninitialized));
653        assert_eq!(word.load(), GcStatus::Uninitialized);
654    }
655
656    #[test]
657    fn set_in_pause_from_pause_requested() {
658        let word = GcStatusWord::new(GcStatus::PauseRequested);
659        word.set_in_pause();
660        assert_eq!(word.load(), GcStatus::InPause);
661    }
662
663    #[test]
664    #[should_panic(expected = "invalid status")]
665    fn set_in_pause_panics_if_not_requested() {
666        GcStatusWord::new(GcStatus::NotInGC).set_in_pause();
667    }
668
669    #[test]
670    fn set_disabled_from_not_in_gc() {
671        let word = GcStatusWord::new(GcStatus::NotInGC);
672        assert_eq!(word.set_disabled(), Ok(true));
673        assert_eq!(word.load(), GcStatus::Disabled(1));
674    }
675
676    #[test]
677    fn set_disabled_nests() {
678        let word = GcStatusWord::new(GcStatus::Disabled(1));
679        assert_eq!(word.set_disabled(), Ok(false));
680        assert_eq!(word.load(), GcStatus::Disabled(2));
681
682        assert_eq!(word.set_disabled(), Ok(false));
683        assert_eq!(word.load(), GcStatus::Disabled(3));
684    }
685
686    #[test]
687    fn set_disabled_fails_without_changing_status() {
688        for status in [
689            GcStatus::Uninitialized,
690            GcStatus::InConcurrentGC,
691            GcStatus::PauseRequested,
692            GcStatus::InPause,
693        ] {
694            let word = GcStatusWord::new(status);
695            assert_eq!(word.set_disabled(), Err(status));
696            assert_eq!(word.load(), status);
697        }
698    }
699
700    #[test]
701    fn set_enabled_decrements_nesting() {
702        let word = GcStatusWord::new(GcStatus::Disabled(3));
703        assert!(!word.set_enabled());
704        assert_eq!(word.load(), GcStatus::Disabled(2));
705    }
706
707    #[test]
708    fn set_enabled_to_not_in_gc_at_zero_depth() {
709        let word = GcStatusWord::new(GcStatus::Disabled(1));
710        assert!(word.set_enabled());
711        assert_eq!(word.load(), GcStatus::NotInGC);
712    }
713
714    #[test]
715    fn set_disabled_and_set_enabled_nest_round_trip() {
716        let word = GcStatusWord::new(GcStatus::NotInGC);
717        assert!(word.set_disabled().is_ok());
718        assert!(word.set_disabled().is_ok());
719        assert!(word.set_disabled().is_ok());
720        assert_eq!(word.load(), GcStatus::Disabled(3));
721
722        // Only the call that brings the nesting depth back to 0 (i.e. all the way back to
723        // `NotInGC`) should return `true`.
724        assert!(!word.set_enabled());
725        assert_eq!(word.load(), GcStatus::Disabled(2));
726        assert!(!word.set_enabled());
727        assert_eq!(word.load(), GcStatus::Disabled(1));
728        assert!(word.set_enabled());
729        assert_eq!(word.load(), GcStatus::NotInGC);
730    }
731
732    #[test]
733    fn set_enabled_is_noop_if_not_disabled() {
734        for status in [
735            GcStatus::Uninitialized,
736            GcStatus::NotInGC,
737            GcStatus::InConcurrentGC,
738            GcStatus::InPause,
739            GcStatus::PauseRequested,
740        ] {
741            let word = GcStatusWord::new(status);
742            assert!(!word.set_enabled());
743            assert_eq!(word.load(), status);
744        }
745    }
746
747    #[test]
748    fn set_in_concurrent_gc_from_in_pause() {
749        let word = GcStatusWord::new(GcStatus::InPause);
750        word.set_in_concurrent_gc();
751        assert_eq!(word.load(), GcStatus::InConcurrentGC);
752    }
753
754    #[test]
755    #[should_panic(expected = "invalid status")]
756    fn set_in_concurrent_gc_panics_if_not_in_pause() {
757        GcStatusWord::new(GcStatus::NotInGC).set_in_concurrent_gc();
758    }
759
760    #[test]
761    fn set_not_in_gc_from_in_pause() {
762        let word = GcStatusWord::new(GcStatus::InPause);
763        word.set_not_in_gc();
764        assert_eq!(word.load(), GcStatus::NotInGC);
765    }
766
767    #[test]
768    #[should_panic(expected = "invalid status")]
769    fn set_not_in_gc_panics_if_not_in_pause() {
770        GcStatusWord::new(GcStatus::InConcurrentGC).set_not_in_gc();
771    }
772
773    #[test]
774    fn is_disabled_reflects_status() {
775        assert!(GcStatusWord::new(GcStatus::Disabled(1)).is_disabled());
776        assert!(!GcStatusWord::new(GcStatus::NotInGC).is_disabled());
777    }
778}
779
780/// Statistics for the live bytes in the last GC. The statistics is per space.
781#[derive(Copy, Clone, Debug)]
782pub struct LiveBytesStats {
783    /// Total accumulated bytes of live objects in the space.
784    pub live_bytes: usize,
785    /// Total pages used by the space.
786    pub used_pages: usize,
787    /// Total bytes used by the space, computed from `used_pages`.
788    /// The ratio of live_bytes and used_bytes reflects the utilization of the memory in the space.
789    pub used_bytes: usize,
790}