1use std::{collections::BTreeMap, time::Duration};
2
3use cudf::ExecutionBudgetReport;
4use query_planning::correlation::{PlanNodeId, QueryId};
5
6use super::{CompiledTaskId, PlanTaskMapping, QueryMetrics};
7
8#[derive(Debug, Clone, Default, PartialEq, Eq)]
10pub struct TaskOperatorMetrics {
11 pub query_id: Option<QueryId>,
13 pub operator_kind: String,
15 pub task_count: u64,
17 pub elapsed_time: Duration,
19 pub max_observed_peak_bytes: Option<u64>,
21 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
36pub struct TaskRuntimeMetrics {
37 pub query_id: Option<QueryId>,
39 pub compiled_task_id: String,
41 pub operator_kind: String,
43 pub elapsed_time: Duration,
45 pub max_observed_peak_bytes: Option<u64>,
47 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#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct BudgetedExecutionEvent {
65 pub(crate) query_id: Option<QueryId>,
67 pub operation: String,
69 pub boundary: String,
71 pub budget_bytes: u64,
73 pub enforcement: String,
75 pub status: String,
78 pub allocator_high_water_bytes: Option<u64>,
80 pub retained_bytes: Option<u64>,
82 pub output_bytes: Option<u64>,
84 pub output_rows: Option<u64>,
86 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 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 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 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 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 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 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 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 pub fn record_compiled_pipeline_attempt(&mut self) {
271 self.compiled_pipeline_attempts = self.compiled_pipeline_attempts.saturating_add(1);
272 }
273
274 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 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}