use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CollectionType {
None,
Young,
Old,
Full,
}
#[derive(Debug, Clone)]
pub struct HeapMetrics {
pub young_utilization: f64,
pub old_free_bytes: usize,
pub bytes_since_gc: usize,
pub gc_threshold: usize,
pub avg_gc_pause_secs: f64,
}
pub struct AdaptiveScheduler {
last_young_gc: Instant,
last_old_gc: Instant,
young_alloc_bytes: usize,
young_gc_count: u32,
old_gc_count: u32,
young_utilization_threshold: f64,
headroom_factor: f64,
total_alloc_bytes: u64,
young_pause_avg: Duration,
old_pause_avg: Duration,
}
impl AdaptiveScheduler {
pub fn new() -> Self {
Self {
last_young_gc: Instant::now(),
last_old_gc: Instant::now(),
young_alloc_bytes: 0,
young_gc_count: 0,
old_gc_count: 0,
young_utilization_threshold: 0.8,
headroom_factor: 2.0,
total_alloc_bytes: 0,
young_pause_avg: Duration::ZERO,
old_pause_avg: Duration::ZERO,
}
}
pub fn with_config(young_utilization_threshold: f64, headroom_factor: f64) -> Self {
Self {
young_utilization_threshold,
headroom_factor,
..Self::new()
}
}
pub fn should_collect(&self, metrics: &HeapMetrics) -> CollectionType {
if metrics.young_utilization > self.young_utilization_threshold {
return CollectionType::Young;
}
let elapsed = self.last_old_gc.elapsed();
let elapsed_secs = elapsed.as_secs_f64();
if elapsed_secs > 0.0 && self.young_alloc_bytes > 0 {
let alloc_rate = self.young_alloc_bytes as f64 / elapsed_secs;
let old_free = metrics.old_free_bytes as f64;
if alloc_rate > 0.0 {
let time_to_full = old_free / alloc_rate;
let ref_pause = if metrics.avg_gc_pause_secs > 0.0 {
metrics.avg_gc_pause_secs
} else {
self.old_pause_avg.as_secs_f64()
};
let effective_pause = if ref_pause > 0.0 {
ref_pause
} else {
0.001 };
if time_to_full < self.headroom_factor * effective_pause {
return CollectionType::Old;
}
}
}
if metrics.bytes_since_gc >= metrics.gc_threshold {
return CollectionType::Full;
}
CollectionType::None
}
pub fn record_young_gc(&mut self, pause_duration: Duration) {
self.young_gc_count += 1;
self.young_alloc_bytes = 0;
self.last_young_gc = Instant::now();
self.young_pause_avg = ema_duration(self.young_pause_avg, pause_duration, 0.3);
}
pub fn record_old_gc(&mut self, pause_duration: Duration) {
self.old_gc_count += 1;
self.young_alloc_bytes = 0; self.last_old_gc = Instant::now();
self.old_pause_avg = ema_duration(self.old_pause_avg, pause_duration, 0.3);
}
pub fn record_full_gc(&mut self, pause_duration: Duration) {
self.record_young_gc(pause_duration);
self.record_old_gc(pause_duration);
}
pub fn record_allocation(&mut self, bytes: usize) {
self.young_alloc_bytes += bytes;
self.total_alloc_bytes += bytes as u64;
}
pub fn young_gc_count(&self) -> u32 {
self.young_gc_count
}
pub fn old_gc_count(&self) -> u32 {
self.old_gc_count
}
pub fn young_alloc_bytes(&self) -> usize {
self.young_alloc_bytes
}
pub fn total_alloc_bytes(&self) -> u64 {
self.total_alloc_bytes
}
pub fn young_utilization_threshold(&self) -> f64 {
self.young_utilization_threshold
}
pub fn headroom_factor(&self) -> f64 {
self.headroom_factor
}
pub fn young_pause_avg(&self) -> Duration {
self.young_pause_avg
}
pub fn old_pause_avg(&self) -> Duration {
self.old_pause_avg
}
pub fn allocation_rate(&self) -> f64 {
let elapsed = self.last_old_gc.elapsed().as_secs_f64();
if elapsed > 0.0 {
self.young_alloc_bytes as f64 / elapsed
} else {
0.0
}
}
}
impl Default for AdaptiveScheduler {
fn default() -> Self {
Self::new()
}
}
fn ema_duration(current: Duration, new_sample: Duration, alpha: f64) -> Duration {
if current == Duration::ZERO {
return new_sample;
}
let current_nanos = current.as_nanos() as f64;
let sample_nanos = new_sample.as_nanos() as f64;
let result_nanos = alpha * sample_nanos + (1.0 - alpha) * current_nanos;
Duration::from_nanos(result_nanos as u64)
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_scheduler_defaults() {
let s = AdaptiveScheduler::new();
assert_eq!(s.young_gc_count(), 0);
assert_eq!(s.old_gc_count(), 0);
assert_eq!(s.young_alloc_bytes(), 0);
assert_eq!(s.young_utilization_threshold(), 0.8);
assert_eq!(s.headroom_factor(), 2.0);
}
#[test]
fn test_scheduler_custom_config() {
let s = AdaptiveScheduler::with_config(0.5, 3.0);
assert_eq!(s.young_utilization_threshold(), 0.5);
assert_eq!(s.headroom_factor(), 3.0);
}
#[test]
fn test_triggers_young_gc_on_utilization() {
let s = AdaptiveScheduler::with_config(0.8, 2.0);
let metrics = HeapMetrics {
young_utilization: 0.9, old_free_bytes: 10_000_000,
bytes_since_gc: 0,
gc_threshold: 4_000_000,
avg_gc_pause_secs: 0.001,
};
assert_eq!(s.should_collect(&metrics), CollectionType::Young);
}
#[test]
fn test_no_collection_below_thresholds() {
let s = AdaptiveScheduler::new();
let metrics = HeapMetrics {
young_utilization: 0.3,
old_free_bytes: 10_000_000,
bytes_since_gc: 1_000,
gc_threshold: 4_000_000,
avg_gc_pause_secs: 0.001,
};
assert_eq!(s.should_collect(&metrics), CollectionType::None);
}
#[test]
fn test_fallback_full_gc_on_byte_threshold() {
let s = AdaptiveScheduler::new();
let metrics = HeapMetrics {
young_utilization: 0.3, old_free_bytes: 10_000_000, bytes_since_gc: 5_000_000, gc_threshold: 4_000_000,
avg_gc_pause_secs: 0.001,
};
assert_eq!(s.should_collect(&metrics), CollectionType::Full);
}
#[test]
fn test_record_allocation() {
let mut s = AdaptiveScheduler::new();
s.record_allocation(1000);
s.record_allocation(500);
assert_eq!(s.young_alloc_bytes(), 1500);
assert_eq!(s.total_alloc_bytes(), 1500);
}
#[test]
fn test_record_young_gc_resets_alloc_counter() {
let mut s = AdaptiveScheduler::new();
s.record_allocation(5000);
assert_eq!(s.young_alloc_bytes(), 5000);
s.record_young_gc(Duration::from_millis(1));
assert_eq!(s.young_alloc_bytes(), 0);
assert_eq!(s.young_gc_count(), 1);
assert_eq!(s.total_alloc_bytes(), 5000);
}
#[test]
fn test_record_old_gc_resets_counter() {
let mut s = AdaptiveScheduler::new();
s.record_allocation(3000);
s.record_old_gc(Duration::from_millis(5));
assert_eq!(s.young_alloc_bytes(), 0);
assert_eq!(s.old_gc_count(), 1);
}
#[test]
fn test_record_full_gc() {
let mut s = AdaptiveScheduler::new();
s.record_allocation(7000);
s.record_full_gc(Duration::from_millis(10));
assert_eq!(s.young_gc_count(), 1);
assert_eq!(s.old_gc_count(), 1);
assert_eq!(s.young_alloc_bytes(), 0);
}
#[test]
fn test_zero_allocation_rate_no_old_gc() {
let s = AdaptiveScheduler::new();
let metrics = HeapMetrics {
young_utilization: 0.3,
old_free_bytes: 100, bytes_since_gc: 0,
gc_threshold: 4_000_000,
avg_gc_pause_secs: 0.001,
};
assert_eq!(s.should_collect(&metrics), CollectionType::None);
}
#[test]
fn test_allocation_rate_computation() {
let mut s = AdaptiveScheduler::new();
assert_eq!(s.allocation_rate(), 0.0);
s.record_allocation(10000);
let rate = s.allocation_rate();
assert!(rate >= 0.0);
}
#[test]
fn test_pause_avg_first_sample() {
let mut s = AdaptiveScheduler::new();
assert_eq!(s.young_pause_avg(), Duration::ZERO);
s.record_young_gc(Duration::from_millis(10));
assert_eq!(s.young_pause_avg(), Duration::from_millis(10));
}
#[test]
fn test_pause_avg_ema_converges() {
let mut s = AdaptiveScheduler::new();
for _ in 0..20 {
s.record_young_gc(Duration::from_millis(5));
}
let avg_ms = s.young_pause_avg().as_millis();
assert!(
avg_ms >= 4 && avg_ms <= 6,
"Expected ~5ms, got {}ms",
avg_ms
);
}
#[test]
fn test_ema_duration_first_sample() {
let result = ema_duration(Duration::ZERO, Duration::from_millis(100), 0.3);
assert_eq!(result, Duration::from_millis(100));
}
#[test]
fn test_ema_duration_blends() {
let current = Duration::from_millis(10);
let new_sample = Duration::from_millis(20);
let result = ema_duration(current, new_sample, 0.5);
assert_eq!(result.as_millis(), 15);
}
#[test]
fn test_old_gen_predictive_trigger() {
let mut s = AdaptiveScheduler::with_config(0.8, 2.0);
s.record_allocation(10_000_000);
let metrics = HeapMetrics {
young_utilization: 0.3, old_free_bytes: 100, bytes_since_gc: 1_000, gc_threshold: 4_000_000,
avg_gc_pause_secs: 10.0, };
let result = s.should_collect(&metrics);
assert_eq!(result, CollectionType::Old);
}
}