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

   ## Summary
   
   Adds an **opt-in off-heap storage mode for the SSE per-segment group-by 
state** — the group-key
   tables and the fixed-width aggregation result holders — targeting 
high-cardinality group-bys where
   the on-heap maps/arrays drive GC pressure and heap-retention spikes. Default 
**off**; no behavior
   change unless enabled.
   
   - Server config: `pinot.server.query.executor.groupby.offheap` (default 
`false`)
   - Query option: `SET groupByOffHeap = true;` (overrides the server config 
per query, same precedent
     as `numGroupsLimit`)
   - Optional bounded per-thread buffer reuse: 
`pinot.server.query.executor.groupby.offheap.pool.max.bytes.per.thread`
     (default `0` = off)
   
   Group ids remain dense ints in insertion order, so **no 
`AggregationFunction` changes** are needed,
   and there are no wire or storage format changes. Grouping sets stay on-heap.
   
   ## Design
   
   New `pinot-core` package 
`org.apache.pinot.core.query.aggregation.groupby.offheap`, backed by
   `PinotDataBuffer.allocateDirect` (so the bytes show up in the existing 
direct-buffer accounting),
   with absolute-indexed direct `ByteBuffer` views on all hot paths 
(transparent wrapper fallback for
   structures beyond the 2GB view limit — exercised in tests and at 100M 
groups):
   
   | Structure | Replaces | Layout |
   |---|---|---|
   | `OffHeapIntGroupIdMap` | `IntGroupIdMap` (dict tier), raw INT/FLOAT 
fastutil maps | 8-byte slots `[key+1][groupId]`, LF 0.5, linear probing, 
out-of-band `-1` key |
   | `OffHeapLongGroupIdMap` | `Long2IntOpenHashMap` (dict long tier, raw 
LONG/DOUBLE, packed two-int keys) | 16-byte slots, zero key out-of-band |
   | `OffHeapBytesGroupIdMap` | `Object2IntOpenHashMap<String/…>` (raw 
STRING/BYTES/BIG_DECIMAL, multi-column packed keys) | DuckDB-style two-part 
table: 8-byte directory entries (16-bit salt \| 48-bit payload offset) over 
append-only 256KB payload chunks storing `[hash][groupId][keyLen][key bytes]`; 
the stored hash makes directory resize free of key reads |
   | `OffHeapDouble/Long/IntGroupByResultHolder` | the on-heap array holders | 
fixed-width direct memory, identical semantics (defaults, `ensureCapacity` 
growth) |
   
   Doubles/floats are keyed via `doubleToLongBits`/`floatToIntBits` for exact 
fastutil parity (NaN
   collapse, ±0.0 distinct). Strings are encoded with an inline UTF-8 encoder 
byte-identical to
   `String.getBytes(UTF_8)` (including surrogate handling), so keys 
hash/compare identically to the
   on-heap path.
   
   **Lifecycle**: a `ResourceTrackingGroupKeyGenerator` wraps the generator and 
owns every off-heap
   resource, so the existing generator-`close()` call sites release all direct 
memory — including the
   `FilteredGroupByOperator` shared-generator case. This PR also hardens close 
paths that previously
   leaked on exceptions (guards in 
`GroupByOperator`/`FilteredGroupByOperator`/`DefaultGroupByExecutor`,
   widened `finally` coverage in both combine operators, and a drain for 
abandoned queued blocks in the
   streaming combine — gated to off-heap blocks, because on-heap generator 
`close()` trims a per-worker
   thread-local map and must not run cross-thread).
   
   **Buffer pool**: `OffHeapGroupByBufferPool` optionally caches freed buffers 
per thread with exact-size
   free lists under a byte cap, mirroring the on-heap thread-local map reuse 
across queries; pooled bytes
   stay visible in direct-buffer usage, and structures zero-fill on acquire so 
dirty reuse is safe.
   
   ## Benchmarks
   
   New in `pinot-perf`: `BenchmarkOffHeapGroupBySSE` (5 tiers × flag), 
`BenchmarkOffHeapGroupByLargeSSE`
   (1M groups), `BenchmarkOffHeapGroupByHugeSSE` (10M groups, segment phase), 
`BenchmarkOffHeapGroupIdMaps`
   (micro), and `OffHeapGroupByMemoryFootprint` (deterministic 
retained-heap/direct harness, up to 100M
   groups). Measured on an M-series Mac, JDK 25, fixed heaps, `-prof gc`:
   
   **~80K groups per segment (cache-resident, the off-heap worst case)** — 
latency `RAW_MULTI` −20%,
   other tiers +10–22% premium; allocation −13..−27%; GC counts −10..−40%. This 
premium is why the
   feature is a per-query/per-server opt-in.
   
   **1M groups (latency inverts at scale)** — segment phase `DICT_INT` −12% (49 
vs 56ms, alloc
   65MB→0.3MB/op); full query `DICT_INT` −50% (160 vs 323ms), `RAW_STRING` −21% 
(515 vs 648ms, GC time
   in measurement 3057→20ms). On-heap error bars were ±80–250ms (GC-driven) vs 
±2–6ms off-heap.
   
   **10M groups (segment phase)** — `DICT_INT` −15% (657 vs 770ms, alloc 
519MB→0.4MB/op, GC 67→0ms);
   `RAW_STRING` latency parity with GC time 2946→45ms.
   
   **Retained memory for the per-segment state (map + 2 holders, GC-forced)**:
   
   | Scale | On-heap | Off-heap |
   |---|---|---|
   | int, 4M groups | 136MB heap | 0 heap / 125MB direct |
   | string, 4M groups | 389MB heap | 0.2MB heap / 298MB direct |
   | int, 100M groups | 2.57GB heap | ~0 heap / 3.57GB direct, lookups 1.9× 
faster |
   | string, 100M groups | 8.7GB heap, build 146s, lookup 124s (GC-bound) | 
4.4MB heap / 8.2GB direct, build 39.5s (3.7×), lookup 31.8s (3.9×), 0 GC |
   
   (The int-100M direct total is larger than on-heap at that exact count — LF 
0.5 vs 0.75 on a pow2
   boundary; the point of the mode is *where* the bytes live, not always fewer 
of them.)
   
   ## Included fix (first commit, affects the default on-heap path)
   
   `NoDictionarySingleColumnGroupKeyGenerator` keeps the null group **outside** 
its key map for
   primitive types but did not count it in `getNumKeys()` / 
`getCurrentGroupKeyUpperBound()`. Since the
   null group takes the *next* dense id, the upper bound could equal an issued 
id — a latent
   result-holder under-sizing (AIOOBE) and wrong `numKeys` bookkeeping wherever 
those counts are used
   (e.g. `TableResizer` trim decisions). Split into its own commit so it can be 
evaluated independently.
   
   ## Testing
   
   - `OffHeapGroupByQueriesTest` — end-to-end differential battery running 
every query with the flag on
     and off and comparing results row-for-row, with per-query direct-memory 
leak assertions.
   - `OffHeapGroupKeyGeneratorParityTest` — generator-level parity incl. 
null-group id bookkeeping.
   - Per-structure unit tests for the three maps, three holders, pool, and 
UTF-8 encoder (differential
     vs the JDK, incl. surrogates), plus forced wrapper-fallback (>2GB view) 
runs for every structure.
   - `StreamingGroupByCombineOperatorTest` drain tests pinning the 
abandoned-block release behavior.
   - Existing group-by/null-handling/streaming suites: 577+ tests green; 
spotless/checkstyle/license
     clean on `pinot-spi`, `pinot-common`, `pinot-core`, `pinot-perf`.
   
   ## Follow-ups (out of scope here)
   
   - Off-heap `IndexedTable` for the cross-segment combine (dominates at 10M+ 
groups; the current
     combine is unchanged and mode-independent).
   - MSE group-by operator support.
   - ThreadAccountant integration for off-heap bytes (per-query budgeting) and 
a server gauge for
     pooled bytes.
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


-- 
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