use std::fmt;
use std::sync::{Arc, Mutex, PoisonError};
use arrow::compute::concat_batches;
use arrow::datatypes::{DataType, Schema, SchemaRef};
use datafusion::error::{DataFusionError, Result};
use datafusion::execution::TaskContext;
use datafusion::physical_plan::execution_plan::EmissionType;
use datafusion::physical_plan::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet};
use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
use datafusion::physical_plan::{
DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties, PlanProperties,
SendableRecordBatchStream, collect,
};
use futures::future::{BoxFuture, FutureExt, Shared};
use futures::{StreamExt, TryStreamExt};
use oxidelake_core::BackendKind;
use oxidelake_core::telemetry::TelemetryHub;
use oxidelake_device::GpuBackend;
use super::{ExecConfig, field_type, plan_err, plan_properties};
use crate::operator::JoinBuild;
type SharedBuild = Shared<BoxFuture<'static, std::result::Result<Arc<JoinBuild>, SharedError>>>;
type SharedError = Arc<DataFusionError>;
#[derive(Default)]
struct BuildOnce(Mutex<Option<SharedBuild>>);
impl BuildOnce {
fn get_or_init(&self, init: impl FnOnce() -> SharedBuild) -> SharedBuild {
let mut slot = self.0.lock().unwrap_or_else(PoisonError::into_inner);
slot.get_or_insert_with(init).clone()
}
}
impl fmt::Debug for BuildOnce {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let started = self
.0
.lock()
.unwrap_or_else(PoisonError::into_inner)
.is_some();
f.debug_tuple("BuildOnce").field(&started).finish()
}
}
#[derive(Debug)]
pub struct GpuHashJoinExec {
left: Arc<dyn ExecutionPlan>,
right: Arc<dyn ExecutionPlan>,
left_key: usize,
right_key: usize,
projection: Option<Vec<usize>>,
schema: SchemaRef,
properties: Arc<PlanProperties>,
metrics: ExecutionPlanMetricsSet,
config: ExecConfig,
build: BuildOnce,
}
fn joined_schema(left: &dyn ExecutionPlan, right: &dyn ExecutionPlan) -> SchemaRef {
let fields: Vec<_> = left
.schema()
.fields()
.iter()
.chain(right.schema().fields().iter())
.cloned()
.collect();
Arc::new(Schema::new(fields))
}
impl GpuHashJoinExec {
pub fn try_new(
left: Arc<dyn ExecutionPlan>,
right: Arc<dyn ExecutionPlan>,
left_key: usize,
right_key: usize,
target: BackendKind,
) -> Result<Self> {
let (ls, rs) = (left.schema(), right.schema());
for (schema, key, side) in [(&ls, left_key, "left"), (&rs, right_key, "right")] {
let dt = field_type(schema, key, "GpuHashJoinExec")?;
if *dt != DataType::Int64 {
return plan_err(format!(
"GpuHashJoinExec: {side} key column {key} has type {dt:?}; v1 supports Int64"
));
}
}
let schema = joined_schema(left.as_ref(), right.as_ref());
let properties = plan_properties(
Arc::clone(&schema),
left.output_partitioning().clone(),
EmissionType::Incremental,
);
Ok(Self {
left,
right,
left_key,
right_key,
projection: None,
schema,
properties,
metrics: ExecutionPlanMetricsSet::new(),
config: ExecConfig::new(target),
build: BuildOnce::default(),
})
}
pub fn with_projection(mut self, projection: Option<Vec<usize>>) -> Result<Self> {
let full = joined_schema(self.left.as_ref(), self.right.as_ref());
let schema = match &projection {
Some(indices) if indices.is_empty() => {
return plan_err("GpuHashJoinExec: projection must select at least one column");
}
Some(indices) => Arc::new(full.project(indices)?),
None => full,
};
self.properties = plan_properties(
Arc::clone(&schema),
self.left.output_partitioning().clone(),
EmissionType::Incremental,
);
self.schema = schema;
self.projection = projection;
Ok(self)
}
pub fn with_backend(mut self, backend: Arc<dyn GpuBackend>) -> Self {
self.config.backend = Some(backend);
self
}
pub fn with_telemetry(mut self, telemetry: Arc<TelemetryHub>) -> Self {
self.config.telemetry = Some(telemetry);
self
}
pub fn left_key(&self) -> usize {
self.left_key
}
pub fn right_key(&self) -> usize {
self.right_key
}
pub fn projection(&self) -> Option<&[usize]> {
self.projection.as_deref()
}
pub fn target(&self) -> BackendKind {
self.config.target
}
pub fn left(&self) -> &Arc<dyn ExecutionPlan> {
&self.left
}
pub fn right(&self) -> &Arc<dyn ExecutionPlan> {
&self.right
}
}
impl DisplayAs for GpuHashJoinExec {
fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = |schema: SchemaRef, i: usize| {
schema
.fields()
.get(i)
.map_or_else(|| format!("col{i}"), |fl| fl.name().clone())
};
let on = format!(
"{} = {}",
name(self.left.schema(), self.left_key),
name(self.right.schema(), self.right_key)
);
let projection = match &self.projection {
Some(_) => format!(
", projection=[{}]",
self.schema
.fields()
.iter()
.map(|fl| fl.name().clone())
.collect::<Vec<_>>()
.join(", ")
),
None => String::new(),
};
match t {
DisplayFormatType::Default | DisplayFormatType::Verbose => write!(
f,
"GpuHashJoinExec[{}]: join_type=Inner, on=[{on}]{projection}",
self.config.target
),
DisplayFormatType::TreeRender => write!(f, "backend={}\non={on}", self.config.target),
}
}
}
impl ExecutionPlan for GpuHashJoinExec {
fn name(&self) -> &str {
"GpuHashJoinExec"
}
fn properties(&self) -> &Arc<PlanProperties> {
&self.properties
}
fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
vec![&self.left, &self.right]
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn ExecutionPlan>>,
) -> Result<Arc<dyn ExecutionPlan>> {
let [left, right] = <[Arc<dyn ExecutionPlan>; 2]>::try_from(children).map_err(|c| {
DataFusionError::Plan(format!(
"GpuHashJoinExec expects 2 children, got {}",
c.len()
))
})?;
let mut exec = Self::try_new(
left,
right,
self.left_key,
self.right_key,
self.config.target,
)?
.with_projection(self.projection.clone())?;
exec.config = self.config.clone();
Ok(Arc::new(exec))
}
fn execute(
&self,
partition: usize,
context: Arc<TaskContext>,
) -> Result<SendableRecordBatchStream> {
let left = self.left.execute(partition, Arc::clone(&context))?;
let operator = self.config.operator("GpuHashJoinExec")?;
let baseline = BaselineMetrics::new(&self.metrics, partition);
let (left_key, right_key) = (self.left_key, self.right_key);
let projection = self.projection.clone();
let build_fut = self.build.get_or_init(|| {
let right = Arc::clone(&self.right);
let right_schema = right.schema();
async move {
let batches = collect(right, context).await.map_err(Arc::new)?;
let build = concat_batches(&right_schema, &batches)
.map_err(|e| Arc::new(DataFusionError::from(e)))?;
JoinBuild::new(build, right_key)
.map(Arc::new)
.map_err(|e| Arc::new(DataFusionError::from(e)))
}
.boxed()
.shared()
});
let stream = futures::stream::once(async move {
let build = build_fut.await.map_err(DataFusionError::Shared)?;
Ok::<_, DataFusionError>(left.map(move |batch| {
let batch = batch?;
let _timer = baseline.elapsed_compute().timer();
let out = operator.hash_join(&batch, &build, left_key, projection.as_deref())?;
baseline.record_output(out.num_rows());
Ok(out)
}))
})
.try_flatten();
Ok(Box::pin(RecordBatchStreamAdapter::new(
Arc::clone(&self.schema),
stream,
)))
}
fn metrics(&self) -> Option<MetricsSet> {
Some(self.metrics.clone_inner())
}
}