Jackie-Jiang commented on code in PR #19397:
URL: https://github.com/apache/pinot/pull/19397#discussion_r3897400312


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/io/codec/T64CodecDefinition.java:
##########
@@ -409,19 +390,18 @@ private static void writeBits(byte[] buf, long bitOffset, 
long value, int bitWid
     }
   }
 
-  /// Read `bitWidth` bits from `buf` starting at `bitOffset`.
-  private static long readBits(byte[] buf, long bitOffset, int bitWidth) {
-    long byteIndex = bitOffset >>> 3;
+  /// Read `bitWidth` bits from `buf` starting at `baseOffset + bitOffset` 
without copying the
+  /// packed block into a temporary heap array.
+  private static long readBits(ByteBuffer buf, int baseOffset, long bitOffset, 
int bitWidth) {
+    int idx = baseOffset + (int) (bitOffset >>> 3);
     int bitInByte = (int) (bitOffset & 7);
     long out = 0L;
     int shift = 0;
     int bitsRemaining = bitWidth;
-    int idx = (int) byteIndex;
     while (bitsRemaining > 0) {
-      long b = ((long) buf[idx]) & 0xFFL;
+      long b = ((long) buf.get(idx)) & 0xFFL;

Review Comment:
   [BUG-PERF/C4.3] This replaces one bulk block copy followed by heap-array 
reads with roughly 4–8 absolute direct ByteBuffer.get(int) calls per INT/LONG 
value. The published DELTA+ZSTD ingestion benchmark covers neither T64 nor 
decoding. Add a matched T64 decode JMH benchmark, or retain reusable block 
scratch/use word-at-a-time decoding, before assuming this query-path change is 
safe.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/io/codec/CodecPipelineExecutor.java:
##########
@@ -166,7 +342,43 @@ public void close() {
   /// @param spec       the codec DSL string (e.g. `"LZ4,ZSTD(3)"`)
   /// @param storedType stored data type used for validation
   public static CodecPipelineExecutor create(String spec, DataType storedType) 
{
-    return create(spec, new CodecContext(storedType), CodecRegistry.DEFAULT);
+    // Preserve parser rejection before any normalization-based cache lookup. 
In particular, outer
+    // whitespace must not let an overlong spec collapse to a cached canonical 
spelling.
+    if (spec == null || spec.length() > CodecSpecParser.MAX_SPEC_LENGTH || 
spec.isBlank()) {
+      return create(spec, new CodecContext(storedType), CodecRegistry.DEFAULT);
+    }
+    PlanKey requestedKey = new PlanKey(normalizeRequestedSpec(spec), 
storedType);
+    synchronized (PLAN_CACHE_LOCK) {

Review Comment:
   [BUG-TEST/C6.11] The new process-wide access-ordered plan and alias caches 
are shared concurrent state, but eviction tests are single-threaded and the 
existing concurrency test creates the executor before starting workers. Add a 
test that concurrently calls create() with canonical and alias spellings while 
exceeding the 256-plan limit, then verifies identity, type separation, and 
stale-alias recovery.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/io/codec/CodecPipelineExecutor.java:
##########
@@ -245,115 +464,59 @@ public int maxEncodedSize(int decodedSize, int 
maxStageSize, long maxCumulativeS
             "Codec pipeline cumulative stage-output bound " + cumulativeSize + 
" exceeds " + maxCumulativeSize
                 + " at stage " + i + " for pipeline " + _canonicalSpec);
       }
-    }
-    return size;
-  }
-
-  /// Encodes `src` through the pipeline and returns the encoded bytes ready 
for read.
-  ///
-  /// The readable range is interpreted in the persisted big-endian 
typed-value order, independent
-  /// of the caller view's byte order. The caller's position, limit, and order 
are not modified.
-  ///
-  /// @param src decoded chunk data, ready for read (position=0, 
limit=dataSize)
-  /// @return encoded buffer ready for read; the caller owns this buffer
-  public ByteBuffer encode(ByteBuffer src) throws IOException {
-    ByteBuffer input = src.duplicate().order(ByteOrder.BIG_ENDIAN);
-    ByteBuffer current = input;
-    try {
-      for (BoundStage<?> stage : _stages) {
-        ByteBuffer previous = current;
-        current = stage.encode(previous);
-        if (previous != input && previous != current) {
-          CleanerUtil.cleanQuietly(previous);
-        }
+      if (stageBounds != null) {
+        stageBounds[i] = size;
       }
-      return current;
-    } catch (IOException | RuntimeException | Error e) {
-      if (current != input) {
-        CleanerUtil.cleanQuietly(current);
-      }
-      throw e;
-    }
-  }
-
-  /// Decodes `src` through the reversed pipeline and returns the original 
bytes.
-  ///
-  /// @param src encoded chunk data, ready for read
-  /// @return decoded buffer ready for read; the caller owns this buffer
-  ByteBuffer decode(ByteBuffer src) throws IOException {
-    ByteBuffer current = src;
-    try {
-      for (int i = _stages.size() - 1; i >= 0; i--) {
-        ByteBuffer previous = current;
-        current = _stages.get(i).decode(previous);
-        if (previous != src && previous != current) {
-          CleanerUtil.cleanQuietly(previous);
-        }
-      }
-      return current;
-    } catch (IOException | RuntimeException | Error e) {
-      if (current != src) {
-        CleanerUtil.cleanQuietly(current);
-      }
-      throw e;
     }
+    return size;
   }
 
-  /// Decodes `src` through the reversed pipeline, writing the result directly 
into
-  /// `dst`.  On return `dst` is flipped and ready for read (position=0,
-  /// limit=decoded size).
+  /// Encodes through capacity-bounded caller-owned scratch buffers.
   ///
-  /// For single-stage pipelines (transform-only or compression-only), the 
decoded bytes are
-  /// written directly into `dst`, avoiding an intermediate allocation.  For 
multi-stage
-  /// pipelines an intermediate buffer is allocated for any stage that is not 
the last in the
-  /// reversed pipeline; the final stage writes directly into `dst`.
+  /// Every stage output is written directly into one of two alternating 
direct buffers. The
+  /// returned readable view is owned by `scratch`: callers must consume or 
copy it before the next
+  /// call using that workspace, and must not use it after the workspace is 
closed. The caller's
+  /// source position, limit, and byte order are not modified. Typed input is 
interpreted in
+  /// persisted big-endian order, independent of the caller view's byte order. 
The source must
+  /// not alias this workspace's buffers (including a view returned by a 
previous call).
   ///
-  /// @param src encoded chunk data, ready for read
-  /// @param dst caller-supplied output buffer; must be a *direct* 
[ByteBuffer] when
-  ///            the pipeline requires it (see 
[ChunkCodecHandler#requiresDirectDstBuffer]);
-  ///            must have sufficient [ByteBuffer#capacity()] for the decoded 
data; its
-  ///            position and limit are overwritten before returning
-  /// @throws IOException              if decoding fails
-  /// @throws IllegalArgumentException if `dst` is not direct when required, 
or does not have
-  ///                                  enough capacity
-  void decode(ByteBuffer src, ByteBuffer dst) throws IOException {
-    decode(src, dst, dst.capacity());
-  }
-
-  /// Decodes into `dst` while bounding every intermediate allocation from the 
expected final
-  /// decoded size. This is the segment-reader entry point: codec frames are 
untrusted and may
-  /// advertise arbitrary decoded lengths, so an intermediate stage must never 
allocate directly
-  /// from its frame header.
-  ///
-  /// @param src                 encoded chunk data, ready for read
-  /// @param dst                 caller-owned final output buffer
-  /// @param expectedDecodedSize exact decoded bytes declared and validated by 
the outer format
-  void decode(ByteBuffer src, ByteBuffer dst, int expectedDecodedSize) throws 
IOException {
-    decode(src, dst, expectedDecodedSize, Integer.MAX_VALUE, Long.MAX_VALUE);
-  }
-
-  /// Variant of [#decode(ByteBuffer, ByteBuffer, int)] that caps every 
intermediate scratch
-  /// buffer. Callers reading untrusted persisted data should pass the 
format's validated limit.
-  void decode(ByteBuffer src, ByteBuffer dst, int expectedDecodedSize, int 
maxIntermediateSize)
-      throws IOException {
-    decode(src, dst, expectedDecodedSize, maxIntermediateSize, Long.MAX_VALUE);
-  }
-
-  /// Variant that also caps the sum of stage-output bounds, limiting total 
work and allocation
-  /// churn for an otherwise-valid long pipeline. This convenience overload 
owns a temporary
-  /// workspace; production chunk readers should use the caller-owned 
[DecodeScratch] overload.
-  void decode(ByteBuffer src, ByteBuffer dst, int expectedDecodedSize, int 
maxIntermediateSize,
-      long maxCumulativeSize)
+  /// @param src               decoded chunk data, ready for read
+  /// @param maxStageSize      maximum permitted output bound for any stage
+  /// @param maxCumulativeSize maximum permitted sum of all stage-output bounds
+  /// @param scratch           caller-owned workspace, not shared across 
concurrent calls
+  /// @return scratch-owned encoded bytes ready for read
+  public ByteBuffer encode(ByteBuffer src, int maxStageSize, long 
maxCumulativeSize, EncodeScratch scratch)
       throws IOException {
-    try (DecodeScratch scratch = new DecodeScratch()) {
-      decode(src, dst, expectedDecodedSize, maxIntermediateSize, 
maxCumulativeSize, scratch);
+    scratch.ensureOpen();
+    ByteBuffer current = src.duplicate().order(ByteOrder.BIG_ENDIAN);
+    int[] stageBounds = scratch.stageBounds(_stages.size());
+    encodedStageBounds(current.remaining(), maxStageSize, maxCumulativeSize, 
stageBounds);

Review Comment:
   [BUG-PERF/C4.3] The reusable buffers are permanently sized from generic 
maxEncodedSize bounds, but T64's bound assumes LONG-sized blocks even though 
the executor knows the stored type. For a 1 MiB INT chunk, T64 reports 
2,134,021 bytes versus a type-specific worst case of 1,069,061; a T64,LZ4 
workspace therefore retains about 4.28 MiB instead of the previous roughly 2.14 
MiB peak. Make the bound type-aware before using it to size retained buffers.



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