Repository navigation
refactor(core): drain and size the sort-shuffle buffer through BufferedBatches - #2579
NoahKusaba wants to merge 10 commits into
Conversation
…edBatches The sort-shuffle writer's spill and final encode each took the buffer's batches and index lists apart and rebuilt the `PartitionedBatchIterator` loop themselves, and the writer estimated the buffer's memory by hand: input batch size plus index capacity growth, accumulated per batch. `BufferedBatches` now owns both: - `drain(f)` passes each output batch of at most `batch_size` rows to `f` with its partition, one at a time, and empties the buffer. Spill and encode just call it. - `memory_size()` returns the buffered batches plus the index lists' allocated capacity, kept as a running total as the lists grow, and the writer mirrors it with `try_resize`. This is the same number the writer computed before, so spilling behaves as before. No change to what is written or when the writer spills. API change: `BufferedBatches::new` takes the batch size; `take`, `indices_for`, `indices_allocated_size`, `num_buffered_rows` and `BufferedTake` are replaced by `drain` and `memory_size`.
andygrove
left a comment
There was a problem hiding this comment.
Thanks @NoahKusaba, this is a nice cleanup. Having encode and spill share one gather loop reads much better, and BufferedBatches is the right home for the memory accounting. I checked that memory_size() matches the old number. extend grows the index lists exactly like the old reserve and push loop, and try_resize ends up at the same reservation the try_grow calls added up to, so spill decisions don't change. The encoded bytes are identical too.
I can't approve it as is though, because it raises memory use in encode_buffered_partitions. Every output partition's IPC writer now stays open until the drain finishes, and with zstd each one holds its own compression context. At 200 output partitions that's about 140 MB more per input partition, and the memory pool never sees any of it. A refactor that's meant to be a no-op shouldn't change the memory profile, so this needs to go back to one open writer at a time. Details inline, plus a couple of smaller things.
Since this is on the shuffle write path, could you also share a before/after against main once that's fixed? The sort_shuffle bench is a quick option and covers both encode and spill at 200 partitions:
git checkout main
cargo bench -p ballista-benchmarks --bench sort_shuffle -- --save-baseline main
git checkout refactor/sort-shuffle-buffer-drain
cargo bench -p ballista-benchmarks --bench sort_shuffle -- --baseline mainIf you have TPC-H SF10 handy, a full run from benchmarks/ would be great too:
cargo run --release --bin tpch -- benchmark ballista \
--host localhost --port 50050 \
--iterations 3 --path <tpch parquet dir> --format parquetNo need for an EXPLAIN diff since the plan doesn't change.
`drain` now calls `f` once per non-empty partition with an iterator over its batches, so the encode creates, fills and finishes one `StreamWriter` per partition, as before this refactor, instead of keeping every partition's writer (and, with zstd, its compression context) open until the drain ends. Empty partitions are skipped again. Also tests `memory_size` across an index-list reallocation, and notes in the 56.0.0 upgrade guide that `BufferedBatches` is no longer public.
|
Peak RSS growth during the write, zstd, one 8192-row batch per partition:
I also wrote the same 37-partition shuffle (40 batches with a string column)
No regression. The branch does save a little work: I skipped the TPC-H run since the plan and the output bytes don't change, but |
`PartitionedBatchIterator::new` collected a `Vec<&RecordBatch>` over every buffered batch for each partition, though the refs are the same for all of them. `drain` now builds them once and passes the slice to each iterator.
…le-buffer-drain # Conflicts: # docs/source/upgrading/56.0.0.md
andygrove
left a comment
There was a problem hiding this comment.
Disclaimer: this review was generated by an LLM (Claude Code) at my request, against e01542e0. File/line references were checked against the code, but please treat the conclusions as suggestions and push back where it's wrong.
Thanks @NoahKusaba, everything from the last round is addressed. Encode is back to one open StreamWriter at a time, and your RSS numbers match main. Empty partitions are skipped, the batch refs are built once per drain, and the upgrade note and the reallocation test are in. Thanks for running the benchmarks too. One optional suggestion inline.
|
Note on the "Retrigger CI" commit (d273b17): the failure on 47fc2f9 was a flaky chaos test, not this change.
The executor logs show both executors got the same Flight port ( This is a race in the test harness. This PR doesn't touch the chaos harness or executor startup, and the same suite passed on the rerun. |
…le-buffer-drain # Conflicts: # docs/source/upgrading/56.0.0.md
Which issue does this PR close?
Part of #2578. First of three PRs; no behaviour change.
Rationale for this change
The writer works on
BufferedBatches' internals in three places: encode andspill each rebuild the same gather loop, and the input loop tracks the
buffer's memory by hand. Moving both into
BufferedBatcheskeeps the gatherand the memory accounting in one place, behind a small interface (
push_batch,drain,memory_size,is_empty).#2578's follow-up then adds its second buffer layout inside
BufferedBatches,without touching the writer's encode, spill or memory accounting.
What changes are included in this PR?
drain(f)empties the buffer, passing each output batch (at mostbatch_sizerows) and its partition tof. Encode and spill call it.memory_size()returns the buffered batches' size plus the index lists'capacity, kept as a running total. The writer passes it to
try_resize.newtakesbatch_size.memory_size()equals the number the writer computed before, so every spilldecision, and every output batch, is unchanged.
Are there any user-facing changes?
No behaviour change.
BufferedBatcheswas re-exported fromsort_shufflebut has no users outside the writer, so it is now crate-private. Its API
changes:
new(num_partitions, schema)→new(num_partitions, schema, batch_size)take(),BufferedTake,indices_for(),indices_allocated_size()andnum_buffered_rows()are replaced bydrain()andmemory_size().How was this tested?
cargo test -p ballista-core -p ballista-executor -p ballista, clippy andfmt are clean.