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


##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileSmartTDigestAggregationFunction.java:
##########
@@ -365,7 +361,7 @@ public Double extractFinalResult(@Nullable Object 
intermediateResult) {
       TDigest tDigest = (TDigest) intermediateResult;
       // An empty digest holds the same state as the empty value list below, 
so the two branches must answer alike:
       // which one a query lands in depends only on whether the accumulator 
crossed the conversion threshold.
-      if (_nullHandlingEnabled && tDigest.size() == 0L) {
+      if (_nullHandlingEnabled && tDigest.isEmpty()) {

Review Comment:
   Fixed: an empty TDigest intermediate now returns null when null handling is 
enabled and DEFAULT_FINAL_RESULT when disabled, matching the empty-list path. 
The existing SmartTDigest suite covers both settings.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TDigestUtils.java:
##########
@@ -86,44 +85,65 @@ public static byte[] serialize(TDigest tDigest) {
 
   /// Serializes a digest using `scratchBuffer` for the temporary verbose 
representation when it is large enough.
   ///
-  /// The returned array is independently owned. The scratch buffer is cleared 
and may be reused by the caller after
-  /// this method returns.
+  /// The returned array is independently owned. Pinot-owned digests do not 
use the scratch buffer; callers can
+  /// clear and reuse it after serialization.
   public static byte[] serialize(TDigest tDigest, ByteBuffer scratchBuffer) {
     ByteOrder scratchOrder = scratchBuffer != null ? scratchBuffer.order() : 
null;
     try {
-      if (!(tDigest instanceof MergingDigest)) {
-        // Pinot has always decoded TDigest values as MergingDigest. Preserve 
the previous serialization behavior for
-        // other implementations instead of interpreting their 
implementation-specific payload as MergingDigest
-        // bytes.
-        ByteBuffer buffer = prepareScratchBuffer(scratchBuffer, 
tDigest.byteSize());
-        tDigest.asBytes(buffer);
-        byte[] bytes = new byte[buffer.position()];
-        buffer.flip();
-        buffer.get(bytes);
-        return bytes;
+      if (tDigest instanceof PercentileTDigestAccumulator) {
+        // Preserve the legacy two-level raw flush before final compression. 
Pending serialized state answers
+        // this count from cached metadata, so stored reads do not acquire an 
extra lossy compression pass.
+        tDigest.centroidCount();
+        return ((PercentileTDigestAccumulator) tDigest).serialize();
       }
-
-      int defaultCapacity = getDefaultCentroidCapacity(tDigest.compression());
-      int legacyCapacity = 
getLegacyDefaultCentroidCapacity(tDigest.compression());
-      // centroidCount() drains pending values at the digest's internal 
(two-level) compression, but does not perform
-      // the final, destructive compression used by asBytes(). It therefore 
gives us a safe bound for digests
-      // deserialized from the compact encoding with an explicitly enlarged 
main array while still allowing asBytes()
-      // to run once.
-      int currentCentroidCount = tDigest.centroidCount();
-      long sizeBound = Math.min(Math.max(0L, tDigest.size()), 
Math.max(defaultCapacity, legacyCapacity));
-      int maxCentroids = Math.max(currentCentroidCount, (int) sizeBound);
-      int requiredCapacity = Math.addExact(VERBOSE_HEADER_SIZE,
-          Math.multiplyExact(VERBOSE_CENTROID_SIZE, maxCentroids));
+      byte[] retained = 
PercentileTDigestValueAggregator.getRetainedHistoricalBytes(tDigest);

Review Comment:
   Removed the codec-to-aggregator dependency and static retention special 
case. The six related types now share customobject.tdigest; each digest owns 
serialize() and historical retention. PercentileTDigestValueAggregator is a 
thin adapter, and boundary/provenance helpers are package-private.



##########
compatibility-verifier/tdigest-compatibility/generate-rank-errors.sh:
##########
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+# Regenerate with the pinned legacy utility and 3.3 jar, then compare all case 
keys, inputs and rank errors.
+# Use --update to replace the checked-in reference after intentionally 
changing the seeded input corpus.
+set -euo pipefail
+# Match the unit-test workflow's Maven Central retry and connection settings, 
including standalone runs.
+export MAVEN_OPTS="${MAVEN_OPTS:-} 
-Dmaven.wagon.httpconnectionManager.ttlSeconds=25 \
+-Dmaven.wagon.http.retryHandler.count=30 -Dhttp.keepAlive=false 
-Dmaven.wagon.http.pool=false"
+TDIGEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+TDIGEST_BASELINE=f818bae6d75a91228eb3aac95c921bca4aeb2128
+TDIGEST_WORK="$TDIGEST_ROOT/pinot-segment-local/target/tdigest-rank-oracle"
+TDIGEST_JAR_DIR="$TDIGEST_ROOT/pinot-segment-local/target/tdigest-legacy-reader"
+TDIGEST_JAR="$TDIGEST_JAR_DIR/t-digest-3.3.jar"
+TDIGEST_REFERENCE="$TDIGEST_ROOT/pinot-core/src/test/resources/data/tdigest-3.3-k1-rank-errors.csv"
+mkdir -p "$TDIGEST_WORK" "$TDIGEST_JAR_DIR"
+if ! git -C "$TDIGEST_ROOT" cat-file -e "$TDIGEST_BASELINE^{commit}" 
2>/dev/null; then
+  # CI checks out shallow history. Fetch only the immutable public baseline; 
leave the worktree and POMs alone.
+  for TDIGEST_ATTEMPT in 1 2 3; do
+    if git -C "$TDIGEST_ROOT" -c http.lowSpeedLimit=1 -c http.lowSpeedTime=30 \
+        fetch --no-tags --depth=1 https://github.com/apache/pinot.git 
"$TDIGEST_BASELINE"; then

Review Comment:
   Fixed: the oracle script checks is-shallow-repository and uses --depth=1 
only for an already shallow checkout. Five disposable Git fixtures verify full 
clones stay full, shallow clones stay shallow, and an available baseline does 
not fetch.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/aggregator/PercentileTDigestValueAggregator.java:
##########
@@ -284,52 +270,113 @@ public void add(double value) {
     }
 
     @Override
-    public void add(double value, int weight) {
+    public void add(double value, double weight) {
       if (Double.isNaN(value)) {
         throw new IllegalArgumentException("Cannot add NaN to t-digest");
       }
-      if (weight <= 0) {
+      if (!(weight > 0.0) || !Double.isFinite(weight)) {
         throw new IllegalArgumentException("TDigest weight must be positive: " 
+ weight);
       }
-      invalidateCaches();
+      requireMutable();
+      checkTotalWeight(getTotalWeight() + weight);
       if (value == Double.NEGATIVE_INFINITY) {
-        _negativeInfinityWeight = Math.addExact(_negativeInfinityWeight, 
weight);
+        _negativeInfinityWeight += weight;
       } else if (value == Double.POSITIVE_INFINITY) {
-        _positiveInfinityWeight = Math.addExact(_positiveInfinityWeight, 
weight);
+        _positiveInfinityWeight += weight;
       } else {
         _finiteDigest.add(value, weight);
       }
+      mutated();
     }
 
     @Override
     public void add(TDigest other) {
+      if (other.isEmpty()) {
+        return;
+      }
+      requireMutable();
+      if (!other.hasValidStatistics() && !isEmpty()) {
+        throw corruptedMutation();
+      }
+      if (other.hasValidStatistics()) {
+        checkTotalWeight(getTotalWeight() + other.getTotalWeight());
+      }
       if (other instanceof NonFiniteAwareTDigest) {
         NonFiniteAwareTDigest wrapped = (NonFiniteAwareTDigest) other;
-        long negativeInfinityWeight = wrapped._negativeInfinityWeight;
-        long positiveInfinityWeight = wrapped._positiveInfinityWeight;
-        // MergingDigest.add(List) copies the implementation's 
double-precision weight arrays directly. The generic
-        // add(TDigest) path goes through Centroid.count(), which is limited 
to an int and truncates large weights.
-        wrapped.invalidateCaches();
-        invalidateCaches();
-        if (wrapped._finiteDigest.size() > 0L) {
-          _finiteDigest.add(List.of(wrapped._finiteDigest));
+        double negativeInfinityWeight = wrapped._negativeInfinityWeight;
+        double positiveInfinityWeight = wrapped._positiveInfinityWeight;
+        if (!wrapped._finiteDigest.isEmpty()) {
+          _finiteDigest.add(wrapped._finiteDigest);

Review Comment:
   Fixed: ordinary buffered inputs update extrema for each consumed centroid 
prefix, normalizing existing boundaries before widening those bounds; oversized 
or unsupported-endpoint sources use a safe whole-source primitive merge. Final 
source bounds are never applied to an incomplete prefix. Your exact BYTES -> 
merge -> immediate serialize -> reread regression now has mass 9,999, bounds 
[1, 5,000], valid statistics and q(0.198)=990.9010000000001. A seeded ordinary 
straddle regression also checks mass and first moment, and its negative control 
reproduces the old corruption.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TDigestUtils.java:
##########
@@ -86,44 +85,65 @@ public static byte[] serialize(TDigest tDigest) {
 
   /// Serializes a digest using `scratchBuffer` for the temporary verbose 
representation when it is large enough.
   ///
-  /// The returned array is independently owned. The scratch buffer is cleared 
and may be reused by the caller after
-  /// this method returns.
+  /// The returned array is independently owned. Pinot-owned digests do not 
use the scratch buffer; callers can
+  /// clear and reuse it after serialization.
   public static byte[] serialize(TDigest tDigest, ByteBuffer scratchBuffer) {
     ByteOrder scratchOrder = scratchBuffer != null ? scratchBuffer.order() : 
null;
     try {
-      if (!(tDigest instanceof MergingDigest)) {
-        // Pinot has always decoded TDigest values as MergingDigest. Preserve 
the previous serialization behavior for
-        // other implementations instead of interpreting their 
implementation-specific payload as MergingDigest
-        // bytes.
-        ByteBuffer buffer = prepareScratchBuffer(scratchBuffer, 
tDigest.byteSize());
-        tDigest.asBytes(buffer);
-        byte[] bytes = new byte[buffer.position()];
-        buffer.flip();
-        buffer.get(bytes);
-        return bytes;
+      if (tDigest instanceof PercentileTDigestAccumulator) {
+        // Preserve the legacy two-level raw flush before final compression. 
Pending serialized state answers
+        // this count from cached metadata, so stored reads do not acquire an 
extra lossy compression pass.
+        tDigest.centroidCount();

Review Comment:
   Fixed: serialize is now the native polymorphic contract, with no 
centroidCount preflush or encode/reinspect bridge. SQL/reduction mode 
compresses pending input directly at public compression; Type-10 decoding 
preserves that mode. Added Random(9), 16 x 2,000 uniform values, compression 
100 with serialization before querying P75. The independent 270-case reducer 
corpus also serializes before querying its result.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/aggregator/PercentileTDigestValueAggregator.java:
##########
@@ -152,130 +166,102 @@ private static NonFiniteAwareTDigest 
asNonFiniteAware(TDigest value) {
   }
 
   private void updateMaxByteSize(TDigest value) {
-    long defaultCapacity = getDefaultCentroidCapacity(value.compression());
-    long maxCentroids = Math.min(value.size(), defaultCapacity);
-    _maxByteSize = Math.max(_maxByteSize, getMaxVerboseByteSize(maxCentroids));
-  }
-
-  private static int getMaxVerboseByteSize(long centroidCount) {
-    long maxCentroids = Math.max(centroidCount, 0L);
-    return Math.toIntExact(Math.addExact(VERBOSE_HEADER_SIZE,
-        Math.multiplyExact(VERBOSE_CENTROID_SIZE, maxCentroids)));
-  }
-
-  private static long getDefaultCentroidCapacity(double compression) {
-    double normalizedCompression = Math.max(MIN_COMPRESSION, compression);
-    int padding = normalizedCompression < 30.0 ? 
LOW_COMPRESSION_CAPACITY_PADDING
-        : DEFAULT_CENTROID_CAPACITY_PADDING;
-    long defaultCapacity = (long) Math.ceil(2.0 * normalizedCompression + 
padding);
-    long legacyCapacity = Math.addExact(Math.multiplyExact(2L, (long) 
Math.ceil(compression)),
-        DEFAULT_CENTROID_CAPACITY_PADDING);
-    return Math.max(defaultCapacity, legacyCapacity);
+    _maxByteSize = Math.max(_maxByteSize, value.maxSerializedByteSize());

Review Comment:
   Fixed: fresh compression-100 state keeps a 3,392-byte compatible-output 
bound, including the 1,049-scalar case, without flushing on each row. Temporary 
raw/incoming buffer lengths no longer become a permanent Star-tree sizing 
maximum. Oversized historical bytes retain a separate safe bound; tests verify 
the bound covers actual serialization.



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