Coverage Report

Created: 2026-10-28 14:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exprs/datasketches_hll_util.cpp
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
#include "exprs/datasketches_hll_util.h"
19
20
#include <bthread/bthread.h>
21
22
#include <cstring>
23
#include <exception>
24
#include <mutex>
25
#include <new>
26
#include <stdexcept>
27
28
#include "common/check.h"
29
#include "common/exception.h"
30
#include "common/status.h"
31
#include "util/debug_points.h"
32
33
namespace doris::datasketches_hll {
34
35
struct alignas(std::max_align_t) AllocationHeader {
36
    AllocationHeader* previous;
37
    AllocationHeader* next;
38
    detail::AllocationOwner* owner;
39
    size_t count;
40
};
41
42
namespace {
43
44
using namespace datasketches::hll_constants;
45
46
thread_local detail::AllocationOwner* pthread_scope = nullptr;
47
48
64
bthread_key_t scope_key() {
49
64
    static bthread_key_t key;
50
64
    static std::once_flag once;
51
64
    std::call_once(once, [] { DORIS_CHECK(bthread_key_create(&key, nullptr) == 0); });
52
64
    return key;
53
64
}
54
55
30.4k
detail::AllocationOwner* current_scope() {
56
30.4k
    return bthread_self() == 0
57
30.4k
                   ? pthread_scope
58
30.4k
                   : static_cast<detail::AllocationOwner*>(bthread_getspecific(scope_key()));
59
30.4k
}
60
61
18.9k
void set_scope(detail::AllocationOwner* scope) {
62
18.9k
    if (bthread_self() == 0) {
63
18.9k
        pthread_scope = scope;
64
18.9k
    } else {
65
32
        DORIS_CHECK(bthread_setspecific(scope_key(), scope) == 0);
66
32
    }
67
18.9k
}
68
69
class ActiveAllocationOwner {
70
public:
71
3.39k
    explicit ActiveAllocationOwner(detail::AllocationOwner* owner) : _previous(current_scope()) {
72
3.39k
        set_scope(owner);
73
3.39k
    }
74
3.39k
    ~ActiveAllocationOwner() { set_scope(_previous); }
75
76
private:
77
    detail::AllocationOwner* _previous;
78
};
79
80
// Reuse the library's protected static header readers without constructing a sketch or
81
// duplicating the mode/type bit decoding. This abstract adapter is never instantiated.
82
struct HeaderReader : datasketches::HllSketchImpl<Alloc> {
83
    using datasketches::HllSketchImpl<Alloc>::extractCurMode;
84
    using datasketches::HllSketchImpl<Alloc>::extractTgtHllType;
85
};
86
87
347k
uint32_t read_uint32(const uint8_t* bytes) {
88
347k
    uint32_t value;
89
347k
    std::memcpy(&value, bytes, sizeof(value));
90
347k
    return value;
91
347k
}
92
93
1.16k
void check_set_size(const uint8_t* bytes, size_t size, uint8_t lg_k, bool compact) {
94
1.16k
    datasketches::HllUtil<Alloc>::checkMemSize(HASH_SET_INT_ARR_START, size);
95
1.16k
    const auto count = read_uint32(bytes + HASH_SET_COUNT_INT);
96
    // SET promotes to HLL above 3/4 of its largest table (K/8 entries). Bound the count before
97
    // computeLgArrInts(), whose power-of-two and load-factor arithmetic uses uint32_t.
98
1.16k
    if (lg_k <= 7 || count > (RESIZE_NUMER * (uint32_t {1} << (lg_k - 3))) / RESIZE_DENOM) {
99
4
        throw std::invalid_argument("Invalid HLL SET coupon count or lgK");
100
4
    }
101
1.15k
    size_t items = count;
102
1.15k
    if (!compact) {
103
        // The library evaluates 1 << lgArr before checking the payload. For example, lgArr=32
104
        // is an undefined shift. Preserve its support for older images with an unset lgArr.
105
739
        const uint8_t lg_arr = bytes[LG_ARR_BYTE] < LG_INIT_SET_SIZE
106
739
                                       ? datasketches::HllUtil<Alloc>::computeLgArrInts(
107
93
                                                 datasketches::SET, count, lg_k)
108
739
                                       : bytes[LG_ARR_BYTE];
109
739
        if (lg_arr > lg_k - 3) {
110
22
            throw std::invalid_argument("Invalid HLL SET array exponent");
111
22
        }
112
717
        items = size_t {1} << lg_arr;
113
717
    }
114
1.13k
    datasketches::HllUtil<Alloc>::checkMemSize(HASH_SET_INT_ARR_START + items * sizeof(uint32_t),
115
1.13k
                                               size);
116
1.13k
    if (!compact) {
117
        // newSet() copies the whole table but trusts the declared count. With ten occupied
118
        // slots and count=1, a later compact serializer allocates 16 bytes and writes 52.
119
713
        uint32_t occupied = 0;
120
343k
        for (size_t i = 0; i < items; ++i) {
121
342k
            occupied += read_uint32(bytes + HASH_SET_INT_ARR_START + i * sizeof(uint32_t)) != EMPTY;
122
342k
        }
123
713
        if (occupied != count) {
124
533
            throw std::invalid_argument("HLL SET coupon count does not match occupied slots");
125
533
        }
126
713
    }
127
1.13k
}
128
129
void check_aux_map(const uint8_t* bytes, size_t size, size_t offset, uint8_t lg_k, uint32_t count,
130
490
                   bool compact) {
131
490
    const uint32_t registers = uint32_t {1} << lg_k;
132
    // There can be at most one auxiliary entry per register. This also bounds the uint32_t
133
    // capacity arithmetic used by the library to rebuild a compact map.
134
490
    if (count > registers) {
135
4
        throw std::invalid_argument("HLL_4 auxiliary count exceeds register count");
136
4
    }
137
486
    size_t items = count;
138
486
    if (!compact) {
139
210
        const auto lg_arr = bytes[LG_ARR_BYTE];
140
        // An updatable map starts at LG_AUX_ARR_INTS[lgK] and grows at 3/4 load, so at most
141
        // K entries need 2*K slots. Reject bad exponents before the library's 1 << lgArr.
142
210
        if (lg_arr < LG_AUX_ARR_INTS[lg_k] || lg_arr > lg_k + 1) {
143
26
            throw std::invalid_argument("Invalid HLL_4 auxiliary array exponent");
144
26
        }
145
184
        items = size_t {1} << lg_arr;
146
184
    }
147
460
    datasketches::HllUtil<Alloc>::checkMemSize(offset + items * sizeof(uint32_t), size);
148
149
    // AuxHashMap::deserialize owns a raw map until it returns. mustAdd() throws for duplicate
150
    // slots before that ownership transfers, leaking the map. Check the same masked slot keys
151
    // before allocating it. This bitmap is needed only for sketches with an auxiliary map.
152
460
    DorisVector<uint8_t> seen(registers / 8, 0);
153
2.25k
    for (size_t i = 0; i < items; ++i) {
154
1.90k
        const auto pair = read_uint32(bytes + offset + i * sizeof(uint32_t));
155
1.90k
        if (pair == EMPTY) {
156
            // Compact maps contain only real entries. Treating EMPTY as slot 0 would let
157
            // an AUX_TOKEN at register 0 pass this check, but mustFindValueFor(0) still fails.
158
1.13k
            if (compact) {
159
26
                throw std::invalid_argument("HLL_4 compact auxiliary map contains an EMPTY entry");
160
26
            }
161
1.11k
            continue;
162
1.13k
        }
163
762
        const auto slot = datasketches::HllUtil<Alloc>::getLow26(pair) & (registers - 1);
164
762
        const auto bit = static_cast<uint8_t>(1U << (slot % 8));
165
762
        if (seen[slot / 8] & bit) {
166
76
            throw std::invalid_argument("HLL_4 auxiliary map contains duplicate slots");
167
76
        }
168
686
        seen[slot / 8] |= bit;
169
686
    }
170
    // A nonempty map is insufficient: every AUX_TOKEN must refer to a slot in that map.
171
    // For example, token at register 0 with only slot 3 in the map makes mergeHll() throw
172
    // after allocating its target array. Reject that image even on the unchanged-byte path.
173
302k
    for (uint32_t slot = 0; slot < registers; ++slot) {
174
301k
        const auto byte = bytes[HLL_BYTE_ARR_START + slot / 2];
175
301k
        const auto value = static_cast<uint8_t>((byte >> ((slot % 2) * 4)) & loNibbleMask);
176
301k
        if (value == AUX_TOKEN && !(seen[slot / 8] & (1U << (slot % 8)))) {
177
90
            throw std::invalid_argument("HLL_4 AUX_TOKEN has no matching auxiliary slot");
178
90
        }
179
301k
    }
180
358
}
181
182
1.71k
bool check_hll_size(const uint8_t* bytes, size_t size, uint8_t lg_k, bool compact) {
183
1.71k
    datasketches::HllUtil<Alloc>::checkMemSize(HLL_BYTE_ARR_START, size);
184
1.71k
    const auto type = HeaderReader::extractTgtHllType(bytes[MODE_BYTE]);
185
1.71k
    const size_t offset =
186
1.71k
            HLL_BYTE_ARR_START + datasketches::HllArray<Alloc>::hllArrBytes(type, lg_k);
187
1.71k
    datasketches::HllUtil<Alloc>::checkMemSize(offset, size);
188
1.71k
    const auto aux_count = read_uint32(bytes + AUX_COUNT_INT);
189
1.71k
    if (type != datasketches::HLL_4) {
190
        // newHll() installs any nonempty auxiliary map through an unchecked Hll4Array cast.
191
        // HLL_6/HLL_8 have no map member, so accepting such an image writes outside the object.
192
871
        if (aux_count != 0) {
193
20
            throw std::invalid_argument("HLL_6/HLL_8 cannot contain an auxiliary map");
194
20
        }
195
851
        return false;
196
871
    }
197
844
    if (aux_count != 0) {
198
490
        check_aux_map(bytes, size, offset, lg_k, aux_count, compact);
199
490
        return true;
200
490
    }
201
    // HLL_4 uses nibble 0xf to reference its auxiliary map. The library accepts auxCount=0
202
    // with such a nibble, then dereferences a null map during union/conversion, even on a no-op.
203
735k
    for (size_t i = HLL_BYTE_ARR_START; i < offset; ++i) {
204
735k
        if ((bytes[i] & loNibbleMask) == AUX_TOKEN ||
205
735k
            ((bytes[i] >> 4) & loNibbleMask) == AUX_TOKEN) {
206
2
            throw std::invalid_argument("HLL_4 contains AUX_TOKEN without an auxiliary map");
207
2
        }
208
735k
    }
209
352
    return false;
210
354
}
211
212
} // namespace
213
214
AllocationScope::AllocationScope()
215
6.08k
        : _previous(current_scope()), _exceptions(std::uncaught_exceptions()) {
216
6.08k
    set_scope(&_owner);
217
6.08k
}
218
219
6.08k
AllocationScope::~AllocationScope() {
220
6.08k
    set_scope(_previous);
221
6.08k
    if (std::uncaught_exceptions() == _exceptions) {
222
5.16k
        _owner.release();
223
5.16k
    }
224
6.08k
}
225
226
7.77k
detail::AllocationOwner::~AllocationOwner() {
227
7.77k
    clear();
228
7.77k
}
229
230
7.77k
void detail::AllocationOwner::clear() noexcept {
231
7.90k
    while (_head != nullptr) {
232
125
        auto* header = _head;
233
125
        _head = header->next;
234
125
        CustomStdAllocator<AllocationHeader>().deallocate(header, header->count);
235
125
    }
236
7.77k
}
237
238
5.16k
void detail::AllocationOwner::release() noexcept {
239
16.0k
    while (_head != nullptr) {
240
10.8k
        auto* header = _head;
241
10.8k
        _head = header->next;
242
        // Returned sketches keep normal library ownership after a successful call.
243
10.8k
        header->owner = nullptr;
244
10.8k
    }
245
5.16k
}
246
247
21.0k
void* allocate(size_t bytes) {
248
21.0k
    DBUG_EXECUTE_IF("datasketches_hll.before_allocate", { DBUG_RUN_CALLBACK(); });
249
21.0k
    auto* scope = current_scope();
250
21.0k
    const size_t count =
251
21.0k
            1 + bytes / sizeof(AllocationHeader) + (bytes % sizeof(AllocationHeader) != 0);
252
21.0k
    auto* header = CustomStdAllocator<AllocationHeader>().allocate(count);
253
21.0k
    *header = {.previous = nullptr,
254
21.0k
               .next = scope == nullptr ? nullptr : scope->_head,
255
21.0k
               .owner = scope,
256
21.0k
               .count = count};
257
21.0k
    if (scope != nullptr) {
258
17.5k
        if (header->next != nullptr) {
259
10.6k
            header->next->previous = header;
260
10.6k
        }
261
17.5k
        scope->_head = header;
262
17.5k
    }
263
21.0k
    return header + 1;
264
21.0k
}
265
266
20.8k
void deallocate(void* ptr) noexcept {
267
20.8k
    auto* header = static_cast<AllocationHeader*>(ptr) - 1;
268
20.8k
    if (header->owner != nullptr) {
269
6.57k
        if (header->previous != nullptr) {
270
3.25k
            header->previous->next = header->next;
271
3.32k
        } else {
272
3.32k
            header->owner->_head = header->next;
273
3.32k
        }
274
6.57k
        if (header->next != nullptr) {
275
3.68k
            header->next->previous = header->previous;
276
3.68k
        }
277
6.57k
    }
278
20.8k
    CustomStdAllocator<AllocationHeader>().deallocate(header, header->count);
279
20.8k
}
280
281
struct Union::Impl {
282
    using LibraryUnion = datasketches::hll_union_alloc<Alloc>;
283
284
    detail::AllocationOwner allocations;
285
    alignas(LibraryUnion) std::byte storage[sizeof(LibraryUnion)];
286
    bool valid = false;
287
288
5.05k
    LibraryUnion* get() { return std::launder(reinterpret_cast<LibraryUnion*>(storage)); }
289
0
    const LibraryUnion* get() const {
290
0
        return std::launder(reinterpret_cast<const LibraryUnion*>(storage));
291
0
    }
292
293
1.68k
    ~Impl() {
294
1.68k
        if (valid) {
295
1.65k
            get()->~LibraryUnion();
296
1.65k
        }
297
        // Failed library updates can leave a dangling gadget pointer. AllocationOwner frees
298
        // the remaining blocks directly, instead of dereferencing that invalid gadget.
299
1.68k
    }
300
};
301
302
1.68k
void Union::Deleter::operator()(Impl* ptr) const noexcept {
303
1.68k
    ptr->~Impl();
304
1.68k
    datasketches_hll::deallocate(ptr);
305
1.68k
}
306
307
Union::Union(uint8_t lg_k, const Alloc& allocator)
308
1.69k
        : _impl(new(datasketches_hll::allocate(sizeof(Impl))) Impl()) {
309
1.69k
    ActiveAllocationOwner scope(&_impl->allocations);
310
1.69k
    new (_impl->storage) Impl::LibraryUnion(lg_k, allocator);
311
1.69k
    _impl->valid = true;
312
1.69k
}
313
314
1.67k
Union::~Union() = default;
315
1
Union::Union(Union&&) noexcept = default;
316
1
Union& Union::operator=(Union&&) noexcept = default;
317
318
2
Union::Union(const Union& other) : _impl(new(datasketches_hll::allocate(sizeof(Impl))) Impl()) {
319
2
    DORIS_CHECK(other._impl->valid);
320
2
    ActiveAllocationOwner scope(&_impl->allocations);
321
2
    new (_impl->storage) Impl::LibraryUnion(*other._impl->get());
322
2
    _impl->valid = true;
323
2
}
324
325
1
Union& Union::operator=(const Union& other) {
326
1
    Union copy(other);
327
1
    *this = std::move(copy);
328
1
    return *this;
329
1
}
330
331
1.70k
void Union::update(const Sketch& sketch) {
332
1.70k
    DORIS_CHECK(_impl->valid);
333
1.70k
    ActiveAllocationOwner scope(&_impl->allocations);
334
1.70k
    try {
335
1.70k
        _impl->get()->update(sketch);
336
1.70k
    } catch (...) {
337
        // The library can free its gadget during promotion and then throw before replacing
338
        // the pointer. This state must be discarded; its allocation owner remains intact.
339
20
        _impl->valid = false;
340
20
        throw;
341
20
    }
342
1.70k
}
343
344
1.57k
Sketch Union::get_result(datasketches::target_hll_type type) const {
345
1.57k
    DORIS_CHECK(_impl->valid);
346
1.57k
    AllocationScope scope;
347
1.57k
    return _impl->get()->get_result(type);
348
1.57k
}
349
350
70
double Union::get_estimate() const {
351
70
    DORIS_CHECK(_impl->valid);
352
70
    return _impl->get()->get_estimate();
353
70
}
354
355
3
double Union::get_lower_bound(uint8_t std_devs) const {
356
3
    DORIS_CHECK(_impl->valid);
357
3
    return _impl->get()->get_lower_bound(std_devs);
358
3
}
359
360
3
double Union::get_upper_bound(uint8_t std_devs) const {
361
3
    DORIS_CHECK(_impl->valid);
362
3
    return _impl->get()->get_upper_bound(std_devs);
363
3
}
364
365
38
uint8_t Union::get_lg_config_k() const {
366
38
    DORIS_CHECK(_impl->valid);
367
38
    return _impl->get()->get_lg_config_k();
368
38
}
369
370
8
double Union::get_rel_err(bool upper, bool unioned, uint8_t lg_k, uint8_t std_devs) {
371
8
    return Impl::LibraryUnion::get_rel_err(upper, unioned, lg_k, std_devs);
372
8
}
373
374
4.49k
Sketch deserialize(const StringRef& value, std::string_view action) {
375
4.49k
    AllocationScope allocations;
376
    // The library reads the preamble byte before it checks the length.
377
4.49k
    if (value.size == 0) {
378
6
        throw Exception(ErrorCode::CORRUPTION, "HLL sketch data corrupted when {}: empty input.",
379
6
                        action);
380
6
    }
381
4.48k
    try {
382
        // All modes have an eight-byte preamble. An invalid lgK would otherwise reach shifts used
383
        // to size the register/coupon arrays before the library validates it.
384
4.48k
        datasketches::HllUtil<Alloc>::checkMemSize(EMPTY_SKETCH_SIZE_BYTES, value.size);
385
4.48k
        const auto* bytes = reinterpret_cast<const uint8_t*>(value.data);
386
4.48k
        const auto lg_k = datasketches::HllUtil<Alloc>::checkLgK(bytes[LG_K_BYTE]);
387
4.48k
        const auto mode = HeaderReader::extractCurMode(bytes[MODE_BYTE]);
388
4.48k
        const bool compact = (bytes[FLAGS_BYTE] & COMPACT_FLAG_MASK) != 0;
389
4.48k
        bool has_aux_map = false;
390
4.48k
        if (mode == datasketches::SET) {
391
1.16k
            check_set_size(bytes, value.size, lg_k, compact);
392
3.32k
        } else if (mode == datasketches::HLL) {
393
1.71k
            has_aux_map = check_hll_size(bytes, value.size, lg_k, compact);
394
1.71k
        }
395
        // AuxHashMap::deserialize reads uint32_t directly. STRING rows have no alignment
396
        // guarantee: a 233-byte HLL_6 before this HLL_4 leaves its auxiliary entries unaligned.
397
        // Only this library reader needs an aligned copy; the other payloads use memcpy.
398
4.48k
        if (has_aux_map && reinterpret_cast<uintptr_t>(value.data) % alignof(uint32_t) != 0) {
399
133
            DorisVector<uint32_t> aligned(value.size / sizeof(uint32_t) +
400
133
                                          (value.size % sizeof(uint32_t) != 0));
401
133
            std::memcpy(aligned.data(), value.data, value.size);
402
133
            return Sketch::deserialize(aligned.data(), value.size, Alloc());
403
133
        }
404
4.35k
        return Sketch::deserialize(value.data, value.size, Alloc());
405
4.48k
    } catch (const doris::Exception& e) {
406
49
        throw Exception(e.code(), "Failed to deserialize HLL sketch when {}: {}", action,
407
49
                        e.to_string());
408
831
    } catch (const std::exception& e) {
409
831
        throw Exception(ErrorCode::CORRUPTION, "HLL sketch data corrupted when {}: {}", action,
410
831
                        e.what());
411
831
    } catch (...) {
412
0
        throw Exception(ErrorCode::CORRUPTION,
413
0
                        "HLL sketch data corrupted when {}: unknown exception.", action);
414
0
    }
415
4.48k
}
416
417
} // namespace doris::datasketches_hll