use std::{collections::BTreeMap, time::Duration};
use cudf::ExecutionBudgetReport;
use query_planning::{
correlation::{PlanNodeId, QueryId, TraceId},
dynamic_filter::{DynamicFilter, DynamicFilterRoute},
plan::{GraphAlgorithm, NodeId},
};
use super::{
BudgetWindow, BudgetWindowPeaks, PlanExecutionId, QueryMetricsCheckpoint, RuntimeObservationConfig,
RuntimeObservationHandle, RuntimeObservationSession, SourceReadFact,
};
use crate::{
metrics::{
BoundaryMetrics, DynamicFilterSourceSnapshot, GraphAlgorithmRuntimeMetrics, OperatorMetrics, QueryMetrics,
SourceReadRuntimeMetrics,
},
observability::graph_observation::GraphMeasuredPeakProvenance,
};
#[derive(Debug, Clone)]
pub struct MetricsRecorder {
observation: RuntimeObservationHandle,
plan_execution_id: PlanExecutionId,
}
impl MetricsRecorder {
pub fn new(observation: RuntimeObservationHandle, plan_execution_id: PlanExecutionId) -> Self {
Self { observation, plan_execution_id }
}
pub fn bind(
observation: &RuntimeObservationHandle,
plan_execution_id: PlanExecutionId,
query_id: &str,
trace_id: Option<&str>,
) -> Self {
let observation = if observation.has_owner() {
observation.clone()
} else {
RuntimeObservationSession::new(
QueryId::new(query_id),
trace_id.map(TraceId::new),
RuntimeObservationConfig::native_execution(),
)
.handle()
};
Self::new(observation, plan_execution_id)
}
pub const fn plan_execution_id(&self) -> &PlanExecutionId {
&self.plan_execution_id
}
pub fn checkpoint(&self) -> Option<QueryMetricsCheckpoint> {
self.observation.metrics_checkpoint(&self.plan_execution_id)
}
pub fn freeze(&self) -> Option<QueryMetricsCheckpoint> {
self.observation.freeze_plan_metrics(&self.plan_execution_id)
}
pub fn progress(&self) -> ExecutionProgress {
self.observation.execution_progress(&self.plan_execution_id)
}
pub fn boundary(&self) -> BoundaryMetrics {
self.observation.boundary_metrics(&self.plan_execution_id)
}
fn apply(&self, reduce: impl FnOnce(&mut QueryMetrics)) {
self.observation.apply_metrics(&self.plan_execution_id, reduce);
}
fn source(&self, fact: SourceReadFact) {
self.observation.record_source_read(&self.plan_execution_id, fact);
}
pub fn record_source_read(&self, fact: SourceReadFact) {
self.source(fact);
}
pub fn record_source_read_runtime(&self, row: SourceReadRuntimeMetrics) {
self.observation.record_source_read_row(&self.plan_execution_id, row);
}
pub fn record_input_batch(&self, rows: u64) {
self.source(SourceReadFact::InputBatch { rows });
}
pub fn record_parquet_source(&self, known_bytes: Option<u64>, selected_row_groups: u64, read_columns: u64) {
self.source(SourceReadFact::ParquetSource {
files: 1,
known_bytes: known_bytes.unwrap_or(0),
selected_row_groups,
read_columns,
});
}
pub fn record_source_chunk_planned(&self, row_groups: u64) {
self.source(SourceReadFact::ChunkPlanned { row_groups });
}
pub fn record_source_chunk_attempt(&self) {
self.source(SourceReadFact::ChunkAttempted);
}
pub fn record_source_chunk_commit(&self, row_groups: u64) {
self.source(SourceReadFact::ChunkCommitted { chunk_count: 1, row_groups });
}
pub fn record_source_chunk_commit_count(&self, chunk_count: u64, row_groups: u64) {
self.source(SourceReadFact::ChunkCommitted { chunk_count, row_groups });
}
pub fn record_source_chunk_retry(&self) {
self.source(SourceReadFact::ChunkRetried);
}
pub fn record_source_read_prefetch_started(&self) {
self.source(SourceReadFact::PrefetchStarted);
}
pub fn record_source_read_prefetch_completed(&self) {
self.source(SourceReadFact::PrefetchCompleted);
}
pub fn record_source_read_prefetch_skipped(&self) {
self.source(SourceReadFact::PrefetchSkipped);
}
pub fn record_source_read_prefetch_abandoned(&self) {
self.source(SourceReadFact::PrefetchAbandoned);
}
pub fn add_source_read_time(&self, elapsed: Duration) {
self.source(SourceReadFact::SourceReadTime(elapsed));
}
pub fn add_operator_time(&self, elapsed: Duration) {
self.apply(|metrics| saturating_add_duration(&mut metrics.operator_time, elapsed));
}
pub fn add_chunk_operator_time(&self, elapsed: Duration) {
self.apply(|metrics| saturating_add_duration(&mut metrics.chunk_operator_time, elapsed));
}
pub fn add_final_merge_time(&self, elapsed: Duration) {
self.apply(|metrics| saturating_add_duration(&mut metrics.final_merge_time, elapsed));
}
pub fn add_final_sort_time(&self, elapsed: Duration) {
self.apply(|metrics| saturating_add_duration(&mut metrics.final_sort_time, elapsed));
}
pub fn add_materialization_time(&self, elapsed: Duration) {
self.apply(|metrics| saturating_add_duration(&mut metrics.materialization_time, elapsed));
}
pub fn record_gpu_resident_input_handoff(&self) {
self.apply(|metrics| metrics.boundary.record_gpu_resident_input_handoff());
}
pub fn record_host_to_device_upload(&self, rows: u64, bytes: u64) {
self.apply(|metrics| metrics.boundary.record_host_to_device_upload(rows, bytes));
}
pub fn record_device_to_host_materialization(&self, rows: u64, bytes: u64) {
self.apply(|metrics| metrics.boundary.record_device_to_host_materialization(rows, bytes));
}
pub fn record_device_memory_observation(&self, bytes: u64) {
self.apply(|metrics| metrics.record_device_memory_observation(bytes));
}
pub fn record_allocator_high_water_bytes(&self, bytes: u64) {
self.apply(|metrics| metrics.record_allocator_high_water_bytes(bytes));
}
pub fn record_operator_metrics(&self, row: OperatorMetrics) {
self.apply(|metrics| metrics.record_operator_metrics(row));
}
pub fn record_aggregate_strategy_decision(&self, node_id: impl ToString, facts: &BTreeMap<&'static str, String>) {
let node_id = node_id.to_string();
let facts = facts.clone();
self.apply(|metrics| metrics.record_aggregate_strategy_decision(node_id, &facts));
}
pub fn record_partial_aggregate(&self, input_rows: u64, output_rows: u64, retained_bytes: u64) {
self.apply(|metrics| metrics.record_partial_aggregate(input_rows, output_rows, retained_bytes));
}
pub fn record_final_merge_attempt(&self, merge_fan_in: usize) {
self.apply(|metrics| metrics.record_final_merge_attempt(merge_fan_in));
}
pub fn record_final_merge_level(&self) {
self.apply(QueryMetrics::record_final_merge_level);
}
pub fn record_join_strategy_decision(&self, node_id: impl ToString, facts: &BTreeMap<&'static str, String>) {
let node_id = node_id.to_string();
let facts = facts.clone();
self.apply(|metrics| metrics.record_join_strategy_decision(node_id, &facts));
}
pub fn record_hash_join_build(&self) {
self.apply(QueryMetrics::record_hash_join_build);
}
pub fn record_filtered_join_build(&self) {
self.apply(QueryMetrics::record_filtered_join_build);
}
pub fn record_one_shot_join(&self) {
self.apply(QueryMetrics::record_one_shot_join);
}
pub fn record_hash_join_hash_table_bytes(&self, bytes: u64) {
self.apply(|metrics| metrics.record_hash_join_hash_table_bytes(bytes));
}
pub fn record_filtered_join_hash_table_bytes(&self, bytes: u64) {
self.apply(|metrics| metrics.record_filtered_join_hash_table_bytes(bytes));
}
pub fn record_filtered_join_probe_state_bytes(&self, bytes: u64) {
self.apply(|metrics| metrics.record_filtered_join_probe_state_bytes(bytes));
}
pub fn record_mixed_join_size_data_bytes(&self, bytes: u64) {
self.apply(|metrics| metrics.record_mixed_join_size_data_bytes(bytes));
}
pub fn record_one_shot_join_index_bytes(&self, bytes: u64) {
self.apply(|metrics| metrics.record_one_shot_join_index_bytes(bytes));
}
pub fn record_one_shot_join_materialization_bytes(&self, bytes: u64) {
self.apply(|metrics| metrics.record_one_shot_join_materialization_bytes(bytes));
}
pub fn record_one_shot_join_output_bytes(&self, bytes: u64) {
self.apply(|metrics| metrics.record_one_shot_join_output_bytes(bytes));
}
pub fn record_dynamic_filter_produced(
&self,
route: &DynamicFilterRoute,
filter: &DynamicFilter,
producer_rows: u64,
) {
self.apply(|metrics| metrics.record_dynamic_filter_produced(route, filter, producer_rows));
}
pub fn record_dynamic_filter_skipped(&self, route: &DynamicFilterRoute, reason: impl Into<String>) {
let reason = reason.into();
self.apply(|metrics| metrics.record_dynamic_filter_skipped(route, reason));
}
pub fn record_dynamic_filter_applied(
&self,
route: &DynamicFilterRoute,
filter: &DynamicFilter,
before: DynamicFilterSourceSnapshot,
after: DynamicFilterSourceSnapshot,
) {
self.apply(|metrics| metrics.record_dynamic_filter_applied(route, filter, before, after));
}
pub fn record_dynamic_filter_applied_post_decode(
&self,
route: &DynamicFilterRoute,
filter: &DynamicFilter,
rows_before: Option<u64>,
rows_after: Option<u64>,
) {
self.apply(|metrics| metrics.record_dynamic_filter_applied_post_decode(route, filter, rows_before, rows_after));
}
pub fn record_dynamic_filter_source_post_decode_stage(
&self,
route: &DynamicFilterRoute,
filter: &DynamicFilter,
rows_before: Option<u64>,
rows_after: Option<u64>,
) {
self.apply(|metrics| {
metrics.record_dynamic_filter_source_post_decode_stage(route, filter, rows_before, rows_after);
});
}
pub fn record_dynamic_filter_post_decode_rows(
&self,
route: &DynamicFilterRoute,
filter: &DynamicFilter,
rows_before: Option<u64>,
rows_after: Option<u64>,
) {
self.apply(|metrics| metrics.record_dynamic_filter_post_decode_rows(route, filter, rows_before, rows_after));
}
pub fn record_dynamic_filter_source_reader_predicate_rows(
&self,
route: &DynamicFilterRoute,
filter: &DynamicFilter,
source_file_index: usize,
source_chunk_index: usize,
rows_before: Option<u64>,
rows_after: Option<u64>,
) {
self.apply(|metrics| {
metrics.record_dynamic_filter_source_reader_predicate_rows(
route,
filter,
source_file_index,
source_chunk_index,
rows_before,
rows_after,
);
});
}
pub fn record_dynamic_filter_source_reader_predicate_batch_rows(
&self,
route: &DynamicFilterRoute,
filter: &DynamicFilter,
rows_before: Option<u64>,
rows_after: Option<u64>,
) {
self.apply(|metrics| {
metrics.record_dynamic_filter_source_reader_predicate_batch_rows(route, filter, rows_before, rows_after);
});
}
pub fn record_dynamic_filter_reader_predicate_source_reduction(
&self,
rows_before: Option<u64>,
rows_after: Option<u64>,
) {
self.apply(|metrics| metrics.record_dynamic_filter_reader_predicate_source_reduction(rows_before, rows_after));
}
pub fn record_graph_algorithm_runtime(&self, row: GraphAlgorithmRuntimeMetrics) {
self.apply(|metrics| metrics.record_graph_algorithm_runtime(row));
}
pub fn record_graph_claim_calibration(&self, node_id: NodeId, algorithm: GraphAlgorithm) {
self.apply(|metrics| metrics.record_graph_claim_calibration(node_id, algorithm));
}
pub fn record_graph_claim_calibration_measurement(
&self,
node_id: NodeId,
algorithm: GraphAlgorithm,
measured_peak_bytes: Option<u64>,
measured_peak_provenance: GraphMeasuredPeakProvenance,
) {
self.apply(|metrics| {
metrics.record_graph_claim_calibration_measurement(
node_id,
algorithm,
measured_peak_bytes,
measured_peak_provenance,
);
});
}
pub fn record_graph_claim_calibration_algorithm_started(&self, node_id: NodeId, algorithm: GraphAlgorithm) {
self.apply(|metrics| metrics.record_graph_claim_calibration_algorithm_started(node_id, algorithm));
}
pub fn record_budgeted_execution(&self, operation: impl Into<String>, report: &ExecutionBudgetReport) {
let operation = operation.into();
self.apply(|metrics| metrics.record_budgeted_execution(operation, report));
}
pub fn open_budget_window(&self) -> BudgetWindow {
self.observation.open_budget_window(&self.plan_execution_id)
}
pub fn take_budget_window(&self, window: BudgetWindow) -> BudgetWindowPeaks {
self.observation.take_budget_window(&self.plan_execution_id, window)
}
pub fn close_budget_window(&self, compiled_task_id: impl ToString, window: BudgetWindow) {
self.observation.close_budget_window(&self.plan_execution_id, window, &compiled_task_id.to_string());
}
pub fn record_compiled_pipeline_task_execution(
&self,
compiled_task_id: impl ToString,
operator_kind: impl Into<String>,
) {
let compiled_task_id = compiled_task_id.to_string();
let operator_kind = operator_kind.into();
self.apply(|metrics| metrics.record_compiled_pipeline_task_execution(compiled_task_id, operator_kind));
}
pub fn record_compiled_pipeline_task_mapping(
&self,
compiled_task_id: impl ToString,
operator_kind: impl Into<String>,
plan_node_id: Option<impl Into<PlanNodeId>>,
) {
let compiled_task_id = compiled_task_id.to_string();
let operator_kind = operator_kind.into();
let plan_node_id = plan_node_id.map(Into::into);
self.apply(|metrics| {
metrics.record_compiled_pipeline_task_mapping(compiled_task_id, operator_kind, plan_node_id);
});
}
pub fn record_task_elapsed(&self, task_id: impl ToString, operator_kind: impl Into<String>, elapsed: Duration) {
let task_id = task_id.to_string();
let operator_kind = operator_kind.into();
self.apply(|metrics| metrics.record_task_elapsed(task_id, operator_kind, elapsed));
}
pub fn record_retry_resource_exhausted(&self) {
self.apply(QueryMetrics::record_retry_resource_exhausted);
}
pub fn record_compiled_pipeline_attempt(&self) {
self.apply(QueryMetrics::record_compiled_pipeline_attempt);
}
pub fn record_compiled_pipeline_tasks_executed(&self, tasks: u64) {
self.apply(|metrics| metrics.record_compiled_pipeline_tasks_executed(tasks));
}
pub fn record_compiled_pipeline_host_arrow_sink(&self) {
self.apply(QueryMetrics::record_compiled_pipeline_host_arrow_sink);
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ExecutionProgress {
pub source_chunks_planned: u64,
pub source_chunks_committed: u64,
pub partial_aggregate_count: u64,
}
fn saturating_add_duration(total: &mut Duration, value: Duration) {
*total = total.checked_add(value).unwrap_or(Duration::MAX);
}