Skip to content

Commit c397865

Browse files
committed
fix(reader): replace slab with per-chunk Arena to fix OOB on large layouts
SlicingAllocator on a fixed 4 MB slab failed two ways: 1. Whole-file dict layouts (e.g. symbol column) needed a single 20 MB allocation → immediate OOB on first chunk. 2. Slab reset between chunks (prior fix) made previous chunk's MemorySegments point to overwritten memory. Replace with Arena.ofConfined() per chunk: previous arena is closed before decoding the next chunk; last arena is closed on natural exhaustion or explicit close(). Data is valid until the next hasNext() call — the intended API contract.
1 parent 19e147f commit c397865

4 files changed

Lines changed: 44 additions & 17 deletions

File tree

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Pure-Java reader/writer for the [Vortex](https://github.com/spiraldb/vortex) col
1111
- Pure-Java reader for primitive, sequence, ALP, dict, FSST (stable)
1212
- Local (mmap) or Remote (HTTPS, single read of last 65K) (stable)
1313
- Writer: in progress
14-
- Benchmark vs Rust+JNI: in progress
14+
- Benchmark vs Rust+JNI: Java beats JNI 1.5×–6.5× across read/write workloads (see Benchmarks)
1515
- Full encoding coverage: in progress
1616
- Vectorized decode paths (Panama Vector API): planned
1717
- Iceberg/Spark/Flink integration: not available yet
@@ -81,6 +81,32 @@ file occupies. Decoding reads bytes directly from that view with no copies, no i
8181
Arrow format, and no boundary crossings. The JIT sees the full decode path as ordinary Java
8282
bytecode.
8383

84+
## Benchmarks
85+
86+
JMH throughput (ops/s = full-file scans per second). Higher is better.
87+
88+
**Environment:** Apple M5, OpenJDK 27-jep401ea3 (Valhalla EA), 3 warmup × 3 s, 5 measurement × 5 s, fork 1.
89+
90+
### OHLC read — 10 M rows, 58.9 MB (Rust-written file, single-column projection)
91+
92+
| Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
93+
|----------------|------------------|------------------|--------------|
94+
| close (F64/ALP)| 76.4 ± 1.6 | 50.8 ± 2.2 | **1.5×** |
95+
| volume (I64) | 128.6 ± 1.5 | 52.3 ± 1.1 | **2.5×** |
96+
| symbol (varbin)| 63.4 ± 21.6 | 9.8 ± 1.4 | **6.5×** |
97+
98+
### OHLC write — 10 M rows
99+
100+
| Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
101+
|-----------|--------------|------------------|--------------|
102+
| write | 4.6 ± 0.6 | 0.7 ± 0.0 | **6.5×** |
103+
104+
### Big-file scan — 100 M rows × 4 I64 columns, ~3 GB (Rust-written file, all columns)
105+
106+
| Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
107+
|-----------|--------------|------------------|--------------|
108+
| scan | 20.3 ± 1.0 | 5.7 ± 0.2 | **3.6×** |
109+
84110
## Design principles
85111

86112
- Zero-copy everywhere

reader/src/main/java/io/github/dfa1/vortex/io/VortexHttpReader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ public MemorySegment slice(long offset, long length) {
174174

175175
@Override
176176
public ScanIterator scan(ScanOptions options) {
177-
return new ScanIterator(this, options, arena);
177+
return new ScanIterator(this, options);
178178
}
179179

180180
@Override

reader/src/main/java/io/github/dfa1/vortex/io/VortexReader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public EncodingRegistry registry() {
144144

145145
@Override
146146
public ScanIterator scan(ScanOptions options) {
147-
return new ScanIterator(this, options, this.arena);
147+
return new ScanIterator(this, options);
148148
}
149149

150150
/// Zero-copy slice of the memory-mapped file.

reader/src/main/java/io/github/dfa1/vortex/scan/ScanIterator.java

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,9 @@ public final class ScanIterator implements AutoCloseable {
4747
private static final ValueLayout.OfInt LE_INT = ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
4848
private static final ValueLayout.OfLong LE_LONG = ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
4949

50-
private static final long SLAB_SIZE = 4 * 1024 * 1024L;
51-
5250
private final VortexHandle file;
5351
private final ScanOptions options;
54-
private final Arena arena;
55-
private MemorySegment chunkSlab;
56-
private SegmentAllocator chunkAlloc;
52+
private Arena chunkArena;
5753

5854
private List<ChunkSpec> chunks;
5955
private Map<String, DType> columnDtypes;
@@ -63,10 +59,9 @@ public final class ScanIterator implements AutoCloseable {
6359
private long rowsReturned;
6460
private ScanResult current;
6561

66-
public ScanIterator(VortexHandle file, ScanOptions options, Arena arena) {
62+
public ScanIterator(VortexHandle file, ScanOptions options) {
6763
this.file = file;
6864
this.options = options;
69-
this.arena = arena;
7065
}
7166

7267
private static void collectFlats(Layout layout, List<Layout> out) {
@@ -134,11 +129,15 @@ public boolean hasNext() {
134129
continue;
135130
}
136131

137-
chunkAlloc = SegmentAllocator.slicingAllocator(chunkSlab);
132+
if (chunkArena != null) {
133+
chunkArena.close();
134+
}
135+
chunkArena = Arena.ofConfined();
138136
current = new ScanResult(chunk.rowCount(), buildColumnMap(chunk));
139137
rowsReturned += chunk.rowCount();
140138
return true;
141139
}
140+
close();
142141
return false;
143142
}
144143

@@ -153,6 +152,10 @@ public ScanResult next() {
153152

154153
@Override
155154
public void close() {
155+
if (chunkArena != null) {
156+
chunkArena.close();
157+
chunkArena = null;
158+
}
156159
}
157160

158161
// ── Flat segment decoding ─────────────────────────────────────────────────
@@ -187,8 +190,6 @@ private void initialize() {
187190
chunks = buildChunks(columnFlats);
188191
projectedNames = List.copyOf(columnDtypes.keySet());
189192
projectedDtypes = List.copyOf(columnDtypes.values());
190-
chunkSlab = arena.allocate(SLAB_SIZE, 64);
191-
chunkAlloc = SegmentAllocator.slicingAllocator(chunkSlab);
192193
}
193194

194195
// ── Zone-map pruning ──────────────────────────────────────────────────────
@@ -200,20 +201,20 @@ private Map<String, Array> buildColumnMap(ChunkSpec chunk) {
200201
Layout[] layouts = chunk.columnLayouts();
201202
int n = projectedNames.size();
202203
if (n == 1) {
203-
Array arr = decodeLayout(layouts[0], projectedDtypes.get(0), chunkAlloc);
204+
Array arr = decodeLayout(layouts[0], projectedDtypes.get(0), chunkArena);
204205
if (arr instanceof StructArray sa) {
205206
return expandStruct(sa);
206207
}
207208
return Map.of(projectedNames.get(0), arr);
208209
}
209210
if (n == 2) {
210211
return Map.of(
211-
projectedNames.get(0), decodeLayout(layouts[0], projectedDtypes.get(0), chunkAlloc),
212-
projectedNames.get(1), decodeLayout(layouts[1], projectedDtypes.get(1), chunkAlloc));
212+
projectedNames.get(0), decodeLayout(layouts[0], projectedDtypes.get(0), chunkArena),
213+
projectedNames.get(1), decodeLayout(layouts[1], projectedDtypes.get(1), chunkArena));
213214
}
214215
var scratch = new LinkedHashMap<String, Array>(n);
215216
for (int i = 0; i < n; i++) {
216-
scratch.put(projectedNames.get(i), decodeLayout(layouts[i], projectedDtypes.get(i), chunkAlloc));
217+
scratch.put(projectedNames.get(i), decodeLayout(layouts[i], projectedDtypes.get(i), chunkArena));
217218
}
218219
return Map.copyOf(scratch);
219220
}

0 commit comments

Comments
 (0)