Coverage Report

Created: 2026-10-24 09:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/root/doris/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/factory_creator.h"
57
#include "common/logging.h"
58
59
namespace doris {
60
#include "common/compile_check_begin.h"
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
273k
    Centroid() : Centroid(0.0, 0.0) {}
71
72
439k
    Centroid(Value mean, Weight weight) : _mean(mean), _weight(weight) {}
73
74
231M
    Value mean() const noexcept { return _mean; }
75
76
341M
    Weight weight() const noexcept { return _weight; }
77
78
16.3k
    Value& mean() noexcept { return _mean; }
79
80
14.7k
    Weight& weight() noexcept { return _weight; }
81
82
1.00M
    void add(const Centroid& c) {
83
1.00M
        DCHECK_GT(c._weight, 0);
84
1.00M
        if (_weight != 0.0) {
85
1.00M
            _weight += c._weight;
86
1.00M
            _mean += c._weight * (c._mean - _mean) / _weight;
87
18.4E
        } else {
88
18.4E
            _weight = c._weight;
89
18.4E
            _mean = c._mean;
90
18.4E
        }
91
1.00M
    }
92
93
private:
94
    Value _mean = 0;
95
    Weight _weight = 0;
96
};
97
98
struct CentroidList {
99
801
    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
1.89M
    bool advance() { return ++iter != end; }
104
};
105
106
class CentroidListComparator {
107
public:
108
    CentroidListComparator() = default;
109
110
1.76M
    bool operator()(const CentroidList& left, const CentroidList& right) const {
111
1.76M
        return left.iter->mean() > right.iter->mean();
112
1.76M
    }
113
};
114
115
using CentroidListQueue =
116
        std::priority_queue<CentroidList, std::vector<CentroidList>, CentroidListComparator>;
117
118
struct CentroidComparator {
119
114M
    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.17k
    explicit TDigest(Value compression) : TDigest(compression, 0) {}
140
141
1.17k
    TDigest(Value compression, Index bufferSize) : TDigest(compression, bufferSize, 0) {}
142
143
    TDigest(Value compression, Index unmergedSize, Index mergedSize)
144
1.17k
            : _compression(compression),
145
1.17k
              _max_processed(processedSize(mergedSize, compression)),
146
1.17k
              _max_unprocessed(unprocessedSize(unmergedSize, compression)) {
147
        // Deliberately do not preallocate. Aggregations keep one digest per group
148
        // (percentile_approx, quantile_union), so reserving 2*compression processed and
149
        // 8*compression unprocessed centroids up front costs O(groups * compression) memory
150
        // before any value is stored: 800KB per group at the default compression 10000, which
151
        // is 480GB of virtual address space for 600K singleton groups. The buffers grow on
152
        // demand instead; _max_processed and _max_unprocessed still bound them and drive
153
        // exactly the same compression thresholds as before.
154
1.17k
    }
155
156
    TDigest(std::vector<Centroid>&& processed, std::vector<Centroid>&& unprocessed,
157
            Value compression, Index unmergedSize, Index mergedSize)
158
2
            : TDigest(compression, unmergedSize, mergedSize) {
159
2
        _processed = std::move(processed);
160
2
        _unprocessed = std::move(unprocessed);
161
162
2
        _processed_weight = weight(_processed);
163
2
        _unprocessed_weight = weight(_unprocessed);
164
2
        if (_processed.size() > 0) {
165
2
            _min = std::min(_min, _processed[0].mean());
166
2
            _max = std::max(_max, (_processed.cend() - 1)->mean());
167
2
        }
168
2
        updateCumulative();
169
2
    }
170
171
4
    static Weight weight(std::vector<Centroid>& centroids) noexcept {
172
4
        Weight w = 0.0;
173
6
        for (auto centroid : centroids) {
174
6
            w += centroid.weight();
175
6
        }
176
4
        return w;
177
4
    }
178
179
12.0k
    TDigest(const TDigest&) = default;
180
181
0
    TDigest& operator=(TDigest&& o) {
182
0
        _compression = o._compression;
183
0
        _max_processed = o._max_processed;
184
0
        _max_unprocessed = o._max_unprocessed;
185
0
        _processed_weight = o._processed_weight;
186
0
        _unprocessed_weight = o._unprocessed_weight;
187
0
        _processed = std::move(o._processed);
188
0
        _unprocessed = std::move(o._unprocessed);
189
0
        _cumulative = std::move(o._cumulative);
190
0
        _min = o._min;
191
0
        _max = o._max;
192
0
        return *this;
193
0
    }
194
195
    TDigest(TDigest&& o)
196
            : TDigest(std::move(o._processed), std::move(o._unprocessed), o._compression,
197
0
                      o._max_unprocessed, o._max_processed) {}
198
199
1.17k
    static inline Index processedSize(Index size, Value compression) noexcept {
200
1.17k
        return (size == 0) ? static_cast<Index>(2 * std::ceil(compression)) : size;
201
1.17k
    }
202
203
1.17k
    static inline Index unprocessedSize(Index size, Value compression) noexcept {
204
1.17k
        return (size == 0) ? static_cast<Index>(8 * std::ceil(compression)) : size;
205
1.17k
    }
206
207
    // merge in another t-digest
208
408
    void merge(const TDigest* other) {
209
408
        std::vector<const TDigest*> others {other};
210
408
        add(others.cbegin(), others.cend());
211
408
    }
212
213
1.00k
    const std::vector<Centroid>& processed() const { return _processed; }
214
215
13.0k
    const std::vector<Centroid>& unprocessed() const { return _unprocessed; }
216
217
1
    Index maxUnprocessed() const { return _max_unprocessed; }
218
219
1
    Index maxProcessed() const { return _max_processed; }
220
221
1
    void add(std::vector<const TDigest*> digests) { add(digests.cbegin(), digests.cend()); }
222
223
    // merge in a vector of tdigests in the most efficient manner possible
224
    // in constant space
225
    // works for any value of kHighWater
226
    void add(std::vector<const TDigest*>::const_iterator iter,
227
409
             std::vector<const TDigest*>::const_iterator end) {
228
409
        if (iter != end) {
229
409
            auto size = std::distance(iter, end);
230
409
            TDigestQueue pq(TDigestComparator {});
231
818
            for (; iter != end; iter++) {
232
409
                pq.push((*iter));
233
409
            }
234
409
            std::vector<const TDigest*> batch;
235
409
            batch.reserve(size);
236
237
409
            size_t totalSize = 0;
238
818
            while (!pq.empty()) {
239
409
                const auto* td = pq.top();
240
409
                batch.push_back(td);
241
409
                pq.pop();
242
409
                totalSize += td->totalSize();
243
409
                if (totalSize >= kHighWater || pq.empty()) {
244
409
                    mergeProcessed(batch);
245
409
                    mergeUnprocessed(batch);
246
409
                    processIfNecessary();
247
409
                    batch.clear();
248
409
                    totalSize = 0;
249
409
                }
250
409
            }
251
409
            updateCumulative();
252
409
        }
253
409
    }
254
255
0
    Weight processedWeight() const { return _processed_weight; }
256
257
0
    Weight unprocessedWeight() const { return _unprocessed_weight; }
258
259
26.3k
    bool haveUnprocessed() const { return _unprocessed.size() > 0; }
260
261
409
    size_t totalSize() const { return _processed.size() + _unprocessed.size(); }
262
263
20
    long totalWeight() const { return static_cast<long>(_processed_weight + _unprocessed_weight); }
264
265
    // return the cdf on the t-digest
266
11
    Value cdf(Value x) {
267
11
        if (haveUnprocessed() || isDirty()) {
268
0
            process();
269
0
        }
270
11
        return cdfProcessed(x);
271
11
    }
272
273
166k
    bool isDirty() {
274
166k
        return _processed.size() > _max_processed || _unprocessed.size() > _max_unprocessed;
275
166k
    }
276
277
    // return the cdf on the processed values
278
11
    Value cdfProcessed(Value x) const {
279
11
        VLOG_CRITICAL << "cdf value " << x;
280
11
        VLOG_CRITICAL << "processed size " << _processed.size();
281
11
        if (_processed.size() == 0) {
282
            // no data to examine
283
0
            VLOG_CRITICAL << "no processed values";
284
285
0
            return 0.0;
286
11
        } else if (_processed.size() == 1) {
287
0
            VLOG_CRITICAL << "one processed value "
288
0
                          << " _min " << _min << " _max " << _max;
289
            // exactly one centroid, should have _max==_min
290
0
            auto width = _max - _min;
291
0
            if (x < _min) {
292
0
                return 0.0;
293
0
            } else if (x > _max) {
294
0
                return 1.0;
295
0
            } else if (x - _min <= width) {
296
                // _min and _max are too close together to do any viable interpolation
297
0
                return 0.5;
298
0
            } else {
299
                // interpolate if somehow we have weight > 0 and _max != _min
300
0
                return (x - _min) / (_max - _min);
301
0
            }
302
11
        } else {
303
11
            auto n = _processed.size();
304
11
            if (x <= _min) {
305
1
                VLOG_CRITICAL << "below _min "
306
1
                              << " _min " << _min << " x " << x;
307
1
                return 0;
308
1
            }
309
310
10
            if (x >= _max) {
311
3
                VLOG_CRITICAL << "above _max "
312
3
                              << " _max " << _max << " x " << x;
313
3
                return 1;
314
3
            }
315
316
            // check for the left tail
317
7
            if (x <= mean(0)) {
318
0
                VLOG_CRITICAL << "left tail "
319
0
                              << " _min " << _min << " mean(0) " << mean(0) << " x " << x;
320
321
                // note that this is different than mean(0) > _min ... this guarantees interpolation works
322
0
                if (mean(0) - _min > 0) {
323
0
                    return static_cast<Value>((x - _min) / (mean(0) - _min) * weight(0) /
324
0
                                              _processed_weight / 2.0);
325
0
                } else {
326
0
                    return 0;
327
0
                }
328
0
            }
329
330
            // and the right tail
331
7
            if (x >= mean(n - 1)) {
332
0
                VLOG_CRITICAL << "right tail"
333
0
                              << " _max " << _max << " mean(n - 1) " << mean(n - 1) << " x " << x;
334
335
0
                if (_max - mean(n - 1) > 0) {
336
0
                    return static_cast<Value>(1.0 - (_max - x) / (_max - mean(n - 1)) *
337
0
                                                            weight(n - 1) / _processed_weight /
338
0
                                                            2.0);
339
0
                } else {
340
0
                    return 1;
341
0
                }
342
0
            }
343
344
7
            CentroidComparator cc;
345
7
            auto iter =
346
7
                    std::upper_bound(_processed.cbegin(), _processed.cend(), Centroid(x, 0), cc);
347
348
7
            auto i = std::distance(_processed.cbegin(), iter);
349
7
            auto z1 = x - (iter - 1)->mean();
350
7
            auto z2 = (iter)->mean() - x;
351
7
            DCHECK_LE(0.0, z1);
352
7
            DCHECK_LE(0.0, z2);
353
7
            VLOG_CRITICAL << "middle "
354
7
                          << " z1 " << z1 << " z2 " << z2 << " x " << x;
355
356
7
            return weightedAverage(_cumulative[i - 1], z2, _cumulative[i], z1) / _processed_weight;
357
7
        }
358
11
    }
359
360
    // this returns a quantile on the t-digest
361
1.06k
    Value quantile(Value q) {
362
1.06k
        if (haveUnprocessed() || isDirty()) {
363
1.01k
            process();
364
1.01k
        }
365
1.06k
        return quantileProcessed(q);
366
1.06k
    }
367
368
    // this returns a quantile on the currently processed values without changing the t-digest
369
    // the value will not represent the unprocessed values
370
1.94k
    Value quantileProcessed(Value q) const {
371
1.94k
        if (q < 0 || q > 1) {
372
0
            VLOG_CRITICAL << "q should be in [0,1], got " << q;
373
0
            return NAN;
374
0
        }
375
376
1.94k
        if (_processed.size() == 0) {
377
            // no sorted means no data, no way to get a quantile
378
0
            return NAN;
379
1.94k
        } else if (_processed.size() == 1) {
380
            // with one data point, all quantiles lead to Rome
381
382
1.00k
            return mean(0);
383
1.00k
        }
384
385
        // we know that there are at least two sorted now
386
943
        auto n = _processed.size();
387
388
        // if values were stored in a sorted array, index would be the offset we are Weighterested in
389
943
        const auto index = q * _processed_weight;
390
391
        // at the boundaries, we return _min or _max
392
943
        if (index <= weight(0) / 2.0) {
393
47
            DCHECK_GT(weight(0), 0);
394
47
            return static_cast<Value>(_min + 2.0 * index / weight(0) * (mean(0) - _min));
395
47
        }
396
397
896
        auto iter = std::lower_bound(_cumulative.cbegin(), _cumulative.cend(), index);
398
399
896
        if (iter != _cumulative.cend() && iter != _cumulative.cbegin() &&
400
896
            iter + 1 != _cumulative.cend()) {
401
244
            auto i = std::distance(_cumulative.cbegin(), iter);
402
244
            auto z1 = index - *(iter - 1);
403
244
            auto z2 = *(iter)-index;
404
            // VLOG_CRITICAL << "z2 " << z2 << " index " << index << " z1 " << z1;
405
244
            return weightedAverage(mean(i - 1), z2, mean(i), z1);
406
244
        }
407
408
896
        DCHECK_LE(index, _processed_weight);
409
652
        DCHECK_GE(index, _processed_weight - weight(n - 1) / 2.0);
410
411
652
        auto z1 = static_cast<Value>(index - _processed_weight - weight(n - 1) / 2.0);
412
652
        auto z2 = static_cast<Value>(weight(n - 1) / 2 - z1);
413
652
        return weightedAverage(mean(n - 1), z1, _max, z2);
414
896
    }
415
416
0
    Value compression() const { return _compression; }
417
418
155k
    void add(Value x) { add(x, 1); }
419
420
12.1k
    void compress() { process(); }
421
422
    // add a single centroid to the unprocessed vector, processing previously unprocessed sorted if our limit has
423
    // been reached.
424
165k
    bool add(Value x, Weight w) {
425
165k
        if (std::isnan(x)) {
426
0
            return false;
427
0
        }
428
165k
        _unprocessed.emplace_back(x, w);
429
165k
        _unprocessed_weight += w;
430
165k
        processIfNecessary();
431
165k
        return true;
432
165k
    }
433
434
    void add(std::vector<Centroid>::const_iterator iter,
435
0
             std::vector<Centroid>::const_iterator end) {
436
0
        while (iter != end) {
437
0
            const size_t diff = std::distance(iter, end);
438
0
            const size_t room = _max_unprocessed - _unprocessed.size();
439
0
            auto mid = iter + std::min(diff, room);
440
0
            while (iter != mid) {
441
0
                _unprocessed.push_back(*(iter++));
442
0
            }
443
0
            if (_unprocessed.size() >= _max_unprocessed) {
444
0
                process();
445
0
            }
446
0
        }
447
0
    }
448
449
249
    uint32_t serialized_size() {
450
249
        return static_cast<uint32_t>(sizeof(uint32_t) + sizeof(Value) * 5 + sizeof(Index) * 2 +
451
249
                                     sizeof(uint32_t) * 3 + _processed.size() * sizeof(Centroid) +
452
249
                                     _unprocessed.size() * sizeof(Centroid) +
453
249
                                     _cumulative.size() * sizeof(Weight));
454
249
    }
455
456
123
    size_t serialize(uint8_t* writer) {
457
123
        uint8_t* dst = writer;
458
123
        uint32_t total_size = serialized_size();
459
123
        memcpy(writer, &total_size, sizeof(uint32_t));
460
123
        writer += sizeof(uint32_t);
461
123
        memcpy(writer, &_compression, sizeof(Value));
462
123
        writer += sizeof(Value);
463
123
        memcpy(writer, &_min, sizeof(Value));
464
123
        writer += sizeof(Value);
465
123
        memcpy(writer, &_max, sizeof(Value));
466
123
        writer += sizeof(Value);
467
123
        memcpy(writer, &_max_processed, sizeof(Index));
468
123
        writer += sizeof(Index);
469
123
        memcpy(writer, &_max_unprocessed, sizeof(Index));
470
123
        writer += sizeof(Index);
471
123
        memcpy(writer, &_processed_weight, sizeof(Value));
472
123
        writer += sizeof(Value);
473
123
        memcpy(writer, &_unprocessed_weight, sizeof(Value));
474
123
        writer += sizeof(Value);
475
476
123
        auto size = static_cast<uint32_t>(_processed.size());
477
123
        memcpy(writer, &size, sizeof(uint32_t));
478
123
        writer += sizeof(uint32_t);
479
273k
        for (int i = 0; i < size; i++) {
480
273k
            memcpy(writer, &_processed[i], sizeof(Centroid));
481
273k
            writer += sizeof(Centroid);
482
273k
        }
483
484
123
        size = static_cast<uint32_t>(_unprocessed.size());
485
123
        memcpy(writer, &size, sizeof(uint32_t));
486
123
        writer += sizeof(uint32_t);
487
        //TODO(weixiang): may be once memcpy is enough!
488
4.21k
        for (int i = 0; i < size; i++) {
489
4.09k
            memcpy(writer, &_unprocessed[i], sizeof(Centroid));
490
4.09k
            writer += sizeof(Centroid);
491
4.09k
        }
492
493
123
        size = static_cast<uint32_t>(_cumulative.size());
494
123
        memcpy(writer, &size, sizeof(uint32_t));
495
123
        writer += sizeof(uint32_t);
496
274k
        for (int i = 0; i < size; i++) {
497
273k
            memcpy(writer, &_cumulative[i], sizeof(Weight));
498
273k
            writer += sizeof(Weight);
499
273k
        }
500
123
        return writer - dst;
501
123
    }
502
503
121
    void unserialize(const uint8_t* type_reader) {
504
121
        uint32_t total_length = 0;
505
121
        memcpy(&total_length, type_reader, sizeof(uint32_t));
506
121
        type_reader += sizeof(uint32_t);
507
121
        memcpy(&_compression, type_reader, sizeof(Value));
508
121
        type_reader += sizeof(Value);
509
121
        memcpy(&_min, type_reader, sizeof(Value));
510
121
        type_reader += sizeof(Value);
511
121
        memcpy(&_max, type_reader, sizeof(Value));
512
121
        type_reader += sizeof(Value);
513
514
121
        memcpy(&_max_processed, type_reader, sizeof(Index));
515
121
        type_reader += sizeof(Index);
516
121
        memcpy(&_max_unprocessed, type_reader, sizeof(Index));
517
121
        type_reader += sizeof(Index);
518
121
        memcpy(&_processed_weight, type_reader, sizeof(Value));
519
121
        type_reader += sizeof(Value);
520
121
        memcpy(&_unprocessed_weight, type_reader, sizeof(Value));
521
121
        type_reader += sizeof(Value);
522
523
121
        uint32_t size;
524
121
        memcpy(&size, type_reader, sizeof(uint32_t));
525
121
        type_reader += sizeof(uint32_t);
526
121
        _processed.resize(size);
527
269k
        for (int i = 0; i < size; i++) {
528
269k
            memcpy(&_processed[i], type_reader, sizeof(Centroid));
529
269k
            type_reader += sizeof(Centroid);
530
269k
        }
531
121
        memcpy(&size, type_reader, sizeof(uint32_t));
532
121
        type_reader += sizeof(uint32_t);
533
121
        _unprocessed.resize(size);
534
4.21k
        for (int i = 0; i < size; i++) {
535
4.09k
            memcpy(&_unprocessed[i], type_reader, sizeof(Centroid));
536
4.09k
            type_reader += sizeof(Centroid);
537
4.09k
        }
538
121
        memcpy(&size, type_reader, sizeof(uint32_t));
539
121
        type_reader += sizeof(uint32_t);
540
121
        _cumulative.resize(size);
541
269k
        for (int i = 0; i < size; i++) {
542
269k
            memcpy(&_cumulative[i], type_reader, sizeof(Weight));
543
269k
            type_reader += sizeof(Weight);
544
269k
        }
545
121
    }
546
547
private:
548
    Value _compression;
549
550
    Value _min = std::numeric_limits<Value>::max();
551
552
    // min() is the smallest positive value, so use lowest() for non-positive inputs.
553
    Value _max = std::numeric_limits<Value>::lowest();
554
555
    Index _max_processed;
556
557
    Index _max_unprocessed;
558
559
    Value _processed_weight = 0.0;
560
561
    Value _unprocessed_weight = 0.0;
562
563
    std::vector<Centroid> _processed;
564
565
    std::vector<Centroid> _unprocessed;
566
567
    std::vector<Weight> _cumulative;
568
569
    // return mean of i-th centroid
570
2.20k
    Value mean(int64_t i) const noexcept { return _processed[i].mean(); }
571
572
    // return weight of i-th centroid
573
114M
    Weight weight(int64_t i) const noexcept { return _processed[i].weight(); }
574
575
    // append all unprocessed centroids into current unprocessed vector
576
409
    void mergeUnprocessed(const std::vector<const TDigest*>& tdigests) {
577
409
        if (tdigests.size() == 0) {
578
0
            return;
579
0
        }
580
581
        // Insert directly instead of reserving the exact merged size: reserve() reallocates
582
        // and copies the whole buffer whenever `total` exceeds the current capacity, so
583
        // merging many small digests would cost O(n^2) copies. insert() grows geometrically.
584
409
        for (const auto& td : tdigests) {
585
409
            _unprocessed.insert(_unprocessed.end(), td->_unprocessed.cbegin(),
586
409
                                td->_unprocessed.cend());
587
409
            _unprocessed_weight += td->_unprocessed_weight;
588
409
        }
589
409
    }
590
591
    // merge all processed centroids together into a single sorted vector
592
409
    void mergeProcessed(const std::vector<const TDigest*>& tdigests) {
593
409
        if (tdigests.size() == 0) {
594
0
            return;
595
0
        }
596
597
409
        size_t total = 0;
598
409
        CentroidListQueue pq(CentroidListComparator {});
599
409
        for (const auto& td : tdigests) {
600
409
            const auto& sorted = td->_processed;
601
409
            auto size = sorted.size();
602
409
            if (size > 0) {
603
403
                pq.push(CentroidList(sorted));
604
403
                total += size;
605
403
                _processed_weight += td->_processed_weight;
606
403
            }
607
409
        }
608
409
        if (total == 0) {
609
6
            return;
610
6
        }
611
612
403
        if (_processed.size() > 0) {
613
399
            pq.push(CentroidList(_processed));
614
399
            total += _processed.size();
615
399
        }
616
617
403
        std::vector<Centroid> sorted;
618
403
        VLOG_CRITICAL << "total " << total;
619
403
        sorted.reserve(total);
620
621
1.93M
        while (!pq.empty()) {
622
1.93M
            auto best = pq.top();
623
1.93M
            pq.pop();
624
1.93M
            sorted.push_back(*(best.iter));
625
1.93M
            if (best.advance()) {
626
1.86M
                pq.push(best);
627
1.86M
            }
628
1.93M
        }
629
403
        _processed = std::move(sorted);
630
403
        if (_processed.size() > 0) {
631
403
            _min = std::min(_min, _processed[0].mean());
632
403
            _max = std::max(_max, (_processed.cend() - 1)->mean());
633
403
        }
634
403
    }
635
636
166k
    void processIfNecessary() {
637
166k
        if (isDirty()) {
638
401
            process();
639
401
        }
640
166k
    }
641
642
13.9k
    void updateCumulative() {
643
13.9k
        const auto n = _processed.size();
644
13.9k
        _cumulative.clear();
645
13.9k
        _cumulative.reserve(n + 1);
646
13.9k
        Weight previous = 0.0;
647
114M
        for (Index i = 0; i < n; i++) {
648
114M
            Weight current = weight(i);
649
114M
            auto halfCurrent = static_cast<Weight>(current / 2.0);
650
114M
            _cumulative.push_back(previous + halfCurrent);
651
114M
            previous = previous + current;
652
114M
        }
653
13.9k
        _cumulative.push_back(previous);
654
13.9k
    }
655
656
    // merges _unprocessed centroids and _processed centroids together and processes them
657
    // when complete, _unprocessed will be empty and _processed will have at most _max_processed centroids
658
13.5k
    void process() {
659
13.5k
        CentroidComparator cc;
660
        // select percentile_approx(lo_orderkey,0.5) from lineorder;
661
        // have test pdqsort and RadixSort, find here pdqsort performance is better when data is struct Centroid
662
        // But when sort plain type like int/float of std::vector<T>, find RadixSort is better
663
13.5k
        pdqsort(_unprocessed.begin(), _unprocessed.end(), cc);
664
13.5k
        auto count = _unprocessed.size();
665
13.5k
        _unprocessed.insert(_unprocessed.end(), _processed.cbegin(), _processed.cend());
666
13.5k
        std::inplace_merge(_unprocessed.begin(), _unprocessed.begin() + count, _unprocessed.end(),
667
13.5k
                           cc);
668
669
13.5k
        _processed_weight += _unprocessed_weight;
670
13.5k
        _unprocessed_weight = 0;
671
13.5k
        _processed.clear();
672
673
13.5k
        _processed.push_back(_unprocessed[0]);
674
13.5k
        Weight wSoFar = _unprocessed[0].weight();
675
13.5k
        Weight wLimit = _processed_weight * integratedQ(1.0);
676
677
13.5k
        auto end = _unprocessed.end();
678
114M
        for (auto iter = _unprocessed.cbegin() + 1; iter < end; iter++) {
679
114M
            const auto& centroid = *iter;
680
114M
            Weight projectedW = wSoFar + centroid.weight();
681
114M
            if (projectedW <= wLimit) {
682
1.00M
                wSoFar = projectedW;
683
1.00M
                (_processed.end() - 1)->add(centroid);
684
113M
            } else {
685
113M
                auto k1 = integratedLocation(wSoFar / _processed_weight);
686
113M
                wLimit = _processed_weight * integratedQ(static_cast<Value>(k1 + 1.0));
687
113M
                wSoFar += centroid.weight();
688
113M
                _processed.emplace_back(centroid);
689
113M
            }
690
114M
        }
691
13.5k
        _unprocessed.clear();
692
13.5k
        _min = std::min(_min, _processed[0].mean());
693
13.5k
        VLOG_CRITICAL << "new _min " << _min;
694
13.5k
        _max = std::max(_max, (_processed.cend() - 1)->mean());
695
13.5k
        VLOG_CRITICAL << "new _max " << _max;
696
13.5k
        updateCumulative();
697
13.5k
    }
698
699
0
    size_t checkWeights(const std::vector<Centroid>& sorted, Value total) {
700
0
        size_t badWeight = 0;
701
0
        auto k1 = 0.0;
702
0
        auto q = 0.0;
703
0
        for (auto iter = sorted.cbegin(); iter != sorted.cend(); iter++) {
704
0
            auto w = iter->weight();
705
0
            auto dq = w / total;
706
0
            auto k2 = integratedLocation(static_cast<Value>(q + dq));
707
0
            if (k2 - k1 > 1 && w != 1) {
708
0
                VLOG_CRITICAL << "Oversize centroid at " << std::distance(sorted.cbegin(), iter)
709
0
                              << " k1 " << k1 << " k2 " << k2 << " dk " << (k2 - k1) << " w " << w
710
0
                              << " q " << q;
711
0
                badWeight++;
712
0
            }
713
0
            if (k2 - k1 > 1.5 && w != 1) {
714
0
                VLOG_CRITICAL << "Egregiously Oversize centroid at "
715
0
                              << std::distance(sorted.cbegin(), iter) << " k1 " << k1 << " k2 "
716
0
                              << k2 << " dk " << (k2 - k1) << " w " << w << " q " << q;
717
0
                badWeight++;
718
0
            }
719
0
            q += dq;
720
0
            k1 = k2;
721
0
        }
722
0
723
0
        return badWeight;
724
0
    }
725
726
    /**
727
    * Converts a quantile into a centroid scale value.  The centroid scale is nomin_ally
728
    * the number k of the centroid that a quantile point q should belong to.  Due to
729
    * round-offs, however, we can't align things perfectly without splitting points
730
    * and sorted.  We don't want to do that, so we have to allow for offsets.
731
    * In the end, the criterion is that any quantile range that spans a centroid
732
    * scale range more than one should be split across more than one centroid if
733
    * possible.  This won't be possible if the quantile range refers to a single point
734
    * or an already existing centroid.
735
    * <p/>
736
    * This mapping is steep near q=0 or q=1 so each centroid there will correspond to
737
    * less q range.  Near q=0.5, the mapping is flatter so that sorted there will
738
    * represent a larger chunk of quantiles.
739
    *
740
    * @param q The quantile scale value to be mapped.
741
    * @return The centroid scale value corresponding to q.
742
    */
743
113M
    Value integratedLocation(Value q) const {
744
113M
        return static_cast<Value>(_compression * (std::asin(2.0 * q - 1.0) + M_PI / 2) / M_PI);
745
113M
    }
746
747
113M
    Value integratedQ(Value k) const {
748
113M
        return static_cast<Value>(
749
113M
                (std::sin(std::min(k, _compression) * M_PI / _compression - M_PI / 2) + 1) / 2);
750
113M
    }
751
752
    /**
753
     * Same as {@link #weightedAverageSorted(Value, Value, Value, Value)} but flips
754
     * the order of the variables if <code>x2</code> is greater than
755
     * <code>x1</code>.
756
    */
757
903
    static Value weightedAverage(Value x1, Value w1, Value x2, Value w2) {
758
903
        return (x1 <= x2) ? weightedAverageSorted(x1, w1, x2, w2)
759
903
                          : weightedAverageSorted(x2, w2, x1, w1);
760
903
    }
761
762
    /**
763
    * Compute the weighted average between <code>x1</code> with a weight of
764
    * <code>w1</code> and <code>x2</code> with a weight of <code>w2</code>.
765
    * This expects <code>x1</code> to be less than or equal to <code>x2</code>
766
    * and is guaranteed to return a number between <code>x1</code> and
767
    * <code>x2</code>.
768
    */
769
903
    static Value weightedAverageSorted(Value x1, Value w1, Value x2, Value w2) {
770
903
        DCHECK_LE(x1, x2);
771
903
        const Value x = (x1 * w1 + x2 * w2) / (w1 + w2);
772
903
        return std::max(x1, std::min(x, x2));
773
903
    }
774
775
0
    static Value interpolate(Value x, Value x0, Value x1) { return (x - x0) / (x1 - x0); }
776
777
    /**
778
    * Computes an interpolated value of a quantile that is between two sorted.
779
    *
780
    * Index is the quantile desired multiplied by the total number of samples - 1.
781
    *
782
    * @param index              Denormalized quantile desired
783
    * @param previousIndex      The denormalized quantile corresponding to the center of the previous centroid.
784
    * @param nextIndex          The denormalized quantile corresponding to the center of the following centroid.
785
    * @param previousMean       The mean of the previous centroid.
786
    * @param nextMean           The mean of the following centroid.
787
    * @return  The interpolated mean.
788
    */
789
    static Value quantile(Value index, Value previousIndex, Value nextIndex, Value previousMean,
790
0
                          Value nextMean) {
791
0
        const auto delta = nextIndex - previousIndex;
792
0
        const auto previousWeight = (nextIndex - index) / delta;
793
0
        const auto nextWeight = (index - previousIndex) / delta;
794
0
        return previousMean * previousWeight + nextMean * nextWeight;
795
0
    }
796
};
797
#include "common/compile_check_end.h"
798
} // namespace doris