1use std::{collections::BTreeMap, time::Duration};
10
11use cudf::ExecutionBudgetReport;
12use query_planning::{
13 correlation::{PlanNodeId, QueryId, TraceId},
14 dynamic_filter::{DynamicFilter, DynamicFilterRoute},
15 plan::{GraphAlgorithm, NodeId},
16};
17
18use super::{
19 BudgetWindow, BudgetWindowPeaks, PlanExecutionId, QueryMetricsCheckpoint, RuntimeObservationConfig,
20 RuntimeObservationHandle, RuntimeObservationSession, SourceReadFact,
21};
22use crate::{
23 metrics::{
24 BoundaryMetrics, DynamicFilterSourceSnapshot, GraphAlgorithmRuntimeMetrics, OperatorMetrics, QueryMetrics,
25 SourceReadRuntimeMetrics,
26 },
27 observability::graph_observation::GraphMeasuredPeakProvenance,
28};
29
30#[derive(Debug, Clone)]
32pub struct MetricsRecorder {
33 observation: RuntimeObservationHandle,
34 plan_execution_id: PlanExecutionId,
35}
36
37impl MetricsRecorder {
38 pub fn new(observation: RuntimeObservationHandle, plan_execution_id: PlanExecutionId) -> Self {
40 Self { observation, plan_execution_id }
41 }
42
43 pub fn bind(
50 observation: &RuntimeObservationHandle,
51 plan_execution_id: PlanExecutionId,
52 query_id: &str,
53 trace_id: Option<&str>,
54 ) -> Self {
55 let observation = if observation.has_owner() {
56 observation.clone()
57 } else {
58 RuntimeObservationSession::new(
59 QueryId::new(query_id),
60 trace_id.map(TraceId::new),
61 RuntimeObservationConfig::native_execution(),
62 )
63 .handle()
64 };
65 Self::new(observation, plan_execution_id)
66 }
67
68 pub const fn plan_execution_id(&self) -> &PlanExecutionId {
70 &self.plan_execution_id
71 }
72
73 pub fn checkpoint(&self) -> Option<QueryMetricsCheckpoint> {
75 self.observation.metrics_checkpoint(&self.plan_execution_id)
76 }
77
78 pub fn freeze(&self) -> Option<QueryMetricsCheckpoint> {
82 self.observation.freeze_plan_metrics(&self.plan_execution_id)
83 }
84
85 pub fn progress(&self) -> ExecutionProgress {
88 self.observation.execution_progress(&self.plan_execution_id)
89 }
90
91 pub fn boundary(&self) -> BoundaryMetrics {
94 self.observation.boundary_metrics(&self.plan_execution_id)
95 }
96
97 fn apply(&self, reduce: impl FnOnce(&mut QueryMetrics)) {
98 self.observation.apply_metrics(&self.plan_execution_id, reduce);
99 }
100
101 fn source(&self, fact: SourceReadFact) {
102 self.observation.record_source_read(&self.plan_execution_id, fact);
103 }
104
105 pub fn record_source_read(&self, fact: SourceReadFact) {
109 self.source(fact);
110 }
111
112 pub fn record_source_read_runtime(&self, row: SourceReadRuntimeMetrics) {
114 self.observation.record_source_read_row(&self.plan_execution_id, row);
115 }
116
117 pub fn record_input_batch(&self, rows: u64) {
119 self.source(SourceReadFact::InputBatch { rows });
120 }
121
122 pub fn record_parquet_source(&self, known_bytes: Option<u64>, selected_row_groups: u64, read_columns: u64) {
124 self.source(SourceReadFact::ParquetSource {
125 files: 1,
126 known_bytes: known_bytes.unwrap_or(0),
127 selected_row_groups,
128 read_columns,
129 });
130 }
131
132 pub fn record_source_chunk_planned(&self, row_groups: u64) {
134 self.source(SourceReadFact::ChunkPlanned { row_groups });
135 }
136
137 pub fn record_source_chunk_attempt(&self) {
139 self.source(SourceReadFact::ChunkAttempted);
140 }
141
142 pub fn record_source_chunk_commit(&self, row_groups: u64) {
144 self.source(SourceReadFact::ChunkCommitted { chunk_count: 1, row_groups });
145 }
146
147 pub fn record_source_chunk_commit_count(&self, chunk_count: u64, row_groups: u64) {
149 self.source(SourceReadFact::ChunkCommitted { chunk_count, row_groups });
150 }
151
152 pub fn record_source_chunk_retry(&self) {
154 self.source(SourceReadFact::ChunkRetried);
155 }
156
157 pub fn record_source_read_prefetch_started(&self) {
159 self.source(SourceReadFact::PrefetchStarted);
160 }
161
162 pub fn record_source_read_prefetch_completed(&self) {
164 self.source(SourceReadFact::PrefetchCompleted);
165 }
166
167 pub fn record_source_read_prefetch_skipped(&self) {
169 self.source(SourceReadFact::PrefetchSkipped);
170 }
171
172 pub fn record_source_read_prefetch_abandoned(&self) {
174 self.source(SourceReadFact::PrefetchAbandoned);
175 }
176
177 pub fn add_source_read_time(&self, elapsed: Duration) {
179 self.source(SourceReadFact::SourceReadTime(elapsed));
180 }
181
182 pub fn add_operator_time(&self, elapsed: Duration) {
186 self.apply(|metrics| saturating_add_duration(&mut metrics.operator_time, elapsed));
187 }
188
189 pub fn add_chunk_operator_time(&self, elapsed: Duration) {
191 self.apply(|metrics| saturating_add_duration(&mut metrics.chunk_operator_time, elapsed));
192 }
193
194 pub fn add_final_merge_time(&self, elapsed: Duration) {
196 self.apply(|metrics| saturating_add_duration(&mut metrics.final_merge_time, elapsed));
197 }
198
199 pub fn add_final_sort_time(&self, elapsed: Duration) {
201 self.apply(|metrics| saturating_add_duration(&mut metrics.final_sort_time, elapsed));
202 }
203
204 pub fn add_materialization_time(&self, elapsed: Duration) {
206 self.apply(|metrics| saturating_add_duration(&mut metrics.materialization_time, elapsed));
207 }
208
209 pub fn record_gpu_resident_input_handoff(&self) {
213 self.apply(|metrics| metrics.boundary.record_gpu_resident_input_handoff());
214 }
215
216 pub fn record_host_to_device_upload(&self, rows: u64, bytes: u64) {
218 self.apply(|metrics| metrics.boundary.record_host_to_device_upload(rows, bytes));
219 }
220
221 pub fn record_device_to_host_materialization(&self, rows: u64, bytes: u64) {
223 self.apply(|metrics| metrics.boundary.record_device_to_host_materialization(rows, bytes));
224 }
225
226 pub fn record_device_memory_observation(&self, bytes: u64) {
230 self.apply(|metrics| metrics.record_device_memory_observation(bytes));
231 }
232
233 pub fn record_allocator_high_water_bytes(&self, bytes: u64) {
235 self.apply(|metrics| metrics.record_allocator_high_water_bytes(bytes));
236 }
237
238 pub fn record_operator_metrics(&self, row: OperatorMetrics) {
240 self.apply(|metrics| metrics.record_operator_metrics(row));
241 }
242
243 pub fn record_aggregate_strategy_decision(&self, node_id: impl ToString, facts: &BTreeMap<&'static str, String>) {
247 let node_id = node_id.to_string();
248 let facts = facts.clone();
249 self.apply(|metrics| metrics.record_aggregate_strategy_decision(node_id, &facts));
250 }
251
252 pub fn record_partial_aggregate(&self, input_rows: u64, output_rows: u64, retained_bytes: u64) {
254 self.apply(|metrics| metrics.record_partial_aggregate(input_rows, output_rows, retained_bytes));
255 }
256
257 pub fn record_final_merge_attempt(&self, merge_fan_in: usize) {
259 self.apply(|metrics| metrics.record_final_merge_attempt(merge_fan_in));
260 }
261
262 pub fn record_final_merge_level(&self) {
264 self.apply(QueryMetrics::record_final_merge_level);
265 }
266
267 pub fn record_join_strategy_decision(&self, node_id: impl ToString, facts: &BTreeMap<&'static str, String>) {
271 let node_id = node_id.to_string();
272 let facts = facts.clone();
273 self.apply(|metrics| metrics.record_join_strategy_decision(node_id, &facts));
274 }
275
276 pub fn record_hash_join_build(&self) {
278 self.apply(QueryMetrics::record_hash_join_build);
279 }
280
281 pub fn record_filtered_join_build(&self) {
283 self.apply(QueryMetrics::record_filtered_join_build);
284 }
285
286 pub fn record_one_shot_join(&self) {
288 self.apply(QueryMetrics::record_one_shot_join);
289 }
290
291 pub fn record_hash_join_hash_table_bytes(&self, bytes: u64) {
293 self.apply(|metrics| metrics.record_hash_join_hash_table_bytes(bytes));
294 }
295
296 pub fn record_filtered_join_hash_table_bytes(&self, bytes: u64) {
298 self.apply(|metrics| metrics.record_filtered_join_hash_table_bytes(bytes));
299 }
300
301 pub fn record_filtered_join_probe_state_bytes(&self, bytes: u64) {
303 self.apply(|metrics| metrics.record_filtered_join_probe_state_bytes(bytes));
304 }
305
306 pub fn record_mixed_join_size_data_bytes(&self, bytes: u64) {
308 self.apply(|metrics| metrics.record_mixed_join_size_data_bytes(bytes));
309 }
310
311 pub fn record_one_shot_join_index_bytes(&self, bytes: u64) {
313 self.apply(|metrics| metrics.record_one_shot_join_index_bytes(bytes));
314 }
315
316 pub fn record_one_shot_join_materialization_bytes(&self, bytes: u64) {
318 self.apply(|metrics| metrics.record_one_shot_join_materialization_bytes(bytes));
319 }
320
321 pub fn record_one_shot_join_output_bytes(&self, bytes: u64) {
323 self.apply(|metrics| metrics.record_one_shot_join_output_bytes(bytes));
324 }
325
326 pub fn record_dynamic_filter_produced(
330 &self,
331 route: &DynamicFilterRoute,
332 filter: &DynamicFilter,
333 producer_rows: u64,
334 ) {
335 self.apply(|metrics| metrics.record_dynamic_filter_produced(route, filter, producer_rows));
336 }
337
338 pub fn record_dynamic_filter_skipped(&self, route: &DynamicFilterRoute, reason: impl Into<String>) {
340 let reason = reason.into();
341 self.apply(|metrics| metrics.record_dynamic_filter_skipped(route, reason));
342 }
343
344 pub fn record_dynamic_filter_applied(
346 &self,
347 route: &DynamicFilterRoute,
348 filter: &DynamicFilter,
349 before: DynamicFilterSourceSnapshot,
350 after: DynamicFilterSourceSnapshot,
351 ) {
352 self.apply(|metrics| metrics.record_dynamic_filter_applied(route, filter, before, after));
353 }
354
355 pub fn record_dynamic_filter_applied_post_decode(
357 &self,
358 route: &DynamicFilterRoute,
359 filter: &DynamicFilter,
360 rows_before: Option<u64>,
361 rows_after: Option<u64>,
362 ) {
363 self.apply(|metrics| metrics.record_dynamic_filter_applied_post_decode(route, filter, rows_before, rows_after));
364 }
365
366 pub fn record_dynamic_filter_source_post_decode_stage(
368 &self,
369 route: &DynamicFilterRoute,
370 filter: &DynamicFilter,
371 rows_before: Option<u64>,
372 rows_after: Option<u64>,
373 ) {
374 self.apply(|metrics| {
375 metrics.record_dynamic_filter_source_post_decode_stage(route, filter, rows_before, rows_after);
376 });
377 }
378
379 pub fn record_dynamic_filter_post_decode_rows(
381 &self,
382 route: &DynamicFilterRoute,
383 filter: &DynamicFilter,
384 rows_before: Option<u64>,
385 rows_after: Option<u64>,
386 ) {
387 self.apply(|metrics| metrics.record_dynamic_filter_post_decode_rows(route, filter, rows_before, rows_after));
388 }
389
390 pub fn record_dynamic_filter_source_reader_predicate_rows(
392 &self,
393 route: &DynamicFilterRoute,
394 filter: &DynamicFilter,
395 source_file_index: usize,
396 source_chunk_index: usize,
397 rows_before: Option<u64>,
398 rows_after: Option<u64>,
399 ) {
400 self.apply(|metrics| {
401 metrics.record_dynamic_filter_source_reader_predicate_rows(
402 route,
403 filter,
404 source_file_index,
405 source_chunk_index,
406 rows_before,
407 rows_after,
408 );
409 });
410 }
411
412 pub fn record_dynamic_filter_source_reader_predicate_batch_rows(
414 &self,
415 route: &DynamicFilterRoute,
416 filter: &DynamicFilter,
417 rows_before: Option<u64>,
418 rows_after: Option<u64>,
419 ) {
420 self.apply(|metrics| {
421 metrics.record_dynamic_filter_source_reader_predicate_batch_rows(route, filter, rows_before, rows_after);
422 });
423 }
424
425 pub fn record_dynamic_filter_reader_predicate_source_reduction(
427 &self,
428 rows_before: Option<u64>,
429 rows_after: Option<u64>,
430 ) {
431 self.apply(|metrics| metrics.record_dynamic_filter_reader_predicate_source_reduction(rows_before, rows_after));
432 }
433
434 pub fn record_graph_algorithm_runtime(&self, row: GraphAlgorithmRuntimeMetrics) {
438 self.apply(|metrics| metrics.record_graph_algorithm_runtime(row));
439 }
440
441 pub fn record_graph_claim_calibration(&self, node_id: NodeId, algorithm: GraphAlgorithm) {
443 self.apply(|metrics| metrics.record_graph_claim_calibration(node_id, algorithm));
444 }
445
446 pub fn record_graph_claim_calibration_measurement(
448 &self,
449 node_id: NodeId,
450 algorithm: GraphAlgorithm,
451 measured_peak_bytes: Option<u64>,
452 measured_peak_provenance: GraphMeasuredPeakProvenance,
453 ) {
454 self.apply(|metrics| {
455 metrics.record_graph_claim_calibration_measurement(
456 node_id,
457 algorithm,
458 measured_peak_bytes,
459 measured_peak_provenance,
460 );
461 });
462 }
463
464 pub fn record_graph_claim_calibration_algorithm_started(&self, node_id: NodeId, algorithm: GraphAlgorithm) {
466 self.apply(|metrics| metrics.record_graph_claim_calibration_algorithm_started(node_id, algorithm));
467 }
468
469 pub fn record_budgeted_execution(&self, operation: impl Into<String>, report: &ExecutionBudgetReport) {
473 let operation = operation.into();
474 self.apply(|metrics| metrics.record_budgeted_execution(operation, report));
475 }
476
477 pub fn open_budget_window(&self) -> BudgetWindow {
479 self.observation.open_budget_window(&self.plan_execution_id)
480 }
481
482 pub fn take_budget_window(&self, window: BudgetWindow) -> BudgetWindowPeaks {
485 self.observation.take_budget_window(&self.plan_execution_id, window)
486 }
487
488 pub fn close_budget_window(&self, compiled_task_id: impl ToString, window: BudgetWindow) {
490 self.observation.close_budget_window(&self.plan_execution_id, window, &compiled_task_id.to_string());
491 }
492
493 pub fn record_compiled_pipeline_task_execution(
495 &self,
496 compiled_task_id: impl ToString,
497 operator_kind: impl Into<String>,
498 ) {
499 let compiled_task_id = compiled_task_id.to_string();
500 let operator_kind = operator_kind.into();
501 self.apply(|metrics| metrics.record_compiled_pipeline_task_execution(compiled_task_id, operator_kind));
502 }
503
504 pub fn record_compiled_pipeline_task_mapping(
506 &self,
507 compiled_task_id: impl ToString,
508 operator_kind: impl Into<String>,
509 plan_node_id: Option<impl Into<PlanNodeId>>,
510 ) {
511 let compiled_task_id = compiled_task_id.to_string();
512 let operator_kind = operator_kind.into();
513 let plan_node_id = plan_node_id.map(Into::into);
514 self.apply(|metrics| {
515 metrics.record_compiled_pipeline_task_mapping(compiled_task_id, operator_kind, plan_node_id);
516 });
517 }
518
519 pub fn record_task_elapsed(&self, task_id: impl ToString, operator_kind: impl Into<String>, elapsed: Duration) {
521 let task_id = task_id.to_string();
522 let operator_kind = operator_kind.into();
523 self.apply(|metrics| metrics.record_task_elapsed(task_id, operator_kind, elapsed));
524 }
525
526 pub fn record_retry_resource_exhausted(&self) {
528 self.apply(QueryMetrics::record_retry_resource_exhausted);
529 }
530
531 pub fn record_compiled_pipeline_attempt(&self) {
533 self.apply(QueryMetrics::record_compiled_pipeline_attempt);
534 }
535
536 pub fn record_compiled_pipeline_tasks_executed(&self, tasks: u64) {
538 self.apply(|metrics| metrics.record_compiled_pipeline_tasks_executed(tasks));
539 }
540
541 pub fn record_compiled_pipeline_host_arrow_sink(&self) {
543 self.apply(QueryMetrics::record_compiled_pipeline_host_arrow_sink);
544 }
545}
546
547#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
549pub struct ExecutionProgress {
550 pub source_chunks_planned: u64,
552 pub source_chunks_committed: u64,
554 pub partial_aggregate_count: u64,
556}
557
558fn saturating_add_duration(total: &mut Duration, value: Duration) {
559 *total = total.checked_add(value).unwrap_or(Duration::MAX);
560}