1use std::collections::VecDeque;
6use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
7use std::sync::{Arc, PoisonError, RwLock};
8use std::time::Duration;
9
10use serde::{Deserialize, Serialize};
11
12use crate::BackendKind;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum MemoryTier {
17 Device,
19 Host,
21 Disk,
23}
24
25impl MemoryTier {
26 const fn index(self) -> usize {
27 match self {
28 MemoryTier::Device => 0,
29 MemoryTier::Host => 1,
30 MemoryTier::Disk => 2,
31 }
32 }
33}
34
35#[derive(Debug)]
37pub struct OperatorStats {
38 id: usize,
39 name: String,
40 backend: BackendKind,
41 rows_in: AtomicU64,
42 rows_out: AtomicU64,
43 batches: AtomicU64,
44 elapsed_ns: AtomicU64,
45 bytes_h2d: AtomicU64,
46 bytes_d2h: AtomicU64,
47 memory_bytes: AtomicU64,
48}
49
50impl OperatorStats {
51 fn new(id: usize, name: String, backend: BackendKind) -> Self {
52 Self {
53 id,
54 name,
55 backend,
56 rows_in: AtomicU64::new(0),
57 rows_out: AtomicU64::new(0),
58 batches: AtomicU64::new(0),
59 elapsed_ns: AtomicU64::new(0),
60 bytes_h2d: AtomicU64::new(0),
61 bytes_d2h: AtomicU64::new(0),
62 memory_bytes: AtomicU64::new(0),
63 }
64 }
65
66 pub const fn id(&self) -> usize {
68 self.id
69 }
70
71 pub fn name(&self) -> &str {
73 &self.name
74 }
75
76 pub const fn backend(&self) -> BackendKind {
78 self.backend
79 }
80
81 pub fn record_batch(&self, rows_in: u64, rows_out: u64, elapsed: Duration) {
83 self.rows_in.fetch_add(rows_in, Ordering::Relaxed);
84 self.rows_out.fetch_add(rows_out, Ordering::Relaxed);
85 self.batches.fetch_add(1, Ordering::Relaxed);
86 let ns = u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX);
87 self.elapsed_ns.fetch_add(ns, Ordering::Relaxed);
88 }
89
90 pub fn record_transfer(&self, h2d: u64, d2h: u64) {
92 self.bytes_h2d.fetch_add(h2d, Ordering::Relaxed);
93 self.bytes_d2h.fetch_add(d2h, Ordering::Relaxed);
94 }
95
96 pub fn set_memory_bytes(&self, bytes: u64) {
98 self.memory_bytes.store(bytes, Ordering::Relaxed);
99 }
100
101 pub fn snapshot(&self) -> OperatorSnapshot {
103 OperatorSnapshot {
104 id: self.id,
105 name: self.name.clone(),
106 backend: self.backend,
107 rows_in: self.rows_in.load(Ordering::Relaxed),
108 rows_out: self.rows_out.load(Ordering::Relaxed),
109 batches: self.batches.load(Ordering::Relaxed),
110 elapsed_ns: self.elapsed_ns.load(Ordering::Relaxed),
111 bytes_h2d: self.bytes_h2d.load(Ordering::Relaxed),
112 bytes_d2h: self.bytes_d2h.load(Ordering::Relaxed),
113 memory_bytes: self.memory_bytes.load(Ordering::Relaxed),
114 }
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct OperatorSnapshot {
121 pub id: usize,
123 pub name: String,
125 pub backend: BackendKind,
127 pub rows_in: u64,
129 pub rows_out: u64,
131 pub batches: u64,
133 pub elapsed_ns: u64,
135 pub bytes_h2d: u64,
137 pub bytes_d2h: u64,
139 pub memory_bytes: u64,
141}
142
143impl OperatorSnapshot {
144 pub fn mean_batch_latency_ms(&self) -> f64 {
146 if self.batches == 0 {
147 0.0
148 } else {
149 self.elapsed_ns as f64 / self.batches as f64 / 1_000_000.0
150 }
151 }
152}
153
154#[derive(Debug, Default)]
156pub struct TierGauges {
157 bytes: [AtomicU64; 3],
158}
159
160impl TierGauges {
161 pub fn set(&self, tier: MemoryTier, bytes: u64) {
163 self.bytes[tier.index()].store(bytes, Ordering::Relaxed);
164 }
165
166 pub fn add(&self, tier: MemoryTier, bytes: u64) {
168 self.bytes[tier.index()].fetch_add(bytes, Ordering::Relaxed);
169 }
170
171 pub fn sub(&self, tier: MemoryTier, bytes: u64) {
173 let _ = self.bytes[tier.index()].fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
174 Some(v.saturating_sub(bytes))
175 });
176 }
177
178 pub fn get(&self, tier: MemoryTier) -> u64 {
180 self.bytes[tier.index()].load(Ordering::Relaxed)
181 }
182
183 pub fn snapshot(&self) -> TierSnapshot {
185 TierSnapshot {
186 device_bytes: self.get(MemoryTier::Device),
187 host_bytes: self.get(MemoryTier::Host),
188 disk_bytes: self.get(MemoryTier::Disk),
189 }
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
195pub struct TierSnapshot {
196 pub device_bytes: u64,
198 pub host_bytes: u64,
200 pub disk_bytes: u64,
202}
203
204#[derive(Debug, Default)]
206pub struct SpillCounters {
207 demotions: AtomicU64,
208 promotions: AtomicU64,
209 spilled_bytes: AtomicU64,
210 reloaded_bytes: AtomicU64,
211}
212
213impl SpillCounters {
214 pub fn record_demotion(&self, bytes: u64) {
216 self.demotions.fetch_add(1, Ordering::Relaxed);
217 self.spilled_bytes.fetch_add(bytes, Ordering::Relaxed);
218 }
219
220 pub fn record_promotion(&self, bytes: u64) {
222 self.promotions.fetch_add(1, Ordering::Relaxed);
223 self.reloaded_bytes.fetch_add(bytes, Ordering::Relaxed);
224 }
225
226 pub fn snapshot(&self) -> SpillSnapshot {
228 SpillSnapshot {
229 demotions: self.demotions.load(Ordering::Relaxed),
230 promotions: self.promotions.load(Ordering::Relaxed),
231 spilled_bytes: self.spilled_bytes.load(Ordering::Relaxed),
232 reloaded_bytes: self.reloaded_bytes.load(Ordering::Relaxed),
233 }
234 }
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
239pub struct SpillSnapshot {
240 pub demotions: u64,
242 pub promotions: u64,
244 pub spilled_bytes: u64,
246 pub reloaded_bytes: u64,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct PlanNodeSummary {
253 pub depth: usize,
255 pub name: String,
257 pub backend: BackendKind,
259 pub detail: String,
261}
262
263pub const MAX_OPERATORS: usize = 4096;
269
270#[derive(Debug, Default)]
272pub struct TelemetryHub {
273 operators: RwLock<VecDeque<Arc<OperatorStats>>>,
274 next_operator_id: AtomicUsize,
275 tiers: TierGauges,
276 spill: SpillCounters,
277 plan: RwLock<Vec<PlanNodeSummary>>,
278}
279
280impl TelemetryHub {
281 pub fn new() -> Arc<Self> {
283 Arc::new(Self::default())
284 }
285
286 pub fn register_operator(
290 &self,
291 name: impl Into<String>,
292 backend: BackendKind,
293 ) -> Arc<OperatorStats> {
294 let id = self.next_operator_id.fetch_add(1, Ordering::Relaxed);
295 let stats = Arc::new(OperatorStats::new(id, name.into(), backend));
296 let mut ops = self
297 .operators
298 .write()
299 .unwrap_or_else(PoisonError::into_inner);
300 if ops.len() >= MAX_OPERATORS {
301 ops.pop_front();
302 }
303 ops.push_back(Arc::clone(&stats));
304 stats
305 }
306
307 pub fn clear_operators(&self) {
310 self.operators
311 .write()
312 .unwrap_or_else(PoisonError::into_inner)
313 .clear();
314 }
315
316 pub fn set_plan(&self, nodes: Vec<PlanNodeSummary>) {
318 *self.plan.write().unwrap_or_else(PoisonError::into_inner) = nodes;
319 }
320
321 pub const fn tiers(&self) -> &TierGauges {
323 &self.tiers
324 }
325
326 pub const fn spill(&self) -> &SpillCounters {
328 &self.spill
329 }
330
331 pub fn snapshot(&self) -> TelemetrySnapshot {
333 let operators = self
334 .operators
335 .read()
336 .unwrap_or_else(PoisonError::into_inner)
337 .iter()
338 .map(|o| o.snapshot())
339 .collect();
340 let plan = self
341 .plan
342 .read()
343 .unwrap_or_else(PoisonError::into_inner)
344 .clone();
345 TelemetrySnapshot {
346 operators,
347 tiers: self.tiers.snapshot(),
348 spill: self.spill.snapshot(),
349 plan,
350 }
351 }
352}
353
354#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
356pub struct TelemetrySnapshot {
357 pub operators: Vec<OperatorSnapshot>,
359 pub tiers: TierSnapshot,
361 pub spill: SpillSnapshot,
363 pub plan: Vec<PlanNodeSummary>,
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn operators_register_in_order_and_accumulate() {
373 let hub = TelemetryHub::new();
374 let a = hub.register_operator("GpuFilterExec", BackendKind::Cuda);
375 let b = hub.register_operator("GpuAggregateExec", BackendKind::CpuSimd);
376 assert_eq!((a.id(), b.id()), (0, 1));
377 a.record_batch(100, 40, Duration::from_millis(2));
378 a.record_batch(100, 60, Duration::from_millis(4));
379 a.record_transfer(800, 480);
380 a.set_memory_bytes(4096);
381 let snap = hub.snapshot();
382 assert_eq!(snap.operators.len(), 2);
383 let op = &snap.operators[0];
384 assert_eq!((op.rows_in, op.rows_out, op.batches), (200, 100, 2));
385 assert_eq!(
386 (op.bytes_h2d, op.bytes_d2h, op.memory_bytes),
387 (800, 480, 4096)
388 );
389 assert!((op.mean_batch_latency_ms() - 3.0).abs() < 1e-9);
390 assert_eq!(snap.operators[1].mean_batch_latency_ms(), 0.0);
391 }
392
393 #[test]
394 fn tiers_and_spill_counters() {
395 let hub = TelemetryHub::new();
396 hub.tiers().add(MemoryTier::Host, 1000);
397 hub.tiers().sub(MemoryTier::Host, 300);
398 hub.tiers().sub(MemoryTier::Disk, 5); hub.tiers().set(MemoryTier::Device, 42);
400 hub.spill().record_demotion(700);
401 hub.spill().record_promotion(700);
402 let snap = hub.snapshot();
403 assert_eq!(
404 snap.tiers,
405 TierSnapshot {
406 device_bytes: 42,
407 host_bytes: 700,
408 disk_bytes: 0
409 }
410 );
411 assert_eq!(snap.spill.demotions, 1);
412 assert_eq!(snap.spill.promotions, 1);
413 assert_eq!(snap.spill.spilled_bytes, 700);
414 }
415
416 #[test]
417 fn plan_summary_is_replaced() {
418 let hub = TelemetryHub::new();
419 hub.set_plan(vec![PlanNodeSummary {
420 depth: 0,
421 name: "GpuFilterExec".into(),
422 backend: BackendKind::Metal,
423 detail: "a > 1".into(),
424 }]);
425 assert_eq!(hub.snapshot().plan.len(), 1);
426 hub.set_plan(Vec::new());
427 assert!(hub.snapshot().plan.is_empty());
428 }
429}