Skip to content

Commit 36b7322

Browse files
committed
Add pure t-digest merge-order reproducer
1 parent 73f569b commit 36b7322

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.pinot.segment.local.aggregator;
20+
21+
import com.tdunning.math.stats.MergingDigest;
22+
import com.tdunning.math.stats.TDigest;
23+
import java.nio.ByteBuffer;
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
import java.util.Random;
27+
import org.testng.annotations.Test;
28+
29+
import static org.testng.Assert.assertTrue;
30+
31+
32+
/**
33+
* Pure t-digest reproducer for the merge-order sensitivity observed while upgrading Pinot from
34+
* t-digest 3.2 to 3.3 in PR 18103.
35+
*
36+
* <p>The sample data intentionally mirrors Pinot's pre-aggregated percentileTDigest star-tree
37+
* path: many tiny leaf digests (two points each), most values near zero, sparse large spikes near
38+
* the upper tail, and repeated serialize/deserialize round-trips between hierarchical merge levels.
39+
*
40+
* <p>On t-digest 3.3 this deterministic generator produces a large divergence between sequential
41+
* merging and hierarchical merging at low compression, while compression 500 restores stable
42+
* results. The generator depends only on t-digest APIs so it can be copied directly into upstream
43+
* t-digest tests for further investigation.
44+
*/
45+
public class TDigestMergeOrderReproducerTest {
46+
private static final int SCALE = 10_000;
47+
private static final int NUM_DIGESTS = 1_024;
48+
private static final int VALUES_PER_DIGEST = 2;
49+
private static final int BATCH_SIZE = 16;
50+
private static final int DATA_SEED = 5;
51+
52+
@Test
53+
public void testTailSpikesScenarioRequiresHighCompressionForStableHierarchicalMerges() {
54+
double divergenceAt100 = maxMergeOrderDivergence(100);
55+
double divergenceAt150 = maxMergeOrderDivergence(150);
56+
double divergenceAt200 = maxMergeOrderDivergence(200);
57+
double divergenceAt500 = maxMergeOrderDivergence(500);
58+
59+
assertTrue(divergenceAt100 > 0.02,
60+
String.format("Expected large merge-order divergence at compression 100 but saw %.6f", divergenceAt100));
61+
assertTrue(divergenceAt150 > 0.02,
62+
String.format("Expected large merge-order divergence at compression 150 but saw %.6f", divergenceAt150));
63+
assertTrue(divergenceAt200 > 0.02,
64+
String.format("Expected large merge-order divergence at compression 200 but saw %.6f", divergenceAt200));
65+
assertTrue(divergenceAt500 < 0.001,
66+
String.format("Expected stable merge-order behavior at compression 500 but saw %.6f", divergenceAt500));
67+
}
68+
69+
private double maxMergeOrderDivergence(int compression) {
70+
List<TDigest> leafDigests = createLeafDigests(compression);
71+
TDigest sequential = roundTrip(mergeSequential(leafDigests));
72+
TDigest hierarchical = roundTrip(mergeHierarchical(leafDigests, BATCH_SIZE));
73+
74+
double maxNormalizedDivergence = 0d;
75+
for (int percentile = 0; percentile <= 100; percentile++) {
76+
double quantile = percentile / 100d;
77+
double delta = Math.abs(sequential.quantile(quantile) - hierarchical.quantile(quantile)) / SCALE;
78+
maxNormalizedDivergence = Math.max(maxNormalizedDivergence, delta);
79+
}
80+
return maxNormalizedDivergence;
81+
}
82+
83+
private List<TDigest> createLeafDigests(int compression) {
84+
Random random = new Random(DATA_SEED);
85+
List<TDigest> digests = new ArrayList<>(NUM_DIGESTS);
86+
for (int i = 0; i < NUM_DIGESTS; i++) {
87+
TDigest digest = TDigest.createMergingDigest(compression);
88+
for (int j = 0; j < VALUES_PER_DIGEST; j++) {
89+
digest.add(nextTailSpikeValue(random));
90+
}
91+
digests.add(roundTrip(digest));
92+
}
93+
return digests;
94+
}
95+
96+
private TDigest mergeSequential(List<TDigest> digests) {
97+
TDigest accumulator = roundTrip(digests.get(0));
98+
for (int i = 1; i < digests.size(); i++) {
99+
accumulator.add(digests.get(i));
100+
}
101+
return accumulator;
102+
}
103+
104+
private TDigest mergeHierarchical(List<TDigest> digests, int batchSize) {
105+
List<TDigest> currentLevel = digests;
106+
while (currentLevel.size() > 1) {
107+
List<TDigest> nextLevel = new ArrayList<>((currentLevel.size() + batchSize - 1) / batchSize);
108+
for (int start = 0; start < currentLevel.size(); start += batchSize) {
109+
int end = Math.min(start + batchSize, currentLevel.size());
110+
TDigest accumulator = roundTrip(currentLevel.get(start));
111+
for (int i = start + 1; i < end; i++) {
112+
accumulator.add(currentLevel.get(i));
113+
}
114+
nextLevel.add(roundTrip(accumulator));
115+
}
116+
currentLevel = nextLevel;
117+
}
118+
return currentLevel.get(0);
119+
}
120+
121+
private double nextTailSpikeValue(Random random) {
122+
double roll = random.nextDouble();
123+
if (roll < 0.97d) {
124+
return random.nextDouble() * 100d;
125+
}
126+
if (roll < 0.995d) {
127+
return 9_900d + random.nextDouble() * 50d;
128+
}
129+
return random.nextDouble() * SCALE;
130+
}
131+
132+
private TDigest roundTrip(TDigest digest) {
133+
ByteBuffer buffer = ByteBuffer.allocate(digest.smallByteSize());
134+
digest.asSmallBytes(buffer);
135+
buffer.flip();
136+
return MergingDigest.fromBytes(buffer);
137+
}
138+
}

0 commit comments

Comments
 (0)