Skip to main content

query_runtime/metrics/
task.rs

1use std::{collections::BTreeMap, time::Duration};
2
3use cudf::ExecutionBudgetReport;
4use query_planning::correlation::{PlanNodeId, QueryId};
5
6use super::{CompiledTaskId, PlanTaskMapping, QueryMetrics};
7
8/// Metrics aggregated across tasks of the same operator kind.
9#[derive(Debug, Clone, Default, PartialEq, Eq)]
10pub struct TaskOperatorMetrics {
11    /// Query-wide diagnostic correlation id, when assigned.
12    pub query_id: Option<QueryId>,
13    /// Operator-kind label shared by the aggregated tasks.
14    pub operator_kind: String,
15    /// Number of tasks observed for this operator kind.
16    pub task_count: u64,
17    /// Cumulative elapsed time across tasks.
18    pub elapsed_time: Duration,
19    /// Largest lower-level observed allocator peak for this operator kind.
20    pub max_observed_peak_bytes: Option<u64>,
21    /// Largest lower-level retained byte count for this operator kind.
22    pub max_retained_bytes: Option<u64>,
23}
24
25impl TaskOperatorMetrics {
26    pub(super) fn merge_from(&mut self, other: Self) {
27        self.task_count = self.task_count.saturating_add(other.task_count);
28        self.elapsed_time += other.elapsed_time;
29        self.max_observed_peak_bytes = self.max_observed_peak_bytes.max(other.max_observed_peak_bytes);
30        self.max_retained_bytes = self.max_retained_bytes.max(other.max_retained_bytes);
31    }
32}
33
34/// Metrics for one compiled pipeline task.
35#[derive(Debug, Clone, Default, PartialEq, Eq)]
36pub struct TaskRuntimeMetrics {
37    /// Query-wide diagnostic correlation id, when assigned.
38    pub query_id: Option<QueryId>,
39    /// Stable compiled task id.
40    pub compiled_task_id: String,
41    /// Operator-kind label for the task.
42    pub operator_kind: String,
43    /// Cumulative elapsed task time.
44    pub elapsed_time: Duration,
45    /// Largest lower-level observed allocator peak for the task.
46    pub max_observed_peak_bytes: Option<u64>,
47    /// Largest lower-level retained byte count for the task.
48    pub max_retained_bytes: Option<u64>,
49}
50
51impl TaskRuntimeMetrics {
52    pub(super) fn merge_from(&mut self, other: Self) {
53        if self.operator_kind.is_empty() || self.operator_kind == "unknown" {
54            self.operator_kind = other.operator_kind;
55        }
56        self.elapsed_time += other.elapsed_time;
57        self.max_observed_peak_bytes = self.max_observed_peak_bytes.max(other.max_observed_peak_bytes);
58        self.max_retained_bytes = self.max_retained_bytes.max(other.max_retained_bytes);
59    }
60}
61
62/// Successful lower-level execution-budget report captured as a metrics row.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct BudgetedExecutionEvent {
65    /// Query-wide diagnostic correlation id, when assigned.
66    pub(crate) query_id: Option<QueryId>,
67    /// Native operation label that produced the report.
68    pub operation: String,
69    /// Lower-level budget boundary label.
70    pub boundary: String,
71    /// Bytes requested for this execution stage, which can differ from the member limit.
72    pub budget_bytes: u64,
73    /// Lower-level budget enforcement label.
74    pub enforcement: String,
75    /// Lower-level budget status label; always `reservation_enforced`,
76    /// because a budget report exists only for a completed operation.
77    pub status: String,
78    /// Allocator high-water bytes reported by lower layers.
79    pub allocator_high_water_bytes: Option<u64>,
80    /// Retained bytes reported by lower layers.
81    pub retained_bytes: Option<u64>,
82    /// Output bytes reported by lower layers.
83    pub output_bytes: Option<u64>,
84    /// Output rows reported by lower layers.
85    pub output_rows: Option<u64>,
86    /// Additional lower-level context facts.
87    pub context_facts: BTreeMap<String, String>,
88}
89
90impl BudgetedExecutionEvent {
91    fn new(operation: impl Into<String>, report: &ExecutionBudgetReport) -> Self {
92        Self {
93            query_id: None,
94            operation: operation.into(),
95            boundary: report.request().boundary().to_string(),
96            budget_bytes: report.request().budget_bytes() as u64,
97            enforcement: report.enforcement().as_str().to_string(),
98            status: "reservation_enforced".to_string(),
99            allocator_high_water_bytes: report.allocator_high_water_bytes().map(|bytes| bytes as u64),
100            retained_bytes: report.retained_bytes().map(|bytes| bytes as u64),
101            output_bytes: report.output_bytes().map(|bytes| bytes as u64),
102            output_rows: report.output_rows().map(|rows| rows as u64),
103            context_facts: report.context_facts().clone(),
104        }
105    }
106}
107
108impl QueryMetrics {
109    /// Records a lower-level cuDF execution-budget report.
110    pub fn record_budgeted_execution(&mut self, operation: impl Into<String>, report: &ExecutionBudgetReport) {
111        let observed_peak_bytes = report.allocator_high_water_bytes().map(|bytes| bytes as u64);
112        let retained_bytes = report.retained_bytes().map(|bytes| bytes as u64);
113        self.max_task_observed_peak_bytes = self.max_task_observed_peak_bytes.max(observed_peak_bytes);
114        self.max_task_retained_bytes = self.max_task_retained_bytes.max(retained_bytes);
115        let mut event = BudgetedExecutionEvent::new(operation, report);
116        event.query_id = self.inherited_query_id();
117        self.budgeted_execution_events.push(event);
118    }
119
120    /// Records execution of a compiled pipeline task.
121    pub fn record_compiled_pipeline_task_execution(
122        &mut self,
123        compiled_task_id: impl ToString,
124        operator_kind: impl Into<String>,
125    ) {
126        self.record_task_execution(compiled_task_id, operator_kind);
127    }
128
129    pub(crate) fn record_task_execution(&mut self, task_id: impl ToString, operator_kind: impl Into<String>) {
130        self.register_task_definition(task_id, operator_kind);
131    }
132
133    /// Registers one explicit task definition, incrementing operator task count
134    /// only when the task id is first observed.
135    pub(crate) fn register_task_definition(&mut self, task_id: impl ToString, operator_kind: impl Into<String>) {
136        let compiled_task_id = task_id.to_string();
137        let operator_kind = operator_kind.into();
138        let query_id = self.inherited_query_id();
139        let first_definition =
140            self.task_kind_by_compiled_task_id.insert(compiled_task_id.clone(), operator_kind.clone()).is_none();
141        let task = self.task_runtime_metrics.entry(compiled_task_id.clone()).or_insert_with(|| TaskRuntimeMetrics {
142            query_id: query_id.clone(),
143            compiled_task_id,
144            operator_kind: operator_kind.clone(),
145            ..Default::default()
146        });
147        task.operator_kind = operator_kind.clone();
148        let row = self.task_operator_metrics.entry(operator_kind.clone()).or_insert_with(|| TaskOperatorMetrics {
149            query_id: query_id.clone(),
150            operator_kind,
151            ..Default::default()
152        });
153        if first_definition {
154            row.task_count = row.task_count.saturating_add(1);
155        }
156    }
157
158    /// Records a diagnostic mapping between a compiled task and its source plan node.
159    pub fn record_compiled_pipeline_task_mapping(
160        &mut self,
161        compiled_task_id: impl ToString,
162        operator_kind: impl Into<String>,
163        plan_node_id: Option<impl Into<PlanNodeId>>,
164    ) {
165        self.plan_task_mapping_rows.push(PlanTaskMapping {
166            query_id: self.query_id.clone(),
167            plan_node_id: plan_node_id.map(Into::into),
168            compiled_task_id: Some(CompiledTaskId::new(compiled_task_id)),
169            operator_kind: Some(operator_kind.into()),
170            datafusion_shape: self.plan_task_mapping_datafusion_shape.clone(),
171        });
172    }
173
174    /// Adds elapsed time to a task and its operator-kind aggregate.
175    pub fn record_task_elapsed(&mut self, task_id: impl ToString, operator_kind: impl Into<String>, elapsed: Duration) {
176        let compiled_task_id = task_id.to_string();
177        let operator_kind = operator_kind.into();
178        let query_id = self.inherited_query_id();
179        self.task_kind_by_compiled_task_id.insert(compiled_task_id.clone(), operator_kind.clone());
180        let row = self.task_runtime_metrics.entry(compiled_task_id.clone()).or_insert_with(|| TaskRuntimeMetrics {
181            query_id: query_id.clone(),
182            compiled_task_id,
183            operator_kind: operator_kind.clone(),
184            ..Default::default()
185        });
186        row.operator_kind = operator_kind.clone();
187        row.elapsed_time += elapsed;
188
189        let operator = self.task_operator_metrics.entry(operator_kind.clone()).or_insert_with(|| TaskOperatorMetrics {
190            query_id: query_id.clone(),
191            operator_kind,
192            ..Default::default()
193        });
194        operator.elapsed_time += elapsed;
195    }
196
197    pub(crate) fn record_task_budget_observation(
198        &mut self,
199        compiled_task_id: &str,
200        observed_peak_bytes: Option<u64>,
201        retained_bytes: Option<u64>,
202    ) {
203        let operator_kind =
204            self.task_kind_by_compiled_task_id.get(compiled_task_id).cloned().unwrap_or_else(|| "unknown".to_string());
205        let query_id = self.inherited_query_id();
206        let row = self.task_runtime_metrics.entry(compiled_task_id.to_string()).or_insert_with(|| TaskRuntimeMetrics {
207            query_id: query_id.clone(),
208            compiled_task_id: compiled_task_id.to_string(),
209            operator_kind: operator_kind.clone(),
210            ..Default::default()
211        });
212        row.operator_kind = operator_kind.clone();
213        row.max_observed_peak_bytes = row.max_observed_peak_bytes.max(observed_peak_bytes);
214        row.max_retained_bytes = row.max_retained_bytes.max(retained_bytes);
215
216        let operator = self.task_operator_metrics.entry(operator_kind.clone()).or_insert_with(|| TaskOperatorMetrics {
217            query_id: query_id.clone(),
218            operator_kind,
219            ..Default::default()
220        });
221        operator.max_observed_peak_bytes = operator.max_observed_peak_bytes.max(observed_peak_bytes);
222        operator.max_retained_bytes = operator.max_retained_bytes.max(retained_bytes);
223    }
224
225    /// Builds the query-local mapping surface between plan node ids and compiled task ids.
226    pub fn plan_task_mappings(&self) -> Vec<PlanTaskMapping> {
227        let mut mappings = BTreeMap::<(Option<String>, Option<String>, Option<String>), PlanTaskMapping>::new();
228        for mapping in &self.plan_task_mapping_rows {
229            let mut mapping = mapping.clone();
230            if mapping.datafusion_shape.is_none() {
231                mapping.datafusion_shape.clone_from(&self.plan_task_mapping_datafusion_shape);
232            }
233            let key = (
234                mapping.plan_node_id.as_ref().map(|value| value.as_str().to_string()),
235                mapping.compiled_task_id.as_ref().map(|value| value.as_str().to_string()),
236                mapping.operator_kind.clone(),
237            );
238            mappings.entry(key).or_insert(mapping);
239        }
240        for (compiled_task_id, runtime) in &self.task_runtime_metrics {
241            let key = (None, Some(compiled_task_id.clone()), Some(runtime.operator_kind.clone()));
242            mappings.entry(key).or_insert_with(|| PlanTaskMapping {
243                query_id: runtime.query_id.clone().unwrap_or_else(|| self.query_id.clone()),
244                plan_node_id: None,
245                compiled_task_id: Some(compiled_task_id.as_str().into()),
246                operator_kind: Some(runtime.operator_kind.clone()),
247                datafusion_shape: self.plan_task_mapping_datafusion_shape.clone(),
248            });
249        }
250        for operator in &self.operators {
251            let key = (Some(operator.node_id.clone()), None, Some(operator.operator_kind.clone()));
252            mappings.entry(key).or_insert_with(|| PlanTaskMapping {
253                query_id: operator.query_id.clone().unwrap_or_else(|| self.query_id.clone()),
254                plan_node_id: Some(PlanNodeId::new(operator.node_id.clone())),
255                compiled_task_id: None,
256                operator_kind: Some(operator.operator_kind.clone()),
257                datafusion_shape: self.plan_task_mapping_datafusion_shape.clone(),
258            });
259        }
260        mappings.into_values().collect()
261    }
262
263    /// Records one retry caused by resource exhaustion.
264    pub fn record_retry_resource_exhausted(&mut self) {
265        self.retry_count = self.retry_count.saturating_add(1);
266        self.retry_resource_exhausted_count = self.retry_resource_exhausted_count.saturating_add(1);
267    }
268
269    /// Records one compiled-pipeline execution attempt.
270    pub fn record_compiled_pipeline_attempt(&mut self) {
271        self.compiled_pipeline_attempts = self.compiled_pipeline_attempts.saturating_add(1);
272    }
273
274    /// Adds completed compiled-pipeline task count.
275    pub fn record_compiled_pipeline_tasks_executed(&mut self, tasks: u64) {
276        self.compiled_pipeline_tasks_executed = self.compiled_pipeline_tasks_executed.saturating_add(tasks);
277    }
278
279    /// Records one compiled-pipeline Host Arrow sink.
280    pub fn record_compiled_pipeline_host_arrow_sink(&mut self) {
281        self.compiled_pipeline_host_arrow_sinks = self.compiled_pipeline_host_arrow_sinks.saturating_add(1);
282    }
283}