gortiz opened a new pull request, #19021:
URL: https://github.com/apache/pinot/pull/19021

   ## Add a Foreign Function & Memory (Panama) based PinotDataBuffer 
implementation
   
   ### Motivation
   
   Pinot's long-indexed (>2 GB) off-heap buffers are currently backed by 
`sun.misc.Unsafe`
   (`UnsafePinotBuffer` + `MmapMemory`, with `chronicle-core` reflection for 
`mmap`). `Unsafe` is
   deprecated and slated for removal, its `mmap` path reflects into 
JDK-internal `sun.nio.ch` classes
   (requiring `--add-opens` and failing without them), and it does not work on 
Windows.
   
   Now that Pinot builds on JDK 25, the Foreign Function & Memory API 
(`java.lang.foreign`, finalized
   in JDK 22) is a first-class replacement: `MemorySegment` supports `long` 
indexing natively, is
   memory-safe, needs no reflection or `--add-opens`, and works on every OS.
   
   This PR adds an **opt-in** FFM-backed `PinotDataBuffer`. **The default is 
unchanged** (`Unsafe`
   wrapped in `SmallWithFallback`); the new factory is selected only via the 
existing
   `pinot.offheap.buffer.factory` config or the `PINOT_BUFFER_LIBRARY` 
environment variable.
   
   ### What's in this PR
   
   - **New module `pinot-segment-spi-foreign`** (Java 25):
     - `ForeignPinotBuffer` — native-order `PinotDataBuffer` over 
`MemorySegment` (`*_UNALIGNED`
       layouts, `asSlice` views, `asByteBuffer` borrowed views, 
`MemorySegment.copy` bulk get/put,
       `force()` flush).
     - `ForeignPinotBufferFactory` — `Arena.ofShared()` for `allocateDirect`,
       `FileChannel.map(mode, offset, size, arena)` for `mapFile` (no 
reflection, no page-alignment
       handling, no `chronicle-core`, long-sized maps directly). Non-native 
order reuses
       `NonNativePinotDataBuffer`.
   - Wired into `pinot-bom`, the root reactor, and depended on by 
`pinot-segment-local` so it is on the
     server/broker runtime classpath and bundled in the distribution.
   - `pinot-perf` benchmark updates (below).
   
   ### Why a separate module (not `pinot-segment-spi`)
   
   `pinot-segment-spi` is intentionally pinned to Java 11 bytecode because it 
is in the JDBC/Java driver
   dependency closure (`pinot-jdbc-client → pinot-java-client → pinot-common → 
pinot-segment-spi`),
   which must keep running on JDK 11. `java.lang.foreign` requires Java 21+, so 
it cannot live there. A
   dedicated Java 25 module preserves the driver's Java 11 guarantee — the 
dependency edge points *into*
   the new module, never out of the driver closure. (Verified: 
`pinot-segment-spi` bytecode stays major
   version 55 / Java 11; the new module is major version 69 / Java 25.)
   
   ### Zero-copy `ByteBuffer` views preserved (important for RoaringBitmap et 
al.)
   
   `PinotDataBuffer.toDirectByteBuffer(...)` must return a **view**, not a 
copy, of a buffer region — it
   is what lets consumers such as RoaringBitmap read a region *in place* (e.g. 
via
   `viewAsRoaringBitmap`, which wraps the returned `ByteBuffer` in an 
`ImmutableRoaringBitmap`) with no
   allocation on the query path, straight off the mmap'd file pages.
   
   The FFM implementation keeps this zero-copy behavior via 
`MemorySegment.asByteBuffer()`, which
   returns a direct `ByteBuffer` aliasing the same native memory (verified: the 
view is direct, writes
   are visible in both directions, and the `ByteBuffer`'s native address equals 
the segment's). This is
   actually cleaner than the `Unsafe` path, which produces the same view by 
*reflecting* into private
   `DirectByteBuffer` constructors with per-JDK-version fallbacks 
(`ByteBufferUtil`) — all of that
   reflection is unnecessary with FFM. The view is scope-bound ("borrowed"): 
valid while the backing
   `Arena` is alive, and using it after close throws `IllegalStateException` 
instead of `Unsafe`'s
   undefined behavior. A dedicated test 
(`testToDirectByteBufferIsZeroCopyView`) locks this in.
   
   ### Backward compatibility
   
   - **No behavioral change by default.** No public API changes. Opt-in factory 
FQN unchanged, so
     existing `pinot.offheap.buffer.factory` / `PINOT_BUFFER_LIBRARY` values 
keep working.
   
   ### Performance
   
   JMH, macOS / JDK 25, single fork — representative results (numbers are from 
a developer laptop and
   have run-to-run variance; the takeaways, not the exact figures, are the 
point). **Bottom line:
   read/write is at parity with `Unsafe` on both micro and end-to-end query 
benchmarks; the only
   regression is one-time allocation, which only affects the >2 GB path and 
amortizes to zero for
   long-lived segment buffers.**
   
   **1. Read/write hot path (`BenchmarkPinotDataBuffer`, ns/op, value length 
1024):**
   
   | op | unsafe | foreign | bytebuffer |
   |----|-------:|--------:|-----------:|
   | batchRead   | 29.6 | 30.0 | 29.6 |
   | batchWrite  | 22.9 | 22.3 | 22.6 |
   | nonBatchRead  | 230 | 192 | 26 |
   | nonBatchWrite | 210 | 184 | 27 |
   
   FFM matches `Unsafe` on bulk copy and is a bit faster on per-element access. 
(`ByteBuffer` is faster
   still for per-element reads, but is capped at 2 GB — it cannot back the 
buffers this factory exists
   for.)
   
   **2. Bulk copy required an override.** With the base 
`copyTo`/`readFrom(byte[])` (which allocates a
   throwaway `ByteBuffer` per call), foreign `batchRead(1024)` was **41.5 
ns/op**; overriding them with
   `MemorySegment.copy` brought it to **30.0 ns/op** — parity with `Unsafe`.
   
   **3. Allocation is slower — profiled to explain why.** Isolated 
allocate-vs-close, 1 MB buffer, per
   op:
   
   | | allocate | close |
   |-|---------:|------:|
   | foreign (FFM)         | 14.6 µs | 7.0 µs |
   | unsafe                |  9.5 µs | 0.02 µs |
   | unsafe (no zero-fill) |  0.02 µs | 0.02 µs |
   
   The 1 MB zero-fill (~9.5 µs) is paid by **both** (it is essentially all of 
`Unsafe`'s allocate time —
   `malloc` reuses the warm freed block). The foreign gap decomposes into **~7 
µs close** (async-profiler
   shows ~82% of allocate time is `Arena.ofShared().close()`, which performs a 
JVM-wide
   `VM_HandshakeAllThreads` to guarantee no thread is mid-access before 
freeing) plus **~5 µs allocate**
   (constructing the `MemorySegment` + shared `MemorySessionImpl` + `Cleaner` 
registration). For tiny
   buffers the gap is larger relatively (~6.4 µs vs ~0.28 µs) as there is no 
zero-fill to amortize
   against. This is a **one-time, per-buffer cost** on the >2 GB path only (the 
default
   `SmallWithFallback` routing keeps ≤2 GB buffers on `ByteBuffer`), where 
buffers are long-lived and
   allocated/freed once — so it does not surface on the query path.
   
   **4. End-to-end queries (`BenchmarkQueriesSSQE`, 4M rows across mmap'd 
segments):** `foreign`,
   `unsafe`, and `bytebuffer` are all within run-to-run noise of each other 
across scan-heavy and
   group-by queries — confirming no query-time regression, since segments are 
mmap'd once and read many
   times.
   
   ### Testing
   
   - New `ForeignMemoryPinotDataBufferTest` runs the full existing 
`PinotDataBufferTest` contract
     (including the >2 GB `LARGE_BUFFER_SIZE` allocate/loadFile/mapFile cases 
in both byte orders), plus
     FFM-specific tests: zero-copy `ByteBuffer` view semantics, use-after-close 
fails deterministically
     (`IllegalStateException`), `long`-offset addressing beyond 2³¹, and 
read-only-mapped writes
     rejected (`IllegalArgumentException`). 19 tests, all passing on a clean 
build.
   - `pinot-perf`: `BenchmarkPinotDataBuffer` gains `foreign`/`wrapper+foreign` 
options;
     `BenchmarkQueriesSSQE` gains an opt-in `_bufferLibrary` parameter 
(defaults to `default`, so the
     standard matrix is unaffected). Both benchmark forks now pass the standard 
`--add-opens` set so the
     `Unsafe` factory (which needs them for its `mmap` path) can be benchmarked.
   
   ### Notes
   
   - Requires a JVM ≥ 21 to load (Java 25 bytecode); only reachable when 
explicitly selected, and Pinot
     services run on JDK 25.
   - No new JVM flags are required for the FFM path (`Arena` / `MemorySegment` 
/ `FileChannel.map` are
     not restricted methods).
   
   release-notes: Add an opt-in `java.lang.foreign` (Panama) based off-heap 
buffer implementation,
   selectable via `pinot.offheap.buffer.factory` / `PINOT_BUFFER_LIBRARY`. The 
default (Unsafe/ByteBuffer)
   is unchanged.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to