mmtk/
global_state.rs

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