Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #25583 +/- ##
==========================================
+ Coverage 82.42% 82.49% +0.07%
==========================================
Files 1139 1141 +2
Lines 435372 437653 +2281
Branches 435372 437653 +2281
==========================================
+ Hits 358845 361045 +2200
+ Misses 54826 54821 -5
- Partials 21701 21787 +86 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
sunchao
left a comment
There was a problem hiding this comment.
Thanks, Liang-Chi. Reviewed 32cf894 against base 0576a0b; no actionable findings.
The review covered container invariants and concurrency, operator/source/sink delegation, FFI/API compatibility, tests, and performance. The append-only index preserves registration order, duplicates, and shared native metric values. Partition selection and exclusion of unpartitioned metrics match the documented contract, and FFI forwards to the producer's implementation.
Validation:
- All 36 metrics unit tests and the concurrent shared-plan integration test passed locally with the declared dependencies, including Arrow 60.0.0, and no dependency overrides.
- Current-head CI has 39 successful checks and three skipped extended-test checks, with none failed or pending. I did not rerun the full extended workspace suite locally.
- Independent container benchmarks used separate base/head release-nonlto builds, eight metrics per partition, and base/head/head/base run order. Indexed lookup stayed around 67–72 ns at both tested sizes (1 and 8,192 partitions), while filtering a full snapshot took about 0.63 ms at 8,192 partitions.
The costs are real: container registration was roughly 2x slower for the small set and 4.4x slower at 8,192 partitions; conversion also became more expensive. These are disclosed tradeoffs. The measurements were on a shared host, and ordinary query execution without repeated partition reporting and whole-Comet workloads were not benchmarked, so these results do not establish a general query speedup or slowdown.
The intentional FFI ABI change is documented, targets main, and carries the required api change label. LGTM.
|
This includes api change. @alamb Do you want to take a look before I merge this? Thanks. |
I didn't make it through this entire description (it seems also to have a bunch of internal stuff and talks about pending validation 😕 )
I wonder if you could pare down the description into something easier to read / know what is important to read for context on future PRs |
|
Thanks @alamb for the feedback! I've trimmed the description to focus on the changes and removed the unnecessary testing details. |
| // specific language governing permissions and limitations | ||
| // under the License. | ||
|
|
||
| //! Container costs; run alongside the physical-plan shared-tree benchmark. |
There was a problem hiding this comment.
what does "container" mean in this context? Also what is the "physical-plan shared-tree benchmark"? Maybe we could add a url link
There was a problem hiding this comment.
@alamb Removed this benchmark and its terminology. The replacement in datafusion/core/benches/partition_metrics.rs runs a SQL query before reading the plan's metrics.
| @@ -0,0 +1,116 @@ | |||
| // Licensed to the Apache Software Foundation (ASF) under one | |||
There was a problem hiding this comment.
I don't understand the value of this benchmark -- it seems like a better benchmark would be to run an actual query (SELECT ....) and then call get_metrics and metrics_per-partitition 🤔
I think it could be removed
There was a problem hiding this comment.
@alamb Agreed, removed. The replacement uses SELECT a + 1 AS b FROM t WHERE a < 16 and measures both metrics retrieval and execution with per-partition reporting.
| #[derive(Default, Debug)] | ||
| struct IndexedMetricsSet { | ||
| metrics: MetricsSet, | ||
| partitions: HashMap<usize, Vec<usize>>, |
There was a problem hiding this comment.
Could we document what the entries in this this usize mean?
Is it a map from partition --> indexes in Metrics set for that partition?
There was a problem hiding this comment.
@alamb Yes—the values are positions in the registry's metrics vector, in registration order. I've documented this on PartitionIndex::positions.
| // under the License. | ||
|
|
||
| //! Run with `cargo bench -p datafusion-physical-plan --bench partition_metrics`. | ||
| //! SQL cannot express per-task metrics reporting while retaining a shared plan. |
There was a problem hiding this comment.
why can't SQL express per-task metrics? That doesn't make any sense to me
There was a problem hiding this comment.
@alamb You're right; that comment was incorrect. I've removed it and switched to a benchmark that builds the plan from SQL, executes it, and reads its metrics in Rust.
| None | ||
| } | ||
|
|
||
| /// Return a snapshot of metrics whose [`Metric::partition`] is `Some(partition)`. |
There was a problem hiding this comment.
I think we have had challenges in the past with _partition type APIs on ExecutionPlan to add partition aware APIs. For example, the similar partition_statistics was deprecated -- see https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.ExecutionPlan.html#method.partition_statistics
I wonder if it would make more sense to try and plumb the notion of partition more deeply in MetricsSet itself somehow? You already kind of do this for alreayd with IndexedMetricSet
maybe we could make MetricsSet itself store metrics per-partition (rather than a flat Vec)
and then you could add apis to metricsSet to access per partition information 🤔
There was a problem hiding this comment.
@alamb Implemented this as plan.metrics().map(|m| m.for_partition(partition)). Snapshots retain a fixed registration boundary and share an index updated only on partition reads, keeping index maintenance off the registration path.
|
Thanks @alamb, I've moved partition selection to |
alamb
left a comment
There was a problem hiding this comment.
Tanks @viirya -- this is looking better
I don't quite understand some parts of this design, though the API looks nice to me
One thought i had while reading this was that you seem to have two types of MetricsSet -- one for a single partition and one for many partitiones
Maybe we could make that expliciy in the type system like
enum MetricsSet {
All {
metrics: Vec<Arc<Metric>>,
index: ... , // maybe
},
Partition {
Vec<Arc<Metric>>
}
}That way you could have
pub fn for_partition(&self, partition: usize) -> Self {
/// convert MetricsSet::All to MetricsSet::Partition
}Or something
| /// Add the specified metric | ||
| /// Add the specified metric. | ||
| /// | ||
| /// Mutating a deferred snapshot first copies its members into an independent |
There was a problem hiding this comment.
This seems like an implementation detail -- I think the docs on the overall structure are probably enough
|
|
||
| /// Return a snapshot containing only metrics with `partition == Some(partition)`. | ||
| /// | ||
| /// For registry-backed snapshots, this incrementally indexes registrations |
There was a problem hiding this comment.
this likewise seems like a bunch of implementation specific detail -- I think it would help if the comments only focused on the end user visible behavior. I am not sure how to interpret all the stuff about cloning matching handles, et
| // specific language governing permissions and limitations | ||
| // under the License. | ||
|
|
||
| //! Fixed-membership snapshots of an append-only registry. |
There was a problem hiding this comment.
Could we maybe make this specific tot he code that is in the module -- somethig like
//! Metric [`Registry`] implementation
|
|
||
| //! Fixed-membership snapshots of an append-only registry. | ||
| //! | ||
| //! Registration only appends to a vector. Partition readers share an index that |
There was a problem hiding this comment.
this is all details of the implementaiton that is probably not useful in the module level comments
|
|
||
| #[derive(Debug, Default)] | ||
| pub(super) struct Registry { | ||
| pub(super) metrics: Vec<Arc<Metric>>, |
There was a problem hiding this comment.
recommend keeping this private and accessing via a method rather than direct field access
| let Self::Owned(metrics) = self else { | ||
| unreachable!() | ||
| }; | ||
| metrics.push(metric); |
There was a problem hiding this comment.
doesn't this also invalidate the partition index, if there is one?
| } | ||
| } | ||
|
|
||
| fn select(&mut self, partition: usize, end: usize) -> Vec<Arc<Metric>> { |
There was a problem hiding this comment.
could we please document what partition and end mean in this? Is the end relative to just the metrics in the partition? or all the metrics?
| Self::Owned(match self { | ||
| Self::Owned(metrics) => metrics | ||
| .iter() | ||
| .filter(|m| m.partition() == Some(partition)) |
There was a problem hiding this comment.
isn't this the filter you were trying to avoid? shouldn't this be using the index if it is available 😕
| use std::sync::{Arc, OnceLock}; | ||
|
|
||
| #[derive(Debug, Default)] | ||
| pub(super) struct Registry { |
There was a problem hiding this comment.
I am sorry I don't understand this design -- maybe we can comment why bother creating the PartitionIndex at all? It seems like it just adds overhead (a new hash map and a bunch of allocations)
| // Number of registry entries already examined, including global metrics. | ||
| indexed: usize, | ||
| // Partition ID -> positions in Registry::metrics, in registration order. | ||
| positions: HashMap<usize, Vec<usize>>, |
There was a problem hiding this comment.
Since the Arc's are only a few more pointers, I wonder if you considered having this HashMap<usize, Arc<Metric> or something, so getting the partition's metrics would be an update to the index, and then a clone of the relevant Vec
Which issue does this PR close?
Closes #25582.
Rationale for this change
Consumers reporting metrics for one partition currently clone all registered metrics before filtering. This becomes increasingly expensive when a physical plan is retained across many partition executions.
What changes are included in this PR?
MetricsSet::for_partitionwith indexed partition selection.metrics()does not need to clone every metric handle. Build and update the shared partition index only when selecting a partition.What is the testing strategy for this PR?
Unit and integration tests cover partition selection, snapshot isolation, concurrent registration, and FFI round trips.
Are there any user-facing changes?
Callers can select a partition with
plan.metrics().map(|m| m.for_partition(partition)). Unpartitioned metrics remain available in the full set; unknown partitions produce an empty set.The existing
ExecutionPlanAPI and FFI layout remain unchanged. Partition selection does not eliminate work already performed by a metrics provider, such as FFI transport.