Skip to content

Reuse fixed-row buffers for DataTable result construction - #19533

Merged
xiangfu0 merged 3 commits into
apache:masterfrom
xiangfu0:xiangfu0/java-datatable-row-buffer
Sep 14, 2026
Merged

Reuse fixed-row buffers for DataTable result construction#19533
xiangfu0 merged 3 commits into
apache:masterfrom
xiangfu0:xiangfu0/java-datatable-row-buffer

Conversation

@xiangfu0

@xiangfu0 xiangfu0 commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

PR flow

Reuse a single row buffer for all rows, avoiding per-row allocation.

flowchart TD
  N0["Constructor#58; allocate buffer #40;F1#41;"]:::stAdded
  N1["startRow#58; increment row count #40;F1#44; F2#41;"]:::stModified
  N2["setters#58; write to buffer #40;F1#44; F3#41;"]:::stModified
  N3["finishRow#58; copy buffer to output #40;F1#44; F3#41;"]:::stModified
  N0 -->|"buffer allocated in constructor used by setters"| N2
  N1 -->|"startRow called before setters #40;test#41;"| N2
  N2 -->|"setters called before finishRow #40;test#41;"| N3
  N3 -->|"finishRow followed by startRow for next row #40;test#41;"| N1
  classDef stAdded fill:#dafbe1,stroke:#1a7f37,color:#1f2328,stroke-width:2px
  classDef stModified fill:#fff8c5,stroke:#9a6700,color:#1f2328,stroke-width:2px
  classDef stRemoved fill:#ffebe9,stroke:#cf222e,color:#1f2328,stroke-width:2px
  classDef stUnchanged fill:#f6f8fa,stroke:#656d76,color:#1f2328,stroke-width:1px
Loading

AI-generated · Green: added · Yellow: modified · Red: removed · Gray: existing

Diff evidence
  • F1: pinot-core/src/main/java/org/apache/pinot/core/common/datatable/BaseDataTableBuilder.java — before · after
  • F2: pinot-core/src/main/java/org/apache/pinot/core/common/datatable/DataTableBuilder.java — before · after
  • F3: pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java — before · after
  • Regenerate PR flow

DataTable result construction allocates a byte array and ByteBuffer for every row. Allocate one fixed-row buffer in the BaseDataTableBuilder constructor and reuse it without clearing or resetting it between rows. Every production row writer populates every column; each setter writes its complete slot and sets its own position, and finishRow() copies the bytes into the output stream. V4 wire encoding and variable-length offsets are preserved.

Constructor allocation also means a zero-row builder now allocates one schema-sized scratch buffer.

The builder interface now documents that each row must populate every column. The regression test covers fully populated rows, reverse column-write order, empty variable-length values, explicit nulls, NaN payloads and signed zero.

Reproduction and benchmark

Rerun on September 12, 2026 (PDT), comparing measured PR revision b51ffd1e43467462d34001dcf424f35635d75156 with upstream 5771d6acea60cd72738116835111cf7c49965373. The exact benchmark source is unchanged from the earlier experiment (SHA-256 d7f6e90a71ab371017d85c7fea454435415c214bdc7e832138cd1474a35878b5).

buildDataTable measures V4 DataTable construction. serializeDataTable measures construction plus wire serialization, using build().toBytes(). NUMERIC contains INT/LONG/DOUBLE columns; MIXED adds STRING/BYTES/INT_ARRAY with 128 deterministic input values. Every measured fork validates all logical output fields during setup, outside timing. Rows 0 and 1 are controls; 5,000 rows exercises repeated-row allocation.

JMH 1.37, Temurin JDK 25+36-LTS, Apple M2 Max/macOS, one thread, 512 MiB heap. Three alternating AB/BA/AB pairs, one fresh fork per method/shape/row-count cell per arm, three 500 ms warmups and five 500 ms measurements, with GC profiling: 72 forks total. Both arms use the same frozen runtime; only the freshly compiled BaseDataTableBuilder.class differs. Runtime hashes were verified before and after every arm. The machine was on AC power with ordinary desktop/security processes active; no task builds or competing task benchmarks ran during timing.

With the benchmark source on each arm's classpath, run the following once per arm in AB/BA/AB order:

java -cp "$ARM_CLASSPATH" org.openjdk.jmh.Main \
  '^org.apache.pinot.perf.BenchmarkDataTableBuilder.*' \
  -p _numRows=0,1,5000 -p _shape=NUMERIC,MIXED \
  -wi 3 -i 5 -w 500ms -r 500ms -f 1 -t 1 \
  -jvmArgs '-Xms512m -Xmx512m' -prof gc -foe true \
  -rf json -rff "$RESULT_JSON"

Benchmark results for b51ffd1e43

Times and allocations are medians of the three arm means. Paired ratios are medians of the three baseline/candidate time ratios, so values above 1 favor the candidate. Parentheses show the minimum and maximum paired ratios, not confidence intervals.

Operation Rows Shape Baseline µs/op Candidate µs/op Paired ratio (range) Allocation B/op before → after
buildDataTable 0 MIXED 0.050 0.052 0.975× (0.954–0.991) 872 → 928
buildDataTable 0 NUMERIC 0.045 0.046 0.954× (0.948–0.994) 848 → 888
buildDataTable 1 MIXED 0.120 0.126 0.954× (0.937–0.961) 1272 → 1216
buildDataTable 1 NUMERIC 0.062 0.056 1.109× (1.104–1.120) 1016 → 960
buildDataTable 5000 MIXED 304.953 265.684 1.143× (1.136–1.158) 1484183 → 924292
buildDataTable 5000 NUMERIC 81.816 57.617 1.420× (1.420–1.434) 843497 → 363593
serializeDataTable 0 MIXED 0.382 0.386 0.990× (0.990–1.145) 2680 → 2736
serializeDataTable 0 NUMERIC 0.319 0.322 0.991× (0.983–1.167) 1832 → 1872
serializeDataTable 1 MIXED 0.355 0.361 0.985× (0.981–0.992) 3032 → 2976
serializeDataTable 1 NUMERIC 0.332 0.324 1.018× (1.009–1.080) 2296 → 2240
serializeDataTable 5000 MIXED 350.118 299.698 1.184× (1.160–1.194) 2366318 → 1806386
serializeDataTable 5000 NUMERIC 98.645 71.906 1.364× (1.344–1.403) 1244873 → 764969

At 5,000 rows, construction improves 1.420× for NUMERIC and 1.143× for MIXED; construction plus serialization improves 1.364× and 1.184×. The corresponding allocation savings are approximately 480 KB/op for NUMERIC and 560 KB/op for MIXED.

The optimization does not improve every small-result case. One-row MIXED construction is 0.954× (about 5% slower), and construction plus serialization is 0.985×, despite 56 fewer allocated bytes/op. Empty builders allocate 40 additional bytes/op for NUMERIC and 56 for MIXED; empty construction ratios are 0.954× and 0.975×. Empty construction-plus-serialization ratios vary across pairs, as shown above.

Eight full V4 wire outputs (NUMERIC/MIXED at 0, 1, 32 and 5,000 rows) are byte-identical between arms. These are descriptive component measurements on a shared workstation; they exclude query execution, network, broker reduction and ingestion. Three paired forks per case do not establish a production confidence bound.

Validation

Latest review cleanup (4dd42c503588d0f944700d316c58a0b8036df567): made the constructor-only row-size field a local variable and rebased onto upstream 9ee1c681be85a3f018c7b00dcff48aa8d4180800. All 10 DataTableSerDeTest cases and the four required Spotless, Checkstyle, and license checks passed on JDK 25. Direct warning-enabled compilation of the changed class passed with no warnings on added lines; the optional reactor warning check still encounters the existing missing NotNull dependency described below. The benchmark figures above remain measurements of b51ffd1e43; they were not rerun for this cleanup.

At measured revision b51ffd1e43, 83 focused tests passed on JDK 25: 10 DataTable serde, 2 selection utility, 5 selection service, 22 ordered selection/broker reduction, and 44 inter-segment GROUP BY tests. The serde cases cover empty tables and all data types; the updated row-reuse case fills every column.

Spotless, Checkstyle, and license format/check passed with no automatic source changes. Independent review across all eight Pinot review domains found no actionable issues. The warning-enabled reactor compilation still fails in unchanged ZstandardDecompressor.java:51 due to missing org.jetbrains.annotations.NotNull; direct JDK 25 -Xlint:all compilation of all three changed files passed with no warnings on added lines (only existing license-header and raw/unchecked Map-array warnings).

@xiangfu0 xiangfu0 added the performance Related to performance optimization label Sep 12, 2026
@codecov-commenter

codecov-commenter commented Sep 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.72%. Comparing base (5771d6a) to head (4dd42c5).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff            @@
##             master   #19533   +/-   ##
=========================================
  Coverage     67.72%   67.72%           
  Complexity     1450     1450           
=========================================
  Files          3490     3490           
  Lines        225032   225032           
  Branches      35527    35527           
=========================================
+ Hits         152393   152404   +11     
+ Misses        60612    60595   -17     
- Partials      12027    12033    +6     
Flag Coverage Δ
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 67.72% <100.00%> (+<0.01%) ⬆️
lane-a 100.00% <ø> (ø)
lane-b 0.00% <ø> (ø)
temurin 67.72% <100.00%> (+<0.01%) ⬆️
unittests 67.72% <100.00%> (+<0.01%) ⬆️
unittests1 57.81% <100.00%> (+<0.01%) ⬆️
unittests2 39.50% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@xiangfu0
xiangfu0 force-pushed the xiangfu0/java-datatable-row-buffer branch from 655e259 to f55d2af Compare September 12, 2026 19:25

@Jackie-Jiang Jackie-Jiang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check if we always fill all columns of a row. If so, we can just allocate a buffer in the constructor, and there is no need to clear/reset the buffer.

@xiangfu0

Copy link
Copy Markdown
Contributor Author

@Jackie-Jiang Confirmed: every production row writer populates every column, including null values. Each setter writes its complete fixed-width slot (or offset/length pair) and sets its own buffer position; finishRow() copies the bytes into the output stream. There is no dependency on clearing or resetting the buffer.

Addressed in b51ffd1: the buffer is now allocated once in the constructor and is final, and startRow() only increments the row count. I also documented the complete-row contract and updated the regression test to populate every column, including reverse-order writes, empty variable-length values, explicit nulls, NaN payloads, and signed zero.

All 83 focused serde, selection, and GROUP BY tests passed on JDK 25, along with Spotless, Checkstyle, and license checks. Changed-file warning-enabled compilation passed with no warnings on added lines. The existing full warning-enabled reactor compilation issue with the missing NotNull dependency is documented in the PR description, and the earlier benchmark results are explicitly labeled as belonging to the previous revision.

Building multi-row DataTables creates a short-lived fixed-row buffer for every row. Reuse the builder-owned buffer while resetting its contents and state so partially written rows retain their existing wire representation.
Allocate the row scratch buffer once in the constructor after verifying all
production callers populate every column. Document the complete-row contract
and cover reverse-order writes, empty values, and explicit nulls in serde.

Reproduction:
Construct a multi-row V4 DataTable. The previous startRow implementation
cleared the entire scratch buffer and reset its state even though every
setter seeks to and overwrites a complete column slot.

Validation:
83 focused serde, selection, and GROUP BY tests passed on JDK 25.
Spotless, Checkstyle, and license checks passed. Scoped lint compilation
passed with no new-line warnings; full warning-enabled reactor compilation
remains blocked by an unchanged missing JetBrains NotNull dependency.
The reused row buffer only needs the computed row size while it is allocated.
Remove the unused instance field and keep rowSizeInBytes local to the
constructor, addressing the remaining review comment.

Validation: 10 DataTableSerDeTest cases passed on JDK 25.
@xiangfu0
xiangfu0 force-pushed the xiangfu0/java-datatable-row-buffer branch from b51ffd1 to 4dd42c5 Compare September 14, 2026 06:27
@xiangfu0
xiangfu0 merged commit 66fca21 into apache:master Sep 14, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Related to performance optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants