jayzhan211 commented on PR #25051:
URL: https://github.com/apache/datafusion/pull/25051#issuecomment-5618493056

   **`update_batch`'s timer is paid per row on the merge and convert-to-state 
paths**
   
   `merge_batch` fans out to `self.update_batch(&[val])` once per state row 
(array_agg.rs:1044), and `update_batch` → `update_batch_impl(values, true)` 
does `Instant::now()` + `elapsed()` + an atomic add on each call. I confirmed 
it with a probe: a 5-row `List<Int32>` state batch produces 5 metric recordings 
inside a single `merge_batch`.
   
   Two problems:
   
   1. Metrics are always *collected* (`analyze_level` only gates display), so 
`distinct_metric` is always `Some` and every Final/FinalPartitioned 
`array_agg(DISTINCT)` merge now pays two clock reads plus an atomic per row. 
That's the same per-row overhead the `grouped_update_batch_metric` / 
`update_batch_grouped` split was added to remove on the update path — the merge 
path just didn't get the treatment.
   2. Merge time is recorded into `agg_expr_N_internal_distinct_time` while 
also being counted by the existing `merge` timer. That contradicts 
`metrics.md`: "Grouped accumulation records this once per input batch, rather 
than once per group", and "complement the `update`, `merge`, `state`, and 
`evaluate` timers rather than subdividing".
   
   Suggest timing once at the batch boundary and using the untimed impl inside:
   
   ```diff
        fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> {
            if states.is_empty() {
                return Ok(());
            }
   
            assert_eq_or_internal_err!(states.len(), 1, "expects single state");
   
   +        // Time the whole merge once: the per-element calls below must not 
each
   +        // take a timestamp.
   +        let distinct_metric = self.distinct_metric.clone();
   +        let distinct_start = distinct_metric.as_ref().map(|_| 
Instant::now());
   +
            // The DISTINCT state is `List<value>`.
   -        states[0]
   +        let result = states[0]
                .as_list::<i32>()
                .iter()
                .flatten()
   -            .try_for_each(|val| self.update_batch(&[val]))
   +            .try_for_each(|val| self.update_batch_impl(&[val], false));
   +
   +        if let (Some(metric), Some(start)) = (distinct_metric, 
distinct_start) {
   +            metric.add_duration(start.elapsed());
   +        }
   +        result
        }
   ```
   
   The same shape exists in `GroupsAccumulatorAdapter::convert_to_state`
   (functions-aggregate-common/src/aggregate/groups_accumulator.rs:478): it 
builds a
   fresh accumulator per row through the factory — so `set_metrics` → 
`metric()` also
   runs per row — and then calls `update_batch`, timing each row separately on 
the
   skip-partial-aggregation path.
   
   ```diff
   -            converted_accumulator.update_batch(&values_to_accumulate)?;
   +            // Row-at-a-time conversion: use the untimed variant so this 
path
   +            // does not take a timestamp per row.
   +            
converted_accumulator.update_batch_grouped(&values_to_accumulate)?;
   ```
   
   If you want convert-to-state time attributed to the subphase, hoist a single
   `Instant` around the `for row_idx in 0..num_rows` loop using the metric from 
the
   first converted accumulator, the way `invoke_per_accumulator` does.


-- 
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