mod cpu_worker;
mod io_loop;
use std::any::Any;
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, OnceLock};
use std::task::{Context, Poll};
use crate::analytics::{QueryErrorKind, build_metrics_set_for_explain};
use crate::chunk_fetcher::{batch_byte_size, batch_byte_size_uncompressed};
use crate::dataframe_query_common::{
DataframeClientAPI, IndexValuesMap, PlanSummary, group_chunk_infos_by_segment_id,
segment_partition_hash,
};
use crate::pipeline_budget::PipelineBudget;
use arrow::array::RecordBatch;
use arrow::compute::SortOptions;
use arrow::datatypes::{Schema, SchemaRef};
use cpu_worker::{CpuWorkerMsg, chunk_store_cpu_worker_thread};
use datafusion::common::{exec_datafusion_err, exec_err, plan_err};
use datafusion::config::ConfigOptions;
use datafusion::error::DataFusionError;
use datafusion::execution::{RecordBatchStream, SendableRecordBatchStream, TaskContext};
use datafusion::physical_expr::expressions::Column;
use datafusion::physical_expr::{
EquivalenceProperties, LexOrdering, Partitioning, PhysicalExpr, PhysicalSortExpr,
};
use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
use datafusion::physical_plan::metrics::MetricsSet;
use datafusion::physical_plan::{DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties};
use futures::FutureExt as _;
use futures_util::Stream;
use io_loop::chunk_stream_io_loop;
use itertools::Itertools as _;
use re_dataframe::{Index, QueryExpression, TimelineName};
use re_protos::cloud::v1alpha1::ext::ScanSegmentTableDataframe;
use re_types_core::SegmentId;
use re_redap_client::{ApiError, ApiResult};
use crate::IntoDfError as _;
use re_sorbet::{ColumnDescriptor, ColumnSelector};
use tokio::runtime::Handle;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::task::JoinHandle;
use tracing::Instrument as _;
const CPU_THREAD_IO_CHANNEL_SIZE: usize = 32;
#[cfg(not(target_arch = "wasm32"))]
#[inline]
#[must_use]
pub(crate) fn attach_trace_context(
trace_headers: Option<&crate::TraceHeaders>,
) -> Option<re_perf_telemetry::external::opentelemetry::ContextGuard> {
trace_headers?.attach()
}
#[derive(Debug)]
pub(crate) struct SegmentStreamExec<T: DataframeClientAPI> {
props: Arc<PlanProperties>,
index_values: IndexValuesMap,
chunk_info: Arc<BTreeMap<SegmentId, Vec<RecordBatch>>>,
query_expression: QueryExpression,
projected_schema: Arc<Schema>,
target_partitions: usize,
client: T,
limit_rows: Option<usize>,
trace_headers: Option<crate::TraceHeaders>,
server_trace_id: Option<re_redap_client::TraceId>,
pending_analytics: crate::PendingQueryAnalytics,
captured_collectors: Vec<crate::MetricsCollector>,
partitions_remaining: Arc<AtomicUsize>,
snapshot_sent: Arc<AtomicBool>,
pipeline_budget: Arc<PipelineBudget>,
plan_summary: PlanSummary,
}
pub struct DataframeSegmentStreamInner<T: DataframeClientAPI> {
projected_schema: SchemaRef,
client: T,
chunk_infos: Vec<RecordBatch>,
filtered_index_timeline: Option<TimelineName>,
chunk_tx: Option<Sender<ApiResult<CpuWorkerMsg>>>,
store_output_channel: Receiver<RecordBatch>,
io_join_handle: Option<JoinHandle<ApiResult<()>>>,
cpu_join_handle: Option<JoinHandle<ApiResult<()>>>,
trace_headers: Option<crate::TraceHeaders>,
server_trace_id: Option<re_redap_client::TraceId>,
pending_analytics: crate::PendingQueryAnalytics,
captured_collectors: Vec<crate::MetricsCollector>,
partitions_remaining: Arc<AtomicUsize>,
snapshot_sent: Arc<AtomicBool>,
completed: bool,
pipeline_budget: Arc<PipelineBudget>,
}
pub struct DataframeSegmentStream<T: DataframeClientAPI> {
inner: Option<DataframeSegmentStreamInner<T>>,
}
impl<T: DataframeClientAPI> Stream for DataframeSegmentStream<T> {
type Item = Result<RecordBatch, DataFusionError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this_outer = self.get_mut();
let Some(this) = this_outer.inner.as_mut() else {
return Poll::Ready(None);
};
#[cfg(not(target_arch = "wasm32"))]
let _trace_guard = attach_trace_context(this.trace_headers.as_ref());
if let Some(join_handle) = this.cpu_join_handle.take_if(|h| h.is_finished())
&& let Some(cpu_join_result) = join_handle.now_or_never()
{
match cpu_join_result {
Err(err) => {
this.pending_analytics.record_error(QueryErrorKind::Decode);
return Poll::Ready(Some(exec_err!("{err}")));
}
Ok(Err(err)) => {
this.pending_analytics.record_error(QueryErrorKind::Decode);
return Poll::Ready(Some(Err(err
.with_trace_id(this.server_trace_id)
.into_df_error())));
}
Ok(Ok(())) => {}
}
}
if let Some(join_handle) = this.io_join_handle.take_if(|h| h.is_finished())
&& let Some(io_join_result) = join_handle.now_or_never()
{
match io_join_result {
Err(err) => {
this.pending_analytics.record_error(QueryErrorKind::Other);
return Poll::Ready(Some(exec_err!("{err}")));
}
Ok(Err(err)) => {
this.pending_analytics.record_error(QueryErrorKind::Other);
return Poll::Ready(Some(Err(err
.with_trace_id(this.server_trace_id)
.into_df_error())));
}
Ok(Ok(())) => {}
}
}
if this.io_join_handle.is_none()
&& let Some(chunk_tx) = this.chunk_tx.take()
{
let io_handle = Handle::current();
let client = this.client.clone();
let chunk_infos = this.chunk_infos.clone();
let filtered_index_timeline = this.filtered_index_timeline;
let pending_analytics = this.pending_analytics.clone();
let pipeline_budget = Arc::clone(&this.pipeline_budget);
let io_span = tracing::info_span!("chunk_io_pipeline");
this.io_join_handle = Some(
io_handle.spawn(
async move {
chunk_stream_io_loop(
client,
chunk_infos,
filtered_index_timeline,
chunk_tx,
pending_analytics,
pipeline_budget,
)
.await
}
.instrument(io_span),
),
);
}
let result = this
.store_output_channel
.poll_recv(cx)
.map(|result| Ok(result).transpose());
if matches!(&result, Poll::Ready(Some(Ok(_)))) {
this.pending_analytics.record_first_chunk();
}
if matches!(&result, Poll::Ready(None)) {
this.completed = true;
this.maybe_emit_snapshot();
this_outer.inner = None;
}
result
}
}
impl<T: DataframeClientAPI> RecordBatchStream for DataframeSegmentStream<T> {
fn schema(&self) -> SchemaRef {
self.inner
.as_ref()
.map(|inner| inner.projected_schema.clone())
.unwrap_or_else(|| Schema::empty().into())
}
}
fn build_and_push_snapshot(
captured_collectors: &[crate::MetricsCollector],
pending_analytics: &crate::PendingQueryAnalytics,
) {
let snapshot = crate::metrics_capture::build_query_snapshot(
pending_analytics.metrics(),
pending_analytics.total_duration(),
pending_analytics.time_to_first_chunk(),
pending_analytics.error_kind(),
pending_analytics.direct_terminal_reason(),
);
crate::metrics_capture::push_snapshot(captured_collectors, &snapshot);
}
fn emit_snapshot_if_last_partition(
captured_collectors: &[crate::MetricsCollector],
partitions_remaining: &AtomicUsize,
snapshot_sent: &AtomicBool,
pending_analytics: &crate::PendingQueryAnalytics,
) {
if captured_collectors.is_empty() {
return;
}
let prev = partitions_remaining.fetch_sub(1, Ordering::AcqRel);
if prev != 1 {
return; }
if snapshot_sent
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
return;
}
build_and_push_snapshot(captured_collectors, pending_analytics);
}
fn emit_snapshot_drop_fallback(
captured_collectors: &[crate::MetricsCollector],
snapshot_sent: &AtomicBool,
pending_analytics: &crate::PendingQueryAnalytics,
) {
if captured_collectors.is_empty() {
return;
}
if snapshot_sent
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
return;
}
build_and_push_snapshot(captured_collectors, pending_analytics);
}
impl<T: DataframeClientAPI> DataframeSegmentStreamInner<T> {
fn maybe_emit_snapshot(&self) {
emit_snapshot_if_last_partition(
&self.captured_collectors,
&self.partitions_remaining,
&self.snapshot_sent,
&self.pending_analytics,
);
}
}
impl<T: DataframeClientAPI> Drop for DataframeSegmentStreamInner<T> {
fn drop(&mut self) {
if self.completed {
return;
}
emit_snapshot_drop_fallback(
&self.captured_collectors,
&self.snapshot_sent,
&self.pending_analytics,
);
}
}
impl<T: DataframeClientAPI> SegmentStreamExec<T> {
#[tracing::instrument(level = "info", skip_all)]
pub fn try_new(
table_schema: &SchemaRef,
sort_index: Option<Index>,
projection: Option<&Vec<usize>>,
num_partitions: usize,
chunk_info_batches: Option<RecordBatch>,
mut query_expression: QueryExpression,
index_values: IndexValuesMap,
client: T,
limit_rows: Option<usize>,
trace_headers: Option<crate::TraceHeaders>,
server_trace_id: Option<re_redap_client::TraceId>,
pending_analytics: crate::PendingQueryAnalytics,
captured_collectors: Vec<crate::MetricsCollector>,
) -> datafusion::common::Result<Self> {
let projected_schema = match projection {
Some(p) => Arc::new(table_schema.project(p)?),
None => Arc::clone(table_schema),
};
if projection.is_some_and(|projection| !projection.is_empty()) {
let selection = projected_schema
.fields()
.iter()
.map(|field| {
ColumnDescriptor::try_from_arrow_field(None, field).map(ColumnSelector::from)
})
.try_collect()
.map_err(|err| exec_datafusion_err!("{err}"))?;
query_expression.selection = Some(selection);
}
let orderings =
if projected_schema.fields().iter().any(|f| {
f.name().as_str() == ScanSegmentTableDataframe::COLUMN_RERUN_SEGMENT_ID_NAME
}) {
let segment_col = Arc::new(Column::new(
ScanSegmentTableDataframe::COLUMN_RERUN_SEGMENT_ID_NAME,
0,
)) as Arc<dyn PhysicalExpr>;
let order_col = sort_index
.and_then(|index| {
let index_name = index.as_str();
projected_schema
.fields()
.iter()
.enumerate()
.find(|(_idx, field)| field.name() == index_name)
.map(|(index_col, _)| Column::new(index_name, index_col))
})
.map(|expr| Arc::new(expr) as Arc<dyn PhysicalExpr>);
let mut physical_ordering = vec![PhysicalSortExpr::new(
segment_col,
SortOptions::new(false, true),
)];
if let Some(col_expr) = order_col {
physical_ordering.push(PhysicalSortExpr::new(
col_expr,
SortOptions::new(false, true),
));
}
vec![
LexOrdering::new(physical_ordering)
.expect("LexOrdering should return Some since input is not empty"),
]
} else {
vec![]
};
let eq_properties =
EquivalenceProperties::new_with_orderings(Arc::clone(&projected_schema), orderings);
let partition_in_output_schema = projection.map(|p| p.contains(&0)).unwrap_or(false);
let output_partitioning = if partition_in_output_schema {
Partitioning::Hash(
vec![Arc::new(Column::new(
ScanSegmentTableDataframe::COLUMN_RERUN_SEGMENT_ID_NAME,
0,
))],
num_partitions,
)
} else {
Partitioning::UnknownPartitioning(num_partitions)
};
let props = PlanProperties::new(
eq_properties,
output_partitioning,
EmissionType::Incremental,
Boundedness::Bounded,
)
.into();
let total_uncompressed: usize = chunk_info_batches
.iter()
.map(|b| batch_byte_size_uncompressed(b).unwrap_or_else(|| batch_byte_size(b)))
.sum::<u64>() as usize;
let chunk_info = group_chunk_infos_by_segment_id(chunk_info_batches.as_slice())?;
drop(chunk_info_batches);
let plan_summary = PlanSummary::from_query_info(&pending_analytics.metrics().query_info);
let partitions_remaining = Arc::new(AtomicUsize::new(num_partitions));
let snapshot_sent = Arc::new(AtomicBool::new(false));
let pipeline_budget = Arc::new(PipelineBudget::new_with_metrics(
total_uncompressed,
num_partitions,
Arc::clone(pending_analytics.metrics()),
));
Ok(Self {
props,
chunk_info,
query_expression,
index_values,
projected_schema,
target_partitions: num_partitions,
client,
limit_rows,
trace_headers,
server_trace_id,
pending_analytics,
pipeline_budget,
plan_summary,
captured_collectors,
partitions_remaining,
snapshot_sent,
})
}
}
impl<T: DataframeClientAPI> ExecutionPlan for SegmentStreamExec<T> {
fn name(&self) -> &'static str {
"SegmentStreamExec"
}
fn as_any(&self) -> &dyn Any {
self
}
fn properties(&self) -> &Arc<PlanProperties> {
&self.props
}
fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
vec![]
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn ExecutionPlan>>,
) -> datafusion::common::Result<Arc<dyn ExecutionPlan>> {
if children.is_empty() {
Ok(self)
} else {
plan_err!("SegmentStreamExec does not support children")
}
}
fn execute(
&self,
partition: usize,
_context: Arc<TaskContext>,
) -> datafusion::common::Result<SendableRecordBatchStream> {
re_tracing::profile_function!();
#[cfg(not(target_arch = "wasm32"))]
let _trace_guard = attach_trace_context(self.trace_headers.as_ref());
let pipeline_budget = Arc::clone(&self.pipeline_budget);
let (chunk_tx, chunk_rx) = tokio::sync::mpsc::channel(CPU_THREAD_IO_CHANNEL_SIZE);
let random_state = ahash::RandomState::with_seeds(0, 0, 0, 0);
let chunk_infos: Vec<_> = {
re_tracing::profile_scope!("concat_chunk_infos_per_segment");
self.chunk_info
.iter()
.filter(|(segment_id, _)| {
let hash_value = segment_partition_hash(segment_id, &random_state) as usize;
hash_value % self.target_partitions == partition
})
.filter(|(segment_id, _)| {
self.index_values
.as_ref()
.is_none_or(|iv| iv.contains_key(*segment_id))
})
.map(|(_, batches)| re_arrow_util::concat_polymorphic_batches(batches))
.try_collect()
.map_err(|err| {
ApiError::deserialization_with_source(
None,
err,
"concatenating chunk-info batches per segment",
)
.into_df_error()
})?
};
if chunk_infos.is_empty() {
emit_snapshot_if_last_partition(
&self.captured_collectors,
&self.partitions_remaining,
&self.snapshot_sent,
&self.pending_analytics,
);
let stream: DataframeSegmentStream<T> = DataframeSegmentStream { inner: None };
return Ok(Box::pin(stream));
}
let client = self.client.clone();
let (batches_tx, batches_rx) = tokio::sync::mpsc::channel(CPU_THREAD_IO_CHANNEL_SIZE);
let query_expression = self.query_expression.clone();
let projected_schema = self.projected_schema.clone();
let limit_rows = self.limit_rows;
let cpu_join_handle = Some(
CpuRuntime::try_get()?.handle().spawn(
chunk_store_cpu_worker_thread(
chunk_rx,
batches_tx,
query_expression,
projected_schema,
self.index_values.clone(),
limit_rows,
Arc::clone(&pipeline_budget),
)
.instrument(tracing::info_span!("cpu_worker")),
),
);
let filtered_index_timeline = self.query_expression.filtered_index;
let stream = DataframeSegmentStreamInner {
projected_schema: self.projected_schema.clone(),
store_output_channel: batches_rx,
client,
chunk_infos,
filtered_index_timeline,
chunk_tx: Some(chunk_tx),
io_join_handle: None,
cpu_join_handle,
trace_headers: self.trace_headers.clone(),
server_trace_id: self.server_trace_id,
pending_analytics: self.pending_analytics.clone(),
pipeline_budget,
captured_collectors: self.captured_collectors.clone(),
partitions_remaining: Arc::clone(&self.partitions_remaining),
snapshot_sent: Arc::clone(&self.snapshot_sent),
completed: false,
};
let stream = DataframeSegmentStream {
inner: Some(stream),
};
Ok(Box::pin(stream))
}
fn metrics(&self) -> Option<MetricsSet> {
Some(build_metrics_set_for_explain(
self.pending_analytics.metrics(),
self.target_partitions,
self.pending_analytics.time_to_first_chunk(),
))
}
fn repartitioned(
&self,
target_partitions: usize,
_config: &ConfigOptions,
) -> datafusion::common::Result<Option<Arc<dyn ExecutionPlan>>> {
if target_partitions == self.target_partitions {
return Ok(None);
}
let mut plan = Self {
props: self.props.clone(),
chunk_info: self.chunk_info.clone(),
query_expression: self.query_expression.clone(),
index_values: self.index_values.clone(),
projected_schema: self.projected_schema.clone(),
target_partitions,
client: self.client.clone(),
limit_rows: self.limit_rows,
trace_headers: self.trace_headers.clone(),
server_trace_id: self.server_trace_id,
pending_analytics: self.pending_analytics.clone(),
pipeline_budget: Arc::clone(&self.pipeline_budget),
plan_summary: self.plan_summary.clone(),
captured_collectors: self.captured_collectors.clone(),
partitions_remaining: Arc::new(AtomicUsize::new(target_partitions)),
snapshot_sent: Arc::clone(&self.snapshot_sent),
};
let partitioning = match &plan.props.as_ref().partitioning {
Partitioning::RoundRobinBatch(_) => Partitioning::RoundRobinBatch(target_partitions),
Partitioning::UnknownPartitioning(_) => {
Partitioning::UnknownPartitioning(target_partitions)
}
Partitioning::Hash(expr, _) => Partitioning::Hash(expr.clone(), target_partitions),
};
plan.props = self
.props
.as_ref()
.clone()
.with_partitioning(partitioning)
.into();
Ok(Some(Arc::new(plan) as Arc<dyn ExecutionPlan>))
}
}
impl<T: DataframeClientAPI> DisplayAs for SegmentStreamExec<T> {
fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"SegmentStreamExec: num_partitions={:?}",
self.target_partitions,
)?;
match t {
DisplayFormatType::Default | DisplayFormatType::TreeRender => Ok(()),
DisplayFormatType::Verbose => {
let s = &self.plan_summary;
write!(
f,
", query_type={}, chunks={}, segments={}, bytes={}, \
filters_pushed_down={}, filters_applied_client_side={}, \
entity_path_narrowing={}",
s.query_type,
s.query_chunks,
s.query_segments,
re_format::format_bytes(s.query_bytes as _),
s.filters_pushed_down,
s.filters_applied_client_side,
s.entity_path_narrowing_applied,
)
}
}
}
}
fn shared_cpu_runtime_threads() -> usize {
crate::rerun_sdk_num_cpus().unwrap_or_else(crate::available_cpus)
}
#[derive(Debug)]
struct CpuRuntime {
runtime: tokio::runtime::Runtime,
}
impl CpuRuntime {
#[tracing::instrument(level = "trace", skip_all)]
fn try_new(num_threads: usize) -> Result<Self, DataFusionError> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(num_threads)
.thread_name("datafusion_cpu_worker")
.build()?;
Ok(Self { runtime })
}
fn try_get() -> Result<Arc<Self>, DataFusionError> {
static SHARED: OnceLock<Arc<CpuRuntime>> = OnceLock::new();
if let Some(runtime) = SHARED.get() {
return Ok(Arc::clone(runtime));
}
static INIT: parking_lot::Mutex<()> = parking_lot::Mutex::new(());
let _guard = INIT.lock();
if let Some(runtime) = SHARED.get() {
return Ok(Arc::clone(runtime));
}
let runtime = Self::try_new(shared_cpu_runtime_threads())?;
Ok(Arc::clone(SHARED.get_or_init(|| Arc::new(runtime))))
}
fn handle(&self) -> &Handle {
self.runtime.handle()
}
}