Coverage Report

Created: 2026-07-21 15:58

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/util/tdigest.h
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
/*
19
 * Licensed to Derrick R. Burns under one or more
20
 * contributor license agreements.  See the NOTICES file distributed with
21
 * this work for additional information regarding copyright ownership.
22
 * The ASF licenses this file to You under the Apache License, Version 2.0
23
 * (the "License"); you may not use this file except in compliance with
24
 * the License.  You may obtain a copy of the License at
25
 *
26
 *     http://www.apache.org/licenses/LICENSE-2.0
27
 *
28
 * Unless required by applicable law or agreed to in writing, software
29
 * distributed under the License is distributed on an "AS IS" BASIS,
30
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31
 * See the License for the specific language governing permissions and
32
 * limitations under the License.
33
 */
34
35
// T-Digest :  Percentile and Quantile Estimation of Big Data
36
// A new data structure for accurate on-line accumulation of rank-based statistics
37
// such as quantiles and trimmed means.
38
// See original paper: "Computing extremely accurate quantiles using t-digest"
39
// by Ted Dunning and Otmar Ertl for more details
40
// https://github.com/tdunning/t-digest/blob/07b8f2ca2be8d0a9f04df2feadad5ddc1bb73c88/docs/t-digest-paper/histo.pdf.
41
// https://github.com/derrickburns/tdigest
42
43
#pragma once
44
45
#include <pdqsort.h>
46
47
#include <algorithm>
48
#include <cfloat>
49
#include <cmath>
50
#include <iostream>
51
#include <memory>
52
#include <queue>
53
#include <utility>
54
#include <vector>
55
56
#include "common/check.h"
57
#include "common/factory_creator.h"
58
#include "common/logging.h"
59
60
namespace doris {
61
62
using Value = float;
63
using Weight = float;
64
using Index = size_t;
65
66
const size_t kHighWater = 40000;
67
68
class Centroid {
69
public:
70
17.6k
    Centroid() : Centroid(0.0, 0.0) {}
71
72
57.8k
    Centroid(Value mean, Weight weight) : _mean(mean), _weight(weight) {}
73
74
865k
    Value mean() const noexcept { return _mean; }
75
76
77.5k
    Weight weight() const noexcept { return _weight; }
77
78
2.77k
    Value& mean() noexcept { return _mean; }
79
80
1.56k
    Weight& weight() noexcept { return _weight; }
81
82
32.3k
    void add(const Centroid& c) {
83
32.3k
        DCHECK_GT(c._weight, 0);
84
32.3k
        if (_weight != 0.0) {
85
32.3k
            _weight += c._weight;
86
32.3k
            _mean += c._weight * (c._mean - _mean) / _weight;
87
32.3k
        } else {
88
0
            _weight = c._weight;
89
0
            _mean = c._mean;
90
0
        }
91
32.3k
    }
92
93
private:
94
    Value _mean = 0;
95
    Weight _weight = 0;
96
};
97
98
struct CentroidList {
99
1
    CentroidList(const std::vector<Centroid>& s) : iter(s.cbegin()), end(s.cend()) {}
100
    std::vector<Centroid>::const_iterator iter;
101
    std::vector<Centroid>::const_iterator end;
102
103
100
    bool advance() { return ++iter != end; }
104
};
105
106
class CentroidListComparator {
107
public:
108
    CentroidListComparator() = default;
109
110
0
    bool operator()(const CentroidList& left, const CentroidList& right) const {
111
0
        return left.iter->mean() > right.iter->mean();
112
0
    }
113
};
114
115
using CentroidListQueue =
116
        std::priority_queue<CentroidList, std::vector<CentroidList>, CentroidListComparator>;
117
118
struct CentroidComparator {
119
432k
    bool operator()(const Centroid& a, const Centroid& b) const { return a.mean() < b.mean(); }
120
};
121
122
class TDigest {
123
    ENABLE_FACTORY_CREATOR(TDigest);
124
125
    class TDigestComparator {
126
    public:
127
        TDigestComparator() = default;
128
129
0
        bool operator()(const TDigest* left, const TDigest* right) const {
130
0
            return left->totalSize() > right->totalSize();
131
0
        }
132
    };
133
    using TDigestQueue =
134
            std::priority_queue<const TDigest*, std::vector<const TDigest*>, TDigestComparator>;
135
136
public:
137
0
    TDigest() : TDigest(10000) {}
138
139
1.08k
    explicit TDigest(Value compression) : TDigest(compression, 0) {}
140
141
1.08k
    TDigest(Value compression, Index bufferSize) : TDigest(compression, bufferSize, 0) {}
142
143
    TDigest(Value compression, Index unmergedSize, Index mergedSize)
144
1.08k
            : _compression(compression),
145
1.08k
              _max_processed(processedSize(mergedSize, compression)),
146
1.08k
              _max_unprocessed(unprocessedSize(unmergedSize, compression)) {
147
1.08k
        _processed.reserve(_max_processed);
148
1.08k
        _unprocessed.reserve(_max_unprocessed + 1);
149
1.08k
    }
150
151
    TDigest(std::vector<Centroid>&& processed, std::vector<Centroid>&& unprocessed,
152
            Value compression, Index unmergedSize, Index mergedSize)
153
0
            : TDigest(compression, unmergedSize, mergedSize) {
154
0
        _processed = std::move(processed);
155
0
        _unprocessed = std::move(unprocessed);
156
0
157
0
        _processed_weight = weight(_processed);
158
0
        _unprocessed_weight = weight(_unprocessed);
159
0
        if (_processed.size() > 0) {
160
0
            _min = std::min(_min, _processed[0].mean());
161
0
            _max = std::max(_max, (_processed.cend() - 1)->mean());
162
0
        }
163
0
        updateCumulative();
164
0
    }
165
166
0
    static Weight weight(std::vector<Centroid>& centroids) noexcept {
167
0
        Weight w = 0.0;
168
0
        for (auto centroid : centroids) {
169
0
            w += centroid.weight();
170
0
        }
171
0
        return w;
172
0
    }
173
174
0
    TDigest& operator=(TDigest&& o) {
175
0
        _compression = o._compression;
176
0
        _max_processed = o._max_processed;
177
0
        _max_unprocessed = o._max_unprocessed;
178
0
        _processed_weight = o._processed_weight;
179
0
        _unprocessed_weight = o._unprocessed_weight;
180
0
        _processed = std::move(o._processed);
181
0
        _unprocessed = std::move(o._unprocessed);
182
0
        _cumulative = std::move(o._cumulative);
183
0
        _min = o._min;
184
0
        _max = o._max;
185
0
        return *this;
186
0
    }
187
188
    TDigest(TDigest&& o)
189
            : TDigest(std::move(o._processed), std::move(o._unprocessed), o._compression,
190
0
                      o._max_unprocessed, o._max_processed) {}
191
192
1.08k
    static inline Index processedSize(Index size, Value compression) noexcept {
193
1.08k
        return (size == 0) ? static_cast<Index>(2 * std::ceil(compression)) : size;
194
1.08k
    }
195
196
1.08k
    static inline Index unprocessedSize(Index size, Value compression) noexcept {
197
1.08k
        return (size == 0) ? static_cast<Index>(8 * std::ceil(compression)) : size;
198
1.08k
    }
199
200
    // merge in another t-digest
201
290
    void merge(const TDigest* other) {
202
290
        std::vector<const TDigest*> others {other};
203
290
        add(others.cbegin(), others.cend());
204
290
    }
205
206
    const std::vector<Centroid>& processed() const { return _processed; }
207
208
0
    const std::vector<Centroid>& unprocessed() const { return _unprocessed; }
209
210
0
    Index maxUnprocessed() const { return _max_unprocessed; }
211
212
0
    Index maxProcessed() const { return _max_processed; }
213
214
    void add(std::vector<const TDigest*> digests) { add(digests.cbegin(), digests.cend()); }
215
216
    // merge in a vector of tdigests in the most efficient manner possible
217
    // in constant space
218
    // works for any value of kHighWater
219
    void add(std::vector<const TDigest*>::const_iterator iter,
220
292
             std::vector<const TDigest*>::const_iterator end) {
221
292
        if (iter != end) {
222
292
            auto size = std::distance(iter, end);
223
292
            TDigestQueue pq(TDigestComparator {});
224
584
            for (; iter != end; iter++) {
225
292
                pq.push((*iter));
226
292
            }
227
292
            std::vector<const TDigest*> batch;
228
292
            batch.reserve(size);
229
230
292
            size_t totalSize = 0;
231
584
            while (!pq.empty()) {
232
292
                const auto* td = pq.top();
233
292
                batch.push_back(td);
234
292
                pq.pop();
235
292
                totalSize += td->totalSize();
236
292
                if (totalSize >= kHighWater || pq.empty()) {
237
292
                    mergeProcessed(batch);
238
292
                    mergeUnprocessed(batch);
239
292
                    processIfNecessary();
240
292
                    batch.clear();
241
292
                    totalSize = 0;
242
292
                }
243
292
            }
244
292
            updateCumulative();
245
292
        }
246
292
    }
247
248
0
    Weight processedWeight() const { return _processed_weight; }
249
250
0
    Weight unprocessedWeight() const { return _unprocessed_weight; }
251
252
447
    bool haveUnprocessed() const { return _unprocessed.size() > 0; }
253
254
292
    size_t totalSize() const { return _processed.size() + _unprocessed.size(); }
255
256
    long totalWeight() const { return static_cast<long>(_processed_weight + _unprocessed_weight); }
257
258
    // return the cdf on the t-digest
259
    Value cdf(Value x) {
260
        if (haveUnprocessed() || isDirty()) {
261
            process();
262
        }
263
        return cdfProcessed(x);
264
    }
265
266
40.5k
    bool isDirty() {
267
40.5k
        return _processed.size() > _max_processed || _unprocessed.size() > _max_unprocessed;
268
40.5k
    }
269
270
    // return the cdf on the processed values
271
0
    Value cdfProcessed(Value x) const {
272
0
        VLOG_CRITICAL << "cdf value " << x;
273
0
        VLOG_CRITICAL << "processed size " << _processed.size();
274
0
        if (_processed.size() == 0) {
275
0
            // no data to examine
276
0
            VLOG_CRITICAL << "no processed values";
277
0
278
0
            return 0.0;
279
0
        } else if (_processed.size() == 1) {
280
0
            VLOG_CRITICAL << "one processed value "
281
0
                          << " _min " << _min << " _max " << _max;
282
0
            // exactly one centroid, should have _max==_min
283
0
            auto width = _max - _min;
284
0
            if (x < _min) {
285
0
                return 0.0;
286
0
            } else if (x > _max) {
287
0
                return 1.0;
288
0
            } else if (x - _min <= width) {
289
0
                // _min and _max are too close together to do any viable interpolation
290
0
                return 0.5;
291
0
            } else {
292
0
                // interpolate if somehow we have weight > 0 and _max != _min
293
0
                return (x - _min) / (_max - _min);
294
0
            }
295
0
        } else {
296
0
            auto n = _processed.size();
297
0
            if (x <= _min) {
298
0
                VLOG_CRITICAL << "below _min "
299
0
                              << " _min " << _min << " x " << x;
300
0
                return 0;
301
0
            }
302
0
303
0
            if (x >= _max) {
304
0
                VLOG_CRITICAL << "above _max "
305
0
                              << " _max " << _max << " x " << x;
306
0
                return 1;
307
0
            }
308
0
309
0
            // check for the left tail
310
0
            if (x <= mean(0)) {
311
0
                VLOG_CRITICAL << "left tail "
312
0
                              << " _min " << _min << " mean(0) " << mean(0) << " x " << x;
313
0
314
0
                // note that this is different than mean(0) > _min ... this guarantees interpolation works
315
0
                if (mean(0) - _min > 0) {
316
0
                    return static_cast<Value>((x - _min) / (mean(0) - _min) * weight(0) /
317
0
                                              _processed_weight / 2.0);
318
0
                } else {
319
0
                    return 0;
320
0
                }
321
0
            }
322
0
323
0
            // and the right tail
324
0
            if (x >= mean(n - 1)) {
325
0
                VLOG_CRITICAL << "right tail"
326
0
                              << " _max " << _max << " mean(n - 1) " << mean(n - 1) << " x " << x;
327
0
328
0
                if (_max - mean(n - 1) > 0) {
329
0
                    return static_cast<Value>(1.0 - (_max - x) / (_max - mean(n - 1)) *
330
0
                                                            weight(n - 1) / _processed_weight /
331
0
                                                            2.0);
332
0
                } else {
333
0
                    return 1;
334
0
                }
335
0
            }
336
0
337
0
            CentroidComparator cc;
338
0
            auto iter =
339
0
                    std::upper_bound(_processed.cbegin(), _processed.cend(), Centroid(x, 0), cc);
340
0
341
0
            auto i = std::distance(_processed.cbegin(), iter);
342
0
            auto z1 = x - (iter - 1)->mean();
343
0
            auto z2 = (iter)->mean() - x;
344
0
            DCHECK_LE(0.0, z1);
345
0
            DCHECK_LE(0.0, z2);
346
0
            VLOG_CRITICAL << "middle "
347
0
                          << " z1 " << z1 << " z2 " << z2 << " x " << x;
348
0
349
0
            return weightedAverage(_cumulative[i - 1], z2, _cumulative[i], z1) / _processed_weight;
350
0
        }
351
0
    }
352
353
    // this returns a quantile on the t-digest
354
414
    Value quantile(Value q) {
355
414
        if (haveUnprocessed() || isDirty()) {
356
334
            process();
357
334
        }
358
414
        return quantileProcessed(q);
359
414
    }
360
361
    void quantiles(const double* quantile_levels, const size_t* permutation, size_t size,
362
22
                   double* result) {
363
22
        if (size == 0) {
364
0
            return;
365
0
        }
366
22
        if (haveUnprocessed() || isDirty()) {
367
21
            process();
368
21
        }
369
370
22
        if (_processed.empty()) {
371
1
            std::fill(result, result + size, NAN);
372
1
            return;
373
1
        }
374
375
21
        if (_processed.size() == 1) {
376
1
            std::fill(result, result + size, static_cast<double>(mean(0)));
377
1
            return;
378
1
        }
379
380
20
        const auto n = _processed.size();
381
20
        size_t cumulative_index = 0;
382
85
        for (size_t result_index = 0; result_index < size; ++result_index) {
383
65
            const size_t level_index = permutation[result_index];
384
65
            const auto q = static_cast<Value>(quantile_levels[level_index]);
385
65
            DORIS_CHECK(q >= 0);
386
65
            DORIS_CHECK(q <= 1);
387
388
65
            const auto index = q * _processed_weight;
389
65
            if (index <= weight(0) / 2.0) {
390
7
                DORIS_CHECK(weight(0) > 0);
391
7
                result[level_index] =
392
7
                        static_cast<double>(_min + 2.0 * index / weight(0) * (mean(0) - _min));
393
7
                continue;
394
7
            }
395
396
1.25k
            while (cumulative_index < _cumulative.size() && _cumulative[cumulative_index] < index) {
397
1.19k
                ++cumulative_index;
398
1.19k
            }
399
400
58
            if (cumulative_index > 0 && cumulative_index + 1 < _cumulative.size()) {
401
51
                auto z1 = index - _cumulative[cumulative_index - 1];
402
51
                auto z2 = _cumulative[cumulative_index] - index;
403
51
                result[level_index] = static_cast<double>(weightedAverage(
404
51
                        mean(cumulative_index - 1), z2, mean(cumulative_index), z1));
405
51
                continue;
406
51
            }
407
408
7
            DORIS_CHECK(index <= _processed_weight);
409
7
            DORIS_CHECK(index >= _processed_weight - weight(n - 1) / 2.0);
410
7
            auto z1 = static_cast<Value>(index - _processed_weight - weight(n - 1) / 2.0);
411
7
            auto z2 = static_cast<Value>(weight(n - 1) / 2 - z1);
412
7
            result[level_index] = static_cast<double>(weightedAverage(mean(n - 1), z1, _max, z2));
413
7
        }
414
20
    }
415
416
    // this returns a quantile on the currently processed values without changing the t-digest
417
    // the value will not represent the unprocessed values
418
414
    Value quantileProcessed(Value q) const {
419
414
        if (q < 0 || q > 1) {
420
0
            VLOG_CRITICAL << "q should be in [0,1], got " << q;
421
0
            return NAN;
422
0
        }
423
424
414
        if (_processed.size() == 0) {
425
            // no sorted means no data, no way to get a quantile
426
1
            return NAN;
427
413
        } else if (_processed.size() == 1) {
428
            // with one data point, all quantiles lead to Rome
429
430
133
            return mean(0);
431
133
        }
432
433
        // we know that there are at least two sorted now
434
280
        auto n = _processed.size();
435
436
        // if values were stored in a sorted array, index would be the offset we are Weighterested in
437
280
        const auto index = q * _processed_weight;
438
439
        // at the boundaries, we return _min or _max
440
280
        if (index <= weight(0) / 2.0) {
441
11
            DCHECK_GT(weight(0), 0);
442
11
            return static_cast<Value>(_min + 2.0 * index / weight(0) * (mean(0) - _min));
443
11
        }
444
445
269
        auto iter = std::lower_bound(_cumulative.cbegin(), _cumulative.cend(), index);
446
447
269
        if (iter != _cumulative.cend() && iter != _cumulative.cbegin() &&
448
269
            iter + 1 != _cumulative.cend()) {
449
215
            auto i = std::distance(_cumulative.cbegin(), iter);
450
215
            auto z1 = index - *(iter - 1);
451
215
            auto z2 = *(iter)-index;
452
            // VLOG_CRITICAL << "z2 " << z2 << " index " << index << " z1 " << z1;
453
215
            return weightedAverage(mean(i - 1), z2, mean(i), z1);
454
215
        }
455
456
269
        DCHECK_LE(index, _processed_weight);
457
54
        DCHECK_GE(index, _processed_weight - weight(n - 1) / 2.0);
458
459
54
        auto z1 = static_cast<Value>(index - _processed_weight - weight(n - 1) / 2.0);
460
54
        auto z2 = static_cast<Value>(weight(n - 1) / 2 - z1);
461
54
        return weightedAverage(mean(n - 1), z1, _max, z2);
462
269
    }
463
464
0
    Value compression() const { return _compression; }
465
466
30.0k
    void add(Value x) { add(x, 1); }
467
468
    void compress() { process(); }
469
470
    // add a single centroid to the unprocessed vector, processing previously unprocessed sorted if our limit has
471
    // been reached.
472
40.1k
    bool add(Value x, Weight w) {
473
40.1k
        if (std::isnan(x)) {
474
3
            return false;
475
3
        }
476
40.1k
        _unprocessed.emplace_back(x, w);
477
40.1k
        _unprocessed_weight += w;
478
40.1k
        processIfNecessary();
479
40.1k
        return true;
480
40.1k
    }
481
482
    void add(std::vector<Centroid>::const_iterator iter,
483
0
             std::vector<Centroid>::const_iterator end) {
484
0
        while (iter != end) {
485
0
            const size_t diff = std::distance(iter, end);
486
0
            const size_t room = _max_unprocessed - _unprocessed.size();
487
0
            auto mid = iter + std::min(diff, room);
488
0
            while (iter != mid) {
489
0
                _unprocessed.push_back(*(iter++));
490
0
            }
491
0
            if (_unprocessed.size() >= _max_unprocessed) {
492
0
                process();
493
0
            }
494
0
        }
495
0
    }
496
497
670
    uint32_t serialized_size() {
498
670
        return static_cast<uint32_t>(sizeof(uint32_t) + sizeof(Value) * 5 + sizeof(Index) * 2 +
499
670
                                     sizeof(uint32_t) * 3 + _processed.size() * sizeof(Centroid) +
500
670
                                     _unprocessed.size() * sizeof(Centroid) +
501
670
                                     _cumulative.size() * sizeof(Weight));
502
670
    }
503
504
333
    size_t serialize(uint8_t* writer) {
505
333
        uint8_t* dst = writer;
506
333
        uint32_t total_size = serialized_size();
507
333
        memcpy(writer, &total_size, sizeof(uint32_t));
508
333
        writer += sizeof(uint32_t);
509
333
        memcpy(writer, &_compression, sizeof(Value));
510
333
        writer += sizeof(Value);
511
333
        memcpy(writer, &_min, sizeof(Value));
512
333
        writer += sizeof(Value);
513
333
        memcpy(writer, &_max, sizeof(Value));
514
333
        writer += sizeof(Value);
515
333
        memcpy(writer, &_max_processed, sizeof(Index));
516
333
        writer += sizeof(Index);
517
333
        memcpy(writer, &_max_unprocessed, sizeof(Index));
518
333
        writer += sizeof(Index);
519
333
        memcpy(writer, &_processed_weight, sizeof(Value));
520
333
        writer += sizeof(Value);
521
333
        memcpy(writer, &_unprocessed_weight, sizeof(Value));
522
333
        writer += sizeof(Value);
523
524
333
        auto size = static_cast<uint32_t>(_processed.size());
525
333
        memcpy(writer, &size, sizeof(uint32_t));
526
333
        writer += sizeof(uint32_t);
527
333
        for (int i = 0; i < size; i++) {
528
0
            memcpy(writer, &_processed[i], sizeof(Centroid));
529
0
            writer += sizeof(Centroid);
530
0
        }
531
532
333
        size = static_cast<uint32_t>(_unprocessed.size());
533
333
        memcpy(writer, &size, sizeof(uint32_t));
534
333
        writer += sizeof(uint32_t);
535
        //TODO(weixiang): may be once memcpy is enough!
536
14.0k
        for (int i = 0; i < size; i++) {
537
13.7k
            memcpy(writer, &_unprocessed[i], sizeof(Centroid));
538
13.7k
            writer += sizeof(Centroid);
539
13.7k
        }
540
541
333
        size = static_cast<uint32_t>(_cumulative.size());
542
333
        memcpy(writer, &size, sizeof(uint32_t));
543
333
        writer += sizeof(uint32_t);
544
429
        for (int i = 0; i < size; i++) {
545
96
            memcpy(writer, &_cumulative[i], sizeof(Weight));
546
96
            writer += sizeof(Weight);
547
96
        }
548
333
        return writer - dst;
549
333
    }
550
551
294
    void unserialize(const uint8_t* type_reader) {
552
294
        uint32_t total_length = 0;
553
294
        memcpy(&total_length, type_reader, sizeof(uint32_t));
554
294
        type_reader += sizeof(uint32_t);
555
294
        memcpy(&_compression, type_reader, sizeof(Value));
556
294
        type_reader += sizeof(Value);
557
294
        memcpy(&_min, type_reader, sizeof(Value));
558
294
        type_reader += sizeof(Value);
559
294
        memcpy(&_max, type_reader, sizeof(Value));
560
294
        type_reader += sizeof(Value);
561
562
294
        memcpy(&_max_processed, type_reader, sizeof(Index));
563
294
        type_reader += sizeof(Index);
564
294
        memcpy(&_max_unprocessed, type_reader, sizeof(Index));
565
294
        type_reader += sizeof(Index);
566
294
        memcpy(&_processed_weight, type_reader, sizeof(Value));
567
294
        type_reader += sizeof(Value);
568
294
        memcpy(&_unprocessed_weight, type_reader, sizeof(Value));
569
294
        type_reader += sizeof(Value);
570
571
294
        uint32_t size;
572
294
        memcpy(&size, type_reader, sizeof(uint32_t));
573
294
        type_reader += sizeof(uint32_t);
574
294
        _processed.resize(size);
575
294
        for (int i = 0; i < size; i++) {
576
0
            memcpy(&_processed[i], type_reader, sizeof(Centroid));
577
0
            type_reader += sizeof(Centroid);
578
0
        }
579
294
        memcpy(&size, type_reader, sizeof(uint32_t));
580
294
        type_reader += sizeof(uint32_t);
581
294
        _unprocessed.resize(size);
582
17.9k
        for (int i = 0; i < size; i++) {
583
17.6k
            memcpy(&_unprocessed[i], type_reader, sizeof(Centroid));
584
17.6k
            type_reader += sizeof(Centroid);
585
17.6k
        }
586
294
        memcpy(&size, type_reader, sizeof(uint32_t));
587
294
        type_reader += sizeof(uint32_t);
588
294
        _cumulative.resize(size);
589
351
        for (int i = 0; i < size; i++) {
590
57
            memcpy(&_cumulative[i], type_reader, sizeof(Weight));
591
57
            type_reader += sizeof(Weight);
592
57
        }
593
294
    }
594
595
private:
596
    Value _compression;
597
598
    Value _min = std::numeric_limits<Value>::max();
599
600
    Value _max = std::numeric_limits<Value>::min();
601
602
    Index _max_processed;
603
604
    Index _max_unprocessed;
605
606
    Value _processed_weight = 0.0;
607
608
    Value _unprocessed_weight = 0.0;
609
610
    std::vector<Centroid> _processed;
611
612
    std::vector<Centroid> _unprocessed;
613
614
    std::vector<Weight> _cumulative;
615
616
    // return mean of i-th centroid
617
759
    Value mean(int64_t i) const noexcept { return _processed[i].mean(); }
618
619
    // return weight of i-th centroid
620
15.7k
    Weight weight(int64_t i) const noexcept { return _processed[i].weight(); }
621
622
    // append all unprocessed centroids into current unprocessed vector
623
292
    void mergeUnprocessed(const std::vector<const TDigest*>& tdigests) {
624
292
        if (tdigests.size() == 0) {
625
0
            return;
626
0
        }
627
628
292
        size_t total = _unprocessed.size();
629
292
        for (const auto& td : tdigests) {
630
292
            total += td->_unprocessed.size();
631
292
        }
632
633
292
        _unprocessed.reserve(total);
634
292
        for (const auto& td : tdigests) {
635
292
            _unprocessed.insert(_unprocessed.end(), td->_unprocessed.cbegin(),
636
292
                                td->_unprocessed.cend());
637
292
            _unprocessed_weight += td->_unprocessed_weight;
638
292
        }
639
292
    }
640
641
    // merge all processed centroids together into a single sorted vector
642
292
    void mergeProcessed(const std::vector<const TDigest*>& tdigests) {
643
292
        if (tdigests.size() == 0) {
644
0
            return;
645
0
        }
646
647
292
        size_t total = 0;
648
292
        CentroidListQueue pq(CentroidListComparator {});
649
292
        for (const auto& td : tdigests) {
650
292
            const auto& sorted = td->_processed;
651
292
            auto size = sorted.size();
652
292
            if (size > 0) {
653
1
                pq.push(CentroidList(sorted));
654
1
                total += size;
655
1
                _processed_weight += td->_processed_weight;
656
1
            }
657
292
        }
658
292
        if (total == 0) {
659
291
            return;
660
291
        }
661
662
1
        if (_processed.size() > 0) {
663
0
            pq.push(CentroidList(_processed));
664
0
            total += _processed.size();
665
0
        }
666
667
1
        std::vector<Centroid> sorted;
668
1
        VLOG_CRITICAL << "total " << total;
669
1
        sorted.reserve(total);
670
671
101
        while (!pq.empty()) {
672
100
            auto best = pq.top();
673
100
            pq.pop();
674
100
            sorted.push_back(*(best.iter));
675
100
            if (best.advance()) {
676
99
                pq.push(best);
677
99
            }
678
100
        }
679
1
        _processed = std::move(sorted);
680
1
        if (_processed.size() > 0) {
681
1
            _min = std::min(_min, _processed[0].mean());
682
1
            _max = std::max(_max, (_processed.cend() - 1)->mean());
683
1
        }
684
1
    }
685
686
40.4k
    void processIfNecessary() {
687
40.4k
        if (isDirty()) {
688
2
            process();
689
2
        }
690
40.4k
    }
691
692
652
    void updateCumulative() {
693
652
        const auto n = _processed.size();
694
652
        _cumulative.clear();
695
652
        _cumulative.reserve(n + 1);
696
652
        Weight previous = 0.0;
697
15.8k
        for (Index i = 0; i < n; i++) {
698
15.1k
            Weight current = weight(i);
699
15.1k
            auto halfCurrent = static_cast<Weight>(current / 2.0);
700
15.1k
            _cumulative.push_back(previous + halfCurrent);
701
15.1k
            previous = previous + current;
702
15.1k
        }
703
652
        _cumulative.push_back(previous);
704
652
    }
705
706
    // merges _unprocessed centroids and _processed centroids together and processes them
707
    // when complete, _unprocessed will be empty and _processed will have at most _max_processed centroids
708
360
    void process() {
709
360
        CentroidComparator cc;
710
        // select percentile_approx(lo_orderkey,0.5) from lineorder;
711
        // have test pdqsort and RadixSort, find here pdqsort performance is better when data is struct Centroid
712
        // But when sort plain type like int/float of std::vector<T>, find RadixSort is better
713
360
        pdqsort(_unprocessed.begin(), _unprocessed.end(), cc);
714
360
        auto count = _unprocessed.size();
715
360
        _unprocessed.insert(_unprocessed.end(), _processed.cbegin(), _processed.cend());
716
360
        std::inplace_merge(_unprocessed.begin(), _unprocessed.begin() + count, _unprocessed.end(),
717
360
                           cc);
718
719
360
        _processed_weight += _unprocessed_weight;
720
360
        _unprocessed_weight = 0;
721
360
        _processed.clear();
722
723
360
        _processed.push_back(_unprocessed[0]);
724
360
        Weight wSoFar = _unprocessed[0].weight();
725
360
        Weight wLimit = _processed_weight * integratedQ(1.0);
726
727
360
        auto end = _unprocessed.end();
728
47.4k
        for (auto iter = _unprocessed.cbegin() + 1; iter < end; iter++) {
729
47.0k
            const auto& centroid = *iter;
730
47.0k
            Weight projectedW = wSoFar + centroid.weight();
731
47.0k
            if (projectedW <= wLimit) {
732
32.3k
                wSoFar = projectedW;
733
32.3k
                (_processed.end() - 1)->add(centroid);
734
32.3k
            } else {
735
14.7k
                auto k1 = integratedLocation(wSoFar / _processed_weight);
736
14.7k
                wLimit = _processed_weight * integratedQ(static_cast<Value>(k1 + 1.0));
737
14.7k
                wSoFar += centroid.weight();
738
14.7k
                _processed.emplace_back(centroid);
739
14.7k
            }
740
47.0k
        }
741
360
        _unprocessed.clear();
742
360
        _min = std::min(_min, _processed[0].mean());
743
360
        VLOG_CRITICAL << "new _min " << _min;
744
360
        _max = std::max(_max, (_processed.cend() - 1)->mean());
745
360
        VLOG_CRITICAL << "new _max " << _max;
746
360
        updateCumulative();
747
360
    }
748
749
0
    size_t checkWeights(const std::vector<Centroid>& sorted, Value total) {
750
0
        size_t badWeight = 0;
751
0
        auto k1 = 0.0;
752
0
        auto q = 0.0;
753
0
        for (auto iter = sorted.cbegin(); iter != sorted.cend(); iter++) {
754
0
            auto w = iter->weight();
755
0
            auto dq = w / total;
756
0
            auto k2 = integratedLocation(static_cast<Value>(q + dq));
757
0
            if (k2 - k1 > 1 && w != 1) {
758
0
                VLOG_CRITICAL << "Oversize centroid at " << std::distance(sorted.cbegin(), iter)
759
0
                              << " k1 " << k1 << " k2 " << k2 << " dk " << (k2 - k1) << " w " << w
760
0
                              << " q " << q;
761
0
                badWeight++;
762
0
            }
763
0
            if (k2 - k1 > 1.5 && w != 1) {
764
0
                VLOG_CRITICAL << "Egregiously Oversize centroid at "
765
0
                              << std::distance(sorted.cbegin(), iter) << " k1 " << k1 << " k2 "
766
0
                              << k2 << " dk " << (k2 - k1) << " w " << w << " q " << q;
767
0
                badWeight++;
768
0
            }
769
0
            q += dq;
770
0
            k1 = k2;
771
0
        }
772
0
773
0
        return badWeight;
774
0
    }
775
776
    /**
777
    * Converts a quantile into a centroid scale value.  The centroid scale is nomin_ally
778
    * the number k of the centroid that a quantile point q should belong to.  Due to
779
    * round-offs, however, we can't align things perfectly without splitting points
780
    * and sorted.  We don't want to do that, so we have to allow for offsets.
781
    * In the end, the criterion is that any quantile range that spans a centroid
782
    * scale range more than one should be split across more than one centroid if
783
    * possible.  This won't be possible if the quantile range refers to a single point
784
    * or an already existing centroid.
785
    * <p/>
786
    * This mapping is steep near q=0 or q=1 so each centroid there will correspond to
787
    * less q range.  Near q=0.5, the mapping is flatter so that sorted there will
788
    * represent a larger chunk of quantiles.
789
    *
790
    * @param q The quantile scale value to be mapped.
791
    * @return The centroid scale value corresponding to q.
792
    */
793
14.7k
    Value integratedLocation(Value q) const {
794
14.7k
        return static_cast<Value>(_compression * (std::asin(2.0 * q - 1.0) + M_PI / 2) / M_PI);
795
14.7k
    }
796
797
15.0k
    Value integratedQ(Value k) const {
798
15.0k
        return static_cast<Value>(
799
15.0k
                (std::sin(std::min(k, _compression) * M_PI / _compression - M_PI / 2) + 1) / 2);
800
15.0k
    }
801
802
    /**
803
     * Same as {@link #weightedAverageSorted(Value, Value, Value, Value)} but flips
804
     * the order of the variables if <code>x2</code> is greater than
805
     * <code>x1</code>.
806
    */
807
334
    static Value weightedAverage(Value x1, Value w1, Value x2, Value w2) {
808
334
        return (x1 <= x2) ? weightedAverageSorted(x1, w1, x2, w2)
809
334
                          : weightedAverageSorted(x2, w2, x1, w1);
810
334
    }
811
812
    /**
813
    * Compute the weighted average between <code>x1</code> with a weight of
814
    * <code>w1</code> and <code>x2</code> with a weight of <code>w2</code>.
815
    * This expects <code>x1</code> to be less than or equal to <code>x2</code>
816
    * and is guaranteed to return a number between <code>x1</code> and
817
    * <code>x2</code>.
818
    */
819
334
    static Value weightedAverageSorted(Value x1, Value w1, Value x2, Value w2) {
820
334
        DCHECK_LE(x1, x2);
821
334
        const Value x = (x1 * w1 + x2 * w2) / (w1 + w2);
822
334
        return std::max(x1, std::min(x, x2));
823
334
    }
824
825
0
    static Value interpolate(Value x, Value x0, Value x1) { return (x - x0) / (x1 - x0); }
826
827
    /**
828
    * Computes an interpolated value of a quantile that is between two sorted.
829
    *
830
    * Index is the quantile desired multiplied by the total number of samples - 1.
831
    *
832
    * @param index              Denormalized quantile desired
833
    * @param previousIndex      The denormalized quantile corresponding to the center of the previous centroid.
834
    * @param nextIndex          The denormalized quantile corresponding to the center of the following centroid.
835
    * @param previousMean       The mean of the previous centroid.
836
    * @param nextMean           The mean of the following centroid.
837
    * @return  The interpolated mean.
838
    */
839
    static Value quantile(Value index, Value previousIndex, Value nextIndex, Value previousMean,
840
0
                          Value nextMean) {
841
0
        const auto delta = nextIndex - previousIndex;
842
0
        const auto previousWeight = (nextIndex - index) / delta;
843
0
        const auto nextWeight = (index - previousIndex) / delta;
844
0
        return previousMean * previousWeight + nextMean * nextWeight;
845
0
    }
846
};
847
} // namespace doris