use crate::app::mem_drift::MemoryDrift;
use crate::app::syscpu::CpuSampler;
use crate::ecs::World;
use concinnity_core::memory::{LedgerSnapshot, MemStats, MemTag, Realm, SizeClass};
use std::time::{Duration, Instant};
const SAMPLE_INTERVAL: Duration = Duration::from_millis(500);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Unit {
Bytes,
Cores,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Meter {
pub caption: &'static str,
pub parts: &'static str,
pub tracked: Option<f64>,
pub used: Option<f64>,
pub total: Option<f64>,
pub unit: Unit,
}
impl Meter {
pub(crate) fn tracked_frac(&self) -> f32 {
frac(self.tracked, self.total)
}
pub(crate) fn used_frac(&self) -> f32 {
frac(self.used, self.total)
}
pub(crate) fn value_text(&self) -> String {
let render = match self.unit {
Unit::Bytes => bytes_text,
Unit::Cores => cores_text,
};
let suffix = match self.unit {
Unit::Bytes => "",
Unit::Cores => " cores",
};
format!(
"{} / {} / {}{suffix}",
render(self.tracked),
render(self.used),
render(self.total),
)
}
}
fn frac(value: Option<f64>, total: Option<f64>) -> f32 {
match (value, total) {
(Some(v), Some(t)) if t > 0.0 => (v / t).clamp(0.0, 1.0) as f32,
_ => 0.0,
}
}
fn bytes_text(value: Option<f64>) -> String {
match value {
Some(v) => {
let (div, suffix) = byte_scale(v);
format!("{:.1} {suffix}", v / div)
}
None => "--".to_string(),
}
}
fn cores_text(value: Option<f64>) -> String {
match value {
Some(v) => format!("{v:.1}"),
None => "--".to_string(),
}
}
fn byte_scale(largest: f64) -> (f64, &'static str) {
const KB: f64 = 1024.0;
const MB: f64 = 1024.0 * KB;
const GB: f64 = 1024.0 * MB;
if largest >= GB {
(GB, "GB")
} else if largest >= MB {
(MB, "MB")
} else if largest >= KB {
(KB, "KB")
} else {
(1.0, "B")
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub(crate) struct HealthSnapshot {
pub heap: Option<MemStats>,
pub rss: Option<u64>,
pub total_ram: Option<u64>,
pub pool_bytes: Option<u64>,
pub vram_bytes: Option<u64>,
pub vram_budget: Option<u64>,
pub systems_cores: Option<f32>,
pub process_cores: Option<f32>,
pub total_cores: Option<usize>,
pub tags: LedgerSnapshot,
pub hot_class: Option<SizeClass>,
pub drift: Option<MemoryDrift>,
}
pub(crate) const MAX_TAG_ROWS: usize = MemTag::COUNT * Realm::COUNT;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TagRow {
pub name: &'static str,
pub realm: &'static str,
pub value: String,
pub over_budget: bool,
}
pub(crate) fn tag_rows(snap: &HealthSnapshot) -> Vec<TagRow> {
Realm::ALL
.into_iter()
.flat_map(|realm| snap.tags.reported(realm))
.map(|usage| TagRow {
name: usage.tag.name(),
realm: usage.realm.name(),
value: match usage.budget {
Some(budget) => format!(
"{} / {}",
bytes_text(Some(usage.bytes as f64)),
bytes_text(Some(budget as f64))
),
None => bytes_text(Some(usage.bytes as f64)),
},
over_budget: usage.over_budget(),
})
.collect()
}
pub(crate) fn meters(snap: &HealthSnapshot) -> [Meter; 3] {
[
Meter {
caption: "RAM",
parts: "Heap / Process / Physical",
tracked: snap.heap.map(|h| h.live_bytes as f64),
used: snap.rss.map(|v| v as f64),
total: snap.total_ram.map(|v| v as f64),
unit: Unit::Bytes,
},
Meter {
caption: "VRAM",
parts: "Pools / Device / Budget",
tracked: snap.pool_bytes.map(|v| v as f64),
used: snap.vram_bytes.map(|v| v as f64),
total: snap.vram_budget.map(|v| v as f64),
unit: Unit::Bytes,
},
Meter {
caption: "CPU",
parts: "Systems / Process / Cores",
tracked: snap.systems_cores.map(|v| v as f64),
used: snap.process_cores.map(|v| v as f64),
total: snap.total_cores.map(|v| v as f64),
unit: Unit::Cores,
},
]
}
pub(crate) fn churn_text(snap: &HealthSnapshot) -> Option<String> {
let heap = snap.heap?;
let live = heap.alloc_count.saturating_sub(heap.free_count);
Some(format!(
"peak {} | {live} live allocs",
bytes_text(Some(heap.peak_bytes as f64)),
))
}
pub(crate) fn drift_text(snap: &HealthSnapshot) -> Option<String> {
let drift = snap.drift?;
Some(format!(
"{} heap {} outside {}",
window_text(drift.window_secs),
whole_signed_bytes(drift.heap_growth_bytes),
whole_signed_bytes(drift.outside_heap_growth_bytes),
))
}
fn whole_signed_bytes(value: i64) -> String {
let (div, suffix) = byte_scale(value.unsigned_abs() as f64);
let sign = if value < 0 { '-' } else { '+' };
format!("{sign}{:.0} {suffix}", value.unsigned_abs() as f64 / div)
}
fn window_text(secs: u64) -> String {
let minutes = secs / 60;
let hours = minutes / 60;
if hours >= 48 {
format!("{}d", hours / 24)
} else if minutes >= 60 {
format!("{hours}h")
} else {
format!("{minutes}m")
}
}
pub(crate) fn hot_class_text(snap: &HealthSnapshot) -> Option<String> {
let class = snap.hot_class?;
Some(format!(
"hot class {} | {} live blocks",
bytes_text(Some(class.min_bytes as f64)),
class.live_blocks,
))
}
#[derive(Debug, Default)]
pub(crate) struct HealthState {
cpu: CpuSampler,
systems_us: u64,
window_start: Option<Instant>,
snapshot: HealthSnapshot,
}
impl HealthState {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn snapshot(&self) -> &HealthSnapshot {
&self.snapshot
}
pub(crate) fn sample(&mut self, world: &World) {
self.sample_at(world, Instant::now());
}
fn sample_at(&mut self, world: &World, now: Instant) {
self.systems_us += world
.profile()
.system_timings()
.iter()
.map(|&(_, micros)| micros as u64)
.sum::<u64>();
let start = *self.window_start.get_or_insert(now);
let elapsed = now.saturating_duration_since(start);
if elapsed < SAMPLE_INTERVAL {
return;
}
let window_us = elapsed.as_micros() as f64;
let systems_cores = (window_us > 0.0).then(|| (self.systems_us as f64 / window_us) as f32);
self.systems_us = 0;
self.window_start = Some(now);
let render = &world.profile().render;
self.snapshot = HealthSnapshot {
heap: concinnity_core::memory::stats(),
rss: crate::app::sysmem::process_resident_bytes(),
total_ram: concinnity_engine::ecs::memory_budget(world).and_then(|b| b.total_ram_bytes),
pool_bytes: pool_bytes(world),
vram_bytes: (render.vram_bytes > 0).then_some(render.vram_bytes),
vram_budget: concinnity_engine::ecs::gpu_profile(world)
.map(|p| p.memory_budget_bytes)
.filter(|&b| b > 0),
systems_cores,
process_cores: self.cpu.sample().or(self.snapshot.process_cores),
total_cores: concinnity_engine::ecs::thread_budget(world).map(|b| b.total_cores),
tags: concinnity_core::memory::ledger().snapshot(),
hot_class: concinnity_core::memory::size_classes().and_then(|c| c.busiest()),
drift: concinnity_engine::ecs::memory_drift(world),
};
}
}
fn pool_bytes(world: &World) -> Option<u64> {
let stats = concinnity_engine::ecs::streaming_stats(world)?;
[stats.texture_bytes, stats.mesh_bytes, stats.chunk_bytes]
.into_iter()
.flatten()
.map(|(resident, _budget)| resident)
.fold(None, |acc: Option<u64>, resident| {
Some(acc.unwrap_or(0) + resident)
})
}
#[cfg(test)]
mod tests {
use super::*;
const GB: u64 = 1024 * 1024 * 1024;
fn ram_snapshot() -> HealthSnapshot {
HealthSnapshot {
heap: Some(MemStats {
live_bytes: GB / 2,
peak_bytes: GB,
alloc_count: 10,
free_count: 4,
}),
rss: Some(2 * GB),
total_ram: Some(32 * GB),
..Default::default()
}
}
#[test]
fn fractions_scale_each_fill_against_the_capacity() {
let ram = meters(&ram_snapshot())[0];
assert!((ram.tracked_frac() - 0.5 / 32.0).abs() < 1e-6);
assert!((ram.used_frac() - 2.0 / 32.0).abs() < 1e-6);
}
#[test]
fn value_text_scales_each_number_independently() {
assert_eq!(
meters(&ram_snapshot())[0].value_text(),
"512.0 MB / 2.0 GB / 32.0 GB"
);
}
#[test]
fn a_small_value_beside_a_huge_capacity_stays_legible() {
let snap = HealthSnapshot {
heap: Some(MemStats {
live_bytes: 30 * 1024 * 1024,
..Default::default()
}),
rss: Some(3 * GB),
total_ram: Some(32 * GB),
..Default::default()
};
assert_eq!(
meters(&snap)[0].value_text(),
"30.0 MB / 3.0 GB / 32.0 GB",
"a 30 MB heap must not round away against a 32 GB machine"
);
}
#[test]
fn missing_values_read_as_dashes_and_draw_no_fill() {
let vram = meters(&HealthSnapshot::default())[1];
assert_eq!(vram.value_text(), "-- / -- / --");
assert_eq!(vram.tracked_frac(), 0.0);
assert_eq!(vram.used_frac(), 0.0);
}
#[test]
fn a_known_capacity_scales_even_with_unknown_usage() {
let snap = HealthSnapshot {
vram_budget: Some(8 * GB),
..Default::default()
};
assert_eq!(meters(&snap)[1].value_text(), "-- / -- / 8.0 GB");
}
#[test]
fn an_over_capacity_fill_clamps_to_the_track() {
let snap = HealthSnapshot {
pool_bytes: Some(12 * GB),
vram_budget: Some(8 * GB),
..Default::default()
};
assert_eq!(meters(&snap)[1].tracked_frac(), 1.0);
}
#[test]
fn a_zero_capacity_yields_an_empty_fill() {
let snap = HealthSnapshot {
rss: Some(GB),
total_ram: Some(0),
..Default::default()
};
let ram = meters(&snap)[0];
assert_eq!(ram.used_frac(), 0.0);
assert!(ram.used_frac().is_finite());
}
#[test]
fn cpu_reads_in_cores_against_the_core_count() {
let snap = HealthSnapshot {
systems_cores: Some(0.4),
process_cores: Some(3.2),
total_cores: Some(10),
..Default::default()
};
let cpu = meters(&snap)[2];
assert_eq!(cpu.unit, Unit::Cores);
assert_eq!(cpu.value_text(), "0.4 / 3.2 / 10.0 cores");
assert!((cpu.used_frac() - 0.32).abs() < 1e-6);
}
#[test]
fn cpu_tracked_may_exceed_used_when_the_main_thread_blocks() {
let snap = HealthSnapshot {
systems_cores: Some(0.9),
process_cores: Some(0.5),
total_cores: Some(8),
..Default::default()
};
let cpu = meters(&snap)[2];
assert!(cpu.tracked_frac() > cpu.used_frac());
}
#[test]
fn byte_scale_picks_the_unit_from_the_largest_value() {
assert_eq!(byte_scale(512.0).1, "B");
assert_eq!(byte_scale(2048.0).1, "KB");
assert_eq!(byte_scale(5.0 * 1024.0 * 1024.0).1, "MB");
assert_eq!(byte_scale(3.0 * GB as f64).1, "GB");
}
#[test]
fn churn_text_reports_peak_and_live_allocations() {
let text = churn_text(&ram_snapshot()).expect("the snapshot carries heap stats");
assert_eq!(text, "peak 1.0 GB | 6 live allocs");
}
#[test]
fn churn_text_is_absent_without_the_tracking_allocator() {
assert_eq!(churn_text(&HealthSnapshot::default()), None);
}
#[test]
fn an_unreported_ledger_lists_no_rows() {
assert!(tag_rows(&HealthSnapshot::default()).is_empty());
}
fn tagged_snapshot() -> HealthSnapshot {
let ledger = concinnity_core::memory::Ledger::new();
ledger.set(MemTag::Textures, Realm::Device, 3 * GB / 2);
ledger.set_budget(MemTag::Textures, Realm::Device, Some(2 * GB));
ledger.set(MemTag::Meshes, Realm::Device, GB / 4);
ledger.set(MemTag::Scratch, Realm::Host, 64 * 1024);
HealthSnapshot {
tags: ledger.snapshot(),
..Default::default()
}
}
#[test]
fn the_breakdown_lists_host_rows_before_device_rows() {
let rows = tag_rows(&tagged_snapshot());
let names: Vec<&str> = rows.iter().map(|r| r.name).collect();
let realms: Vec<&str> = rows.iter().map(|r| r.realm).collect();
assert_eq!(names, ["Scratch", "Textures", "Meshes"]);
assert_eq!(realms, ["RAM", "VRAM", "VRAM"]);
}
#[test]
fn a_budgeted_row_reads_against_its_ceiling() {
let rows = tag_rows(&tagged_snapshot());
assert_eq!(rows[1].value, "1.5 GB / 2.0 GB");
assert!(!rows[1].over_budget);
assert_eq!(rows[2].value, "256.0 MB");
}
#[test]
fn a_row_past_its_budget_is_flagged() {
let ledger = concinnity_core::memory::Ledger::new();
ledger.set(MemTag::Chunks, Realm::Device, 3 * GB);
ledger.set_budget(MemTag::Chunks, Realm::Device, Some(GB));
let snap = HealthSnapshot {
tags: ledger.snapshot(),
..Default::default()
};
let rows = tag_rows(&snap);
assert_eq!(rows.len(), 1);
assert!(rows[0].over_budget);
}
#[test]
fn the_breakdown_never_outgrows_the_panels_row_budget() {
let ledger = concinnity_core::memory::Ledger::new();
for tag in MemTag::ALL {
for realm in Realm::ALL {
ledger.set(tag, realm, 1);
}
}
let snap = HealthSnapshot {
tags: ledger.snapshot(),
..Default::default()
};
assert_eq!(tag_rows(&snap).len(), MAX_TAG_ROWS);
}
#[test]
fn the_drift_line_separates_heap_growth_from_growth_outside_it() {
let snap = HealthSnapshot {
drift: Some(MemoryDrift {
heap_growth_bytes: 2 * 1024 * 1024,
outside_heap_growth_bytes: 412 * 1024 * 1024,
window_secs: 2 * 3600,
verdict: crate::app::mem_drift::DriftVerdict::OutsideHeap,
}),
..Default::default()
};
assert_eq!(
drift_text(&snap).expect("a baseline was captured"),
"2h heap +2 MB outside +412 MB"
);
}
#[test]
fn the_drift_line_signs_a_shrinking_term() {
let snap = HealthSnapshot {
drift: Some(MemoryDrift {
heap_growth_bytes: -(3 * 1024 * 1024 * 1024),
outside_heap_growth_bytes: 0,
window_secs: 45 * 60,
verdict: crate::app::mem_drift::DriftVerdict::Settled,
}),
..Default::default()
};
assert_eq!(
drift_text(&snap).expect("a baseline was captured"),
"45m heap -3 GB outside +0 B"
);
}
#[test]
fn a_multi_day_window_reads_in_days() {
assert_eq!(window_text(0), "0m");
assert_eq!(window_text(59 * 60), "59m");
assert_eq!(window_text(3 * 3600), "3h");
assert_eq!(window_text(47 * 3600), "47h");
assert_eq!(window_text(41 * 24 * 3600), "41d");
}
#[test]
fn the_drift_line_is_absent_before_a_baseline_exists() {
assert_eq!(drift_text(&HealthSnapshot::default()), None);
}
#[test]
fn the_hot_class_line_is_absent_without_the_detail_feature() {
assert_eq!(hot_class_text(&HealthSnapshot::default()), None);
}
#[test]
fn the_hot_class_line_names_the_class_and_its_live_blocks() {
let snap = HealthSnapshot {
hot_class: Some(SizeClass {
min_bytes: 128,
max_bytes: 255,
allocs: 900,
live_blocks: 42,
}),
..Default::default()
};
assert_eq!(
hot_class_text(&snap).expect("a class was measured"),
"hot class 128.0 B | 42 live blocks"
);
}
}