1use std::time::Duration;
2
3use query_planning::correlation::QueryId;
4
5use super::{MetricsProjection, QueryMetrics, optional_u64};
6use crate::observability::QueryMetricsCheckpoint;
7
8#[derive(Debug, Clone, Default, PartialEq, Eq)]
10pub struct SourceReadRuntimeMetrics {
11 pub query_id: Option<QueryId>,
13 pub source_node_id: String,
15 pub read_kind: String,
17 pub read_strategy: String,
19 pub parquet_read_evidence: SourceReadParquetEvidenceMetrics,
21 pub batch_index: u64,
23 pub chunk_count: u64,
25 pub first_chunk_index: u64,
27 pub last_chunk_index: u64,
29 pub first_file_index: u64,
31 pub last_file_index: u64,
33 pub file_count: u64,
35 pub row_group_count: u64,
37 pub read_columns_per_file: u64,
39 pub read_columns_total: u64,
41 pub known_file_bytes: u64,
43 pub estimated_source_read_bytes: u64,
45 pub known_row_group_bytes: u64,
47 pub known_projected_compressed_bytes: u64,
49 pub known_projected_uncompressed_bytes: u64,
51 pub known_pure_filter_compressed_bytes: u64,
53 pub known_pure_filter_uncompressed_bytes: u64,
55 pub output_rows: u64,
57 pub reader_predicate: bool,
59 pub reader_dynamic_filter_count: u64,
61 pub source_batching_reason: String,
63 pub source_chunk_planning_mode: String,
65 pub source_chunk_planning_reason: String,
67 pub elapsed: Duration,
69}
70
71impl SourceReadRuntimeMetrics {
72 pub fn to_stable_kv_row(&self) -> String {
74 format!(
75 "source_node_id={},read_kind={},read_strategy={},batch_index={},chunk_count={},first_chunk_index={},last_chunk_index={},first_file_index={},last_file_index={},file_count={},row_group_count={},read_columns_per_file={},read_columns_total={},known_file_bytes={},estimated_source_read_bytes={},known_row_group_bytes={},known_projected_compressed_bytes={},known_projected_uncompressed_bytes={},known_pure_filter_compressed_bytes={},known_pure_filter_uncompressed_bytes={},output_rows={},reader_predicate={},reader_dynamic_filter_count={},source_batching_reason={},source_chunk_planning_mode={},source_chunk_planning_reason={},elapsed_ns={},parquet_read_input_kind={},remote_read_mode={},parquet_read_backend={},remote_backend_build_status={},remote_read_bytes={},remote_read_range_count={},device_read_support={},parquet_pruning_input_row_groups={},parquet_row_groups_after_stats_filter={},parquet_row_groups_after_bloom_filter={},parquet_bloom_pruning_status={}",
76 self.source_node_id,
77 self.read_kind,
78 self.read_strategy,
79 self.batch_index,
80 self.chunk_count,
81 self.first_chunk_index,
82 self.last_chunk_index,
83 self.first_file_index,
84 self.last_file_index,
85 self.file_count,
86 self.row_group_count,
87 self.read_columns_per_file,
88 self.read_columns_total,
89 self.known_file_bytes,
90 self.estimated_source_read_bytes,
91 self.known_row_group_bytes,
92 self.known_projected_compressed_bytes,
93 self.known_projected_uncompressed_bytes,
94 self.known_pure_filter_compressed_bytes,
95 self.known_pure_filter_uncompressed_bytes,
96 self.output_rows,
97 self.reader_predicate,
98 self.reader_dynamic_filter_count,
99 self.source_batching_reason,
100 self.source_chunk_planning_mode,
101 self.source_chunk_planning_reason,
102 self.elapsed.as_nanos().min(u128::from(u64::MAX)),
103 self.parquet_read_evidence.input_kind,
104 self.parquet_read_evidence.remote_read_mode,
105 self.parquet_read_evidence.backend,
106 self.parquet_read_evidence.backend_build_status,
107 optional_u64(self.parquet_read_evidence.remote_bytes),
108 optional_u64(self.parquet_read_evidence.remote_range_count),
109 self.parquet_read_evidence.device_read_support,
110 optional_u64(self.parquet_read_evidence.pruning_input_row_groups),
111 optional_u64(self.parquet_read_evidence.row_groups_after_stats_filter),
112 optional_u64(self.parquet_read_evidence.row_groups_after_bloom_filter),
113 self.parquet_read_evidence.bloom_pruning_status,
114 )
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct SourceReadParquetEvidenceMetrics {
121 pub input_kind: String,
123 pub remote_read_mode: String,
125 pub backend: String,
127 pub backend_build_status: String,
129 pub remote_bytes: Option<u64>,
131 pub remote_range_count: Option<u64>,
133 pub device_read_support: String,
135 pub pruning_input_row_groups: Option<u64>,
137 pub row_groups_after_stats_filter: Option<u64>,
139 pub row_groups_after_bloom_filter: Option<u64>,
141 pub bloom_pruning_status: String,
143}
144
145impl Default for SourceReadParquetEvidenceMetrics {
146 fn default() -> Self {
147 Self {
148 input_kind: "none".to_string(),
149 remote_read_mode: "none".to_string(),
150 backend: "none".to_string(),
151 backend_build_status: "none".to_string(),
152 remote_bytes: None,
153 remote_range_count: None,
154 device_read_support: "none".to_string(),
155 pruning_input_row_groups: None,
156 row_groups_after_stats_filter: None,
157 row_groups_after_bloom_filter: None,
158 bloom_pruning_status: "not_reported".to_string(),
159 }
160 }
161}
162
163impl QueryMetrics {
164 pub fn apply_owner_checkpoint(&mut self, checkpoint: &QueryMetricsCheckpoint) {
171 let query_id = self.query_id.clone();
172 let trace_id = self.trace_id.clone();
173 let plan_task_mapping_datafusion_shape = self.plan_task_mapping_datafusion_shape.clone();
174 *self = checkpoint.metrics.clone();
175 self.query_id = query_id;
176 self.trace_id = trace_id;
177 self.plan_task_mapping_datafusion_shape = plan_task_mapping_datafusion_shape;
178 self.apply_source_read_checkpoint(checkpoint);
179 self.stamp_existing_rows_with_query_id();
180 }
181
182 pub(crate) fn apply_source_read_checkpoint(&mut self, checkpoint: &QueryMetricsCheckpoint) {
189 let totals = &checkpoint.summary.source_read;
190 self.input_batches = totals.input_batches;
191 self.input_rows = totals.input_rows;
192 self.parquet_files = totals.parquet_files;
193 self.parquet_bytes_known = totals.parquet_bytes_known;
194 self.row_groups_selected = totals.row_groups_selected;
195 self.read_columns = totals.read_columns;
196 self.source_chunks_planned = totals.source_chunks_planned;
197 self.source_chunks_attempted = totals.source_chunks_attempted;
198 self.source_chunks_committed = totals.source_chunks_committed;
199 self.source_chunks_retried = totals.source_chunks_retried;
200 self.row_groups_planned = totals.row_groups_planned;
201 self.row_groups_committed = totals.row_groups_committed;
202 self.source_read_prefetches_started = totals.source_read_prefetches_started;
203 self.source_read_prefetches_completed = totals.source_read_prefetches_completed;
204 self.source_read_prefetches_skipped = totals.source_read_prefetches_skipped;
205 self.source_read_prefetches_abandoned = totals.source_read_prefetches_abandoned;
206 self.source_metadata_load_time = totals.source_metadata_load_time;
207 self.source_read_queue_wait_time = totals.source_read_queue_wait_time;
208 self.parquet_read_time = totals.parquet_read_time;
209 self.source_read_time = totals.source_read_time;
210
211 let query_id = self.inherited_query_id();
212 self.source_read_runtime_rows = checkpoint
213 .source_read_rows
214 .iter()
215 .cloned()
216 .map(|mut row| {
217 if row.query_id.is_none() {
218 row.query_id.clone_from(&query_id);
219 }
220 row
221 })
222 .collect();
223 self.projection = Some(MetricsProjection {
224 revision: Some(checkpoint.revision),
225 sealed: checkpoint.sealed,
226 completeness: checkpoint.completeness,
227 });
228 }
229
230 pub fn record_source_read_runtime(&mut self, mut metrics: SourceReadRuntimeMetrics) {
232 if metrics.query_id.is_none() {
233 metrics.query_id = self.inherited_query_id();
234 }
235 self.source_read_runtime_rows.push(metrics);
236 }
237
238 pub fn record_input_batch(&mut self, rows: u64) {
240 self.input_batches += 1;
241 self.input_rows += rows;
242 }
243
244 pub fn record_parquet_source(&mut self, known_bytes: Option<u64>, selected_row_groups: u64, read_columns: u64) {
246 self.parquet_files += 1;
247 self.parquet_bytes_known += known_bytes.unwrap_or(0);
248 self.row_groups_selected += selected_row_groups;
249 self.read_columns += read_columns;
250 }
251
252 pub fn record_source_chunk_planned(&mut self, row_groups: u64) {
254 self.source_chunks_planned = self.source_chunks_planned.saturating_add(1);
255 self.row_groups_planned = self.row_groups_planned.saturating_add(row_groups);
256 }
257
258 pub fn record_source_chunk_attempt(&mut self) {
260 self.source_chunks_attempted = self.source_chunks_attempted.saturating_add(1);
261 }
262
263 pub fn record_source_chunk_commit(&mut self, row_groups: u64) {
265 self.record_source_chunk_commit_count(1, row_groups);
266 }
267
268 pub fn record_source_chunk_commit_count(&mut self, chunk_count: u64, row_groups: u64) {
270 self.source_chunks_committed = self.source_chunks_committed.saturating_add(chunk_count);
271 self.row_groups_committed = self.row_groups_committed.saturating_add(row_groups);
272 }
273
274 pub fn record_source_chunk_retry(&mut self) {
276 self.source_chunks_retried = self.source_chunks_retried.saturating_add(1);
277 }
278
279 pub fn record_source_read_prefetch_started(&mut self) {
281 self.source_read_prefetches_started = self.source_read_prefetches_started.saturating_add(1);
282 }
283
284 pub fn record_source_read_prefetch_completed(&mut self) {
286 self.source_read_prefetches_completed = self.source_read_prefetches_completed.saturating_add(1);
287 }
288
289 pub fn record_source_read_prefetch_skipped(&mut self) {
291 self.source_read_prefetches_skipped = self.source_read_prefetches_skipped.saturating_add(1);
292 }
293
294 pub fn record_source_read_prefetch_abandoned(&mut self) {
296 self.source_read_prefetches_abandoned = self.source_read_prefetches_abandoned.saturating_add(1);
297 }
298}
299
300#[cfg(test)]
301mod projection_tests {
302 use std::time::Duration;
303
304 use super::{QueryMetrics, SourceReadRuntimeMetrics};
305 use crate::observability::{
306 MetricsCaptureMode, MetricsCompleteness, QueryMetricsCheckpoint, QueryMetricsSummary, SourceReadTotals,
307 };
308
309 fn checkpoint(revision: u64, input_rows: u64, capture_mode: MetricsCaptureMode) -> QueryMetricsCheckpoint {
310 QueryMetricsCheckpoint {
311 revision,
312 sealed: false,
313 summary: QueryMetricsSummary {
314 source_read: SourceReadTotals {
315 input_rows,
316 source_read_queue_wait_time: Duration::from_millis(input_rows),
317 ..SourceReadTotals::default()
318 },
319 },
320 source_read_rows: vec![SourceReadRuntimeMetrics { batch_index: revision, ..Default::default() }],
321 metrics: QueryMetrics::default(),
322 completeness: MetricsCompleteness {
323 capture_mode,
324 detail_rows_dropped: 0,
325 truncated: false,
326 totals_saturated: false,
327 },
328 }
329 }
330
331 #[test]
332 fn projecting_a_checkpoint_again_does_not_add_it_twice() {
333 let checkpoint = checkpoint(4, 10, MetricsCaptureMode::Bounded);
334 let mut metrics = QueryMetrics::new("projection-query");
335
336 metrics.apply_source_read_checkpoint(&checkpoint);
337 let once = metrics.clone();
338 metrics.apply_source_read_checkpoint(&checkpoint);
339
340 assert_eq!(metrics, once, "a repeated projection must be idempotent, not additive");
341 assert_eq!(metrics.input_rows, 10);
342 assert_eq!(metrics.source_read_runtime_rows.len(), 1);
343 assert_eq!(metrics.projection.and_then(|projection| projection.revision), Some(4));
344 }
345
346 #[test]
347 fn a_later_checkpoint_replaces_an_earlier_one() {
348 let mut metrics = QueryMetrics::new("projection-query");
349
350 metrics.apply_source_read_checkpoint(&checkpoint(1, 10, MetricsCaptureMode::Bounded));
351 metrics.apply_source_read_checkpoint(&checkpoint(2, 25, MetricsCaptureMode::Bounded));
352
353 assert_eq!(metrics.input_rows, 25, "a preview then a final projection must not sum");
354 assert_eq!(metrics.source_read_queue_wait_time, Duration::from_millis(25));
355 assert_eq!(metrics.source_read_runtime_rows[0].batch_index, 2);
356 }
357
358 #[test]
359 fn projected_rows_inherit_the_query_id() {
360 let mut metrics = QueryMetrics::new("projection-query");
361
362 metrics.apply_source_read_checkpoint(&checkpoint(1, 1, MetricsCaptureMode::Bounded));
363
364 assert_eq!(
365 metrics.source_read_runtime_rows[0].query_id.as_ref().map(|id| id.as_str()),
366 Some("projection-query")
367 );
368 }
369
370 #[test]
371 fn merging_projections_never_claims_more_than_either_side() {
372 let mut bounded = QueryMetrics::new("merged-query");
373 bounded.apply_source_read_checkpoint(&checkpoint(3, 1, MetricsCaptureMode::Bounded));
374 let mut disabled = QueryMetrics::new("merged-query");
375 disabled.apply_source_read_checkpoint(&checkpoint(7, 1, MetricsCaptureMode::Disabled));
376
377 bounded.merge_from(disabled);
378
379 let projection = bounded.projection.expect("both sides were projected");
380 assert_eq!(projection.revision, None, "a merge of two checkpoints is no single revision");
381 assert_eq!(projection.completeness.capture_mode, MetricsCaptureMode::Disabled);
382 assert!(!projection.completeness.detail_is_complete());
383 }
384
385 #[test]
386 fn json_reports_whether_its_rows_are_the_whole_detail() {
387 let unprojected = QueryMetrics::new("json-query");
388 let mut truncated = QueryMetrics::new("json-query");
389 let mut lossy = checkpoint(9, 1, MetricsCaptureMode::Bounded);
390 lossy.completeness.detail_rows_dropped = 3;
391 lossy.completeness.truncated = true;
392 truncated.apply_source_read_checkpoint(&lossy);
393 let mut not_captured = QueryMetrics::new("json-query");
394 not_captured.apply_source_read_checkpoint(&checkpoint(2, 1, MetricsCaptureMode::Disabled));
395
396 assert_eq!(unprojected.to_json()["completeness"]["status"], "unknown");
397 let completeness = &truncated.to_json()["completeness"];
398 assert_eq!(completeness["status"], "incomplete");
399 assert_eq!(completeness["revision"], 9);
400 assert_eq!(completeness["detail_rows_dropped"], 3);
401 assert_eq!(not_captured.to_json()["completeness"]["status"], "not_captured");
402 }
403
404 #[test]
405 fn an_unprojected_side_does_not_erase_a_projection() {
406 let mut accumulated = QueryMetrics::new("merged-query");
407 let mut stage = QueryMetrics::new("merged-query");
408 stage.apply_source_read_checkpoint(&checkpoint(5, 1, MetricsCaptureMode::Bounded));
409
410 accumulated.merge_from(stage);
411
412 assert_eq!(accumulated.projection.and_then(|projection| projection.revision), Some(5));
413 }
414}