use atomic_refcell::AtomicRefCell;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Instant;
pub struct GlobalState {
pub(crate) gc_status: GcStatusWord,
pub(crate) gc_start_time: AtomicRefCell<Option<Instant>>,
pub(crate) emergency_collection: AtomicBool,
pub(crate) user_triggered_collection: AtomicBool,
pub(crate) internal_triggered_collection: AtomicBool,
pub(crate) last_internal_triggered_collection: AtomicBool,
pub(crate) allocation_success: AtomicBool,
pub(crate) max_collection_attempts: AtomicUsize,
pub(crate) cur_collection_attempts: AtomicUsize,
pub(crate) scanned_stacks: AtomicUsize,
pub(crate) stacks_prepared: AtomicBool,
pub(crate) allocation_bytes: AtomicUsize,
pub(crate) inside_harness: AtomicBool,
#[cfg(feature = "malloc_counted_size")]
pub(crate) malloc_bytes: AtomicUsize,
pub(crate) live_bytes_in_last_gc: AtomicRefCell<HashMap<&'static str, LiveBytesStats>>,
pub(crate) used_pages_after_last_gc: AtomicUsize,
}
impl GlobalState {
pub fn is_initialized(&self) -> bool {
self.gc_status.is_initialized()
}
pub fn set_collection_kind(
&self,
last_collection_was_exhaustive: bool,
heap_can_grow: bool,
) -> bool {
self.cur_collection_attempts.store(
if self.user_triggered_collection.load(Ordering::Relaxed) {
1
} else {
self.determine_collection_attempts()
},
Ordering::Relaxed,
);
let emergency_collection = !self.is_internal_triggered_collection()
&& last_collection_was_exhaustive
&& self.cur_collection_attempts.load(Ordering::Relaxed) > 1
&& !heap_can_grow;
self.emergency_collection
.store(emergency_collection, Ordering::Relaxed);
emergency_collection
}
fn determine_collection_attempts(&self) -> usize {
if !self.allocation_success.load(Ordering::Relaxed) {
self.max_collection_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.allocation_success.store(false, Ordering::Relaxed);
self.max_collection_attempts.store(1, Ordering::Relaxed);
}
self.max_collection_attempts.load(Ordering::Relaxed)
}
fn is_internal_triggered_collection(&self) -> bool {
let is_internal_triggered = self
.last_internal_triggered_collection
.load(Ordering::SeqCst);
assert!(
!is_internal_triggered,
"We have no concurrent GC implemented. We should not have internally triggered GC"
);
is_internal_triggered
}
pub fn is_emergency_collection(&self) -> bool {
self.emergency_collection.load(Ordering::Relaxed)
}
pub fn is_user_triggered_collection(&self) -> bool {
self.user_triggered_collection.load(Ordering::Relaxed)
}
pub fn reset_collection_trigger(&self) {
self.last_internal_triggered_collection.store(
self.internal_triggered_collection.load(Ordering::SeqCst),
Ordering::Relaxed,
);
self.internal_triggered_collection
.store(false, Ordering::SeqCst);
self.user_triggered_collection
.store(false, Ordering::Relaxed);
}
pub fn stacks_prepared(&self) -> bool {
self.stacks_prepared.load(Ordering::SeqCst)
}
pub fn prepare_for_stack_scanning(&self) {
self.scanned_stacks.store(0, Ordering::SeqCst);
self.stacks_prepared.store(false, Ordering::SeqCst);
}
pub fn inform_stack_scanned(&self, n_mutators: usize) -> bool {
let old = self.scanned_stacks.fetch_add(1, Ordering::SeqCst);
debug_assert!(
old < n_mutators,
"The number of scanned stacks ({}) is more than the number of mutators ({})",
old,
n_mutators
);
let scanning_done = old + 1 == n_mutators;
if scanning_done {
self.stacks_prepared.store(true, Ordering::SeqCst);
}
scanning_done
}
pub fn increase_allocation_bytes_by(&self, size: usize) -> usize {
let old_allocation_bytes = self.allocation_bytes.fetch_add(size, Ordering::SeqCst);
trace!(
"Stress GC: old_allocation_bytes = {}, size = {}, allocation_bytes = {}",
old_allocation_bytes,
size,
self.allocation_bytes.load(Ordering::Relaxed),
);
old_allocation_bytes + size
}
#[cfg(feature = "malloc_counted_size")]
pub fn get_malloc_bytes_in_pages(&self) -> usize {
crate::util::conversions::bytes_to_pages_up(self.malloc_bytes.load(Ordering::Relaxed))
}
#[cfg(feature = "malloc_counted_size")]
pub(crate) fn increase_malloc_bytes_by(&self, size: usize) {
self.malloc_bytes.fetch_add(size, Ordering::SeqCst);
}
#[cfg(feature = "malloc_counted_size")]
pub(crate) fn decrease_malloc_bytes_by(&self, size: usize) {
self.malloc_bytes.fetch_sub(size, Ordering::SeqCst);
}
pub(crate) fn set_used_pages_after_last_gc(&self, pages: usize) {
self.used_pages_after_last_gc
.store(pages, Ordering::Relaxed);
}
pub(crate) fn get_used_pages_after_last_gc(&self) -> usize {
self.used_pages_after_last_gc.load(Ordering::Relaxed)
}
}
impl Default for GlobalState {
fn default() -> Self {
Self {
gc_status: GcStatusWord::new(GcStatus::Uninitialized),
gc_start_time: AtomicRefCell::new(None),
stacks_prepared: AtomicBool::new(false),
emergency_collection: AtomicBool::new(false),
user_triggered_collection: AtomicBool::new(false),
internal_triggered_collection: AtomicBool::new(false),
last_internal_triggered_collection: AtomicBool::new(false),
allocation_success: AtomicBool::new(false),
max_collection_attempts: AtomicUsize::new(0),
cur_collection_attempts: AtomicUsize::new(0),
scanned_stacks: AtomicUsize::new(0),
allocation_bytes: AtomicUsize::new(0),
inside_harness: AtomicBool::new(false),
#[cfg(feature = "malloc_counted_size")]
malloc_bytes: AtomicUsize::new(0),
live_bytes_in_last_gc: AtomicRefCell::new(HashMap::new()),
used_pages_after_last_gc: AtomicUsize::new(0),
}
}
}
#[derive(PartialEq, Copy, Clone, Debug)]
pub enum GcStatus {
Uninitialized,
NotInGC,
InConcurrentGC,
InPause,
PauseRequested,
Disabled(usize),
}
pub(crate) struct GcStatusWord(AtomicUsize);
impl GcStatusWord {
const TAG_BITS: u32 = 3;
const TAG_MASK: usize = (1 << Self::TAG_BITS) - 1;
fn encode(status: GcStatus) -> usize {
match status {
GcStatus::Uninitialized => 0,
GcStatus::NotInGC => 1,
GcStatus::InConcurrentGC => 2,
GcStatus::InPause => 3,
GcStatus::PauseRequested => 4,
GcStatus::Disabled(depth) => {
debug_assert!(
depth < (1 << (usize::BITS - Self::TAG_BITS)),
"GC-disable nesting depth overflows the bits reserved for it"
);
5 | (depth << Self::TAG_BITS)
}
}
}
fn decode(bits: usize) -> GcStatus {
match bits & Self::TAG_MASK {
0 => GcStatus::Uninitialized,
1 => GcStatus::NotInGC,
2 => GcStatus::InConcurrentGC,
3 => GcStatus::InPause,
4 => GcStatus::PauseRequested,
5 => GcStatus::Disabled(bits >> Self::TAG_BITS),
_ => unreachable!("invalid encoded GcStatus tag"),
}
}
pub(crate) fn new(status: GcStatus) -> Self {
GcStatusWord(AtomicUsize::new(Self::encode(status)))
}
pub(crate) fn load(&self) -> GcStatus {
Self::decode(self.0.load(Ordering::SeqCst))
}
fn transition_inner<F: FnMut(GcStatus) -> GcStatus>(&self, mut f: F) -> GcStatus {
let old_bits = self
.0
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |bits| {
Some(Self::encode(f(Self::decode(bits))))
})
.unwrap(); Self::decode(old_bits)
}
fn transition<F: FnMut(GcStatus) -> GcStatus>(&self, mut f: F) -> GcStatus {
let mut maybe_new_state = None;
let old_state = self.transition_inner(|old_state| {
let new_state = f(old_state);
maybe_new_state = Some(new_state);
new_state
});
let new_state = maybe_new_state.unwrap();
log::trace!("GC status transitioned from {old_state:?} to {new_state:?}");
old_state
}
fn try_transition_inner<F: FnMut(GcStatus) -> Option<GcStatus>>(
&self,
mut f: F,
) -> Result<GcStatus, GcStatus> {
self.0
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |bits| {
f(Self::decode(bits)).map(Self::encode)
})
.map(Self::decode)
.map_err(Self::decode)
}
fn try_transition<F: FnMut(GcStatus) -> Option<GcStatus>>(
&self,
mut f: F,
) -> Result<GcStatus, GcStatus> {
let mut maybe_new_state = None;
let result = self.try_transition_inner(|old_state| {
maybe_new_state = f(old_state);
maybe_new_state
});
match result {
Ok(old_state) => {
let new_state = maybe_new_state.unwrap();
log::trace!("GC status transitioned from {old_state:?} to {new_state:?}")
}
Err(old_state) => {
log::trace!("GC status transition attempted, but remains {old_state:?}")
}
}
result
}
pub(crate) fn is_initialized(&self) -> bool {
self.load() != GcStatus::Uninitialized
}
pub(crate) fn is_disabled(&self) -> bool {
matches!(self.load(), GcStatus::Disabled(_))
}
pub(crate) fn set_initialized(&self) {
self.transition(|status| {
assert!(
status == GcStatus::Uninitialized,
"Trying to set initialized GC status when it is not uninitialized"
);
GcStatus::NotInGC
});
}
pub(crate) fn set_uninitialized(&self) {
self.transition(|status| {
assert!(
status != GcStatus::Uninitialized,
"Trying to set uninitialized GC status when it is already uninitialized"
);
GcStatus::Uninitialized
});
}
pub(crate) fn set_in_pause(&self) {
self.transition(|status| {
assert!(
status == GcStatus::PauseRequested,
"Trying to set in-pause GC status in invalid status: {:?}",
status
);
GcStatus::InPause
});
}
pub(crate) fn set_in_concurrent_gc(&self) {
self.transition(|status| {
assert!(
status == GcStatus::InPause,
"Trying to set in-concurrent-gc GC status in invalid status: {:?}",
status
);
GcStatus::InConcurrentGC
});
}
pub(crate) fn set_not_in_gc(&self) {
self.transition(|status| {
assert!(
status == GcStatus::InPause,
"Trying to set not-in-gc GC status in invalid status: {:?}",
status
);
GcStatus::NotInGC
});
}
pub(crate) fn set_disabled(&self) -> Result<bool, GcStatus> {
self.try_transition(|status| match status {
GcStatus::Disabled(depth) => Some(GcStatus::Disabled(depth + 1)),
GcStatus::NotInGC => Some(GcStatus::Disabled(1)),
_ => None,
})
.map(|old_status| old_status == GcStatus::NotInGC)
}
pub(crate) fn set_enabled(&self) -> bool {
let old = self.transition(|status| match status {
GcStatus::Disabled(1) => GcStatus::NotInGC,
GcStatus::Disabled(depth) => GcStatus::Disabled(depth - 1),
other => other,
});
old == GcStatus::Disabled(1)
}
pub(crate) fn try_request_pause(&self) -> Result<(), GcStatus> {
self.try_transition(|status| match status {
GcStatus::Disabled(_) | GcStatus::Uninitialized | GcStatus::PauseRequested => None,
GcStatus::NotInGC | GcStatus::InConcurrentGC => Some(GcStatus::PauseRequested),
_ => panic!("Trying to request a GC pause in invalid status: {status:?}"),
})
.map(|_| ())
}
}
#[cfg(test)]
mod gc_status_tests {
use super::{GcStatus, GcStatusWord};
#[test]
fn encode_decode_roundtrip() {
let statuses = [
GcStatus::Uninitialized,
GcStatus::NotInGC,
GcStatus::InConcurrentGC,
GcStatus::InPause,
GcStatus::PauseRequested,
GcStatus::Disabled(1),
GcStatus::Disabled(42),
];
for status in statuses {
assert_eq!(GcStatusWord::decode(GcStatusWord::encode(status)), status);
}
}
#[test]
fn new_and_load_roundtrip() {
let statuses = [
GcStatus::Uninitialized,
GcStatus::NotInGC,
GcStatus::InConcurrentGC,
GcStatus::InPause,
GcStatus::PauseRequested,
GcStatus::Disabled(1),
GcStatus::Disabled(42),
];
for status in statuses {
assert_eq!(GcStatusWord::new(status).load(), status);
}
}
#[test]
fn set_initialized_from_uninitialized() {
let word = GcStatusWord::new(GcStatus::Uninitialized);
assert!(!word.is_initialized());
word.set_initialized();
assert_eq!(word.load(), GcStatus::NotInGC);
assert!(word.is_initialized());
}
#[test]
#[should_panic(expected = "not uninitialized")]
fn set_initialized_panics_if_already_initialized() {
GcStatusWord::new(GcStatus::NotInGC).set_initialized();
}
#[test]
fn set_uninitialized_from_not_in_gc() {
let word = GcStatusWord::new(GcStatus::NotInGC);
word.set_uninitialized();
assert_eq!(word.load(), GcStatus::Uninitialized);
}
#[test]
#[should_panic(expected = "already uninitialized")]
fn set_uninitialized_panics_if_already_uninitialized() {
GcStatusWord::new(GcStatus::Uninitialized).set_uninitialized();
}
#[test]
fn try_request_pause_from_not_in_gc() {
let word = GcStatusWord::new(GcStatus::NotInGC);
assert!(word.try_request_pause().is_ok());
assert_eq!(word.load(), GcStatus::PauseRequested);
}
#[test]
fn try_request_pause_from_in_concurrent_gc() {
let word = GcStatusWord::new(GcStatus::InConcurrentGC);
assert!(word.try_request_pause().is_ok());
assert_eq!(word.load(), GcStatus::PauseRequested);
}
#[test]
fn try_request_pause_when_already_requested() {
let word = GcStatusWord::new(GcStatus::PauseRequested);
assert_eq!(word.try_request_pause(), Err(GcStatus::PauseRequested));
assert_eq!(word.load(), GcStatus::PauseRequested);
}
#[test]
#[should_panic(expected = "invalid status")]
fn try_request_pause_panics_when_already_in_pause() {
let _ = GcStatusWord::new(GcStatus::InPause).try_request_pause();
}
#[test]
fn try_request_pause_when_disabled() {
let word = GcStatusWord::new(GcStatus::Disabled(1));
assert_eq!(word.try_request_pause(), Err(GcStatus::Disabled(1)));
assert_eq!(word.load(), GcStatus::Disabled(1));
}
#[test]
fn try_request_pause_when_uninitialized() {
let word = GcStatusWord::new(GcStatus::Uninitialized);
assert_eq!(word.try_request_pause(), Err(GcStatus::Uninitialized));
assert_eq!(word.load(), GcStatus::Uninitialized);
}
#[test]
fn set_in_pause_from_pause_requested() {
let word = GcStatusWord::new(GcStatus::PauseRequested);
word.set_in_pause();
assert_eq!(word.load(), GcStatus::InPause);
}
#[test]
#[should_panic(expected = "invalid status")]
fn set_in_pause_panics_if_not_requested() {
GcStatusWord::new(GcStatus::NotInGC).set_in_pause();
}
#[test]
fn set_disabled_from_not_in_gc() {
let word = GcStatusWord::new(GcStatus::NotInGC);
assert_eq!(word.set_disabled(), Ok(true));
assert_eq!(word.load(), GcStatus::Disabled(1));
}
#[test]
fn set_disabled_nests() {
let word = GcStatusWord::new(GcStatus::Disabled(1));
assert_eq!(word.set_disabled(), Ok(false));
assert_eq!(word.load(), GcStatus::Disabled(2));
assert_eq!(word.set_disabled(), Ok(false));
assert_eq!(word.load(), GcStatus::Disabled(3));
}
#[test]
fn set_disabled_fails_without_changing_status() {
for status in [
GcStatus::Uninitialized,
GcStatus::InConcurrentGC,
GcStatus::PauseRequested,
GcStatus::InPause,
] {
let word = GcStatusWord::new(status);
assert_eq!(word.set_disabled(), Err(status));
assert_eq!(word.load(), status);
}
}
#[test]
fn set_enabled_decrements_nesting() {
let word = GcStatusWord::new(GcStatus::Disabled(3));
assert!(!word.set_enabled());
assert_eq!(word.load(), GcStatus::Disabled(2));
}
#[test]
fn set_enabled_to_not_in_gc_at_zero_depth() {
let word = GcStatusWord::new(GcStatus::Disabled(1));
assert!(word.set_enabled());
assert_eq!(word.load(), GcStatus::NotInGC);
}
#[test]
fn set_disabled_and_set_enabled_nest_round_trip() {
let word = GcStatusWord::new(GcStatus::NotInGC);
assert!(word.set_disabled().is_ok());
assert!(word.set_disabled().is_ok());
assert!(word.set_disabled().is_ok());
assert_eq!(word.load(), GcStatus::Disabled(3));
assert!(!word.set_enabled());
assert_eq!(word.load(), GcStatus::Disabled(2));
assert!(!word.set_enabled());
assert_eq!(word.load(), GcStatus::Disabled(1));
assert!(word.set_enabled());
assert_eq!(word.load(), GcStatus::NotInGC);
}
#[test]
fn set_enabled_is_noop_if_not_disabled() {
for status in [
GcStatus::Uninitialized,
GcStatus::NotInGC,
GcStatus::InConcurrentGC,
GcStatus::InPause,
GcStatus::PauseRequested,
] {
let word = GcStatusWord::new(status);
assert!(!word.set_enabled());
assert_eq!(word.load(), status);
}
}
#[test]
fn set_in_concurrent_gc_from_in_pause() {
let word = GcStatusWord::new(GcStatus::InPause);
word.set_in_concurrent_gc();
assert_eq!(word.load(), GcStatus::InConcurrentGC);
}
#[test]
#[should_panic(expected = "invalid status")]
fn set_in_concurrent_gc_panics_if_not_in_pause() {
GcStatusWord::new(GcStatus::NotInGC).set_in_concurrent_gc();
}
#[test]
fn set_not_in_gc_from_in_pause() {
let word = GcStatusWord::new(GcStatus::InPause);
word.set_not_in_gc();
assert_eq!(word.load(), GcStatus::NotInGC);
}
#[test]
#[should_panic(expected = "invalid status")]
fn set_not_in_gc_panics_if_not_in_pause() {
GcStatusWord::new(GcStatus::InConcurrentGC).set_not_in_gc();
}
#[test]
fn is_disabled_reflects_status() {
assert!(GcStatusWord::new(GcStatus::Disabled(1)).is_disabled());
assert!(!GcStatusWord::new(GcStatus::NotInGC).is_disabled());
}
}
#[derive(Copy, Clone, Debug)]
pub struct LiveBytesStats {
pub live_bytes: usize,
pub used_pages: usize,
pub used_bytes: usize,
}