1use std::collections::HashMap;
9use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicUsize, Ordering};
10use std::sync::Arc;
11use std::time::Instant;
12
13use parking_lot::Mutex;
14
15use parking_lot::RwLock;
16
17use crate::executor::streaming::plan::types::PhysicalOperatorId;
18
19#[derive(Debug, Clone, Default)]
21pub struct OperatorProfile {
22 pub physical_operator_id: PhysicalOperatorId,
23 pub node_id: i64,
24 pub partition_id: Option<usize>,
25 pub name: String,
26 pub open_time_us: u64,
27 pub next_time_us: u64,
28 pub close_time_us: u64,
29 pub output_rows: u64,
30 pub input_rows: u64,
34 pub exhausted: bool,
39 pub advance_count: u64,
42 pub peak_memory: u64,
43 pub peak_memory_bytes: u64,
44 pub spilled_bytes: u64,
45 pub spill_count: u64,
46 pub spilled_rows: u64,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub struct OperatorProfileKey {
58 pub physical_operator_id: PhysicalOperatorId,
59 pub partition_id: Option<usize>,
60}
61
62impl OperatorProfileKey {
63 pub const fn new(
64 physical_operator_id: PhysicalOperatorId,
65 partition_id: Option<usize>,
66 ) -> Self {
67 Self {
68 physical_operator_id,
69 partition_id,
70 }
71 }
72}
73
74#[derive(Debug)]
80pub struct ProfileEntry {
81 pub physical_operator_id: PhysicalOperatorId,
82 pub node_id: AtomicI64,
83 pub partition_id: Option<usize>,
84 pub name: parking_lot::Mutex<String>,
85 pub open_time_us: AtomicU64,
86 pub next_time_us: AtomicU64,
87 pub close_time_us: AtomicU64,
88 pub output_rows: AtomicU64,
89 pub input_rows: AtomicU64,
90 pub exhausted: AtomicBool,
91 pub advance_count: AtomicU64,
92 pub peak_memory_bytes: AtomicU64,
93 pub spilled_bytes: AtomicU64,
94 pub spill_count: AtomicU64,
95 pub spilled_rows: AtomicU64,
96}
97
98impl ProfileEntry {
99 pub fn new(profile: &OperatorProfile) -> Self {
100 Self {
101 physical_operator_id: profile.physical_operator_id,
102 node_id: AtomicI64::new(profile.node_id),
103 partition_id: profile.partition_id,
104 name: parking_lot::Mutex::new(profile.name.clone()),
105 open_time_us: AtomicU64::new(profile.open_time_us),
106 next_time_us: AtomicU64::new(profile.next_time_us),
107 close_time_us: AtomicU64::new(profile.close_time_us),
108 output_rows: AtomicU64::new(profile.output_rows),
109 input_rows: AtomicU64::new(profile.input_rows),
110 exhausted: AtomicBool::new(profile.exhausted),
111 advance_count: AtomicU64::new(profile.advance_count),
112 peak_memory_bytes: AtomicU64::new(profile.peak_memory_bytes),
113 spilled_bytes: AtomicU64::new(profile.spilled_bytes),
114 spill_count: AtomicU64::new(profile.spill_count),
115 spilled_rows: AtomicU64::new(profile.spilled_rows),
116 }
117 }
118
119 pub fn snapshot(&self) -> OperatorProfile {
121 OperatorProfile {
122 physical_operator_id: self.physical_operator_id,
123 node_id: self.node_id.load(Ordering::Relaxed),
124 partition_id: self.partition_id,
125 name: self.name.lock().clone(),
126 open_time_us: self.open_time_us.load(Ordering::Relaxed),
127 next_time_us: self.next_time_us.load(Ordering::Relaxed),
128 close_time_us: self.close_time_us.load(Ordering::Relaxed),
129 output_rows: self.output_rows.load(Ordering::Relaxed),
130 input_rows: self.input_rows.load(Ordering::Relaxed),
131 exhausted: self.exhausted.load(Ordering::Relaxed),
132 advance_count: self.advance_count.load(Ordering::Relaxed),
133 peak_memory: self.peak_memory_bytes.load(Ordering::Relaxed),
134 peak_memory_bytes: self.peak_memory_bytes.load(Ordering::Relaxed),
135 spilled_bytes: self.spilled_bytes.load(Ordering::Relaxed),
136 spill_count: self.spill_count.load(Ordering::Relaxed),
137 spilled_rows: self.spilled_rows.load(Ordering::Relaxed),
138 }
139 }
140}
141
142#[derive(Debug)]
152pub struct ProfileBoard {
153 entries: RwLock<HashMap<OperatorProfileKey, Arc<ProfileEntry>>>,
154 pub total_rows: AtomicU64,
155 pub total_time_us: AtomicU64,
156 start_time: Mutex<Option<Instant>>,
157 end_time: Mutex<Option<Instant>>,
158 pub parallel_wall_time_us: AtomicU64,
159 pub parallel_work_time_us: AtomicU64,
160 pub parallel_workers: AtomicUsize,
161 pub parallel_buffered_chunks_peak: AtomicUsize,
162 pub parallel_buffered_bytes_peak: AtomicUsize,
163}
164
165impl Default for ProfileBoard {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171impl ProfileBoard {
172 pub fn new() -> Self {
173 Self {
174 entries: RwLock::new(HashMap::new()),
175 total_rows: AtomicU64::new(0),
176 total_time_us: AtomicU64::new(0),
177 start_time: Mutex::new(None),
178 end_time: Mutex::new(None),
179 parallel_wall_time_us: AtomicU64::new(0),
180 parallel_work_time_us: AtomicU64::new(0),
181 parallel_workers: AtomicUsize::new(0),
182 parallel_buffered_chunks_peak: AtomicUsize::new(0),
183 parallel_buffered_bytes_peak: AtomicUsize::new(0),
184 }
185 }
186
187 pub fn record_start(&self) {
188 *self.start_time.lock() = Some(Instant::now());
189 }
190
191 pub fn record_end(&self) {
192 let elapsed = self
193 .start_time
194 .lock()
195 .map(|t| t.elapsed().as_micros() as u64)
196 .unwrap_or(0);
197 self.total_time_us.store(elapsed, Ordering::Relaxed);
198 *self.end_time.lock() = Some(Instant::now());
199 }
200
201 pub fn register_operator(&self, profile: &OperatorProfile) -> Arc<ProfileEntry> {
206 let key = OperatorProfileKey::new(profile.physical_operator_id, profile.partition_id);
207 let entry = Arc::new(ProfileEntry::new(profile));
208 self.entries.write().insert(key, entry.clone());
209 entry
210 }
211
212 pub fn get_entry(&self, key: &OperatorProfileKey) -> Option<Arc<ProfileEntry>> {
214 self.entries.read().get(key).cloned()
215 }
216
217 pub fn flush_to_collector(&self) -> ProfileCollector {
219 let mut collector = ProfileCollector::new();
220 let guard = self.entries.read();
221 for entry in guard.values() {
222 collector.operators.insert(
223 OperatorProfileKey::new(entry.physical_operator_id, entry.partition_id),
224 entry.snapshot(),
225 );
226 }
227 drop(guard);
228 collector.total_rows = self.total_rows.load(Ordering::Relaxed);
229 collector.total_time_us = self.total_time_us.load(Ordering::Relaxed);
230 collector.start_time = *self.start_time.lock();
231 collector.end_time = *self.end_time.lock();
232 collector.parallel_wall_time_us = self.parallel_wall_time_us.load(Ordering::Relaxed);
233 collector.parallel_work_time_us = self.parallel_work_time_us.load(Ordering::Relaxed);
234 collector.parallel_workers = self.parallel_workers.load(Ordering::Relaxed);
235 collector.parallel_buffered_chunks_peak =
236 self.parallel_buffered_chunks_peak.load(Ordering::Relaxed);
237 collector.parallel_buffered_bytes_peak =
238 self.parallel_buffered_bytes_peak.load(Ordering::Relaxed);
239 collector
240 }
241}
242
243#[derive(Debug, Default)]
245pub struct ProfileCollector {
246 pub operators: HashMap<OperatorProfileKey, OperatorProfile>,
247 pub total_rows: u64,
248 pub total_time_us: u64,
249 pub start_time: Option<Instant>,
250 pub end_time: Option<Instant>,
251 pub parallel_wall_time_us: u64,
253 pub parallel_work_time_us: u64,
255 pub parallel_workers: usize,
257 pub parallel_buffered_chunks_peak: usize,
259 pub parallel_buffered_bytes_peak: usize,
261}
262
263impl ProfileCollector {
264 pub fn new() -> Self {
265 Self::default()
266 }
267
268 pub fn record_start(&mut self) {
269 self.start_time = Some(Instant::now());
270 }
271
272 pub fn record_end(&mut self) {
273 self.end_time = Some(Instant::now());
274 if let Some(start) = self.start_time {
275 self.total_time_us = start.elapsed().as_micros() as u64;
276 }
277 }
278
279 pub fn add_rows(&mut self, count: u64) {
280 self.total_rows += count;
281 }
282
283 pub fn record_operator_profile(&mut self, profile: OperatorProfile) {
284 let key = OperatorProfileKey::new(profile.physical_operator_id, profile.partition_id);
285 self.operators.insert(key, profile);
286 }
287
288 pub fn parallel_profile(&self) -> (u64, u64, usize, usize, usize) {
291 (
292 self.parallel_wall_time_us,
293 self.parallel_work_time_us,
294 self.parallel_workers,
295 self.parallel_buffered_chunks_peak,
296 self.parallel_buffered_bytes_peak,
297 )
298 }
299
300 pub fn aggregate_partition_profiles(&mut self, partition_profiles: &[ProfileCollector]) {
305 for pp in partition_profiles {
306 for (key, op) in &pp.operators {
307 let entry = self
308 .operators
309 .entry(*key)
310 .or_insert_with(|| OperatorProfile {
311 physical_operator_id: key.physical_operator_id,
312 node_id: op.node_id,
313 partition_id: key.partition_id,
314 name: op.name.clone(),
315 exhausted: true,
318 ..OperatorProfile::default()
319 });
320 entry.open_time_us += op.open_time_us;
321 entry.next_time_us += op.next_time_us;
322 entry.close_time_us += op.close_time_us;
323 entry.output_rows += op.output_rows;
324 entry.input_rows += op.input_rows;
325 entry.exhausted = entry.exhausted && op.exhausted;
327 entry.advance_count += op.advance_count;
328 entry.peak_memory_bytes = entry.peak_memory_bytes.max(op.peak_memory_bytes);
329 entry.spill_count += op.spill_count;
330 entry.spilled_bytes += op.spilled_bytes;
331 entry.spilled_rows += op.spilled_rows;
332 }
333 self.total_rows += pp.total_rows;
334 }
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_profile_collector() {
344 let mut pc = ProfileCollector::new();
345 pc.record_start();
346 std::thread::sleep(std::time::Duration::from_micros(100));
347 pc.record_end();
348 assert!(pc.total_time_us > 0);
349 }
350
351 #[test]
352 fn test_partition_profile_aggregation_preserves_partition_identity() {
353 let mut first = ProfileCollector::new();
354 first.record_operator_profile(OperatorProfile {
355 physical_operator_id: PhysicalOperatorId(7),
356 node_id: 7,
357 partition_id: Some(0),
358 name: "ScanVertices".to_string(),
359 output_rows: 2,
360 peak_memory_bytes: 10,
361 ..OperatorProfile::default()
362 });
363 let mut second = ProfileCollector::new();
364 second.record_operator_profile(OperatorProfile {
365 physical_operator_id: PhysicalOperatorId(7),
366 node_id: 7,
367 partition_id: Some(1),
368 name: "ScanVertices".to_string(),
369 output_rows: 3,
370 peak_memory_bytes: 20,
371 ..OperatorProfile::default()
372 });
373
374 let mut aggregate = ProfileCollector::new();
375 aggregate.aggregate_partition_profiles(&[first, second]);
376
377 assert_eq!(aggregate.operators.len(), 2);
378 assert_eq!(
379 aggregate
380 .operators
381 .get(&OperatorProfileKey::new(PhysicalOperatorId(7), Some(0)))
382 .expect("partition zero profile")
383 .output_rows,
384 2
385 );
386 assert_eq!(
387 aggregate
388 .operators
389 .get(&OperatorProfileKey::new(PhysicalOperatorId(7), Some(1)))
390 .expect("partition one profile")
391 .peak_memory_bytes,
392 20
393 );
394 }
395
396 #[test]
397 fn test_partition_profile_aggregation_sums_spill_rows() {
398 let mut first = ProfileCollector::new();
399 first.record_operator_profile(OperatorProfile {
400 physical_operator_id: PhysicalOperatorId(9),
401 node_id: 9,
402 partition_id: Some(0),
403 name: "Sort".to_string(),
404 spilled_rows: 100,
405 spilled_bytes: 2048,
406 spill_count: 1,
407 ..OperatorProfile::default()
408 });
409 let mut second = ProfileCollector::new();
410 second.record_operator_profile(OperatorProfile {
411 physical_operator_id: PhysicalOperatorId(9),
412 node_id: 9,
413 partition_id: Some(1),
414 name: "Sort".to_string(),
415 spilled_rows: 50,
416 spilled_bytes: 1024,
417 spill_count: 1,
418 ..OperatorProfile::default()
419 });
420
421 let mut aggregate = ProfileCollector::new();
422 aggregate.aggregate_partition_profiles(&[first, second]);
423 let entry = aggregate
424 .operators
425 .get(&OperatorProfileKey::new(PhysicalOperatorId(9), Some(0)))
426 .expect("partition zero profile");
427 assert_eq!(entry.spilled_rows, 100);
428 let entry = aggregate
429 .operators
430 .get(&OperatorProfileKey::new(PhysicalOperatorId(9), Some(1)))
431 .expect("partition one profile");
432 assert_eq!(entry.spilled_rows, 50);
433 }
434
435 #[test]
436 fn test_partition_profile_aggregation_sums_input_and_ands_exhausted() {
437 let mut first = ProfileCollector::new();
438 first.record_operator_profile(OperatorProfile {
439 physical_operator_id: PhysicalOperatorId(11),
440 node_id: 11,
441 partition_id: Some(0),
442 name: "Filter".to_string(),
443 output_rows: 800,
444 input_rows: 1000,
445 exhausted: true,
446 ..OperatorProfile::default()
447 });
448 let mut second = ProfileCollector::new();
449 second.record_operator_profile(OperatorProfile {
450 physical_operator_id: PhysicalOperatorId(11),
451 node_id: 11,
452 partition_id: Some(1),
453 name: "Filter".to_string(),
454 output_rows: 0,
455 input_rows: 1000,
456 exhausted: false,
457 ..OperatorProfile::default()
458 });
459
460 let mut aggregate = ProfileCollector::new();
461 aggregate.aggregate_partition_profiles(&[first, second]);
462
463 assert_eq!(aggregate.operators.len(), 2);
465 let total_input: u64 = aggregate.operators.values().map(|op| op.input_rows).sum();
469 assert_eq!(total_input, 2000);
470 assert!(
471 aggregate.operators.values().any(|op| !op.exhausted),
472 "a starved partition must stay visible to the feedback fold"
473 );
474 }
475}