This is an automated email from the ASF dual-hosted git repository.

avantgardnerio pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-ballista.git


The following commit(s) were added to refs/heads/main by this push:
     new 3910a6ad0 feat(core): metrics() for OrderedRangeRepartitionExec and 
RuntimeStatsExec (#2250)
3910a6ad0 is described below

commit 3910a6ad05a8076a7a842720c43e5e7c1e78b346
Author: Brent Gardner <[email protected]>
AuthorDate: Fri Aug 7 13:00:11 2026 -0600

    feat(core): metrics() for OrderedRangeRepartitionExec and RuntimeStatsExec 
(#2250)
    
    * feat(core): RuntimeStatsExec metrics — baseline + sketch subset time
    
    Was returning `None` from `metrics()`, so it was invisible in the
    scheduler's stage-metrics dump AND caused display-walker misalignment:
    metrics for later operators (SortExec, DataSourceExec) got labeled as
    this op or ORRE, because `collect_plan_metrics` skips None returns
    while the display visitor increments its index for every node.
    
    Adds `ExecutionPlanMetricsSet` with:
    - `elapsed_compute` / `output_rows` via `BaselineMetrics.record_output`
    - `sketch_time` (subset_time) — isolates the T-Digest
      `merge_unsorted_f64` cost from surrounding evaluate/downcast/flatten,
      so sketch-mode overhead is measurable separately.
    - `sketch_batches` (Count) — number of batches where the sketch was
      actually updated (excludes empty-after-nulls batches).
    
    Timer scoped post-child-poll (Time clone so the timer doesn't hold a
    borrow across the mutable `ingest` call).
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
    
    * feat(core): ORRE metrics — scatter-side timers + fn metrics()
    
    ORRE was constructing BaselineMetrics for its StreamingMerge output
    side but never exposing them (no `fn metrics()` impl), and the scatter
    side — where the actual value-add of range-routing lives — had zero
    instrumentation.
    
    Adds `fn metrics()` so both sides surface, plus per-input-partition
    scatter counters:
    - `scatter_elapsed_compute` — total scatter compute per input,
      timer scoped post child-poll (upstream sort/read not billed).
    - `scatter_split_time` — subset time for `split_batch_by_range`
      alone. Isolates routing-expr evaluation cost from the surrounding
      channel work.
    - `scatter_send_time` — subset time for `senders[out].send().await`.
      High values relative to compute mean downstream merge is draining
      slower than we can scatter (backpressure).
    - `scatter_discover_cuts_time` — one-shot first-batch cost of the
      cut-discovery walk into RuntimeStatsExec.
    - `scatter_input_batches` / `scatter_input_rows` — input volume.
    - `scatter_output_sub_batches` — post-split fanout count. Under skew
      this can be up to K× input_batches.
    
    Compute timer is stopped around `send().await` so backpressure waits
    land in `send_time` alone, not double-counted into elapsed_compute.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
    
    * fixup: drop noisy Time-clone comment, allow too_many_arguments on scatter 
fn
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
 .../execution_plans/ordered_range_repartition.rs   | 74 +++++++++++++++++++++-
 ballista/core/src/execution_plans/runtime_stats.rs | 37 ++++++++++-
 2 files changed, 107 insertions(+), 4 deletions(-)

diff --git a/ballista/core/src/execution_plans/ordered_range_repartition.rs 
b/ballista/core/src/execution_plans/ordered_range_repartition.rs
index c040674da..3c0b42d8d 100644
--- a/ballista/core/src/execution_plans/ordered_range_repartition.rs
+++ b/ballista/core/src/execution_plans/ordered_range_repartition.rs
@@ -87,7 +87,9 @@ use datafusion::physical_expr::{
 use datafusion::physical_plan::execution_plan::{
     CardinalityEffect, EvaluationType, SchedulingType,
 };
-use datafusion::physical_plan::metrics::{BaselineMetrics, 
ExecutionPlanMetricsSet};
+use datafusion::physical_plan::metrics::{
+    BaselineMetrics, Count, ExecutionPlanMetricsSet, MetricBuilder, 
MetricsSet, Time,
+};
 use datafusion::physical_plan::sorts::streaming_merge::StreamingMergeBuilder;
 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
 use datafusion::physical_plan::{
@@ -330,6 +332,10 @@ impl ExecutionPlan for OrderedRangeRepartitionExec {
     }
 
     /// Every input row is emitted exactly once. Overrides default `Unknown`.
+    fn metrics(&self) -> Option<MetricsSet> {
+        Some(self.metrics.clone_inner())
+    }
+
     fn cardinality_effect(&self) -> CardinalityEffect {
         CardinalityEffect::Equal
     }
@@ -431,6 +437,22 @@ impl OrderedRangeRepartitionExec {
             let scatter_senders: Arc<[mpsc::Sender<Result<RecordBatch>>]> =
                 senders.into();
             let guard_senders = scatter_senders.clone();
+            let scatter_metrics = ScatterMetrics {
+                elapsed_compute: MetricBuilder::new(&self.metrics)
+                    .subset_time("scatter_elapsed_compute", input_partition),
+                split_time: MetricBuilder::new(&self.metrics)
+                    .subset_time("scatter_split_time", input_partition),
+                send_time: MetricBuilder::new(&self.metrics)
+                    .subset_time("scatter_send_time", input_partition),
+                discover_cuts_time: MetricBuilder::new(&self.metrics)
+                    .subset_time("scatter_discover_cuts_time", 
input_partition),
+                input_batches: MetricBuilder::new(&self.metrics)
+                    .counter("scatter_input_batches", input_partition),
+                input_rows: MetricBuilder::new(&self.metrics)
+                    .counter("scatter_input_rows", input_partition),
+                scattered_batches: MetricBuilder::new(&self.metrics)
+                    .counter("scatter_output_sub_batches", input_partition),
+            };
             // `guarded_scatter` wraps the body in `catch_unwind`: a panic
             // inside `scatter_input_partition` (or anything it calls) becomes
             // a broadcast error rather than a silent sender-drop that
@@ -444,6 +466,7 @@ impl OrderedRangeRepartitionExec {
                     scatter_senders,
                     cuts_cell,
                     output_partitions,
+                    scatter_metrics,
                 ),
                 guard_senders,
             )));
@@ -488,6 +511,37 @@ impl OrderedRangeRepartitionExec {
     }
 }
 
+/// Per-input-partition scatter counters. Labeled with `input_partition`;
+/// `aggregate_by_name` in the display collapses across all input
+/// partitions for a task-wide total.
+///
+/// Merge-side metrics (per output partition) are recorded separately
+/// by `StreamingMerge` via `BaselineMetrics` in `initialize_state`.
+struct ScatterMetrics {
+    /// Total scatter compute — timer scoped post child-poll so upstream
+    /// stream time (sort, parquet scan) isn't billed here.
+    elapsed_compute: Time,
+    /// `split_batch_by_range` cost isolated from the surrounding
+    /// send/backpressure work. Growing much faster than input_rows
+    /// suggests routing-expression evaluation is the hot loop.
+    split_time: Time,
+    /// `senders[output].send().await` cost. Includes backpressure
+    /// waits — high values mean downstream merge is slow to drain,
+    /// which starves the scatter path.
+    send_time: Time,
+    /// One-shot cost of the first-batch cut discovery walk. Reads the
+    /// upstream `RuntimeStatsExec` and computes K-1 quantiles.
+    discover_cuts_time: Time,
+    /// Batches read from the child. Zero-row batches short-circuit
+    /// so this counts only work-carrying batches.
+    input_batches: Count,
+    /// Rows read from the child.
+    input_rows: Count,
+    /// Sub-batches sent downstream. Under skew this can be much larger
+    /// than input_batches (each input batch splits into up to K subs).
+    scattered_batches: Count,
+}
+
 /// One background task per input partition. Reads the sorted input; on the
 /// first batch, the shared `OnceLock` learns the value cuts. Each batch is
 /// then dispatched via `split_batch_by_range` to K per-input senders (one
@@ -501,6 +555,7 @@ impl OrderedRangeRepartitionExec {
 ///   2. **Send zero-copy Arrow slices** (`batch.slice(start, len)`) instead
 ///      of materialising via `take_arrays`. Turns per-batch scatter into a
 ///      few Arc bumps rather than N × K allocations under skew.
+#[allow(clippy::too_many_arguments)]
 async fn scatter_input_partition(
     child: Arc<dyn ExecutionPlan>,
     input_partition: usize,
@@ -509,6 +564,7 @@ async fn scatter_input_partition(
     senders: Arc<[mpsc::Sender<Result<RecordBatch>>]>,
     cuts_cell: Arc<OnceLock<Vec<f64>>>,
     output_partitions: usize,
+    metrics: ScatterMetrics,
 ) -> Result<()> {
     let mut stream = child.execute(input_partition, ctx)?;
     while let Some(batch_result) = stream.next().await {
@@ -517,9 +573,15 @@ async fn scatter_input_partition(
         if senders.iter().all(|s| s.is_closed()) {
             return Ok(());
         }
+        let compute_timer = metrics.elapsed_compute.timer();
         let batch = batch_result?;
+        metrics.input_batches.add(1);
+        metrics.input_rows.add(batch.num_rows());
         let cuts = cuts_cell.get_or_init(|| {
-            discover_cuts(&child, routing_expr.as_ref(), output_partitions)
+            let discover_timer = metrics.discover_cuts_time.timer();
+            let cuts = discover_cuts(&child, routing_expr.as_ref(), 
output_partitions);
+            discover_timer.done();
+            cuts
         });
         // TODO(perf): input is sorted — this per-row `split_batch_by_range`
         // is legal but wasteful. Two follow-ups worth measuring:
@@ -528,16 +590,24 @@ async fn scatter_input_partition(
         //   2. Send zero-copy `batch.slice(start, len)` instead of
         //      `take_arrays`-materialised sub-batches. Arc bumps replace
         //      allocations under skew.
+        let split_timer = metrics.split_time.timer();
         let splits = split_batch_by_range(&batch, &routing_expr, cuts)?;
+        split_timer.done();
+        // Stop the compute timer around the send.await so backpressure
+        // waits get billed to `send_time` alone, not double-counted.
+        compute_timer.done();
         for (output, sub) in splits.into_iter().enumerate() {
             if sub.num_rows() == 0 {
                 continue;
             }
+            let send_timer = metrics.send_time.timer();
             // `send().await` provides backpressure: full channel suspends
             // this task → suspends input read → propagates upstream. Error
             // on send means downstream dropped its receiver; keep forwarding
             // to other outputs.
             let _ = senders[output].send(Ok(sub)).await;
+            send_timer.done();
+            metrics.scattered_batches.add(1);
         }
     }
     // Senders drop with this task → each output's merger sees EOF on that
diff --git a/ballista/core/src/execution_plans/runtime_stats.rs 
b/ballista/core/src/execution_plans/runtime_stats.rs
index 949241a16..eff45c6f1 100644
--- a/ballista/core/src/execution_plans/runtime_stats.rs
+++ b/ballista/core/src/execution_plans/runtime_stats.rs
@@ -61,6 +61,9 @@ use datafusion::physical_expr::{
     Distribution, OrderingRequirements, PhysicalExpr, PhysicalSortExpr,
 };
 use datafusion::physical_plan::execution_plan::CardinalityEffect;
+use datafusion::physical_plan::metrics::{
+    BaselineMetrics, Count, ExecutionPlanMetricsSet, MetricBuilder, 
MetricsSet, Time,
+};
 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
 use datafusion::physical_plan::{
     DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties, 
PlanProperties,
@@ -96,6 +99,7 @@ pub struct RuntimeStatsExec {
     /// keep writes off any shared lock.
     sketches: Option<Arc<[Mutex<TDigest>]>>,
     properties: Arc<PlanProperties>,
+    metrics: ExecutionPlanMetricsSet,
 }
 
 impl RuntimeStatsExec {
@@ -154,6 +158,7 @@ impl RuntimeStatsExec {
             row_counts,
             sketches,
             properties,
+            metrics: ExecutionPlanMetricsSet::new(),
         })
     }
 
@@ -323,6 +328,10 @@ impl ExecutionPlan for RuntimeStatsExec {
         CardinalityEffect::Equal
     }
 
+    fn metrics(&self) -> Option<MetricsSet> {
+        Some(self.metrics.clone_inner())
+    }
+
     fn with_new_children(
         self: Arc<Self>,
         children: Vec<Arc<dyn ExecutionPlan>>,
@@ -367,19 +376,37 @@ impl ExecutionPlan for RuntimeStatsExec {
             .and_then(|exprs| exprs.first())
             .map(|e| e.expr.clone());
 
+        let baseline = BaselineMetrics::new(&self.metrics, partition);
+        // Separates sketch cost from the rest so we can tell whether the
+        // hot path in sketching mode is the T-Digest merge_unsorted_f64
+        // or something else (evaluate/downcast/flatten).
+        let sketch_time =
+            MetricBuilder::new(&self.metrics).subset_time("sketch_time", 
partition);
+        let sketch_batches =
+            MetricBuilder::new(&self.metrics).counter("sketch_batches", 
partition);
+
         let state = StreamState {
             input: input_stream,
             row_counts,
             sketches,
             routing_expr,
             partition,
+            baseline,
+            sketch_time,
+            sketch_batches,
         };
         let out = futures::stream::unfold(state, |mut state| async move {
-            let batch = state.input.next().await?;
-            let forwarded = batch.and_then(|batch| {
+            // Await the child first so upstream shuffle IO / parquet
+            // reads aren't billed to our elapsed_compute.
+            let next = state.input.next().await?;
+            let elapsed = state.baseline.elapsed_compute().clone();
+            let timer = elapsed.timer();
+            let forwarded = next.and_then(|batch| {
                 state.ingest(&batch)?;
+                state.baseline.record_output(batch.num_rows());
                 Ok(batch)
             });
+            timer.done();
             Some((forwarded, state))
         });
 
@@ -396,6 +423,9 @@ struct StreamState {
     sketches: Option<Arc<[Mutex<TDigest>]>>,
     routing_expr: Option<Arc<dyn datafusion::physical_plan::PhysicalExpr>>,
     partition: usize,
+    baseline: BaselineMetrics,
+    sketch_time: Time,
+    sketch_batches: Count,
 }
 
 impl StreamState {
@@ -457,7 +487,10 @@ impl StreamState {
                         self.partition
                     )
                 })?;
+                let sketch_timer = self.sketch_time.timer();
                 *sketch = sketch.merge_unsorted_f64(values);
+                sketch_timer.done();
+                self.sketch_batches.add(1);
             }
         }
 


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

Reply via email to