use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, PoisonError, RwLock};
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::BackendKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MemoryTier {
Device,
Host,
Disk,
}
impl MemoryTier {
const fn index(self) -> usize {
match self {
MemoryTier::Device => 0,
MemoryTier::Host => 1,
MemoryTier::Disk => 2,
}
}
}
#[derive(Debug)]
pub struct OperatorStats {
id: usize,
name: String,
backend: BackendKind,
rows_in: AtomicU64,
rows_out: AtomicU64,
batches: AtomicU64,
elapsed_ns: AtomicU64,
bytes_h2d: AtomicU64,
bytes_d2h: AtomicU64,
memory_bytes: AtomicU64,
}
impl OperatorStats {
fn new(id: usize, name: String, backend: BackendKind) -> Self {
Self {
id,
name,
backend,
rows_in: AtomicU64::new(0),
rows_out: AtomicU64::new(0),
batches: AtomicU64::new(0),
elapsed_ns: AtomicU64::new(0),
bytes_h2d: AtomicU64::new(0),
bytes_d2h: AtomicU64::new(0),
memory_bytes: AtomicU64::new(0),
}
}
pub const fn id(&self) -> usize {
self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub const fn backend(&self) -> BackendKind {
self.backend
}
pub fn record_batch(&self, rows_in: u64, rows_out: u64, elapsed: Duration) {
self.rows_in.fetch_add(rows_in, Ordering::Relaxed);
self.rows_out.fetch_add(rows_out, Ordering::Relaxed);
self.batches.fetch_add(1, Ordering::Relaxed);
let ns = u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX);
self.elapsed_ns.fetch_add(ns, Ordering::Relaxed);
}
pub fn record_transfer(&self, h2d: u64, d2h: u64) {
self.bytes_h2d.fetch_add(h2d, Ordering::Relaxed);
self.bytes_d2h.fetch_add(d2h, Ordering::Relaxed);
}
pub fn set_memory_bytes(&self, bytes: u64) {
self.memory_bytes.store(bytes, Ordering::Relaxed);
}
pub fn snapshot(&self) -> OperatorSnapshot {
OperatorSnapshot {
id: self.id,
name: self.name.clone(),
backend: self.backend,
rows_in: self.rows_in.load(Ordering::Relaxed),
rows_out: self.rows_out.load(Ordering::Relaxed),
batches: self.batches.load(Ordering::Relaxed),
elapsed_ns: self.elapsed_ns.load(Ordering::Relaxed),
bytes_h2d: self.bytes_h2d.load(Ordering::Relaxed),
bytes_d2h: self.bytes_d2h.load(Ordering::Relaxed),
memory_bytes: self.memory_bytes.load(Ordering::Relaxed),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OperatorSnapshot {
pub id: usize,
pub name: String,
pub backend: BackendKind,
pub rows_in: u64,
pub rows_out: u64,
pub batches: u64,
pub elapsed_ns: u64,
pub bytes_h2d: u64,
pub bytes_d2h: u64,
pub memory_bytes: u64,
}
impl OperatorSnapshot {
pub fn mean_batch_latency_ms(&self) -> f64 {
if self.batches == 0 {
0.0
} else {
self.elapsed_ns as f64 / self.batches as f64 / 1_000_000.0
}
}
}
#[derive(Debug, Default)]
pub struct TierGauges {
bytes: [AtomicU64; 3],
}
impl TierGauges {
pub fn set(&self, tier: MemoryTier, bytes: u64) {
self.bytes[tier.index()].store(bytes, Ordering::Relaxed);
}
pub fn add(&self, tier: MemoryTier, bytes: u64) {
self.bytes[tier.index()].fetch_add(bytes, Ordering::Relaxed);
}
pub fn sub(&self, tier: MemoryTier, bytes: u64) {
let _ = self.bytes[tier.index()].fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
Some(v.saturating_sub(bytes))
});
}
pub fn get(&self, tier: MemoryTier) -> u64 {
self.bytes[tier.index()].load(Ordering::Relaxed)
}
pub fn snapshot(&self) -> TierSnapshot {
TierSnapshot {
device_bytes: self.get(MemoryTier::Device),
host_bytes: self.get(MemoryTier::Host),
disk_bytes: self.get(MemoryTier::Disk),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct TierSnapshot {
pub device_bytes: u64,
pub host_bytes: u64,
pub disk_bytes: u64,
}
#[derive(Debug, Default)]
pub struct SpillCounters {
demotions: AtomicU64,
promotions: AtomicU64,
spilled_bytes: AtomicU64,
reloaded_bytes: AtomicU64,
}
impl SpillCounters {
pub fn record_demotion(&self, bytes: u64) {
self.demotions.fetch_add(1, Ordering::Relaxed);
self.spilled_bytes.fetch_add(bytes, Ordering::Relaxed);
}
pub fn record_promotion(&self, bytes: u64) {
self.promotions.fetch_add(1, Ordering::Relaxed);
self.reloaded_bytes.fetch_add(bytes, Ordering::Relaxed);
}
pub fn snapshot(&self) -> SpillSnapshot {
SpillSnapshot {
demotions: self.demotions.load(Ordering::Relaxed),
promotions: self.promotions.load(Ordering::Relaxed),
spilled_bytes: self.spilled_bytes.load(Ordering::Relaxed),
reloaded_bytes: self.reloaded_bytes.load(Ordering::Relaxed),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct SpillSnapshot {
pub demotions: u64,
pub promotions: u64,
pub spilled_bytes: u64,
pub reloaded_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlanNodeSummary {
pub depth: usize,
pub name: String,
pub backend: BackendKind,
pub detail: String,
}
pub const MAX_OPERATORS: usize = 4096;
#[derive(Debug, Default)]
pub struct TelemetryHub {
operators: RwLock<VecDeque<Arc<OperatorStats>>>,
next_operator_id: AtomicUsize,
tiers: TierGauges,
spill: SpillCounters,
plan: RwLock<Vec<PlanNodeSummary>>,
}
impl TelemetryHub {
pub fn new() -> Arc<Self> {
Arc::new(Self::default())
}
pub fn register_operator(
&self,
name: impl Into<String>,
backend: BackendKind,
) -> Arc<OperatorStats> {
let id = self.next_operator_id.fetch_add(1, Ordering::Relaxed);
let stats = Arc::new(OperatorStats::new(id, name.into(), backend));
let mut ops = self
.operators
.write()
.unwrap_or_else(PoisonError::into_inner);
if ops.len() >= MAX_OPERATORS {
ops.pop_front();
}
ops.push_back(Arc::clone(&stats));
stats
}
pub fn clear_operators(&self) {
self.operators
.write()
.unwrap_or_else(PoisonError::into_inner)
.clear();
}
pub fn set_plan(&self, nodes: Vec<PlanNodeSummary>) {
*self.plan.write().unwrap_or_else(PoisonError::into_inner) = nodes;
}
pub const fn tiers(&self) -> &TierGauges {
&self.tiers
}
pub const fn spill(&self) -> &SpillCounters {
&self.spill
}
pub fn snapshot(&self) -> TelemetrySnapshot {
let operators = self
.operators
.read()
.unwrap_or_else(PoisonError::into_inner)
.iter()
.map(|o| o.snapshot())
.collect();
let plan = self
.plan
.read()
.unwrap_or_else(PoisonError::into_inner)
.clone();
TelemetrySnapshot {
operators,
tiers: self.tiers.snapshot(),
spill: self.spill.snapshot(),
plan,
}
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct TelemetrySnapshot {
pub operators: Vec<OperatorSnapshot>,
pub tiers: TierSnapshot,
pub spill: SpillSnapshot,
pub plan: Vec<PlanNodeSummary>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn operators_register_in_order_and_accumulate() {
let hub = TelemetryHub::new();
let a = hub.register_operator("GpuFilterExec", BackendKind::Cuda);
let b = hub.register_operator("GpuAggregateExec", BackendKind::CpuSimd);
assert_eq!((a.id(), b.id()), (0, 1));
a.record_batch(100, 40, Duration::from_millis(2));
a.record_batch(100, 60, Duration::from_millis(4));
a.record_transfer(800, 480);
a.set_memory_bytes(4096);
let snap = hub.snapshot();
assert_eq!(snap.operators.len(), 2);
let op = &snap.operators[0];
assert_eq!((op.rows_in, op.rows_out, op.batches), (200, 100, 2));
assert_eq!(
(op.bytes_h2d, op.bytes_d2h, op.memory_bytes),
(800, 480, 4096)
);
assert!((op.mean_batch_latency_ms() - 3.0).abs() < 1e-9);
assert_eq!(snap.operators[1].mean_batch_latency_ms(), 0.0);
}
#[test]
fn tiers_and_spill_counters() {
let hub = TelemetryHub::new();
hub.tiers().add(MemoryTier::Host, 1000);
hub.tiers().sub(MemoryTier::Host, 300);
hub.tiers().sub(MemoryTier::Disk, 5); hub.tiers().set(MemoryTier::Device, 42);
hub.spill().record_demotion(700);
hub.spill().record_promotion(700);
let snap = hub.snapshot();
assert_eq!(
snap.tiers,
TierSnapshot {
device_bytes: 42,
host_bytes: 700,
disk_bytes: 0
}
);
assert_eq!(snap.spill.demotions, 1);
assert_eq!(snap.spill.promotions, 1);
assert_eq!(snap.spill.spilled_bytes, 700);
}
#[test]
fn plan_summary_is_replaced() {
let hub = TelemetryHub::new();
hub.set_plan(vec![PlanNodeSummary {
depth: 0,
name: "GpuFilterExec".into(),
backend: BackendKind::Metal,
detail: "a > 1".into(),
}]);
assert_eq!(hub.snapshot().plan.len(), 1);
hub.set_plan(Vec::new());
assert!(hub.snapshot().plan.is_empty());
}
}