fix: report native shuffle write metrics accurately - #5370
Open
sunchao wants to merge 2 commits into
Open
Conversation
sunchao
marked this pull request as ready for review
August 15, 2026 21:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why are the changes needed?
When a shuffle becomes too large to keep in memory, Spark spills intermediate data to local disk. Operators use the Spark UI to answer two different questions about that work: how much memory was occupied by the data that spilled, and how much data was actually written to disk. Spark intentionally exposes these as separate task metrics because the on-disk representation is compressed and is often much smaller than the in-memory representation.
Comet's native shuffle writer did not preserve that distinction. It copied the same on-disk byte count into both
memoryBytesSpilledanddiskBytesSpilled. Worse, its disk counter considered only bytes returned by immediate writes, even though Arrow's batch coalescer can hold those bytes untilflush(). In that common case, a real spill could appear as zero memory spilled and zero disk spilled in the Spark UI.For example, suppose a task spills 256 MiB of in-memory shuffle data and partition indices, which compress to 32 MiB on disk. These numbers are illustrative:
The failure case matters especially for debugging: a task that runs out of disk, encounters bad input, or fails after several spills should not look as though it never spilled at all.
Accurate in-memory accounting also has an Arrow-specific complication. One input batch can be split into many zero-copy slices that all reference the same underlying allocation. The regression test uses 16,384
Int64values, backed by one 128 KiB allocation, and processes them as 16 slices of 1,024 rows. If every slice triggers a spill, simply summing each released memory reservation counts that same 128 KiB allocation 16 times: 2 MiB instead of 128 KiB, before even including the real per-slice partition-index allocations. The underlying allocation remains owned by the original input batch throughout the operation, so it must be counted once for that batch, not once per slice.Finally, native shuffle already measures the time spent interleaving rows into output partitions, but that work was not visible in the SQL exchange metrics. This made it harder to distinguish repartitioning overhead from encoding and compression overhead when investigating slow shuffle stages.
Part of #3996. This PR addresses native shuffle write observability only; shuffle reads and mixed native/JVM scan-input accounting remain outside its scope.
What changes were proposed in this PR?
The change gives native shuffle spill accounting two independent sources of truth and carries them consistently into both of Spark's observability surfaces.
For disk usage, the source of truth is the physical spill file itself. The native writer observes the file position before a spill and again after buffered Arrow data has been flushed. Their difference is the actual compressed number of bytes written, including data produced only during the final flush and data appended to an existing spill file.
For memory usage, the source of truth is the in-memory shuffle state being spilled: Arrow backing allocations plus the partition-index structures that organize its rows. The native writer records the memory released by the spill and also includes the current batch when the memory pool rejected its reservation after that batch had already been buffered. To avoid turning Arrow's zero-copy slicing into fictional memory growth, it remembers which backing allocations have already been counted for the lifetime of the original input batch. Separate input batches still contribute cumulatively, as Spark's spill metrics require.
Those two measurements are exposed separately in the SQL shuffle exchange and copied into their corresponding Spark task metrics only after the native execution plan has published its final values. The copy runs from a task-completion listener registered before the native iterator; Spark invokes completion listeners in reverse registration order, so native cleanup and final metric publication happen first. Because task-completion listeners also run when an attempt fails or is canceled, the same accounting remains available for the attempts that are most useful to diagnose.
The exchange also surfaces the native partition-interleaving timer, making the main stages of native shuffle work visible alongside the existing repartitioning, encoding, and spill metrics. Existing shuffle output semantics, compression behavior, and scan-input accounting are unchanged.
How was this PR tested?
Focused Rust tests exercise both spill triggers: an explicit maximum buffer size and a memory-pool reservation failure. They compare reported disk spill bytes against the actual spill-file sizes, verify zero spill metrics when no spill occurs, and cover the shared-allocation example above. They also verify that separate outer input batches are counted cumulatively even when they reference the same Arrow backing buffer.
The Spark integration tests validate the complete native-to-Spark reporting path on both Spark 3.5 and Spark 4.0:
ShuffleMapTaskstill reports both memory and disk spill bytes.