Skip to content

Commit a50ff9f

Browse files
committed
manual review of README.md
1 parent e1b0d5f commit a50ff9f

2 files changed

Lines changed: 141 additions & 148 deletions

File tree

README.md

Lines changed: 139 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,38 @@
11
# vortex-java
22

33
[![CI](https://github.com/dfa1/vortex-java/actions/workflows/ci.yml/badge.svg)](https://github.com/dfa1/vortex-java/actions)
4+
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/license/Apache-2.0)
45

56
> **Alpha** — not production-ready. APIs will change without notice.
67
78
Pure-Java reader/writer for the [Vortex](https://github.com/spiraldb/vortex) columnar file format.
89

9-
Vortex is a shared open format with multiple independent implementations (Rust, Go, Java).
10-
Files written by any implementation are readable by all others — no vendor lock-in, no
11-
format translation at the boundary.
10+
From (https://vortex.dev/blog/btrblocks-compressor):
11+
> We've written about individual compression codecs in Vortex before: FastLanes for bit-packing integers, FSST for strings, and ALP for floating point. But we've never explained how Vortex decides which codec to use for a given column, or how it layers multiple codecs on top of each other.
1212
13-
## Status
13+
> On TPC-H at scale factor 10, Vortex files are 38% smaller and decompress 10–25x faster than Parquet with ZSTD, without using any general-purpose compression. The difference comes down to how the codecs are selected and composed, a framework inspired by the BtrBlocks paper from TU Munich.
1414
15-
- Pure-Java reader for primitive, sequence, ALP, dict, FSST (stable)
16-
- Local (mmap) or Remote (HTTPS, single read of last 65K) (stable)
17-
- Writer: in progress
18-
- Benchmark vs Rust+JNI: Java beats JNI 1.5×–11.5× across read/write workloads (see Benchmarks)
19-
- **File size trade-off:** Java-written files are larger than Rust-written files (up to ~
20-
with `cascading(3)`). The Rust writer applies more compression passes; the Java writer
21-
covers ALP, bitpacking, and FSST but not the full Rust encoding set yet. Files are still
22-
significantly smaller than CSV. Cross-implementation verified by `FileSizeComparisonIntegrationTest`.
23-
- Full encoding coverage: in progress
24-
- Vectorized decode paths (Panama Vector API): planned
25-
- Iceberg/Spark/Flink integration: not available yet
15+
🎓 Maximilian Kuschewski, David Sauerwein, Adnan Alhomssi, and Viktor Leis. 2023. BtrBlocks: Efficient Columnar Compression for Data Lakes. Proc. ACM Manag. Data 1, 2, Article 118 (June 2023). > https://doi.org/10.1145/3589263
16+
17+
> The core idea: don't pick one codec. Try them all, and let the data decide.
2618
2719
## Motivation
2820

21+
This is a third-party implementation with the idea that files written by any implementation
22+
are readable by all others — no vendor lock-in, no format translation at the boundary.
23+
24+
| Project | Language | Notes |
25+
|-------------------------------------------------------------|----------|---------------------------------------------------------------------|
26+
| [spiraldb/vortex](https://github.com/spiraldb/vortex) | Rust | Reference implementation + JNI bindings |
27+
| [spiraldb/vortex-go](https://github.com/spiraldb/vortex-go) | Go | Pure-language port
28+
2929
The official Vortex ecosystem provides JVM bindings via JNI (bundled native `.so`/`.dylib`).
3030
JNI bindings are fast but add deployment friction: platform-specific artifacts, native build
3131
toolchains, and crash-domain coupling between the JVM and native code.
3232

3333
This library takes a different approach — 100% Java, no JNI, no `sun.misc.Unsafe`.
34-
It uses the Java FFM API (`MemorySegment` / `Arena`, Java 25+) for zero-copy memory-mapped reads, making it easier to:
34+
It uses the Java FFM API (`MemorySegment` / `Arena`, Java 25+) for zero-copy memory-mapped reads,
35+
making it easier to:
3536

3637
- embed in any JVM project without native-library management
3738
- build and test on any platform with a standard JDK
@@ -44,136 +45,6 @@ It uses the Java FFM API (`MemorySegment` / `Arena`, Java 25+) for zero-copy mem
4445
- Anyone who wants mmap‑backed, zero‑copy columnar reads without first decompressing
4546
the whole file (or row chunk)
4647

47-
### Why fewer layers = faster
48-
49-
```
50-
vortex-jni vortex-java
51-
────────────────────────────── ──────────────────────────
52-
┌──────────────────────────┐ ┌──────────────────────┐
53-
│ Java App │ │ Java App │
54-
│ (BigIntVector.get(i)) │ │ (buffer.getAtIndex) │
55-
└────────────┬─────────────┘ └──────────┬───────────┘
56-
│ Arrow Java API │ FFM API
57-
┌────────────▼─────────────┐ │ (MemorySegment,
58-
│ Apache Arrow (Java) │ │ zero-copy slice)
59-
│ VectorSchemaRoot, │ │
60-
│ BigIntVector, … │ │
61-
└────────────┬─────────────┘ ┌──────────▼───────────┐
62-
│ Arrow C Data Interface │ OS mmap region │
63-
│ (ArrowArray/ArrowSchema) │ (file on disk) │
64-
│ + JNI boundary crossing └──────────────────────┘
65-
┌────────────▼─────────────┐
66-
│ Native lib │
67-
│ (.so / .dylib) │
68-
│ Rust decode │
69-
└────────────┬─────────────┘
70-
│ mmap / read
71-
┌────────────▼─────────────┐
72-
│ OS mmap region │
73-
│ (file on disk) │
74-
└──────────────────────────┘
75-
76-
4 layers, 1 JNI crossing, 2 layers, 0 boundary crossings,
77-
Arrow C Data Interface overhead no intermediate format
78-
```
79-
80-
The JNI path pays three costs per batch: (1) a JNI boundary crossing to call into native
81-
code, (2) the Arrow C Data Interface handshake to pass decoded buffers back to the JVM as
82-
`ArrowArray`/`ArrowSchema` structs, and (3) materialising the result into Apache Arrow
83-
`VectorSchemaRoot` objects before the application can read a single value. The JIT cannot
84-
inline or optimise across the JNI boundary.
85-
86-
`vortex-java` eliminates all of that. The FFM API (`MemorySegment`) gives Java code a
87-
typed, bounds-checked view directly into the OS mmap region — the same physical memory the
88-
file occupies. Decoding reads bytes directly from that view with no copies, no intermediate
89-
Arrow format, and no boundary crossings. The JIT sees the full decode path as ordinary Java
90-
bytecode.
91-
92-
## Benchmarks
93-
94-
JMH throughput (ops/s = full-file scans per second). Higher is better.
95-
96-
**Environment:** Apple M5, OpenJDK 27-jep401ea3 (Valhalla EA), 3 warmup × 3 s, 5 measurement × 5 s, fork 1.
97-
98-
### OHLC read — 10 M rows, 58.9 MB (Rust-written file, single-column projection)
99-
100-
| Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
101-
|----------------|------------------|------------------|--------------|
102-
| close (F64/ALP)| 76.7 ± 0.3 | 50.4 ± 2.8 | **1.5×** |
103-
| volume (I64) | 127.9 ± 2.3 | 52.9 ± 0.6 | **2.4×** |
104-
| symbol (varbin)| 110.4 ± 0.4 | 9.6 ± 0.9 | **11.5×** |
105-
106-
### OHLC write — 10 M rows
107-
108-
| Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
109-
|-----------|--------------|------------------|--------------|
110-
| write | 4.4 ± 1.1 | 0.7 ± 0.1 | **6.4×** |
111-
112-
### Big-file scan — 100 M rows × 4 I64 columns, ~3 GB (Rust-written file, all columns)
113-
114-
| Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
115-
|-----------|--------------|------------------|--------------|
116-
| scan | 20.4 ± 0.9 | 5.7 ± 0.6 | **3.6×** |
117-
118-
## Design principles
119-
120-
- Zero-copy everywhere
121-
- No JNI
122-
- No Unsafe -- [FFM vs Unsafe](https://inside.java/2025/06/12/ffm-vs-unsafe/) — Maurizio Cimadamore's deep-dive on why FFM (`MemorySegment`/`Arena`) supersedes `sun.misc.Unsafe`: safety, performance, and the JVM's path forward
123-
- Align with vortex-rust and Vortex-go semantics
124-
- Make the JIT happy (constant layouts, predictable strides, no virtual dispatch in hot loops)
125-
- Prepare for the Vector API / Valhalla
126-
- Rigorous testing: unit tests + property-based testing + cross-language integration tests
127-
128-
### Testing strategy
129-
130-
Unit tests verify internal correctness (encoding round-trips, edge cases), but the format has no
131-
formal specification — the Rust implementation is the ground truth. Unit tests alone miss
132-
cross-language wire-format bugs: Java can round-trip a value internally while writing bytes that
133-
another implementation cannot decode.
134-
135-
The `integration` module addresses this by using the Rust JNI reader as a **test oracle**:
136-
Java writes a file, the Rust reader decodes it, and the values are compared exactly.
137-
[Property-based testing](https://jqwik.net/) (jqwik) generates large, diverse inputs automatically,
138-
covering edge cases no hand-written test would anticipate.
139-
140-
This combination caught two real bugs in ALP floating-point encoding:
141-
- Java selected exponents outside the range Rust's decoder accepts (silent data corruption)
142-
- Java's encode round-trip check used a different floating-point associativity than Rust's decode
143-
(`encoded * (F10[f] * IF10[e])` vs `(encoded * F10[f]) * IF10[e]`), passing values that Rust
144-
decoded differently
145-
146-
Both bugs were invisible to pure-Java tests and would have shipped undetected without the
147-
cross-language oracle.
148-
149-
## Implementations
150-
151-
| Project | Language | Notes |
152-
|-------------------------------------------------------------|----------|---------------------------------------------------------------------|
153-
| [spiraldb/vortex](https://github.com/spiraldb/vortex) | Rust | Reference implementation + JNI bindings |
154-
| [spiraldb/vortex-go](https://github.com/spiraldb/vortex-go) | Go | Pure-language port |
155-
| [dfa1/vortex-java](https://github.com/dfa1/vortex-java) | Java | This project — FFM-based, no JNI, no Unsafe |
156-
157-
All three implementations share the same binary format and can read each other's files.
158-
159-
160-
## Serialization formats
161-
162-
The format uses two serialization libraries for different roles:
163-
164-
| Format | Used for | Why |
165-
|-----------------|--------------------------------------|----------------------------------------------------------------------------------------|
166-
| **FlatBuffers** | Footer, Layout, Array structure | Zero-copy random access — fields read directly from memory-mapped bytes, no allocation |
167-
| **Protobuf** | Codec metadata, DType, Scalar values | Schema evolution and cross-language compatibility for small blobs |
168-
169-
FlatBuffers suit the file-structure layer: the footer is parsed once at open and the layout tree is traversed during
170-
scan — both benefit from direct field access on mapped memory. Protobuf suits codec metadata: tiny blobs parsed once per
171-
chunk, where schema evolution matters more than zero-copy speed.
172-
173-
Replacing protobuf with FlatBuffers is not viable — existing `.vortex` files produced by the Rust reference
174-
implementation embed protobuf bytes in codec metadata blobs, and wire compatibility requires matching the format
175-
exactly.
176-
17748
## Quickstart
17849

17950
Add the library to your build (example, Maven):
@@ -308,6 +179,9 @@ java -jar cli/target/vortex.jar import data/trades.csv
308179
# writes data/trades.vortex, prints size savings
309180
```
310181
182+
183+
# Development
184+
311185
## Requirements
312186
313187
- Java 25+
@@ -340,6 +214,123 @@ java -jar performance/target/benchmarks.jar RustVsJavaWriteBenchmark.javaWrite
340214
java -jar performance/target/benchmarks.jar
341215
```
342216
343-
## License
217+
## Design principles
218+
219+
- Zero-copy everywhere
220+
- No JNI
221+
- No Unsafe -- [FFM vs Unsafe](https://inside.java/2025/06/12/ffm-vs-unsafe/) — Maurizio Cimadamore's deep-dive on why FFM (`MemorySegment`/`Arena`) supersedes `sun.misc.Unsafe`: safety, performance, and the JVM's path forward
222+
- Align with vortex-rust and Vortex-go semantics
223+
- Make the JIT happy (constant layouts, predictable strides, no virtual dispatch in hot loops)
224+
- Prepare for the Vector API / Valhalla
225+
- Rigorous testing: unit tests + property-based testing + cross-language integration tests
226+
- Target Vector API as soon it is available https://openjdk.org/jeps/338
227+
228+
### Testing strategy
229+
230+
Unit tests verify internal correctness (encoding round-trips, edge cases), but the format has no
231+
formal specification — the Rust implementation is the ground truth. Unit tests alone miss
232+
cross-language wire-format bugs: Java can round-trip a value internally while writing bytes that
233+
another implementation cannot decode.
234+
235+
The `integration` module addresses this by using the Rust JNI reader as a **test oracle**:
236+
Java writes a file, the Rust reader decodes it, and the values are compared exactly.
237+
[Property-based testing](https://jqwik.net/) (jqwik) generates large, diverse inputs automatically,
238+
covering edge cases no hand-written test would anticipate.
239+
240+
This combination caught two real bugs in ALP floating-point encoding:
241+
- Java selected exponents outside the range Rust's decoder accepts (silent data corruption)
242+
- Java's encode round-trip check used a different floating-point associativity than Rust's decode
243+
(`encoded * (F10[f] * IF10[e])` vs `(encoded * F10[f]) * IF10[e]`), passing values that Rust
244+
decoded differently
245+
246+
Both bugs were invisible to pure-Java tests and would have shipped undetected without the
247+
cross-language oracle.
248+
249+
## Reference
250+
|
251+
252+
## Benchmarks
253+
254+
JMH throughput (ops/s = full-file scans per second). Higher is better.
255+
256+
**Environment:** Apple M5, OpenJDK 25, 3 warmup × 3 s, 5 measurement × 5 s, fork 1.
257+
258+
### OHLC read — 10 M rows, 58.9 MB (Rust-written file, single-column projection)
259+
260+
| Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
261+
|----------------|------------------|------------------|--------------|
262+
| close (F64/ALP)| 76.7 ± 0.3 | 50.4 ± 2.8 | **1.5×** |
263+
| volume (I64) | 127.9 ± 2.3 | 52.9 ± 0.6 | **2.4×** |
264+
| symbol (varbin)| 110.4 ± 0.4 | 9.6 ± 0.9 | **11.5×** |
265+
266+
### OHLC write — 10 M rows
267+
268+
| Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
269+
|-----------|--------------|------------------|--------------|
270+
| write | 4.4 ± 1.1 | 0.7 ± 0.1 | **6.4×** |
271+
272+
* the Java part is faster but also produces bigger files (there much more work there)
273+
274+
### Big-file scan — 100 M rows × 4 I64 columns, ~3 GB (Rust-written file, all columns)
275+
276+
| Benchmark | Java (ops/s) | JNI/Rust (ops/s) | Java speedup |
277+
|-----------|--------------|------------------|--------------|
278+
| scan | 20.4 ± 0.9 | 5.7 ± 0.6 | **3.6×** |
279+
280+
### Why fewer layers = faster
281+
282+
This is my hypothesis:
283+
```
284+
vortex-jni vortex-java
285+
────────────────────────────── ──────────────────────────
286+
┌──────────────────────────┐ ┌──────────────────────┐
287+
│ Java App │ │ Java App │
288+
│ (BigIntVector.get(i)) │ │ (buffer.getAtIndex) │
289+
└────────────┬─────────────┘ └──────────┬───────────┘
290+
│ Arrow Java API │ FFM API
291+
┌────────────▼─────────────┐ │ (MemorySegment,
292+
│ Apache Arrow (Java) │ │ zero-copy slice)
293+
│ VectorSchemaRoot, │ │
294+
│ BigIntVector, … │ │
295+
└────────────┬─────────────┘ ┌──────────▼───────────┐
296+
│ Arrow C Data Interface │ OS mmap region │
297+
│ (ArrowArray/ArrowSchema) │ (file on disk) │
298+
│ + JNI boundary crossing └──────────────────────┘
299+
┌────────────▼─────────────┐
300+
│ Native lib │
301+
│ (.so / .dylib) │
302+
│ Rust decode │
303+
└────────────┬─────────────┘
304+
│ mmap / read
305+
┌────────────▼─────────────┐
306+
│ OS mmap region │
307+
│ (file on disk) │
308+
└──────────────────────────┘
309+
310+
4 layers, 1 JNI crossing, 2 layers, 0 boundary crossings,
311+
Arrow C Data Interface overhead no intermediate format
312+
```
313+
314+
The JNI path pays three costs per batch: (1) a JNI boundary crossing to call into native
315+
code, (2) the Arrow C Data Interface handshake to pass decoded buffers back to the JVM as
316+
`ArrowArray`/`ArrowSchema` structs, and (3) materialising the result into Apache Arrow
317+
`VectorSchemaRoot` objects before the application can read a single value. The JIT cannot
318+
inline or optimise across the JNI boundary.
319+
320+
`vortex-java` eliminates all of that. The FFM API (`MemorySegment`) gives Java code a
321+
typed, bounds-checked view directly into the OS mmap region — the same physical memory the
322+
file occupies. Decoding reads bytes directly from that view with no copies, no intermediate
323+
Arrow format, and no boundary crossings. The JIT sees the full decode path as ordinary Java
324+
bytecode.
325+
326+
## Contributing
327+
328+
Forks and contributions are welcome! Please feel free to fork the repository and open a pull request.
329+
When submitting a PR, include tests and update documentation where applicable
330+
(follow guidelines in CLAUDE.md).
331+
332+
### AI-assisted development
344333
345-
Apache 2.0
334+
This project uses [Claude Code](https://claude.ai/code) heavily for implementation
335+
work — generating mapping, test generation and documentation.
336+
**Architecture, API design, and all decisions are human-driven**.

TODO.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ written: /var/folders/dq/w0lpx2tj70g0cgv4ckcyth740000gn/T/junit-7248856963330236
3232

3333
## Build
3434

35+
- [ ] add BOM module
36+
- [ ] deploy to maven central
3537
- [ ] drop warnings about flatbuffers
3638
```shell
3739
[WARNING] ****************************************************************************************************************************************************

0 commit comments

Comments
 (0)