use crossbeam::{deque::Steal, epoch::pin as ebr_pin, utils::Backoff};
use std::{
iter::repeat_with,
mem::take,
ops::Range,
sync::{
LazyLock, RwLock,
atomic::{AtomicUsize, Ordering, fence},
},
thread::{self, sleep, spawn},
time::{Duration, Instant},
};
use crate::{
Handle, HeapHeadroom,
epoch::{Color, Phase},
global,
internal::{OBJ_BATCHES_SHARD, ObjBatch},
task::Task,
tls::handle,
};
use rustc_hash::FxHashSet;
#[derive(Clone)]
struct HeartbeatStats {
measured_time: Instant,
heap_usage: usize,
total_alloc: usize,
alloc_per_ms: usize,
alloc_per_ms_smooth: usize,
}
struct CollectionStats {
reclm_per_ms_smooth: AtomicUsize,
coll_time_ms_smooth: AtomicUsize,
desired_heap_limit: AtomicUsize,
}
static HBSTATS: LazyLock<RwLock<HeartbeatStats>> = LazyLock::new(|| {
RwLock::new(HeartbeatStats {
measured_time: Instant::now(),
heap_usage: 0,
total_alloc: 0,
alloc_per_ms: 0,
alloc_per_ms_smooth: 0,
})
});
static CSTATS: CollectionStats = CollectionStats {
reclm_per_ms_smooth: AtomicUsize::new(0),
coll_time_ms_smooth: AtomicUsize::new(0),
desired_heap_limit: AtomicUsize::new(0),
};
const HEARTBEAT_PERIOD_MS: u64 = 500;
const ALLOC_PER_MS_SMOOTH_FACTOR: f64 = 0.5;
const RECLM_PER_MS_SMOOTH_FACTOR: f64 = 0.5;
const COLL_TIME_MS_SMOOTH_FACTOR: f64 = 0.5;
const EXTRA_TUNING_FACTOR: usize = 10;
const LOCAL_MARK_TASKS_BATCH_LIMIT: usize = 2048;
pub(crate) fn collector_loop() {
let handle = handle();
{
let mut stats = HBSTATS.write().unwrap();
stats.total_alloc = global().estimate_total_alloc();
}
spawn(heartbeat_loop);
loop {
let backoff = Backoff::new();
while !is_collection_necessary() {
backoff.snooze();
}
let start = Instant::now();
let recl_at_start = global().estimate_total_reclm();
root_tracing(&handle);
while !completion_tracing(&handle) {}
next_normal(&handle);
record_collection_stats(start, recl_at_start);
}
}
fn heartbeat_loop() {
loop {
sleep(Duration::from_millis(HEARTBEAT_PERIOD_MS));
heartbeat();
}
}
fn heartbeat() -> HeartbeatStats {
let mut stats = HBSTATS.write().unwrap();
let now = Instant::now();
let dur = now - stats.measured_time;
if dur.as_millis() == 0 {
return stats.clone();
}
let new_total_alloc = global().estimate_total_alloc();
let new_total_reclm = global().estimate_total_reclm();
let alloc_diff = new_total_alloc - stats.total_alloc;
let new_alloc_per_ms = alloc_diff / (dur.as_millis() as usize);
let prev_alloc_per_ms_smooth = stats.alloc_per_ms_smooth;
let new_alloc_per_ms_smooth = smooth(
ALLOC_PER_MS_SMOOTH_FACTOR,
prev_alloc_per_ms_smooth,
new_alloc_per_ms,
);
stats.measured_time = now;
stats.total_alloc = new_total_alloc;
stats.heap_usage = new_total_alloc - new_total_reclm;
stats.alloc_per_ms = new_alloc_per_ms;
stats.alloc_per_ms_smooth = new_alloc_per_ms_smooth;
stats.clone()
}
fn record_collection_stats(start: Instant, recl_at_start: usize) {
let end = Instant::now();
let alloc_at_end = global().estimate_total_alloc();
let recl_at_end = global().estimate_total_reclm();
let prev_coll_time_ms = CSTATS.coll_time_ms_smooth.load(Ordering::Relaxed);
let curr_coll_time_ms = (end - start).as_millis().max(1) as usize;
let new_coll_time_ms = smooth(
COLL_TIME_MS_SMOOTH_FACTOR,
prev_coll_time_ms,
curr_coll_time_ms,
);
let prev_reclm_rate = CSTATS.reclm_per_ms_smooth.load(Ordering::Relaxed);
let curr_reclm_rate =
(((recl_at_end - recl_at_start) as f64) / (curr_coll_time_ms as f64)) as usize;
let new_reclm_rate = smooth(RECLM_PER_MS_SMOOTH_FACTOR, prev_reclm_rate, curr_reclm_rate);
let hbstats = heartbeat();
let heap_usage = alloc_at_end - recl_at_end;
#[allow(clippy::manual_checked_ops)]
let extra = if new_reclm_rate == 0 {
global().locals.active_count() * 1024 * 1024 } else {
let headroom_min = match global().heap_headroom() {
HeapHeadroom::FixedMiB(mib) => mib * 1024 * 1024,
HeapHeadroom::Proportional(divisor) => (heap_usage / divisor).max(16 * 1024),
};
((heap_usage * hbstats.alloc_per_ms_smooth / new_reclm_rate / EXTRA_TUNING_FACTOR) as f64)
.sqrt()
.max(headroom_min as f64) as usize
};
let desired_heap_limit = heap_usage + extra;
CSTATS
.reclm_per_ms_smooth
.store(new_reclm_rate, Ordering::Relaxed);
CSTATS
.coll_time_ms_smooth
.store(new_coll_time_ms, Ordering::Relaxed);
CSTATS
.desired_heap_limit
.store(desired_heap_limit, Ordering::Relaxed);
}
fn smooth(factor: f64, prev: usize, curr: usize) -> usize {
if prev == 0 {
curr
} else {
(factor * (prev as f64) + (1.0 - factor) * (curr as f64)) as usize
}
}
fn root_tracing(handle: &Handle) {
debug_assert!(global().load_epoch().phase() == Phase::N);
phase_trans(Phase::RT);
scan_allocated_objs(handle);
drain_mark_tasks(handle);
}
fn scan_allocated_objs(handle: &Handle) {
let guard = handle.pin();
let ebr_guard = &ebr_pin();
let pending = global()
.locals
.iter_all()
.flat_map(|local| unsafe { local.take_obj_batch(guard.white_color() as usize) });
for (batch, size_bytes) in pending {
global().push_fresh_objs(
batch,
size_bytes,
guard.white_color(),
handle.local().select_obj_shard(),
ebr_guard,
);
}
let hazards: FxHashSet<usize> = global()
.collect_hps(ebr_guard)
.into_iter()
.map(|p| p as usize)
.collect();
let white_color = guard.white_color();
let num_threads = global().collector_threads();
parallel_shard_work(num_threads, |range| {
let guard = crate::pin();
let ebr_guard = &ebr_pin();
let rng = &mut fastrand::Rng::new();
for q_idx in range {
let fresh_q = &global().fresh_objs[white_color as usize][q_idx];
while let Some(batch) = fresh_q.try_pop(ebr_guard) {
for obj in batch.iter() {
if obj.root_count() > 0 || hazards.contains(&(obj.address() as usize)) {
obj.mark(&guard);
}
}
let marked_q_idx = rng.usize(0..OBJ_BATCHES_SHARD);
let marked_q = &global().marked_objs[white_color as usize][marked_q_idx];
marked_q.push(batch, ebr_guard);
}
}
});
}
fn completion_tracing(handle: &Handle) -> bool {
debug_assert!({
let curr = global().epoch.load(Ordering::Acquire).phase();
curr == Phase::RT || curr == Phase::CT
});
phase_trans(Phase::CT);
if try_confirm_completion() {
return true;
}
drain_mark_tasks(handle);
false
}
fn try_confirm_completion() -> bool {
let curr_ts = global().load_epoch().timestamp();
if global()
.locals
.iter_all()
.any(|local| local.mt_modified_ts.load(Ordering::Relaxed) == curr_ts)
{
return false;
}
if global()
.locals
.iter_all()
.any(|local| !local.mark_tasks_stealer.is_empty())
{
return false;
}
if global()
.locals
.iter_all()
.any(|local| local.mt_modified_ts.load(Ordering::Relaxed) == curr_ts)
{
return false;
}
true
}
fn drain_mark_tasks(handle: &Handle) -> bool {
let guard = &handle.pin();
let mut executed = false;
while let Some(task) = find_task(handle) {
executed = true;
task.call(guard);
}
executed
}
fn find_task(handle: &Handle) -> Option<Task> {
let local_w = unsafe { &*handle.local().mark_tasks.get() };
local_w.pop().or_else(|| {
repeat_with(|| {
global()
.locals
.iter_all()
.map(|local| {
local
.mark_tasks_stealer
.steal_batch_with_limit_and_pop(local_w, LOCAL_MARK_TASKS_BATCH_LIMIT)
})
.collect::<Steal<Task>>()
})
.find(|s| !s.is_retry())
.and_then(|s| s.success())
})
}
fn phase_trans(new: Phase) {
let epoch = global().load_epoch();
let new_epoch = epoch.with_timestamp(epoch.timestamp() + 1).with_phase(new);
global().epoch.store(new_epoch, Ordering::Release);
fence(Ordering::SeqCst);
wait_all_mutators_unpin(new_epoch.timestamp());
}
fn next_normal(handle: &Handle) {
let prev_epoch = global().load_epoch();
debug_assert!(prev_epoch.phase() == Phase::CT);
debug_assert!(find_task(handle).is_none());
let new_epoch = prev_epoch
.with_timestamp(prev_epoch.timestamp() + 1)
.with_phase(Phase::N)
.with_color(prev_epoch.color().flip());
global().epoch.store(new_epoch, Ordering::Release);
fence(Ordering::SeqCst);
sweep(prev_epoch.color(), handle);
wait_all_mutators_unpin(new_epoch.timestamp());
}
fn sweep(prev_white: Color, _handle: &Handle) {
let next_white = prev_white.flip() as usize;
let num_threads = global().collector_threads();
parallel_shard_work(num_threads, |range| {
let ebr_guard = &ebr_pin();
let mut survived_batch = ObjBatch::default();
let mut reclaimed_bytes = 0usize;
let rng = &mut fastrand::Rng::new();
for q_idx in range {
let marked_q = &global().marked_objs[prev_white as usize][q_idx];
while let Some(batch) = marked_q.try_pop(ebr_guard) {
for obj in batch.into_iter() {
if prev_white == obj.color() {
reclaimed_bytes += size_of_val(&*obj);
drop(obj);
continue;
}
if let Err(e) = survived_batch.push_within_capacity(obj) {
let full = take(&mut survived_batch);
let shard = rng.usize(0..OBJ_BATCHES_SHARD);
global().fresh_objs[next_white][shard].push(full, ebr_guard);
assert!(survived_batch.push_within_capacity(e).is_ok());
}
}
}
}
if !survived_batch.is_empty() {
let shard = rng.usize(0..OBJ_BATCHES_SHARD);
global().fresh_objs[next_white][shard].push(survived_batch, ebr_guard);
}
global()
.stats
.total_reclaimed
.fetch_add(reclaimed_bytes, Ordering::Release);
});
}
fn parallel_shard_work<F>(num_threads: usize, work: F)
where
F: Fn(Range<usize>) + Sync,
{
thread::scope(|s| {
for thread_idx in 0..num_threads {
let work = &work;
s.spawn(move || {
let base = OBJ_BATCHES_SHARD / num_threads;
let start = thread_idx * base;
let end = if thread_idx == num_threads - 1 {
OBJ_BATCHES_SHARD
} else {
start + base
};
work(start..end);
});
}
});
}
fn wait_all_mutators_unpin(new_ts: usize) {
for local in global().locals.iter_using() {
let backoff = Backoff::new();
let mut local_epoch;
loop {
local_epoch = local.epoch.load(Ordering::Acquire);
if !local_epoch.is_pinned() || new_ts <= local_epoch.timestamp() {
break;
}
backoff.snooze();
}
}
}
fn is_collection_necessary() -> bool {
if !global().collection_enabled.load(Ordering::SeqCst) {
CSTATS.desired_heap_limit.store(0, Ordering::Relaxed);
return false;
}
if global().collection_requested.load(Ordering::SeqCst) {
global().collection_requested.store(false, Ordering::SeqCst);
return true;
}
let hbstats = HBSTATS.read().unwrap();
let reclm_per_ms_smooth = CSTATS.reclm_per_ms_smooth.load(Ordering::Relaxed);
let heap_usage = global().estimate_heap_usage();
let heap_limit = CSTATS.desired_heap_limit.load(Ordering::Relaxed);
if heap_limit < heap_usage {
return true;
}
let pure_alloc_rate = hbstats.alloc_per_ms_smooth as isize - reclm_per_ms_smooth as isize;
if pure_alloc_rate < 0 {
return heap_usage >= heap_limit;
}
let pure_alloc_rate = pure_alloc_rate as usize;
if pure_alloc_rate == 0 {
return false;
}
let oom_time_ms = (heap_limit - heap_usage) / pure_alloc_rate;
oom_time_ms <= CSTATS.coll_time_ms_smooth.load(Ordering::Relaxed)
}