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

   ## Summary
   
   This PR stops the single-stage (v1) server from building large responses in 
arrays that grow by doubling. `DataTableBuilderV4` now writes the fixed and 
variable size sections in 16 KB heap pages. `DataTableImplV4` reads these pages 
directly, and `toBytes()` copies them once into one array of the exact final 
size. The wire format does not change, and the broker does not change.
   
   This is the first of three steps:
   
   1. This PR: paged builder sections and an exact-size `toBytes()`. One 
exact-size array replaces four arrays that grow or copy the full response.
   2. Next: send the pages to Netty and gRPC without the final contiguous 
`byte[]`.
   3. Then: check `maxServerResponseSizeBytes` while the response is built, and 
sample the query resource accountant by bytes written.
   
   ## Background
   
   A G1 humongous object is any object of half a region or more: 8 MB with 16 
MB regions. G1 allocates each one in contiguous free regions, and it reclaims 
them only in some collections. Many large humongous allocations in a short time 
can leave G1 with no free regions to evacuate into. Then a young GC fails to 
evacuate and promotes live and dead young objects to the old generation.
   
   A v1 group-by with many `OBJECT` intermediate results (for example 
`DISTINCTCOUNTHLLPLUS`, about 11 KB per group) easily gives responses of 40 MB 
to 80 MB per server. Before this PR, a response of size N went through these 
allocations:
   
   | Step | Allocation |
   |---|---|
   | `BaseDataTableBuilder` variable section (`ByteArrayOutputStream`) | grows 
by doubling, up to 2N |
   | `DataTableBuilderV4.build()` → `toByteArray()` | N (exact copy) |
   | `DataTableImplV4.toBytes()` → new `ByteArrayOutputStream` | grows by 
doubling, up to 2N |
   | `toBytes()` → `toByteArray()` | N (exact copy) |
   
   For N = 67 MB, the peak was about 268 MB of short-lived humongous arrays in 
five allocations. After this PR, the only large array is the exact-size result 
of `toBytes()`. The next PR removes it.
   
   ## Changes
   
   - **`BaseDataTableBuilder`.** The two sections are `PagedPinotOutputStream`s 
with `HeapPageAllocator.createSmall()` (16 KB pages, the same as the 
multi-stage `DataBlockBuilder`). The offset into the variable section uses 
`Math.toIntExact`, so a section larger than 2 GB fails instead of wrapping 
around. The class Javadoc now states that the builder must not get more rows 
after `build()`, because the built table shares its pages.
   - **`DataTableBuilderV4.build()`.** It gives the pages to the table as 
`DataBuffer` views. It does not copy them.
   - **`DataTableImplV4`.**
     - The sections are `DataBuffer`s, and every read uses an absolute offset. 
This follows `BaseDataBlock`, but the array getters read element by element 
with absolute offsets instead of with a stream, because a stream over a paged 
buffer creates a view of every remaining page. The reads no longer move a 
shared `ByteBuffer` position.
     - The deserializing constructor wraps its section arrays in 
`PinotByteBuffer`. It does not add copies.
     - `getCustomObject` still returns a view. It returns a copy only when a 
value crosses a page boundary. On a table built in the same process, the view 
is read-only.
     - `toBytes()` computes the final length first, then writes everything into 
one array. The `RESPONSE_SER_CPU_TIME_NS` and 
`RESPONSE_SER_MEM_ALLOCATED_BYTES` entries are reserved before the length is 
computed and filled in after the copy. Both are `LONG` keys, so their encoded 
length does not depend on the value.
     - The constructor that took two `byte[]` sections now takes two 
`DataBuffer`s. `DataTableBuilderV4` was its only caller.
   - **`CompoundDataBuffer.getBufferIndex`.** It first checks the buffer of the 
previous lookup, then does a binary search. Before, every backward lookup 
scanned from the first buffer. A large table built in the broker merge path 
(`GroupByDataTableReducer`, `AggregationDataTableReducer`) has thousands of 
pages and is read in column and random order, so the scan made a full read 
quadratic.
   
   A builder now allocates two 16 KB pages up front, instead of two 32-byte 
arrays. These are normal young-generation allocations.
   
   ## Tests
   
   - `DataTableSerDeTest#testValuesSpanningPages` (new): `BYTES`, `INT_ARRAY`, 
`STRING_ARRAY`, `BIG_DECIMAL_ARRAY` and `OBJECT` values of up to 40 KB, which 
cross page boundaries. It also has null `OBJECT` values and an `INT` column 
with a null bitmap. The test reads the built (paged) table, 
`toDataOnlyDataTable()` of it, and the deserialized table.
   - `DataTableSerDeTest#testAllDataTypesInOneSchema` now also reads the built 
table before the round trip.
   - `CompoundDataBufferTest#randomAccessAcrossBuffersOfDifferentSizes` (new): 
random and backward reads across buffers of different sizes.
   - All tests in `pinot-segment-spi`, `pinot-common` and `pinot-core` that 
match `*DataBuffer*Test`, `*DataTable*Test`, `*DataBlock*Test`, 
`*GroupBy*QueriesTest`, `*DistinctCount*QueriesTest`, `*NullHandling*Test`, 
`*Selection*QueriesTest` and `AllNullQueriesTest` pass.
   


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