Coverage Report

Created: 2026-11-05 21:23

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
127
bthread_key_t scope_key() {
49
127
    static bthread_key_t key;
50
127
    static std::once_flag once;
51
127
    std::call_once(once, [] { DORIS_CHECK(bthread_key_create(&key, nullptr) == 0); });
52
127
    return key;
53
127
}
54
55
52.1k
detail::AllocationOwner* current_scope() {
56
52.1k
    return bthread_self() == 0
57
52.1k
                   ? pthread_scope
58
52.1k
                   : static_cast<detail::AllocationOwner*>(bthread_getspecific(scope_key()));
59
52.1k
}
60
61
32.7k
void set_scope(detail::AllocationOwner* scope) {
62
32.7k
    if (bthread_self() == 0) {
63
32.6k
        pthread_scope = scope;
64
32.6k
    } else {
65
64
        DORIS_CHECK(bthread_setspecific(scope_key(), scope) == 0);
66
64
    }
67
32.7k
}
68
69
class ActiveAllocationOwner {
70
public:
71
5.53k
    explicit ActiveAllocationOwner(detail::AllocationOwner* owner) : _previous(current_scope()) {
72
5.53k
        set_scope(owner);
73
5.53k
    }
74
5.53k
    ~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
693k
uint32_t read_uint32(const uint8_t* bytes) {
88
693k
    uint32_t value;
89
693k
    std::memcpy(&value, bytes, sizeof(value));
90
693k
    return value;
91
693k
}
92
93
2.18k
void check_set_size(const uint8_t* bytes, size_t size, uint8_t lg_k, bool compact) {
94
2.18k
    datasketches::HllUtil<Alloc>::checkMemSize(HASH_SET_INT_ARR_START, size);
95
2.18k
    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
2.18k
    if (lg_k <= 7 || count > (RESIZE_NUMER * (uint32_t {1} << (lg_k - 3))) / RESIZE_DENOM) {
99
8
        throw std::invalid_argument("Invalid HLL SET coupon count or lgK");
100
8
    }
101
2.18k
    size_t items = count;
102
2.18k
    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
1.44k
        const uint8_t lg_arr = bytes[LG_ARR_BYTE] < LG_INIT_SET_SIZE
106
1.44k
                                       ? datasketches::HllUtil<Alloc>::computeLgArrInts(
107
186
                                                 datasketches::SET, count, lg_k)
108
1.44k
                                       : bytes[LG_ARR_BYTE];
109
1.44k
        if (lg_arr > lg_k - 3) {
110
40
            throw std::invalid_argument("Invalid HLL SET array exponent");
111
40
        }
112
1.40k
        items = size_t {1} << lg_arr;
113
1.40k
    }
114
2.14k
    datasketches::HllUtil<Alloc>::checkMemSize(HASH_SET_INT_ARR_START + items * sizeof(uint32_t),
115
2.14k
                                               size);
116
2.14k
    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
1.39k
        uint32_t occupied = 0;
120
686k
        for (size_t i = 0; i < items; ++i) {
121
684k
            occupied += read_uint32(bytes + HASH_SET_INT_ARR_START + i * sizeof(uint32_t)) != EMPTY;
122
684k
        }
123
1.39k
        if (occupied != count) {
124
1.03k
            throw std::invalid_argument("HLL SET coupon count does not match occupied slots");
125
1.03k
        }
126
1.39k
    }
127
2.14k
}
128
129
void check_aux_map(const uint8_t* bytes, size_t size, size_t offset, uint8_t lg_k, uint32_t count,
130
794
                   bool compact) {
131
794
    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
794
    if (count > registers) {
135
8
        throw std::invalid_argument("HLL_4 auxiliary count exceeds register count");
136
8
    }
137
786
    size_t items = count;
138
786
    if (!compact) {
139
352
        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
352
        if (lg_arr < LG_AUX_ARR_INTS[lg_k] || lg_arr > lg_k + 1) {
143
48
            throw std::invalid_argument("Invalid HLL_4 auxiliary array exponent");
144
48
        }
145
304
        items = size_t {1} << lg_arr;
146
304
    }
147
738
    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
738
    DorisVector<uint8_t> seen(registers / 8, 0);
153
3.67k
    for (size_t i = 0; i < items; ++i) {
154
3.10k
        const auto pair = read_uint32(bytes + offset + i * sizeof(uint32_t));
155
3.10k
        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.89k
            if (compact) {
159
26
                throw std::invalid_argument("HLL_4 compact auxiliary map contains an EMPTY entry");
160
26
            }
161
1.86k
            continue;
162
1.89k
        }
163
1.21k
        const auto slot = datasketches::HllUtil<Alloc>::getLow26(pair) & (registers - 1);
164
1.21k
        const auto bit = static_cast<uint8_t>(1U << (slot % 8));
165
1.21k
        if (seen[slot / 8] & bit) {
166
144
            throw std::invalid_argument("HLL_4 auxiliary map contains duplicate slots");
167
144
        }
168
1.07k
        seen[slot / 8] |= bit;
169
1.07k
    }
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
567k
    for (uint32_t slot = 0; slot < registers; ++slot) {
174
567k
        const auto byte = bytes[HLL_BYTE_ARR_START + slot / 2];
175
567k
        const auto value = static_cast<uint8_t>((byte >> ((slot % 2) * 4)) & loNibbleMask);
176
567k
        if (value == AUX_TOKEN && !(seen[slot / 8] & (1U << (slot % 8)))) {
177
176
            throw std::invalid_argument("HLL_4 AUX_TOKEN has no matching auxiliary slot");
178
176
        }
179
567k
    }
180
568
}
181
182
3.03k
bool check_hll_size(const uint8_t* bytes, size_t size, uint8_t lg_k, bool compact) {
183
3.03k
    datasketches::HllUtil<Alloc>::checkMemSize(HLL_BYTE_ARR_START, size);
184
3.03k
    const auto type = HeaderReader::extractTgtHllType(bytes[MODE_BYTE]);
185
3.03k
    const size_t offset =
186
3.03k
            HLL_BYTE_ARR_START + datasketches::HllArray<Alloc>::hllArrBytes(type, lg_k);
187
3.03k
    datasketches::HllUtil<Alloc>::checkMemSize(offset, size);
188
3.03k
    const auto aux_count = read_uint32(bytes + AUX_COUNT_INT);
189
3.03k
    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
1.56k
        if (aux_count != 0) {
193
32
            throw std::invalid_argument("HLL_6/HLL_8 cannot contain an auxiliary map");
194
32
        }
195
1.53k
        return false;
196
1.56k
    }
197
1.47k
    if (aux_count != 0) {
198
794
        check_aux_map(bytes, size, offset, lg_k, aux_count, compact);
199
794
        return true;
200
794
    }
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
1.45M
    for (size_t i = HLL_BYTE_ARR_START; i < offset; ++i) {
204
1.45M
        if ((bytes[i] & loNibbleMask) == AUX_TOKEN ||
205
1.45M
            ((bytes[i] >> 4) & loNibbleMask) == AUX_TOKEN) {
206
2
            throw std::invalid_argument("HLL_4 contains AUX_TOKEN without an auxiliary map");
207
2
        }
208
1.45M
    }
209
676
    return false;
210
678
}
211
212
} // namespace
213
214
AllocationScope::AllocationScope()
215
10.8k
        : _previous(current_scope()), _exceptions(std::uncaught_exceptions()) {
216
10.8k
    set_scope(&_owner);
217
10.8k
}
218
219
10.8k
AllocationScope::~AllocationScope() {
220
10.8k
    set_scope(_previous);
221
10.8k
    if (std::uncaught_exceptions() == _exceptions) {
222
9.07k
        _owner.release();
223
9.07k
    }
224
10.8k
}
225
226
13.5k
detail::AllocationOwner::~AllocationOwner() {
227
13.5k
    clear();
228
13.5k
}
229
230
13.5k
void detail::AllocationOwner::clear() noexcept {
231
13.8k
    while (_head != nullptr) {
232
250
        auto* header = _head;
233
250
        _head = header->next;
234
250
        CustomStdAllocator<AllocationHeader>().deallocate(header, header->count);
235
250
    }
236
13.5k
}
237
238
9.07k
void detail::AllocationOwner::release() noexcept {
239
27.9k
    while (_head != nullptr) {
240
18.8k
        auto* header = _head;
241
18.8k
        _head = header->next;
242
        // Returned sketches keep normal library ownership after a successful call.
243
18.8k
        header->owner = nullptr;
244
18.8k
    }
245
9.07k
}
246
247
36.0k
void* allocate(size_t bytes) {
248
36.0k
    DBUG_EXECUTE_IF("datasketches_hll.before_allocate", { DBUG_RUN_CALLBACK(); });
249
36.0k
    auto* scope = current_scope();
250
36.0k
    const size_t count =
251
36.0k
            1 + bytes / sizeof(AllocationHeader) + (bytes % sizeof(AllocationHeader) != 0);
252
36.0k
    auto* header = CustomStdAllocator<AllocationHeader>().allocate(count);
253
36.0k
    *header = {.previous = nullptr,
254
36.0k
               .next = scope == nullptr ? nullptr : scope->_head,
255
36.0k
               .owner = scope,
256
36.0k
               .count = count};
257
36.0k
    if (scope != nullptr) {
258
30.1k
        if (header->next != nullptr) {
259
18.1k
            header->next->previous = header;
260
18.1k
        }
261
30.1k
        scope->_head = header;
262
30.1k
    }
263
36.0k
    return header + 1;
264
36.0k
}
265
266
35.5k
void deallocate(void* ptr) noexcept {
267
35.5k
    auto* header = static_cast<AllocationHeader*>(ptr) - 1;
268
35.5k
    if (header->owner != nullptr) {
269
10.9k
        if (header->previous != nullptr) {
270
5.54k
            header->previous->next = header->next;
271
5.54k
        } else {
272
5.44k
            header->owner->_head = header->next;
273
5.44k
        }
274
10.9k
        if (header->next != nullptr) {
275
6.28k
            header->next->previous = header->previous;
276
6.28k
        }
277
10.9k
    }
278
35.5k
    CustomStdAllocator<AllocationHeader>().deallocate(header, header->count);
279
35.5k
}
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
8.24k
    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
2.77k
    ~Impl() {
294
2.77k
        if (valid) {
295
2.70k
            get()->~LibraryUnion();
296
2.70k
        }
297
        // Failed library updates can leave a dangling gadget pointer. AllocationOwner frees
298
        // the remaining blocks directly, instead of dereferencing that invalid gadget.
299
2.77k
    }
300
};
301
302
2.77k
void Union::Deleter::operator()(Impl* ptr) const noexcept {
303
2.77k
    ptr->~Impl();
304
2.77k
    datasketches_hll::deallocate(ptr);
305
2.77k
}
306
307
Union::Union(uint8_t lg_k, const Alloc& allocator)
308
2.78k
        : _impl(new(datasketches_hll::allocate(sizeof(Impl))) Impl()) {
309
2.78k
    ActiveAllocationOwner scope(&_impl->allocations);
310
2.78k
    new (_impl->storage) Impl::LibraryUnion(lg_k, allocator);
311
2.78k
    _impl->valid = true;
312
2.78k
}
313
314
2.74k
Union::~Union() = default;
315
2
Union::Union(Union&&) noexcept = default;
316
2
Union& Union::operator=(Union&&) noexcept = default;
317
318
4
Union::Union(const Union& other) : _impl(new(datasketches_hll::allocate(sizeof(Impl))) Impl()) {
319
4
    DORIS_CHECK(other._impl->valid);
320
4
    ActiveAllocationOwner scope(&_impl->allocations);
321
4
    new (_impl->storage) Impl::LibraryUnion(*other._impl->get());
322
4
    _impl->valid = true;
323
4
}
324
325
2
Union& Union::operator=(const Union& other) {
326
2
    Union copy(other);
327
2
    *this = std::move(copy);
328
2
    return *this;
329
2
}
330
331
2.76k
void Union::update(const Sketch& sketch) {
332
2.76k
    DORIS_CHECK(_impl->valid);
333
2.76k
    ActiveAllocationOwner scope(&_impl->allocations);
334
2.76k
    try {
335
2.76k
        _impl->get()->update(sketch);
336
2.76k
    } 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
40
        _impl->valid = false;
340
40
        throw;
341
40
    }
342
2.76k
}
343
344
2.63k
Sketch Union::get_result(datasketches::target_hll_type type) const {
345
2.63k
    DORIS_CHECK(_impl->valid);
346
2.63k
    AllocationScope scope;
347
2.63k
    return _impl->get()->get_result(type);
348
2.63k
}
349
350
64
double Union::get_estimate() const {
351
64
    DORIS_CHECK(_impl->valid);
352
64
    return _impl->get()->get_estimate();
353
64
}
354
355
6
double Union::get_lower_bound(uint8_t std_devs) const {
356
6
    DORIS_CHECK(_impl->valid);
357
6
    return _impl->get()->get_lower_bound(std_devs);
358
6
}
359
360
6
double Union::get_upper_bound(uint8_t std_devs) const {
361
6
    DORIS_CHECK(_impl->valid);
362
6
    return _impl->get()->get_upper_bound(std_devs);
363
6
}
364
365
64
uint8_t Union::get_lg_config_k() const {
366
64
    DORIS_CHECK(_impl->valid);
367
64
    return _impl->get()->get_lg_config_k();
368
64
}
369
370
16
double Union::get_rel_err(bool upper, bool unioned, uint8_t lg_k, uint8_t std_devs) {
371
16
    return Impl::LibraryUnion::get_rel_err(upper, unioned, lg_k, std_devs);
372
16
}
373
374
8.15k
Sketch deserialize(const StringRef& value, std::string_view action) {
375
8.15k
    AllocationScope allocations;
376
    // The library reads the preamble byte before it checks the length.
377
8.15k
    if (value.size == 0) {
378
8
        throw Exception(ErrorCode::CORRUPTION, "HLL sketch data corrupted when {}: empty input.",
379
8
                        action);
380
8
    }
381
8.14k
    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
8.14k
        datasketches::HllUtil<Alloc>::checkMemSize(EMPTY_SKETCH_SIZE_BYTES, value.size);
385
8.14k
        const auto* bytes = reinterpret_cast<const uint8_t*>(value.data);
386
8.14k
        const auto lg_k = datasketches::HllUtil<Alloc>::checkLgK(bytes[LG_K_BYTE]);
387
8.14k
        const auto mode = HeaderReader::extractCurMode(bytes[MODE_BYTE]);
388
8.14k
        const bool compact = (bytes[FLAGS_BYTE] & COMPACT_FLAG_MASK) != 0;
389
8.14k
        bool has_aux_map = false;
390
8.14k
        if (mode == datasketches::SET) {
391
2.18k
            check_set_size(bytes, value.size, lg_k, compact);
392
5.95k
        } else if (mode == datasketches::HLL) {
393
3.03k
            has_aux_map = check_hll_size(bytes, value.size, lg_k, compact);
394
3.03k
        }
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
8.14k
        if (has_aux_map && reinterpret_cast<uintptr_t>(value.data) % alignof(uint32_t) != 0) {
399
158
            DorisVector<uint32_t> aligned(value.size / sizeof(uint32_t) +
400
158
                                          (value.size % sizeof(uint32_t) != 0));
401
158
            std::memcpy(aligned.data(), value.data, value.size);
402
158
            return Sketch::deserialize(aligned.data(), value.size, Alloc());
403
158
        }
404
7.98k
        return Sketch::deserialize(value.data, value.size, Alloc());
405
8.14k
    } catch (const doris::Exception& e) {
406
98
        throw Exception(e.code(), "Failed to deserialize HLL sketch when {}: {}", action,
407
98
                        e.to_string());
408
1.56k
    } catch (const std::exception& e) {
409
1.56k
        throw Exception(ErrorCode::CORRUPTION, "HLL sketch data corrupted when {}: {}", action,
410
1.56k
                        e.what());
411
1.56k
    } catch (...) {
412
0
        throw Exception(ErrorCode::CORRUPTION,
413
0
                        "HLL sketch data corrupted when {}: unknown exception.", action);
414
0
    }
415
8.14k
}
416
417
} // namespace doris::datasketches_hll