Optimize consuming MAP key access - #19168
Open
xiangfu0 wants to merge 2 commits into
Open
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #19168 +/- ##
============================================
- Coverage 66.62% 66.61% -0.01%
+ Complexity 1423 1417 -6
============================================
Files 3443 3443
Lines 218577 218621 +44
Branches 34792 34801 +9
============================================
+ Hits 145624 145637 +13
- Misses 61218 61248 +30
- Partials 11735 11736 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Contributor
There was a problem hiding this comment.
Pull request overview
This PR optimizes MAP-key lookups in Pinot’s forward index path by enabling selective value extraction (scan serialized MAP entries and deserialize only the matching value) instead of deserializing the entire MAP for every key access.
Changes:
- Added
ForwardIndexReader#getMapValue(...)as a default SPI API, and switchedMapKeyIndexReaderto use it (with a full-map fallback via the default implementation). - Implemented
MapUtils.deserializeMapValue(...)to scan length-prefixed MAP frames and deserialize only the selected value, including support for directByteBufferinputs (e.g., off-heap views). - Introduced a read-only zero-copy
ByteBufferview inMutableOffHeapByteArrayStore, added focused unit tests, and added a JMH benchmark to compare approaches.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pinot-spi/src/test/java/org/apache/pinot/spi/utils/MapUtilsTest.java | Adds unit coverage for selective MAP-value extraction (colliding keys, non-ASCII keys, byte order). |
| pinot-spi/src/main/java/org/apache/pinot/spi/utils/MapUtils.java | Adds selective MAP-value deserialization from a length-prefixed MAP frame, including direct-buffer support. |
| pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/reader/ForwardIndexReader.java | Adds default getMapValue(...) API to allow optimized implementations while preserving fallback behavior. |
| pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/map/MapKeyIndexReaderTest.java | Verifies MapKeyIndexReader works both with selective and fallback implementations. |
| pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/forward/mutable/VarByteSVMutableForwardIndexTest.java | Adds coverage for the mutable forward index’s selective getMapValue(...) path. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/map/MapKeyIndexReader.java | Switches extraction to ForwardIndexReader#getMapValue(...) to enable selective reads. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/forward/VarByteSVMutableForwardIndex.java | Overrides getMapValue(...) to use selective ByteBuffer-based extraction. |
| pinot-segment-local/src/main/java/org/apache/pinot/segment/local/io/writer/impl/MutableOffHeapByteArrayStore.java | Adds a read-only, zero-copy ByteBuffer accessor for stored values. |
| pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkMapKeyAccess.java | Adds JMH benchmark comparing full-map deserialize vs selective key lookup. |
Comment on lines
+233
to
+241
| int valueLength = byteBuffer.getInt(); | ||
| if (!matches) { | ||
| skip(byteBuffer, valueLength); | ||
| continue; | ||
| } | ||
| // Keys within a frame are unique - the write path iterates a Map - so the first match is the only match and | ||
| // the remaining entries never need to be scanned. | ||
| byte[] valueBytes = new byte[valueLength]; | ||
| byteBuffer.get(valueBytes); |
| assertReaderBehavior(new FullMapOnlyReader()); | ||
| } | ||
|
|
||
| private static void assertReaderBehavior(ForwardIndexReader reader) { |
Comment on lines
+76
to
+77
| @SuppressWarnings("rawtypes") | ||
| private abstract static class BaseReader implements ForwardIndexReader { |
Comment on lines
+189
to
+196
| /// Deserializes only the value for the requested key from a length-prefixed MAP frame. | ||
| /// Non-matching keys and values are skipped without allocating byte arrays or invoking Jackson. | ||
| /// | ||
| /// @param bytes Serialized MAP frame | ||
| /// @param key Key whose value should be deserialized | ||
| /// @return Deserialized value, or `null` if the key is missing, has a null value, or cannot be deserialized | ||
| @Nullable | ||
| public static Object deserializeMapValue(byte[] bytes, String key) { |
deserializeMapValue walked every key one relative get at a time - even after a mismatch was already certain - purely to advance the position, and kept scanning the frame after the match was found. Compare through absolute gets so a length mismatch or a differing byte skips the rest of the key outright, and return on the first match. Keys within a frame are unique because the write path iterates a Map, so the first match is the only match. Bounds-check the key length up front so the absolute gets are provably in range and a truncated frame still surfaces as BufferUnderflowException. Isolated JMH, flat string values, fixed-length dotted keys, JDK 25: entries key full map before after 4 first 0.556 0.166 0.114 us/op 16 first 2.163 0.360 0.112 us/op 64 first 8.886 1.074 0.118 us/op 64 last 8.763 1.245 0.593 us/op First-key lookup no longer scales with map size. Allocation is unchanged at 856 B/op versus 62792 B/op for the full-map path. Also cover MapKeyIndexReader, which had no test despite being the caller that changed, over both a reader that overrides getMapValue and one that inherits the default, plus non-ASCII keys and a little-endian buffer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
xiangfu0
force-pushed
the
agent/apache-consuming-map-key-extractor
branch
from
August 7, 2026 09:28
9a75197 to
cf37985
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Consuming MAP key lookups currently materialize and deserialize the full MAP value before extracting one requested key. This change adds a selective forward-index read path that scans the serialized MAP entries and deserializes only the matching value.
Changes
ForwardIndexReader#getMapValueAPI while preserving the existing full-MAP fallback.MapKeyIndexReaderand add focused unit coverage.BenchmarkMapKeyAccessfor comparing full-map and selective lookup costs.Performance
Isolated JMH run for a 64-entry MAP, looking up the last key, on JDK 25 (1 fork, 2 warmup iterations, 3 measurement iterations):
The benchmark is an isolated forward-index lookup measurement; it does not represent an end-to-end broker/server query latency result.
Validation
./mvnw -pl pinot-spi,pinot-segment-local -am -Dtest=MapUtilsTest,VarByteSVMutableForwardIndexTest -Dsurefire.failIfNoSpecifiedTests=false test./mvnw -pl pinot-perf -am -DskipTests package./mvnw spotless:apply -pl pinot-spi,pinot-segment-spi,pinot-segment-local,pinot-perf./mvnw license:format license:check -pl pinot-spi,pinot-segment-spi,pinot-segment-local,pinot-perfgit diff --check