feat: Store only the last row in the previous cursor for round robin tie-breaking purposes - #23619
Conversation
| /// any buffer the cursor's [`CursorValues`] holds. Used to remember a | ||
| /// partition's last row across a batch boundary without keeping the | ||
| /// whole exhausted batch's memory alive (see [`Self::is_eq_to_prev_one`]). | ||
| pub fn last_value(&self) -> T::SingleRowValue { |
There was a problem hiding this comment.
I feel like this should return an Option, in case values() is empty
There was a problem hiding this comment.
values() shouldn't be empty, there are some asserts in the code:
assert!(rows.num_rows() > 0);
assert!(array.len() > 0, "Empty array passed to FieldCursor");
I don't imagine a Cursor with empty values being useful.
impl RowValues {
/// Create a new [`RowValues`] from `rows` and a `reservation`
/// that tracks its memory. There must be at least one row
///
/// Panics if the reservation is not for exactly `rows.size()`
/// bytes or if `rows` is empty.
pub fn new(rows: Arc<Rows>, reservation: MemoryReservation) -> Self {
assert_eq!(
rows.size(),
reservation.size(),
"memory reservation mismatch"
);
assert!(rows.num_rows() > 0);
Self {
rows,
_reservation: reservation,
}
}
}
pub fn new<A: CursorArray<Values = T>>(
options: SortOptions,
array: &A,
reservation: MemoryReservation,
) -> Self {
assert!(array.len() > 0, "Empty array passed to FieldCursor");
let null_threshold = match options.nulls_first {
true => array.null_count(),
false => array.len() - array.null_count(),
};
There was a problem hiding this comment.
The previous code also assumed values is never empty:
fn is_eq_to_prev_row_in_prev_batch(&self, other: &Self) -> bool {
assert_eq!(self.offset, 0);
T::eq(
&self.values,
self.offset,
&other.values,
other.values.len() - 1,
)
}
|
@Dandandan wondering whether we can change ReusableRows to only hold one slot after this change, since the previous cursor will no longer need to be kept alive for the round robin tie-breaking feature. |
d85bdd0 to
28a916d
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #23619 +/- ##
==========================================
+ Coverage 81.02% 81.03% +0.01%
==========================================
Files 1105 1106 +1
Lines 380669 381026 +357
Branches 380669 381026 +357
==========================================
+ Hits 308446 308778 +332
- Misses 53994 53997 +3
- Partials 18229 18251 +22 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| // there is also one cursor needed per stream | ||
| let max_peak = 3 * ipc_batch_size + 2 * cursor_unit + converter_size; | ||
|
|
||
| assert!( |
There was a problem hiding this comment.
tldr: This no longer works after the refactor of SortPreservingMergeStream because peak_bytes reports 0.
SortPreservingMergeStream's reservation is unregistered from the memory pool before the test reads .metrics(), because of how the stream is now (meaning after #23407 and #23702) implemented.
Before: SortPreservingMergeStream implemented Stream directly (fn poll_next(self: Pin<&mut Self>, ...)). The struct — including its MemoryReservation — was boxed and returned as the SendableRecordBatchStream itself. It stayed alive for exactly as long as the caller held the stream, and was only dropped (triggering unregister()) when the caller dropped it — in the test, after tracking_pool.metrics() was read. Peak was correctly captured.
After: the implementation moved to (merge.rs:225-226):
fn create_stream(mut self) -> impl Stream<Item = Result<RecordBatch>> {
async_try_stream(|mut emitter| async move { /* ...self... */ })
}
self (and everything it owns, including the reservation) is now captured inside the generator's async block. Once that block runs to completion — which happens on the final poll_next() call that returns None, i.e. during the test's draining loop, not when the caller later drops the stream object — self is dropped right there.
There was a problem hiding this comment.
I was wondering where this comment went, I meant to post it in #23606
28a916d to
f3920c8
Compare
|
@kumarUjjawal this is the follow-up to #23606, could you please take a look? |
kumarUjjawal
left a comment
There was a problem hiding this comment.
Thanks you @ariel-miculas
I have left few comments. Let me know what you think.
There was a problem hiding this comment.
we should keep at least the single-column no-round-robin case added by #23606? Removing all three disabled-mode cases drops the only direct peak-memory regression coverage for that fix
There was a problem hiding this comment.
We could keep all three, I removed them since there's no longer any difference in the reservation between round robin tie-breaker enabled / disabled
There was a problem hiding this comment.
I've added back the no round robin test cases
This reverts commit 68f46c3.
|
Thanks for the review! As soon as this is merged I'll rebase #23802 on top of main and the tests should pass. |
|
@kumarUjjawal could you please merge this PR? |
|
Thank you @ariel-miculas |
Fixes: apache#23801 It will only work after apache#23619 is merged, until then test_round_robin_tie_breaker_success will fail: Error: Internal("Rows from RowCursorStream is still in use by consumer") test sorts::sort_preserving_merge::tests::test_round_robin_tie_breaker_success ... FAILED The failure is triggered by prev_cursors from SortPreservingMergeStream keeping the previous Cursor alive for round robin tie breaking purposes. The optimization from apache#23619 only keeps the last Row, so there's no longer a need to keep two Rows cached in ReusableRows.
Which issue does this PR close?
Rationale for this change
See the linked issue.
Note that this PR also contains the changes in #23606, so this will have to be rebased
What changes are included in this PR?
Store only the last row in prev_cursors instead of keeping the entire cursor
Are these changes tested?
Added a test to show the improvement
Are there any user-facing changes?
No