use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
pub struct GcStats {
gc_allocations: AtomicU64,
gc_alloc_bytes: AtomicU64,
region_allocations: AtomicU64,
region_alloc_bytes: AtomicU64,
region_poisons: AtomicU64,
gc_collections: AtomicU64,
gc_pause_total_nanos: AtomicU64,
gc_objects_freed: AtomicU64,
gc_bytes_freed: AtomicU64,
boundary_crossings: AtomicU64,
boundary_bytes_copied: AtomicU64,
boundary_copy_total_nanos: AtomicU64,
}
impl GcStats {
pub const fn new() -> Self {
Self {
gc_allocations: AtomicU64::new(0),
gc_alloc_bytes: AtomicU64::new(0),
region_allocations: AtomicU64::new(0),
region_alloc_bytes: AtomicU64::new(0),
region_poisons: AtomicU64::new(0),
gc_collections: AtomicU64::new(0),
gc_pause_total_nanos: AtomicU64::new(0),
gc_objects_freed: AtomicU64::new(0),
gc_bytes_freed: AtomicU64::new(0),
boundary_crossings: AtomicU64::new(0),
boundary_bytes_copied: AtomicU64::new(0),
boundary_copy_total_nanos: AtomicU64::new(0),
}
}
#[inline]
pub fn record_gc_alloc(&self, bytes: usize) {
self.gc_allocations.fetch_add(1, Ordering::Relaxed);
self.gc_alloc_bytes
.fetch_add(bytes as u64, Ordering::Relaxed);
}
#[inline]
pub fn record_region_alloc(&self, bytes: usize) {
self.region_allocations.fetch_add(1, Ordering::Relaxed);
self.region_alloc_bytes
.fetch_add(bytes as u64, Ordering::Relaxed);
}
#[inline]
pub fn record_region_poison(&self) {
self.region_poisons.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn record_gc_pause(&self, pause: Duration, freed_objects: u64, freed_bytes: u64) {
self.gc_collections.fetch_add(1, Ordering::Relaxed);
self.gc_pause_total_nanos
.fetch_add(pause.as_nanos() as u64, Ordering::Relaxed);
self.gc_objects_freed
.fetch_add(freed_objects, Ordering::Relaxed);
self.gc_bytes_freed
.fetch_add(freed_bytes, Ordering::Relaxed);
}
#[inline]
pub fn record_boundary_crossing(&self, bytes: u64, copy_time: Duration) {
self.boundary_crossings.fetch_add(1, Ordering::Relaxed);
self.boundary_bytes_copied
.fetch_add(bytes, Ordering::Relaxed);
self.boundary_copy_total_nanos
.fetch_add(copy_time.as_nanos() as u64, Ordering::Relaxed);
}
pub fn snapshot(&self) -> GcStatsSnapshot {
GcStatsSnapshot {
gc_allocations: self.gc_allocations.load(Ordering::Relaxed),
gc_alloc_bytes: self.gc_alloc_bytes.load(Ordering::Relaxed),
region_allocations: self.region_allocations.load(Ordering::Relaxed),
region_alloc_bytes: self.region_alloc_bytes.load(Ordering::Relaxed),
region_poisons: self.region_poisons.load(Ordering::Relaxed),
gc_collections: self.gc_collections.load(Ordering::Relaxed),
gc_pause_total_nanos: self.gc_pause_total_nanos.load(Ordering::Relaxed),
gc_objects_freed: self.gc_objects_freed.load(Ordering::Relaxed),
gc_bytes_freed: self.gc_bytes_freed.load(Ordering::Relaxed),
boundary_crossings: self.boundary_crossings.load(Ordering::Relaxed),
boundary_bytes_copied: self.boundary_bytes_copied.load(Ordering::Relaxed),
boundary_copy_total_nanos: self.boundary_copy_total_nanos.load(Ordering::Relaxed),
}
}
}
impl Default for GcStats {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GcStatsSnapshot {
pub gc_allocations: u64,
pub gc_alloc_bytes: u64,
pub region_allocations: u64,
pub region_alloc_bytes: u64,
pub region_poisons: u64,
pub gc_collections: u64,
pub gc_pause_total_nanos: u64,
pub gc_objects_freed: u64,
pub gc_bytes_freed: u64,
pub boundary_crossings: u64,
pub boundary_bytes_copied: u64,
pub boundary_copy_total_nanos: u64,
}
impl GcStatsSnapshot {
pub fn total_pause(&self) -> Duration {
Duration::from_nanos(self.gc_pause_total_nanos)
}
pub fn total_boundary_copy(&self) -> Duration {
Duration::from_nanos(self.boundary_copy_total_nanos)
}
}
impl fmt::Display for GcStatsSnapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "GC stats:")?;
writeln!(
f,
" GC allocations: {} ({} bytes)",
self.gc_allocations, self.gc_alloc_bytes
)?;
writeln!(
f,
" Region (bump) allocs: {} ({} bytes)",
self.region_allocations, self.region_alloc_bytes
)?;
writeln!(f, " Region poisons: {}", self.region_poisons)?;
writeln!(f, " GC collections: {}", self.gc_collections)?;
writeln!(f, " Total GC pause time: {:.3?}", self.total_pause())?;
writeln!(f, " Objects freed by GC: {}", self.gc_objects_freed)?;
writeln!(f, " Bytes freed by GC: {}", self.gc_bytes_freed)?;
writeln!(
f,
" Boundary crossings: {} ({} bytes copied)",
self.boundary_crossings, self.boundary_bytes_copied
)?;
write!(
f,
" Boundary copy time: {:.3?}",
self.total_boundary_copy()
)
}
}
pub static GC_STATS: GcStats = GcStats::new();
pub const CLJRS_GC_STATS_ENV: &str = "CLJRS_GC_STATS";
pub fn dump_stats_from_env() {
let target = match std::env::var(CLJRS_GC_STATS_ENV) {
Ok(t) => t,
Err(_) => return,
};
let snapshot = GC_STATS.snapshot();
let result = if target.is_empty() || target == "-" {
println!("{snapshot}");
Ok(())
} else {
std::fs::write(&target, format!("{snapshot}\n"))
};
if let Err(e) = result {
eprintln!("cljrs: failed to write GC stats to {target:?}: {e}");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapshot_zero_by_default() {
let stats = GcStats::new();
let snap = stats.snapshot();
assert_eq!(snap.gc_allocations, 0);
assert_eq!(snap.region_allocations, 0);
assert_eq!(snap.gc_collections, 0);
assert_eq!(snap.total_pause(), Duration::ZERO);
}
#[test]
fn record_gc_alloc_updates_counters() {
let stats = GcStats::new();
stats.record_gc_alloc(48);
stats.record_gc_alloc(48);
let snap = stats.snapshot();
assert_eq!(snap.gc_allocations, 2);
assert_eq!(snap.gc_alloc_bytes, 96);
}
#[test]
fn record_region_alloc_updates_counters() {
let stats = GcStats::new();
stats.record_region_alloc(16);
stats.record_region_alloc(32);
let snap = stats.snapshot();
assert_eq!(snap.region_allocations, 2);
assert_eq!(snap.region_alloc_bytes, 48);
}
#[test]
fn record_gc_pause_updates_counters() {
let stats = GcStats::new();
stats.record_gc_pause(Duration::from_micros(500), 7, 336);
stats.record_gc_pause(Duration::from_micros(250), 3, 144);
let snap = stats.snapshot();
assert_eq!(snap.gc_collections, 2);
assert_eq!(snap.gc_objects_freed, 10);
assert_eq!(snap.gc_bytes_freed, 480);
assert_eq!(snap.total_pause(), Duration::from_micros(750));
}
#[test]
fn record_boundary_crossing_updates_counters() {
let stats = GcStats::new();
stats.record_boundary_crossing(2048, Duration::from_micros(40));
stats.record_boundary_crossing(1024, Duration::from_micros(10));
let snap = stats.snapshot();
assert_eq!(snap.boundary_crossings, 2);
assert_eq!(snap.boundary_bytes_copied, 3072);
assert_eq!(snap.total_boundary_copy(), Duration::from_micros(50));
}
#[test]
fn display_renders_all_fields() {
let snap = GcStatsSnapshot {
gc_allocations: 5,
gc_alloc_bytes: 240,
region_allocations: 3,
region_alloc_bytes: 96,
region_poisons: 0,
gc_collections: 1,
gc_pause_total_nanos: 1_000_000,
gc_objects_freed: 2,
gc_bytes_freed: 96,
boundary_crossings: 4,
boundary_bytes_copied: 8192,
boundary_copy_total_nanos: 2_000_000,
};
let s = format!("{snap}");
assert!(s.contains("GC allocations:"));
assert!(s.contains("Region (bump) allocs:"));
assert!(s.contains("Region poisons:"));
assert!(s.contains("GC collections:"));
assert!(s.contains("Total GC pause time:"));
assert!(s.contains("Objects freed by GC:"));
assert!(s.contains("Bytes freed by GC:"));
assert!(s.contains("Boundary crossings:"));
assert!(s.contains("Boundary copy time:"));
}
}