You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Wide nested scans hold GiBs of native memory: tiny misaligned splits each decode whole column chunks #11123
A JNI scan of one wide, nested Vortex file holds far more native memory than the Arrow batch Java has open. With list layout it is GiBs. Several scans per JVM, as a multi-core Spark executor runs, are enough to OOM-kill executors.
Measured with a single-threaded reader that loads one batch at a time and closes it before loading the next, as VortexPartitionReader does. Live native bytes come from a counting allocator:
File
Splits
Smallest split
Peak live native
180 MB, list layout
139
70 rows
3.57 GiB
111 MB, default layout
59
—
224 MiB
One 176-row output array referenced 42 MB.
Cause:
A scan splits a file wherever any column's chunk ends.
A split decodes the whole chunk of every column it overlaps.
When a struct's fields chunk at unrelated rows, as list elements and dictionary-encoded columns of a wide nested file do, the union of their boundaries leaves spans of a few dozen rows.
Successive splits then each decode the same chunks of the other fields again.
I expected splits to be no narrower than the chunks they decode, so memory stays close to the batches being read.
Steps to reproduce
Write a struct column whose fields chunk at different rows. For example, write two fields with RepartitionStrategy, one chunked every 1,000 rows and the other every 3,000.
Scan it with SplitBy::LayoutSubSplitting (the default).
Collect the splits: there is one at every boundary of either field, so most splits are much narrower than either field's chunks.
On a real file: write a few hundred leaf columns, with lists of structs several levels deep, using list layout. Then scan it through DataSource.open → scan(ScanOptions) → Partition.scanArrow, closing each batch before loading the next.
Environment
Vortex: develop (measured on a fork of develop that has the same scan code)
Entry point: Java JNI scan, also used by the Spark connector
Java 17, macOS (aarch64)
Additional context
What else was ruled out:
Not a leak across files. Scanning the same file six times in one process reaches the same peak each time.
Not batches in flight to Java.NativeRuntime.setWorkerThreads at 4, 1 and 0 barely moves the peak.
Not read-ahead I/O.readConcurrency at 0, 16, 8 and 2 leaves the peak unchanged.
The default 4 × available_parallelism() splits in flight makes this much worse: each of them holds whole chunks. #11124 proposes a per-scan cap for that.
I have a fix ready. The struct reader marks boundaries that only some of its fields have as soft, and LayoutSubSplitting merges spans that end at a soft boundary up to 8,192 rows. On the 180 MB file this cuts peak live memory from 3.57 to 1.10 GiB and scan time from 1.7 to 0.6 s.
Activity