Repository navigation
feat(datafusion): report IcebergTableScan metrics - #2521
geoffreyclaude wants to merge 4 commits into
Conversation
9f036f5 to
fe4a322
Compare
| baseline_metrics: BaselineMetrics, | ||
| ) -> Pin<Box<dyn Stream<Item = DFResult<RecordBatch>> + Send>> { | ||
| futures::stream::poll_fn(move |cx| { | ||
| let baseline_metrics = baseline_metrics.clone(); |
There was a problem hiding this comment.
I don't think baseline_metrics.clone() is needed. BaselineMetrics::elapsed_compute() and record_poll() both take &self, so the captured value can be used directly. The clone is cheap (Arc/Count copies) but it's dead weight on every poll and reads as if there's a borrow problem to work around. Suggest:
futures::stream::poll_fn(move |cx| {
let _timer = baseline_metrics.elapsed_compute().timer();
let poll = stream.as_mut().poll_next(cx);
baseline_metrics.record_poll(poll)
})There was a problem hiding this comment.
Good catch. Removed the per-poll clone() in 19db703.
This is cleaner and also avoids dropping a cloned BaselineMetrics on every poll, which could record end_timestamp earlier than intended.
| let metrics = metrics.clone_inner(); | ||
| assert_eq!(metrics.output_rows(), Some(3)); | ||
| assert!( | ||
| metrics.elapsed_compute().is_some_and(|elapsed| elapsed > 0), |
There was a problem hiding this comment.
stream_with_baseline_metrics_records_rows_and_compute asserts output_rows and elapsed_compute. The PR description also lists output_batches, output_bytes, and completion timestamps as exposed. Those are in fact recorded — BaselineMetrics::record_poll → batch.record_output(...) updates output_batches and output_bytes (see datafusion/physical-expr-common/src/metrics/baseline.rs:331) — so adding assert!(metrics.output_batches() == Some(1)) and assert!(metrics.output_bytes().is_some_and(|b| b > 0)) is a cheap regression guard that matches the documented contract.
There was a problem hiding this comment.
Agreed on covering the rest of the baseline metrics. I added assertions for output_batches, output_bytes, start_timestamp, and end_timestamp in the focused stream_with_baseline_metrics test in 19db703.
I adapted this through MetricsSet::sum(...) + MetricValue matching because DataFusion 53.1 does not expose MetricsSet::output_batches() / output_bytes() convenience methods.
mbutrovich
left a comment
There was a problem hiding this comment.
Minor suggestions, thanks for tackling this @geoffreyclaude!
fb0a0ae to
6dc9f99
Compare
@mbutrovich Thanks for the quick review! PR should now be updated with your suggestions, as two additional commits. I also had to add a small public API snapshot update in f94d61f after rebasing onto current |
e3293de to
f94d61f
Compare
|
This pull request has been marked as stale due to 30 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@iceberg.apache.org list. Thank you for your contributions. |
f94d61f to
c5b450e
Compare
|
@mbutrovich Could you please give this another look? |
Sure thing, I will take a pass today. Thanks for keeping track of this! |
mbutrovich
left a comment
There was a problem hiding this comment.
Thanks for the PR and addressing my previous feedback, @geoffreyclaude! LGTM
|
This pull request has been marked as stale due to 30 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@iceberg.apache.org list. Thank you for your contributions. |
|
This pull request has been closed due to lack of activity. This is not a judgement on the merit of the PR in any way. It is just a way of keeping the PR queue manageable. If you think that is incorrect, or the pull request requires review, you can revive the PR at any time. |
|
I have reopened this since I think it's a valuable contribution. |
|
This pull request has been marked as stale due to 30 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@iceberg.apache.org list. Thank you for your contributions. |
There was a problem hiding this comment.
This already has @mbutrovich's approval, I'm just adding a second pair of eyes since it touches the public API surface. The metrics wiring reads clean and I've got no objection to merging; nothing below is blocking.
The one I'd genuinely tighten while we're here is the integration test. get_test_catalog_and_table() builds a table with no rows, so output_rows is 0 and assert_eq!(metrics.output_rows(), Some(output_rows)) passes whether or not the wrapper ever ran — BaselineMetrics starts that counter at 0. As written it can't catch a regression that bypasses metric collection. Inserting a small known row set and asserting against a count > 0 fixes that, and it also makes the neighbouring elapsed_compute > 0 check meaningful instead of flaky on a fast runner.
The other thing worth a note rather than a change: the unit test asserts output_batches/output_bytes, but the DF55 record_poll docs claim it only touches output_rows and end_time. If CI's green then it clearly does more than the docs say and we're fine — I'd just drop a one-line comment so the next reader doesn't "correct" the assertions.
A few smaller things I left inline:
- the timestamp test helpers use
MetricsSet::sum, which adds up point-in-time values — harmless at one partition, wrong beyond that _timerhas to stay a named binding;let _ = ...would silently zeroelapsed_computereset_statecould deriveCloneinstead of re-listing every field by hand
None of this blocks merge, i think can be merged as is. Will wait for day or two to merge
| Some(self.metrics.clone_inner()) | ||
| } | ||
|
|
||
| fn reset_state(self: Arc<Self>) -> DFResult<Arc<dyn ExecutionPlan>> { |
There was a problem hiding this comment.
minor, but reset_state re-lists every field by hand, so a new stateful field added later gets carried over silently unless someone remembers to touch this too.
Deriving Clone and writing it as let mut plan = (*self).clone(); plan.metrics = ExecutionPlanMetricsSet::new(); Ok(Arc::new(plan)) makes the intent — clone everything, reset the metrics — explicit and drops the boilerplate. Not blocking; the exhaustive literal is at least compile-safe today.
| baseline_metrics: BaselineMetrics, | ||
| ) -> Pin<Box<dyn Stream<Item = DFResult<RecordBatch>> + Send>> { | ||
| futures::stream::poll_fn(move |cx| { | ||
| let _timer = baseline_metrics.elapsed_compute().timer(); |
There was a problem hiding this comment.
worth a short comment that _timer has to stay a named binding — if someone tidies this to let _ = ...elapsed_compute().timer() the guard drops immediately and every elapsed_compute reading silently goes to 0, with the tests still passing. Quiet trap for the next editor.
| futures::stream::poll_fn(move |cx| { | ||
| let _timer = baseline_metrics.elapsed_compute().timer(); | ||
| let poll = stream.as_mut().poll_next(cx); | ||
| baseline_metrics.record_poll(poll) |
There was a problem hiding this comment.
one thing I couldn't confirm from here: the DF55 docs for record_poll say it only updates output_rows and end_time, yet the unit test asserts output_batches == Some(1) and output_bytes == Some(...). If CI's green then record_poll clearly populates those in this version and we're fine — but the published docs contradict it, so the next person could reasonably assume the assertions are wrong and "fix" them.
A one-line comment noting that DF55's record_poll also tracks batches/bytes (or an explicit output_batches().add(1) if it turns out it doesn't) would head that off. Which is it in practice?
| use super::stream_with_baseline_metrics; | ||
|
|
||
| #[test] | ||
| fn stream_with_baseline_metrics_records_rows_and_compute() { |
There was a problem hiding this comment.
this covers the happy single-batch path. Two cheap cases I'd add while it's fresh: an inner stream that yields Err(...) — assert the error passes through unchanged and elapsed_compute is still recorded, since error propagation is the likely real-world failure mode for a remote scan — and a two-batch stream to confirm output_rows accumulates across record_poll calls. Neither is blocking.
| matches_metric: impl Fn(&MetricValue) -> bool, | ||
| ) -> Option<usize> { | ||
| metrics | ||
| .sum(|metric| matches_metric(metric.value())) |
There was a problem hiding this comment.
metric_value_as_usize uses MetricsSet::sum, which is right for the additive counters but off for StartTimestamp/EndTimestamp — those are point-in-time values, not quantities to add up. It works today only because there's a single partition, so there's exactly one matching entry; the moment this scan reports more than one output partition, sum adds two epoch-ns values and hands back garbage.
For the two timestamp helpers I'd iter().find(...) the matching metric instead of summing. Small thing, but it's a trap waiting on any future partitioning change.
| let output_rows = batches.iter().map(|batch| batch.num_rows()).sum(); | ||
|
|
||
| let metrics = scan_plan.metrics().expect("scan should expose metrics"); | ||
| assert_eq!(metrics.output_rows(), Some(output_rows)); |
There was a problem hiding this comment.
the table get_test_catalog_and_table() builds has no rows, so output_rows is 0 here and this assertion holds even if the wrapper never fires — BaselineMetrics initializes that counter to 0. So it can't actually catch a regression where metric collection gets bypassed.
I'd insert a small known row set before scanning and assert against that count (> 0). That also makes the elapsed_compute > 0 check below meaningful instead of flaky — an empty scan can finish inside the clock resolution on a fast runner and record 0ns. test_catalog_backed_provider_insert has the insert path. wdyt?
|
I think we should freeze this change. We are in the processing moving datafusion related things to apache/datafusion-iceberg repo. |
|
@laskoviymishka Thank you for the detailed review! IIUC @blackmwk comment, and as discussed privately with @gabotechs, the recommended way forward would be to close this PR and re-open a similar one on https://github.com/apache/datafusion-iceberg once that repo is ready. I'll of course take that opportunity to integrate @laskoviymishka's suggestions. |
Yes, we should do iin the new repo. I'll close this pr for now. |
Which issue does this PR close?
What changes are included in this PR?
IcebergTableScannow owns a DataFusionExecutionPlanMetricsSet, returns it fromExecutionPlan::metrics(), and resets it fromExecutionPlan::reset_state().The scan output stream is wrapped in a small
poll_fnadapter that recordsBaselineMetricswhile the Iceberg stream is polled. This exposes the standard DataFusion operator metrics such aselapsed_compute,output_rows,output_batches,output_bytes, and completion timestamps inEXPLAIN ANALYZE.Focused tests cover the metrics wrapper directly and the catalog-backed provider execution path, including reset-state behavior.
Are these changes tested?
cargo test -p iceberg-datafusion stream_with_baseline_metrics_records_rows_and_compute --lockedcargo test -p iceberg-datafusion test_catalog_backed_provider_scan_reports_metrics --lockedcargo check -p iceberg-datafusion --lockedcargo test -p iceberg-datafusion --lockedcargo clippy -p iceberg-datafusion --all-targets --locked