sunchao commented on code in PR #5699:
URL: https://github.com/apache/datafusion-comet/pull/5699#discussion_r3938370027


##########
native/core/src/execution/operators/dynamic_filter.rs:
##########
@@ -0,0 +1,1744 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Connect a hash join's completed build domain to its probe input.
+//!
+//! Comet does not run DataFusion's physical optimizer, which normally connects
+//! dynamic-filter producers and consumers. This targeted wiring filters probe
+//! batches and lets a direct Parquet reader use the same live predicate for
+//! pruning. The original join verifies matches, including hash collisions.
+//! This leaves Spark's operator tree and partitioning intact and does
+//! not cross Spark exchanges or JVM/Arrow boundaries.
+
+use std::fmt::Formatter;
+use std::sync::Arc;
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::DataType;
+use datafusion::common::cast::as_boolean_array;
+use datafusion::common::config::ConfigOptions;
+use datafusion::common::{internal_err, JoinType, NullEquality, Result, 
ScalarValue, Statistics};
+use datafusion::datasource::physical_plan::ParquetSource;
+use datafusion::datasource::source::DataSourceExec;
+use datafusion::execution::TaskContext;
+use datafusion::logical_expr::{ColumnarValue, Operator};
+use datafusion::physical_expr::expressions::{
+    lit, BinaryExpr, Column, DynamicFilterPhysicalExpr, IsNotNullExpr,
+};
+use datafusion::physical_expr::PhysicalExpr;
+use datafusion::physical_plan::execution_plan::CardinalityEffect;
+use datafusion::physical_plan::joins::{HashJoinExec, PartitionMode};
+use datafusion::physical_plan::metrics::{ExecutionPlanMetricsSet, 
MetricBuilder, MetricsSet};
+use datafusion::physical_plan::projection::ProjectionExec;
+use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
+use datafusion::physical_plan::{
+    DisplayAs, DisplayFormatType, Distribution, ExecutionPlan, 
ExecutionPlanProperties,
+    PlanProperties, SendableRecordBatchStream,
+};
+use futures::StreamExt;
+
+use super::CometFilterExec;
+
+/// A task-local consumer of DataFusion's build-side runtime filter.
+#[derive(Debug)]
+pub(crate) struct DynamicFilterExec {
+    input: Arc<dyn ExecutionPlan>,
+    predicate: Arc<DynamicFilterPhysicalExpr>,
+    metrics: ExecutionPlanMetricsSet,
+}
+
+impl DynamicFilterExec {
+    fn new(input: Arc<dyn ExecutionPlan>, predicate: 
Arc<DynamicFilterPhysicalExpr>) -> Self {
+        Self {
+            input,
+            predicate,
+            metrics: ExecutionPlanMetricsSet::new(),
+        }
+    }
+}
+
+impl DisplayAs for DynamicFilterExec {
+    fn fmt_as(&self, _t: DisplayFormatType, f: &mut Formatter) -> 
std::fmt::Result {
+        write!(f, "CometDynamicFilterExec")
+    }
+}
+
+impl ExecutionPlan for DynamicFilterExec {
+    fn name(&self) -> &str {
+        "CometDynamicFilterExec"
+    }
+
+    fn properties(&self) -> &Arc<PlanProperties> {
+        // Removing rows preserves the input's schema, ordering and 
partitioning.
+        self.input.properties()
+    }
+
+    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+        vec![&self.input]
+    }
+
+    fn maintains_input_order(&self) -> Vec<bool> {
+        vec![true]
+    }
+
+    fn cardinality_effect(&self) -> CardinalityEffect {
+        CardinalityEffect::LowerEqual
+    }
+
+    fn with_new_children(
+        self: Arc<Self>,
+        mut children: Vec<Arc<dyn ExecutionPlan>>,
+    ) -> Result<Arc<dyn ExecutionPlan>> {
+        if children.len() != 1 {
+            return internal_err!("CometDynamicFilterExec requires one child");
+        }
+        Ok(Arc::new(Self::new(
+            children.remove(0),
+            Arc::clone(&self.predicate),
+        )))
+    }
+
+    fn reset_state(self: Arc<Self>) -> Result<Arc<dyn ExecutionPlan>> {
+        // HashJoinExec resets its producer on reexecution. Never retain a 
previous
+        // build's domain in the consumer. A reset plan safely bypasses 
filtering;
+        // ordinary Spark task attempts each construct a fresh, connected plan.
+        let predicate = Arc::new(DynamicFilterPhysicalExpr::new(
+            self.predicate.children().into_iter().cloned().collect(),
+            lit(true),
+        ));
+        Ok(Arc::new(Self::new(Arc::clone(&self.input), predicate)))
+    }
+
+    fn execute(
+        &self,
+        partition: usize,
+        context: Arc<TaskContext>,
+    ) -> Result<SendableRecordBatchStream> {
+        let input = self.input.execute(partition, context)?;
+        let predicate = Arc::clone(&self.predicate);
+        let evaluated =
+            
MetricBuilder::new(&self.metrics).counter("dynamic_filter_rows_evaluated", 
partition);
+        let pruned =
+            
MetricBuilder::new(&self.metrics).counter("dynamic_filter_rows_pruned", 
partition);
+        let bypassed =
+            
MetricBuilder::new(&self.metrics).counter("dynamic_filter_rows_bypassed", 
partition);
+        // Only dedicated metrics: merging this helper into the Spark join 
must not
+        // add its input/output counts or elapsed time to the join's existing 
metrics.
+        let eval_time =
+            
MetricBuilder::new(&self.metrics).subset_time("dynamic_filter_eval_time", 
partition);
+        let stream = input.map(move |batch| {
+            let batch = batch?;
+            let _timer = eval_time.timer();
+            match predicate.evaluate(&batch)? {

Review Comment:
   **[P2] Evaluate the runtime predicate using only the join-key column**
   
   The predicate receives the entire probe batch here. Its completed-build 
expression is `bounds AND hash_lookup`. When the bounds produce a non-null mask 
retaining some rows, but at most 20% of the batch, DataFusion 54.1 first 
filters the entire batch before evaluating membership. That copies every 
payload column into a temporary batch. The `filter_record_batch` at line 159 
then filters the original batch again, copying the surviving payload a second 
time.
   
   I reproduced this using the actual completed-build predicate with 8,192 
rows, 1,024 selected rows, and 32 BIGINT payload columns: **256 KiB of 
discarded payload copying per batch**. The probe used build keys `0..1024` and 
a permutation of probe keys `0..8192` (`(i * 641) % 8192`). Observing the input 
to the real membership expression confirmed distinct original, temporary, and 
final payload buffers. Evaluating the same predicate on a key-only projection 
produced the identical Boolean mask without the temporary payload copy.
   
   Could we use the existing dynamic-expression remapping mechanism to evaluate 
a zero-copy, key-only batch, then apply its mask to the original batch once? 
Eligibility already guarantees one direct join-key column. This avoids work 
that scales with the probe payload width while retaining the current producer 
and reader behavior. Whole-query performance impact remains unmeasured.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to