Coverage Report

Created: 2026-10-29 11:48

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
26.0k
detail::AllocationOwner* current_scope() {
56
26.0k
    return bthread_self() == 0
57
26.0k
                   ? pthread_scope
58
26.0k
                   : static_cast<detail::AllocationOwner*>(bthread_getspecific(scope_key()));
59
26.0k
}
60
61
16.3k
void set_scope(detail::AllocationOwner* scope) {
62
16.3k
    if (bthread_self() == 0) {
63
16.3k
        pthread_scope = scope;
64
16.3k
    } else {
65
32
        DORIS_CHECK(bthread_setspecific(scope_key(), scope) == 0);
66
32
    }
67
16.3k
}
68
69
class ActiveAllocationOwner {
70
public:
71
2.76k
    explicit ActiveAllocationOwner(detail::AllocationOwner* owner) : _previous(current_scope()) {
72
2.76k
        set_scope(owner);
73
2.76k
    }
74
2.76k
    ~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
346k
uint32_t read_uint32(const uint8_t* bytes) {
88
346k
    uint32_t value;
89
346k
    std::memcpy(&value, bytes, sizeof(value));
90
346k
    return value;
91
346k
}
92
93
1.09k
void check_set_size(const uint8_t* bytes, size_t size, uint8_t lg_k, bool compact) {
94
1.09k
    datasketches::HllUtil<Alloc>::checkMemSize(HASH_SET_INT_ARR_START, size);
95
1.09k
    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.09k
    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.09k
    size_t items = count;
102
1.09k
    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
723
        const uint8_t lg_arr = bytes[LG_ARR_BYTE] < LG_INIT_SET_SIZE
106
723
                                       ? datasketches::HllUtil<Alloc>::computeLgArrInts(
107
93
                                                 datasketches::SET, count, lg_k)
108
723
                                       : bytes[LG_ARR_BYTE];
109
723
        if (lg_arr > lg_k - 3) {
110
20
            throw std::invalid_argument("Invalid HLL SET array exponent");
111
20
        }
112
703
        items = size_t {1} << lg_arr;
113
703
    }
114
1.07k
    datasketches::HllUtil<Alloc>::checkMemSize(HASH_SET_INT_ARR_START + items * sizeof(uint32_t),
115
1.07k
                                               size);
116
1.07k
    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
699
        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
699
        if (occupied != count) {
124
519
            throw std::invalid_argument("HLL SET coupon count does not match occupied slots");
125
519
        }
126
699
    }
127
1.07k
}
128
129
void check_aux_map(const uint8_t* bytes, size_t size, size_t offset, uint8_t lg_k, uint32_t count,
130
397
                   bool compact) {
131
397
    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
397
    if (count > registers) {
135
4
        throw std::invalid_argument("HLL_4 auxiliary count exceeds register count");
136
4
    }
137
393
    size_t items = count;
138
393
    if (!compact) {
139
176
        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
176
        if (lg_arr < LG_AUX_ARR_INTS[lg_k] || lg_arr > lg_k + 1) {
143
24
            throw std::invalid_argument("Invalid HLL_4 auxiliary array exponent");
144
24
        }
145
152
        items = size_t {1} << lg_arr;
146
152
    }
147
369
    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
369
    DorisVector<uint8_t> seen(registers / 8, 0);
153
1.83k
    for (size_t i = 0; i < items; ++i) {
154
1.55k
        const auto pair = read_uint32(bytes + offset + i * sizeof(uint32_t));
155
1.55k
        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
945
            if (compact) {
159
13
                throw std::invalid_argument("HLL_4 compact auxiliary map contains an EMPTY entry");
160
13
            }
161
932
            continue;
162
945
        }
163
608
        const auto slot = datasketches::HllUtil<Alloc>::getLow26(pair) & (registers - 1);
164
608
        const auto bit = static_cast<uint8_t>(1U << (slot % 8));
165
608
        if (seen[slot / 8] & bit) {
166
72
            throw std::invalid_argument("HLL_4 auxiliary map contains duplicate slots");
167
72
        }
168
536
        seen[slot / 8] |= bit;
169
536
    }
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
283k
    for (uint32_t slot = 0; slot < registers; ++slot) {
174
283k
        const auto byte = bytes[HLL_BYTE_ARR_START + slot / 2];
175
283k
        const auto value = static_cast<uint8_t>((byte >> ((slot % 2) * 4)) & loNibbleMask);
176
283k
        if (value == AUX_TOKEN && !(seen[slot / 8] & (1U << (slot % 8)))) {
177
88
            throw std::invalid_argument("HLL_4 AUX_TOKEN has no matching auxiliary slot");
178
88
        }
179
283k
    }
180
284
}
181
182
1.51k
bool check_hll_size(const uint8_t* bytes, size_t size, uint8_t lg_k, bool compact) {
183
1.51k
    datasketches::HllUtil<Alloc>::checkMemSize(HLL_BYTE_ARR_START, size);
184
1.51k
    const auto type = HeaderReader::extractTgtHllType(bytes[MODE_BYTE]);
185
1.51k
    const size_t offset =
186
1.51k
            HLL_BYTE_ARR_START + datasketches::HllArray<Alloc>::hllArrBytes(type, lg_k);
187
1.51k
    datasketches::HllUtil<Alloc>::checkMemSize(offset, size);
188
1.51k
    const auto aux_count = read_uint32(bytes + AUX_COUNT_INT);
189
1.51k
    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
783
        if (aux_count != 0) {
193
16
            throw std::invalid_argument("HLL_6/HLL_8 cannot contain an auxiliary map");
194
16
        }
195
767
        return false;
196
783
    }
197
736
    if (aux_count != 0) {
198
397
        check_aux_map(bytes, size, offset, lg_k, aux_count, compact);
199
397
        return true;
200
397
    }
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
727k
    for (size_t i = HLL_BYTE_ARR_START; i < offset; ++i) {
204
727k
        if ((bytes[i] & loNibbleMask) == AUX_TOKEN ||
205
727k
            ((bytes[i] >> 4) & loNibbleMask) == AUX_TOKEN) {
206
1
            throw std::invalid_argument("HLL_4 contains AUX_TOKEN without an auxiliary map");
207
1
        }
208
727k
    }
209
338
    return false;
210
339
}
211
212
} // namespace
213
214
AllocationScope::AllocationScope()
215
5.41k
        : _previous(current_scope()), _exceptions(std::uncaught_exceptions()) {
216
5.41k
    set_scope(&_owner);
217
5.41k
}
218
219
5.41k
AllocationScope::~AllocationScope() {
220
5.41k
    set_scope(_previous);
221
5.41k
    if (std::uncaught_exceptions() == _exceptions) {
222
4.53k
        _owner.release();
223
4.53k
    }
224
5.41k
}
225
226
6.79k
detail::AllocationOwner::~AllocationOwner() {
227
6.79k
    clear();
228
6.79k
}
229
230
6.79k
void detail::AllocationOwner::clear() noexcept {
231
6.92k
    while (_head != nullptr) {
232
125
        auto* header = _head;
233
125
        _head = header->next;
234
125
        CustomStdAllocator<AllocationHeader>().deallocate(header, header->count);
235
125
    }
236
6.79k
}
237
238
4.53k
void detail::AllocationOwner::release() noexcept {
239
13.9k
    while (_head != nullptr) {
240
9.43k
        auto* header = _head;
241
9.43k
        _head = header->next;
242
        // Returned sketches keep normal library ownership after a successful call.
243
9.43k
        header->owner = nullptr;
244
9.43k
    }
245
4.53k
}
246
247
18.0k
void* allocate(size_t bytes) {
248
18.0k
    DBUG_EXECUTE_IF("datasketches_hll.before_allocate", { DBUG_RUN_CALLBACK(); });
249
18.0k
    auto* scope = current_scope();
250
18.0k
    const size_t count =
251
18.0k
            1 + bytes / sizeof(AllocationHeader) + (bytes % sizeof(AllocationHeader) != 0);
252
18.0k
    auto* header = CustomStdAllocator<AllocationHeader>().allocate(count);
253
18.0k
    *header = {.previous = nullptr,
254
18.0k
               .next = scope == nullptr ? nullptr : scope->_head,
255
18.0k
               .owner = scope,
256
18.0k
               .count = count};
257
18.0k
    if (scope != nullptr) {
258
15.0k
        if (header->next != nullptr) {
259
9.07k
            header->next->previous = header;
260
9.07k
        }
261
15.0k
        scope->_head = header;
262
15.0k
    }
263
18.0k
    return header + 1;
264
18.0k
}
265
266
17.7k
void deallocate(void* ptr) noexcept {
267
17.7k
    auto* header = static_cast<AllocationHeader*>(ptr) - 1;
268
17.7k
    if (header->owner != nullptr) {
269
5.49k
        if (header->previous != nullptr) {
270
2.77k
            header->previous->next = header->next;
271
2.77k
        } else {
272
2.72k
            header->owner->_head = header->next;
273
2.72k
        }
274
5.49k
        if (header->next != nullptr) {
275
3.14k
            header->next->previous = header->previous;
276
3.14k
        }
277
5.49k
    }
278
17.7k
    CustomStdAllocator<AllocationHeader>().deallocate(header, header->count);
279
17.7k
}
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
4.12k
    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.38k
    ~Impl() {
294
1.38k
        if (valid) {
295
1.35k
            get()->~LibraryUnion();
296
1.35k
        }
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.38k
    }
300
};
301
302
1.38k
void Union::Deleter::operator()(Impl* ptr) const noexcept {
303
1.38k
    ptr->~Impl();
304
1.38k
    datasketches_hll::deallocate(ptr);
305
1.38k
}
306
307
Union::Union(uint8_t lg_k, const Alloc& allocator)
308
1.39k
        : _impl(new(datasketches_hll::allocate(sizeof(Impl))) Impl()) {
309
1.39k
    ActiveAllocationOwner scope(&_impl->allocations);
310
1.39k
    new (_impl->storage) Impl::LibraryUnion(lg_k, allocator);
311
1.39k
    _impl->valid = true;
312
1.39k
}
313
314
1.37k
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.38k
void Union::update(const Sketch& sketch) {
332
1.38k
    DORIS_CHECK(_impl->valid);
333
1.38k
    ActiveAllocationOwner scope(&_impl->allocations);
334
1.38k
    try {
335
1.38k
        _impl->get()->update(sketch);
336
1.38k
    } 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.38k
}
343
344
1.31k
Sketch Union::get_result(datasketches::target_hll_type type) const {
345
1.31k
    DORIS_CHECK(_impl->valid);
346
1.31k
    AllocationScope scope;
347
1.31k
    return _impl->get()->get_result(type);
348
1.31k
}
349
350
32
double Union::get_estimate() const {
351
32
    DORIS_CHECK(_impl->valid);
352
32
    return _impl->get()->get_estimate();
353
32
}
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
32
uint8_t Union::get_lg_config_k() const {
366
32
    DORIS_CHECK(_impl->valid);
367
32
    return _impl->get()->get_lg_config_k();
368
32
}
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.07k
Sketch deserialize(const StringRef& value, std::string_view action) {
375
4.07k
    AllocationScope allocations;
376
    // The library reads the preamble byte before it checks the length.
377
4.07k
    if (value.size == 0) {
378
4
        throw Exception(ErrorCode::CORRUPTION, "HLL sketch data corrupted when {}: empty input.",
379
4
                        action);
380
4
    }
381
4.07k
    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.07k
        datasketches::HllUtil<Alloc>::checkMemSize(EMPTY_SKETCH_SIZE_BYTES, value.size);
385
4.07k
        const auto* bytes = reinterpret_cast<const uint8_t*>(value.data);
386
4.07k
        const auto lg_k = datasketches::HllUtil<Alloc>::checkLgK(bytes[LG_K_BYTE]);
387
4.07k
        const auto mode = HeaderReader::extractCurMode(bytes[MODE_BYTE]);
388
4.07k
        const bool compact = (bytes[FLAGS_BYTE] & COMPACT_FLAG_MASK) != 0;
389
4.07k
        bool has_aux_map = false;
390
4.07k
        if (mode == datasketches::SET) {
391
1.09k
            check_set_size(bytes, value.size, lg_k, compact);
392
2.97k
        } else if (mode == datasketches::HLL) {
393
1.51k
            has_aux_map = check_hll_size(bytes, value.size, lg_k, compact);
394
1.51k
        }
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.07k
        if (has_aux_map && reinterpret_cast<uintptr_t>(value.data) % alignof(uint32_t) != 0) {
399
79
            DorisVector<uint32_t> aligned(value.size / sizeof(uint32_t) +
400
79
                                          (value.size % sizeof(uint32_t) != 0));
401
79
            std::memcpy(aligned.data(), value.data, value.size);
402
79
            return Sketch::deserialize(aligned.data(), value.size, Alloc());
403
79
        }
404
3.99k
        return Sketch::deserialize(value.data, value.size, Alloc());
405
4.07k
    } catch (const doris::Exception& e) {
406
49
        throw Exception(e.code(), "Failed to deserialize HLL sketch when {}: {}", action,
407
49
                        e.to_string());
408
784
    } catch (const std::exception& e) {
409
784
        throw Exception(ErrorCode::CORRUPTION, "HLL sketch data corrupted when {}: {}", action,
410
784
                        e.what());
411
784
    } catch (...) {
412
0
        throw Exception(ErrorCode::CORRUPTION,
413
0
                        "HLL sketch data corrupted when {}: unknown exception.", action);
414
0
    }
415
4.07k
}
416
417
} // namespace doris::datasketches_hll