1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
//! Configuration of the garbage collector.
//!
//! The configuration can be accessed using the [`config`][`fn@config`] function.
//!
//! # Automatic collection executions
//!
//! Collections can be automatically started if [`auto_collect`][`fn@Config::auto_collect`] is set to `true`.
//!
//! To determine whether to start a collection, a *threshold* is kept over the number of allocated bytes.
//! When calling a function which may start a collection (e.g. [`Cc::new`][`crate::Cc::new`]),
//! if the number of allocated bytes exceeds the *threshold* a collection is started.
//!
//! At the end of the automatically started collection, if the *threshold* is still lower than the number of allocated bytes
//! then it is doubled until it exceed it.
//!
//! Instead, if the number of allocated bytes exceed the *threshold* multiplied by the [`adjustment_percent`][`fn@Config::adjustment_percent`],
//! then the *threshold* is halved until the condition becomes true.
//!
//! Finally, a collection may also happen if the number of objects buffered to be processed in the next collection (see [`Cc::mark_alive`][`crate::Cc::mark_alive`])
//! exceeds the [`buffered_objects_threshold`][`fn@Config::buffered_objects_threshold`]. This parameter is disabled by default, but can be enabled by
//! using [`set_buffered_objects_threshold`][`fn@Config::set_buffered_objects_threshold`].
use alloc::rc::Rc;
use core::cell::RefCell;
use core::num::NonZeroUsize;
use core::marker::PhantomData;
use thiserror::Error;
use crate::list::CountedList;
use crate::state::State;
use crate::utils;
const DEFAULT_BYTES_THRESHOLD: usize = 100;
utils::rust_cc_thread_local! {
pub(crate) static CONFIG: RefCell<Config> = const { RefCell::new(Config::new()) };
}
/// Access the configuration.
///
/// Returns [`Err`] if the configuration is already being accessed.
///
/// # Panics
///
/// Panics if the provided closure panics.
///
/// # Example
/// ```rust
///# use rust_cc::config::config;
/// let res = config(|config| {
/// // Edit the configuration
/// }).unwrap();
/// ```
pub fn config<F, R>(f: F) -> Result<R, ConfigAccessError>
where
F: FnOnce(&mut Config) -> R,
{
CONFIG.try_with(|config| {
config
.try_borrow_mut()
.or(Err(ConfigAccessError::ConcurrentAccessError))
.map(|mut config| f(&mut config))
}).unwrap_or(Err(ConfigAccessError::AccessError))
}
/// An error returned by [`config`][`fn@config`].
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum ConfigAccessError {
/// The configuration couldn't be accessed.
#[error("couldn't access the configuration")]
AccessError,
/// The configuration was already being accessed.
#[error("the configuration is already being accessed")]
ConcurrentAccessError,
}
/// The configuration of the garbage collector.
#[derive(Debug, Clone)]
pub struct Config {
// The invariant is:
// bytes_threshold * adjustment_percent < allocated_bytes < bytes_threshold
bytes_threshold: usize,
adjustment_percent: f64,
buffered_threshold: Option<NonZeroUsize>,
auto_collect: bool,
_phantom: PhantomData<Rc<()>>, // Make Config !Send and !Sync
}
impl Config {
#[inline]
const fn new() -> Self {
Self {
bytes_threshold: DEFAULT_BYTES_THRESHOLD,
adjustment_percent: 0.1,
buffered_threshold: None,
auto_collect: true,
_phantom: PhantomData,
}
}
/// Returns `true` if collections can be automatically started, `false` otherwise.
#[inline]
pub fn auto_collect(&self) -> bool {
self.auto_collect
}
/// Sets whether collections can be automatically started.
#[inline]
pub fn set_auto_collect(&mut self, auto_collect: bool) {
self.auto_collect = auto_collect;
}
/// Returns the threshold adjustment percent.
///
/// See the [module-level documentation][`mod@crate::config`] for more details.
#[inline]
pub fn adjustment_percent(&self) -> f64 {
self.adjustment_percent
}
/// Sets the threshold adjustment percent.
///
/// See the [module-level documentation][`mod@crate::config`] for more details.
///
/// # Panics
///
/// Panics if the provided `percent` isn't between 0 and 1 (included).
#[inline]
#[track_caller]
pub fn set_adjustment_percent(&mut self, percent: f64) {
assert!(
(0f64..=1f64).contains(&percent),
"percent must be between 0 and 1"
);
self.adjustment_percent = percent;
}
/// Returns the buffered-objects threshold (see [`Cc::mark_alive`][`crate::Cc::mark_alive`]).
///
/// Returns [`None`] if this parameter isn't used to start a collection.
///
/// See the [module-level documentation][`mod@crate::config`] for more details.
#[inline]
pub fn buffered_objects_threshold(&self) -> Option<NonZeroUsize> {
self.buffered_threshold
}
/// Sets the buffered-objects threshold (see [`Cc::mark_alive`][`crate::Cc::mark_alive`]).
///
/// If the provided `threshold` is [`None`], then this parameter will not be used to start a collection.
///
/// See the [module-level documentation][`mod@crate::config`] for more details.
#[inline]
#[track_caller]
pub fn set_buffered_objects_threshold(&mut self, threshold: Option<NonZeroUsize>) {
self.buffered_threshold = threshold;
}
#[inline(always)]
pub(super) fn should_collect(&mut self, state: &State, possible_cycles: &RefCell<CountedList>) -> bool {
if !self.auto_collect {
return false;
}
if state.allocated_bytes() > self.bytes_threshold {
return true;
}
return if let Some(buffered_threshold) = self.buffered_threshold {
possible_cycles.try_borrow().map_or(false, |pc| pc.size() > buffered_threshold.get())
} else {
false
}
}
#[inline(always)]
pub(super) fn adjust(&mut self, state: &State) {
// First case: the threshold might have to be increased
if state.allocated_bytes() >= self.bytes_threshold {
loop {
let Some(new_threshold) = self.bytes_threshold.checked_shl(1) else { break; };
self.bytes_threshold = new_threshold;
if state.allocated_bytes() < self.bytes_threshold {
break;
}
}
return; // Skip the other case
}
// Second case: the threshold might have to be decreased
let allocated = state.allocated_bytes() as f64;
// If adjustment_percent or the result of the multiplication is 0 do nothing
if ((self.bytes_threshold as f64) * self.adjustment_percent) == 0.0 {
return;
}
// No more cases after this, there's no need to use an additional if as above
while allocated <= ((self.bytes_threshold as f64) * self.adjustment_percent) {
let new_threshold = self.bytes_threshold >> 1;
if state.allocated_bytes() >= new_threshold {
break; // If the shift produces a threshold <= allocated, then don't update bytes_threshold to maintain the invariant
}
if new_threshold <= DEFAULT_BYTES_THRESHOLD {
self.bytes_threshold = DEFAULT_BYTES_THRESHOLD;
break;
}
self.bytes_threshold = new_threshold;
}
}
}
impl Default for Config {
#[inline]
fn default() -> Self {
Self::new()
}
}