7373//! ```text
7474//! cost_ns = evaluation time + rows_in * measured overhead
7575//! saving_ns = (rows_in - rows_out) * saving_ns_per_row
76- //! saving_ns_per_row = min_saving_ns_per_row + measured saving
76+ //! saving_ns_per_row = producer work + measured saving
7777//! ```
7878//!
79- //! `min_saving_ns_per_row` comes from the configuration. It is the work
80- //! that a removed row saves after the filter, for example a hash table
81- //! probe in a join. The *measured saving* and the *measured overhead* are
79+ //! The *producer work* is the work that a removed row saves after the
80+ //! filter, in the operator that produced the filter, for example the
81+ //! hash and the hash table lookup of a probe row in a hash join. The
82+ //! producer measures it ([`RemovedRowWork`], see
83+ //! [`DynamicFilterPhysicalExpr::removed_row_work`]): a hash join with a
84+ //! small build side does 2 to 8 ns of work for each probe row (TPC-DS
85+ //! SF1 star joins), a join with a large build side much more. Until the
86+ //! producer has measured [`MIN_OBSERVED_ROWS`] rows, the gate uses
87+ //! `min_saving_ns_per_row` from the configuration (a prior). With more
88+ //! than one dynamic filter in the filter, the smallest measured work is
89+ //! used. The *measured saving* and the *measured overhead* are
8290//! optional: a consumer that can measure more work that a removed row
8391//! saves (the Parquet scan measures the decode time of the columns that
8492//! the filter does not read), or a fixed cost for each evaluated row in
@@ -146,9 +154,11 @@ use std::time::Duration;
146154use datafusion_common:: config:: ExecutionOptions ;
147155use datafusion_physical_expr_common:: physical_expr:: PhysicalExpr ;
148156
149- use crate :: expressions:: DynamicFilterTracking ;
157+ use datafusion_common:: tree_node:: { TreeNode , TreeNodeRecursion } ;
158+
159+ use crate :: expressions:: { DynamicFilterPhysicalExpr , DynamicFilterTracking } ;
150160use crate :: filter_stats:: {
151- Clock , FilterCost , MIN_OBSERVED_ROWS , SystemClock , duration_nanos,
161+ Clock , FilterCost , MIN_OBSERVED_ROWS , RemovedRowWork , SystemClock , duration_nanos,
152162} ;
153163
154164/// A running filter is paused by the cost rule only if its cost is larger
@@ -172,7 +182,8 @@ pub struct OptionalFilterGateConfig {
172182 /// `initial_pause_batches` are used as `initial_pause_batches`.
173183 pub max_pause_batches : usize ,
174184 /// Work, in nanoseconds, that each row removed by the filter saves after
175- /// the filter, at the least. The gate adds the saving that the consumer
185+ /// the filter, until the producer of the filter has measured it (see
186+ /// [`RemovedRowWork`]). The gate adds the saving that the consumer
176187 /// measures (see [`MeasuredRowSaving`]). The gate pauses a filter whose
177188 /// evaluation time is larger than the saving of the rows that it
178189 /// removes. Negative values are used as 0.
@@ -421,9 +432,11 @@ pub struct OptionalFilterGate {
421432 config : OptionalFilterGateConfig ,
422433 /// The clock that consumers use to measure the evaluation time.
423434 clock : Arc < dyn Clock > ,
424- /// The saving that the consumer measures, added to
425- /// `config.min_saving_ns_per_row`.
435+ /// The saving that the consumer measures, added to the producer work.
426436 measured_saving : Option < Arc < MeasuredRowSaving > > ,
437+ /// The work that the producers of the dynamic filters in `filter` do
438+ /// for each removed row, see [`RemovedRowWork`].
439+ producer_work : Vec < Arc < RemovedRowWork > > ,
427440 state : GateState ,
428441 /// True while the current window is a probe after a pause. The cost
429442 /// rule then uses [`RESUME_COST_MARGIN`].
@@ -457,12 +470,22 @@ impl OptionalFilterGate {
457470 pub fn new ( filter : Arc < dyn PhysicalExpr > , config : OptionalFilterGateConfig ) -> Self {
458471 let config = config. normalized ( ) ;
459472 let tracking = DynamicFilterTracking :: classify ( & filter) ;
473+ let mut producer_work = vec ! [ ] ;
474+ filter
475+ . apply ( |expr| {
476+ if let Some ( dynamic) = expr. downcast_ref :: < DynamicFilterPhysicalExpr > ( ) {
477+ producer_work. push ( Arc :: clone ( dynamic. removed_row_work ( ) ) ) ;
478+ }
479+ Ok ( TreeNodeRecursion :: Continue )
480+ } )
481+ . expect ( "the closure is infallible" ) ;
460482 Self {
461483 filter,
462484 tracking,
463485 config,
464486 clock : SystemClock :: shared ( ) ,
465487 measured_saving : None ,
488+ producer_work,
466489 state : GateState :: new_window ( ) ,
467490 probing : false ,
468491 backoff : config. initial_pause_batches ,
@@ -513,13 +536,26 @@ impl OptionalFilterGate {
513536 }
514537
515538 /// The work, in nanoseconds, that the gate assumes each removed row
516- /// saves now: the configured minimum plus the measured saving.
539+ /// saves now: the work of the producer (measured, or the configured
540+ /// `min_saving_ns_per_row` before the measurement) plus the measured
541+ /// saving of the consumer.
517542 pub fn saving_ns_per_row ( & self ) -> f64 {
518543 let measured = self
519544 . measured_saving
520545 . as_ref ( )
521546 . map_or ( 0.0 , |saving| saving. ns_per_row ( ) ) ;
522- self . config . min_saving_ns_per_row + measured
547+ self . producer_work_ns_per_row ( ) + measured
548+ }
549+
550+ /// The work of the producer for each removed row: the smallest measured
551+ /// [`RemovedRowWork`] of the dynamic filters in the filter, or
552+ /// `min_saving_ns_per_row` if none is measured yet.
553+ fn producer_work_ns_per_row ( & self ) -> f64 {
554+ self . producer_work
555+ . iter ( )
556+ . filter_map ( |work| work. ns_per_row ( ) )
557+ . reduce ( f64:: min)
558+ . unwrap_or ( self . config . min_saving_ns_per_row )
523559 }
524560
525561 /// The work, in nanoseconds for each evaluated row, that the gate adds
@@ -1374,6 +1410,32 @@ mod tests {
13741410 assert ! ( !shared_gate( filter, & shared) . is_paused( ) ) ;
13751411 }
13761412
1413+ /// The producer of a dynamic filter measures its work for each row that
1414+ /// the filter removes: once measured, it replaces the configured
1415+ /// `min_saving_ns_per_row`.
1416+ #[ test]
1417+ fn producer_work_replaces_configured_saving ( ) {
1418+ let ( dynamic, filter) = dynamic_filter ( ) ;
1419+ let mut gate = gate_with ( filter) ;
1420+ assert_eq ! ( gate. saving_ns_per_row( ) , 20.0 ) ;
1421+ // Removes 80% at 5 ns for each row: 5 < 0.8 * 20 * 1.1, it stays on.
1422+ for _ in 0 ..4 {
1423+ assert_eq ! ( feed_timed( & mut gate, 0.2 , 5.0 ) , GateDecision :: Evaluate ) ;
1424+ }
1425+ assert ! ( !gate. is_paused( ) ) ;
1426+
1427+ // The producer measures 4 ns for each removed row: 0.8 * 4 < 5.
1428+ let work = dynamic. removed_row_work ( ) ;
1429+ work. record ( MIN_OBSERVED_ROWS , 4 * MIN_OBSERVED_ROWS ) ;
1430+ assert_eq ! ( gate. saving_ns_per_row( ) , 4.0 ) ;
1431+ assert_eq ! ( feed_timed( & mut gate, 0.2 , 5.0 ) , GateDecision :: Evaluate ) ;
1432+ assert_eq ! ( feed_timed( & mut gate, 0.2 , 5.0 ) , GateDecision :: Evaluate ) ;
1433+ assert ! ( gate. is_paused( ) ) ;
1434+
1435+ // A filter without dynamic filters uses the configuration.
1436+ assert_eq ! ( new_gate( ) . saving_ns_per_row( ) , 20.0 ) ;
1437+ }
1438+
13771439 /// A clock that moves by a fixed step each time it is read.
13781440 #[ derive( Debug ) ]
13791441 struct SteppingClock {
0 commit comments