xiangfu0 commented on code in PR #19795:
URL: https://github.com/apache/pinot/pull/19795#discussion_r4222551901


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/PercentileTDigestAccumulator.java:
##########
@@ -696,34 +744,60 @@ public double getMin() {
   @Override
   public double getMax() {
     if (_pendingSerializedTDigest != null) {
-      return 
ByteBuffer.wrap(_pendingSerializedTDigest).getDouble(Integer.BYTES + 
Double.BYTES);
+      return _pendingSerializedMetadata.max();
     }
     flush();
     return _max;
   }
 
-  byte[] serialize() {
+  public byte[] serialize() {
+    if (_legacyDegraded) {
+      if (_pendingSerializedTDigest != null) {
+        return _pendingSerializedTDigest.clone();
+      }
+      if (!_legacySerializedDirty) {
+        return _legacySerializedBytes.clone();
+      }
+      // A merge with a historical invalid distribution remains unknown. Keep 
that explicit in the legacy
+      // encoding instead of passing its invalid centroids into the normal 
sorted K1 merge.
+      return toLegacyDegradedBytes();
+    }
     if (_pendingSerializedTDigest != null) {
-      getSerializedTotalWeight(_pendingSerializedTDigest);
-      if (ByteBuffer.wrap(_pendingSerializedTDigest).getInt() == 
VERBOSE_ENCODING) {
-        byte[] serialized = 
TDigestUtils.makeLegacyCompatible(_pendingSerializedTDigest);
-        return serialized == _pendingSerializedTDigest ? serialized.clone() : 
serialized;
+      if (hasWeightedBoundaryCentroids(_pendingSerializedTDigest, 
_pendingSerializedMetadata.centroidCount())) {
+        materializePendingSerializedTDigest();
+      } else {
+        if (ByteBuffer.wrap(_pendingSerializedTDigest).getInt() == 
TDigestUtils.VERBOSE_ENCODING) {
+          byte[] serialized = 
TDigestUtils.makeLegacyCompatible(_pendingSerializedTDigest, 
_pendingSerializedMetadata);
+          return serialized == _pendingSerializedTDigest ? serialized.clone() 
: serialized;
+        }
+        return _pendingSerializedTDigest.clone();
       }
-      return _pendingSerializedTDigest.clone();
     }
     compress();
+    normalizeBoundaryCentroids();
     return TDigestUtils.makeLegacyCompatible(toVerboseBytes());
   }
 
-  TDigest toTDigest() {
-    if (_pendingSerializedTDigest != null) {
-      return TDigestUtils.deserialize(_pendingSerializedTDigest);
+  private static boolean hasWeightedBoundaryCentroids(byte[] bytes, int count) 
{

Review Comment:
   **[simplification] `hasWeightedBoundaryCentroids` is a hand-rolled 
byte-level re-parse** (duplicating the VERBOSE/SMALL header offsets and the 
boundary-weight rule) over bytes whose centroids `inspectSerialized` already 
scanned once, with the result cached in `_pendingSerializedMetadata`. Adding a 
`weightedBoundaries` flag to `SerializedTDigestMetadata`, computed in the 
existing single scan, deletes this second parser and removes one more place the 
encoding layout is hardcoded.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/aggregator/PercentileTDigestValueAggregator.java:
##########
@@ -152,128 +150,112 @@ private static NonFiniteAwareTDigest 
asNonFiniteAware(TDigest value) {
   }
 
   private void updateMaxByteSize(TDigest value) {

Review Comment:
   **[fragility] `updateMaxByteSize` reaches through two classes' internals to 
size serialization buffers**: an instanceof chain unwrapping 
`NonFiniteAwareTDigest._finiteDigest`, testing it for 
`PercentileTDigestAccumulator`, and combining 
`hasFractionalWeights()`/`getCentroidCountUpperBound()` with a magic `+ 6L` for 
infinity-centroid splits. If either class changes its centroid accounting (e.g. 
boundary repair adding a centroid, or the infinity split rules changing), this 
bound silently under-estimates and `serializeAggregatedValue` fails at 
segment-build time, far from the change. A `maxSerializedByteSize()` on the 
`TDigest` contract would let each implementation own its bound.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/PercentileTDigestAccumulator.java:
##########
@@ -81,11 +61,15 @@ final class PercentileTDigestAccumulator extends TDigest {
   private double[] _incomingWeights;
   private double[] _sortedIncomingMeans;
   private double[] _sortedIncomingWeights;
-  private int[] _incomingOrder;
   private SerializedTDigestInput _serializedTDigestInput;
   private byte[] _pendingSerializedTDigest;
   private double _pendingSerializedTotalWeight = Double.NaN;
+  private SerializedTDigestMetadata _pendingSerializedMetadata;
+  private boolean _legacyDegraded;

Review Comment:
   **[architecture] `_legacyDegraded` is threaded through the whole hot 
accumulator as many special-case branches** (add paths, compress, cdf/quantile, 
both serialize encodings, all serialized-merge entry points, plus the 
`_legacySerializedBytes`/`_legacySerializedDirty` bookkeeping). Every future 
accumulator change must remember to re-check the flag or it silently merges 
poisoned state into the K1 kernel. Since the decision is made once at decode 
time (`needsLegacyFallback`), a small separate degraded `TDigest` 
implementation (bytes + min/max/totalWeight, NaN quantiles, pass-through 
serialize) returned by the decode layer would keep this class single-invariant; 
merging degraded-into-live becomes one case in `add(TDigest)`.



##########
pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunctionTest.java:
##########
@@ -37,17 +39,26 @@
 import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
 import 
org.apache.pinot.core.query.aggregation.function.AggregationFunction.SerializedIntermediateResult;
 import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
+import 
org.apache.pinot.segment.local.customobject.PercentileTDigestAccumulator;
 import org.apache.pinot.segment.local.utils.TDigestUtils;
 import org.apache.pinot.segment.spi.Constants;
+import org.apache.pinot.segment.spi.customobject.TDigest;
+import org.apache.pinot.segment.spi.customobject.TDigest.Centroid;
 import org.apache.pinot.spi.data.FieldSpec.DataType;
 import org.roaringbitmap.RoaringBitmap;
 import org.testng.Assert;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
 
 public class PercentileTDigestAggregationFunctionTest {
   private static final ExpressionContext EXPRESSION = 
ExpressionContext.forIdentifier("col");
+  private static final Map<String, double[]> LEGACY_REDUCER_RANK_ERRORS = 
loadLegacyReducerRankErrors();

Review Comment:
   **[test robustness] The CSV oracle is loaded in a static initializer**, so a 
missing/filtered resource or a malformed row fails every test in this class 
with `ExceptionInInitializerError` rather than one reducer test with a clear 
message; a row with fewer than the expected fields passes loading and later 
surfaces as `ArrayIndexOutOfBoundsException` in the assertion helper. Also note 
the oracle is effectively non-regenerable (requires checking out a pre-PR 
commit, re-adding the removed tdunning dependency, and running with `-da`), and 
its key omits merge order/reducer input, so new test dimensions can silently 
validate against stale maxima. Consider lazy loading with field-count 
validation, and documenting the regeneration recipe next to the CSV.



##########
pinot-perf/src/main/java/org/apache/pinot/perf/aggregation/TDigestBenchmarkUtils.java:
##########
@@ -75,13 +58,18 @@ static byte[] createFixedVerboseBytes(double[] 
sortedValues, double compression)
         start = 1 + (centroidId - 1) * numInteriorValues / 
numInteriorCentroids;
         end = 1 + centroidId * numInteriorValues / numInteriorCentroids;
       }
-      double sum = 0.0;
-      for (int valueId = start; valueId < end; valueId++) {
-        sum += sortedValues[valueId];
+      double mean = sortedValues[start];
+      for (int valueId = start + 1; valueId < end; valueId++) {
+        double fraction = 1.0 / (valueId - start + 1);
+        double value = sortedValues[valueId];
+        mean = Math.copySign(1.0, mean) == Math.copySign(1.0, value)

Review Comment:
   **[benchmark validity] The FIXED_VERBOSE generator changed its bucket-mean 
formula in this same PR** (exact sum/weight -> incremental sign-aware lerp + 
range clamp), so the one benchmark mode whose purpose is feeding byte-identical 
inputs across the implementation swap now produces different bytes pre- vs 
post-PR. Any before/after comparison using FIXED_VERBOSE attributes 
quality/perf drift partly to the input change rather than the merge 
implementation. If the lerp form is needed for the new validation, consider 
noting in the class doc that FIXED_VERBOSE outputs are not comparable across 
this PR boundary.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/PercentileTDigestAccumulator.java:
##########
@@ -1501,26 +1479,35 @@ private void decode() {
       }
       int encodedCentroidCount = _numCentroids;
       ensureCapacity(Math.addExact(encodedCentroidCount, 2));
-      ByteBuffer input = ByteBuffer.wrap(_bytes);
-      input.position(_centroidOffset);
-      double totalWeight = 0.0;
-      if (_centroidSize == VERBOSE_CENTROID_SIZE) {
-        for (int i = 0; i < encodedCentroidCount; i++) {
-          double weight = input.getDouble();
-          _weights[i] = weight;
-          _means[i] = input.getDouble();
-          totalWeight += weight;
-        }
+      if (Double.isNaN(_metadata.totalWeight())) {
+        _metadata = TDigestUtils.inspectSerialized(ByteBuffer.wrap(_bytes), 
_metadata, _means, _weights);
+        _totalWeight = _metadata.totalWeight();
       } else {
+        // Pending state was already inspected before retaining bytes. Its 
later first read only materializes
+        // those validated centroids; regular merged inputs compute flags and 
weights in the decode pass above.
+        ByteBuffer input = ByteBuffer.wrap(_bytes);
+        input.position(_centroidOffset);
         for (int i = 0; i < encodedCentroidCount; i++) {
-          double weight = input.getFloat();
-          _weights[i] = weight;
-          _means[i] = input.getFloat();
-          totalWeight += weight;
+          _weights[i] = _centroidSize == TDigestUtils.VERBOSE_CENTROID_SIZE ? 
input.getDouble() : input.getFloat();
+          double mean = _centroidSize == TDigestUtils.VERBOSE_CENTROID_SIZE ? 
input.getDouble() : input.getFloat();
+          _means[i] = clamp(mean, _min, _max);
+        }
+      }
+      if (!_metadata.needsLegacyFallback()) {
+        if (_metadata.unorderedMeans()) {
+          it.unimi.dsi.fastutil.Arrays.mergeSort(0, encodedCentroidCount,

Review Comment:
   **[convention] CLAUDE.md: "Prefer imports over fully qualified class names 
(e.g., use `import com.foo.Bar` and refer to `Bar`, not `com.foo.Bar` 
inline)."** `it.unimi.dsi.fastutil.Arrays.mergeSort(...)` is written inline 
because `java.util.Arrays` occupies the simple name, but `import static 
it.unimi.dsi.fastutil.Arrays.mergeSort;` satisfies the rule with no clash 
(java.util.Arrays has no mergeSort member).



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