Coverage Report

Created: 2025-04-11 21:03

/root/doris/be/src/util/thread.cpp
Line
Count
Source (jump to first uncovered line)
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
// This file is copied from
18
// https://github.com/apache/impala/blob/branch-2.9.0/be/src/util/thread.cc
19
// and modified by Doris
20
21
#include "thread.h"
22
23
#include <sys/resource.h>
24
25
#ifndef __APPLE__
26
// IWYU pragma: no_include <bits/types/struct_sched_param.h>
27
#include <sched.h>
28
#include <sys/prctl.h>
29
#else
30
#include <pthread.h>
31
32
#include <cstdint>
33
#endif
34
35
// IWYU pragma: no_include <bthread/errno.h>
36
#include <errno.h> // IWYU pragma: keep
37
#include <sys/syscall.h>
38
#include <time.h>
39
#include <unistd.h>
40
41
#include <algorithm>
42
// IWYU pragma: no_include <bits/chrono.h>
43
#include <chrono> // IWYU pragma: keep
44
#include <cstring>
45
#include <functional>
46
#include <limits>
47
#include <map>
48
#include <memory>
49
#include <mutex>
50
#include <ostream>
51
#include <string>
52
#include <vector>
53
54
#include "common/config.h"
55
#include "common/logging.h"
56
#include "gutil/atomicops.h"
57
#include "gutil/dynamic_annotations.h"
58
#include "gutil/stringprintf.h"
59
#include "gutil/strings/substitute.h"
60
#include "http/web_page_handler.h"
61
#include "runtime/thread_context.h"
62
#include "util/debug/sanitizer_scopes.h"
63
#include "util/easy_json.h"
64
#include "util/os_util.h"
65
#include "util/scoped_cleanup.h"
66
#include "util/url_coding.h"
67
68
namespace doris {
69
70
class ThreadMgr;
71
72
__thread Thread* Thread::_tls = nullptr;
73
74
// Singleton instance of ThreadMgr. Only visible in this file, used only by Thread.
75
// // The Thread class adds a reference to thread_manager while it is supervising a thread so
76
// // that a race between the end of the process's main thread (and therefore the destruction
77
// // of thread_manager) and the end of a thread that tries to remove itself from the
78
// // manager after the destruction can be avoided.
79
static std::shared_ptr<ThreadMgr> thread_manager;
80
//
81
// Controls the single (lazy) initialization of thread_manager.
82
static std::once_flag once;
83
84
// A singleton class that tracks all live threads, and groups them together for easy
85
// auditing. Used only by Thread.
86
class ThreadMgr {
87
public:
88
1
    ThreadMgr() : _threads_started_metric(0), _threads_running_metric(0) {}
89
90
0
    ~ThreadMgr() {
91
0
        std::unique_lock<std::mutex> lock(_lock);
92
0
        _thread_categories.clear();
93
0
    }
94
95
    static void set_thread_name(const std::string& name, int64_t tid);
96
97
#ifndef __APPLE__
98
    static void set_idle_sched(int64_t tid);
99
100
    static void set_thread_nice_value(int64_t tid);
101
#endif
102
103
    // not the system TID, since pthread_t is less prone to being recycled.
104
    void add_thread(const pthread_t& pthread_id, const std::string& name,
105
                    const std::string& category, int64_t tid);
106
107
    // Removes a thread from the supplied category. If the thread has
108
    // already been removed, this is a no-op.
109
    void remove_thread(const pthread_t& pthread_id, const std::string& category);
110
111
    void display_thread_callback(const WebPageHandler::ArgumentMap& args, EasyJson* ej) const;
112
113
private:
114
    // Container class for any details we want to capture about a thread
115
    // TODO: Add start-time.
116
    // TODO: Track fragment ID.
117
    class ThreadDescriptor {
118
    public:
119
2.69k
        ThreadDescriptor() {}
120
        ThreadDescriptor(std::string category, std::string name, int64_t thread_id)
121
2.69k
                : _name(std::move(name)), _category(std::move(category)), _thread_id(thread_id) {}
122
123
0
        const std::string& name() const { return _name; }
124
0
        const std::string& category() const { return _category; }
125
0
        int64_t thread_id() const { return _thread_id; }
126
127
    private:
128
        std::string _name;
129
        std::string _category;
130
        int64_t _thread_id;
131
    };
132
133
    void summarize_thread_descriptor(const ThreadDescriptor& desc, EasyJson* ej) const;
134
135
    // A ThreadCategory is a set of threads that are logically related.
136
    // TODO: unordered_map is incompatible with pthread_t, but would be more
137
    // efficient here.
138
    typedef std::map<const pthread_t, ThreadDescriptor> ThreadCategory;
139
140
    // All thread categories, keyed on the category name.
141
    typedef std::map<std::string, ThreadCategory> ThreadCategoryMap;
142
143
    // Protects _thread_categories and thread metrics.
144
    mutable std::mutex _lock;
145
146
    // All thread categories that ever contained a thread, even if empty
147
    ThreadCategoryMap _thread_categories;
148
149
    // Counters to track all-time total number of threads, and the
150
    // current number of running threads.
151
    uint64_t _threads_started_metric;
152
    uint64_t _threads_running_metric;
153
154
    DISALLOW_COPY_AND_ASSIGN(ThreadMgr);
155
};
156
157
2.90k
void ThreadMgr::set_thread_name(const std::string& name, int64_t tid) {
158
2.90k
    if (tid == getpid()) {
159
0
        return;
160
0
    }
161
#ifdef __APPLE__
162
    int err = pthread_setname_np(name.c_str());
163
#else
164
2.90k
    int err = prctl(PR_SET_NAME, name.c_str());
165
2.90k
#endif
166
2.90k
    if (err < 0 && errno != EPERM) {
167
0
        LOG(ERROR) << "set_thread_name";
168
0
    }
169
2.90k
}
170
171
#ifndef __APPLE__
172
0
void ThreadMgr::set_idle_sched(int64_t tid) {
173
0
    if (tid == getpid()) {
174
0
        return;
175
0
    }
176
0
    struct sched_param sp = {.sched_priority = 0};
177
0
    int err = sched_setscheduler(0, SCHED_IDLE, &sp);
178
0
    if (err < 0 && errno != EPERM) {
179
0
        LOG(ERROR) << "set_thread_idle_sched";
180
0
    }
181
0
}
182
183
0
void ThreadMgr::set_thread_nice_value(int64_t tid) {
184
0
    if (tid == getpid()) {
185
0
        return;
186
0
    }
187
    // From Linux kernel:
188
    // In the current implementation, each unit of difference in the nice values of two
189
    // processes results in a factor of 1.25 in the degree to which the
190
    // scheduler favors the higher priority process.  This causes very
191
    // low nice values (+19) to truly provide little CPU to a process
192
    // whenever there is any other higher priority load on the system,
193
    // and makes high nice values (-20) deliver most of the CPU to
194
    // applications that require it (e.g., some audio applications).
195
196
    // Choose 5 as lower priority value, default is 0
197
0
    int err = setpriority(PRIO_PROCESS, 0, config::scan_thread_nice_value);
198
0
    if (err < 0 && errno != EPERM) {
199
0
        LOG(ERROR) << "set_thread_low_priority";
200
0
    }
201
0
}
202
#endif
203
204
void ThreadMgr::add_thread(const pthread_t& pthread_id, const std::string& name,
205
2.68k
                           const std::string& category, int64_t tid) {
206
    // These annotations cause TSAN to ignore the synchronization on lock_
207
    // without causing the subsequent mutations to be treated as data races
208
    // in and of themselves (that's what IGNORE_READS_AND_WRITES does).
209
    //
210
    // Why do we need them here and in SuperviseThread()? TSAN operates by
211
    // observing synchronization events and using them to establish "happens
212
    // before" relationships between threads. Where these relationships are
213
    // not built, shared state access constitutes a data race. The
214
    // synchronization events here, in RemoveThread(), and in
215
    // SuperviseThread() may cause TSAN to establish a "happens before"
216
    // relationship between thread functors, ignoring potential data races.
217
    // The annotations prevent this from happening.
218
2.68k
    ANNOTATE_IGNORE_SYNC_BEGIN();
219
2.68k
    debug::ScopedTSANIgnoreReadsAndWrites ignore_tsan;
220
2.68k
    {
221
2.68k
        std::unique_lock<std::mutex> l(_lock);
222
2.68k
        _thread_categories[category][pthread_id] = ThreadDescriptor(category, name, tid);
223
2.68k
        _threads_running_metric++;
224
2.68k
        _threads_started_metric++;
225
2.68k
    }
226
2.68k
    ANNOTATE_IGNORE_SYNC_END();
227
2.68k
}
228
229
2.66k
void ThreadMgr::remove_thread(const pthread_t& pthread_id, const std::string& category) {
230
2.66k
    ANNOTATE_IGNORE_SYNC_BEGIN();
231
2.66k
    debug::ScopedTSANIgnoreReadsAndWrites ignore_tsan;
232
2.66k
    {
233
2.66k
        std::unique_lock<std::mutex> l(_lock);
234
2.66k
        auto category_it = _thread_categories.find(category);
235
2.66k
        DCHECK(category_it != _thread_categories.end());
236
2.66k
        category_it->second.erase(pthread_id);
237
2.66k
        _threads_running_metric--;
238
2.66k
    }
239
2.66k
    ANNOTATE_IGNORE_SYNC_END();
240
2.66k
}
241
242
void ThreadMgr::display_thread_callback(const WebPageHandler::ArgumentMap& args,
243
0
                                        EasyJson* ej) const {
244
0
    if (args.contains("group")) {
245
0
        const auto& category_name = args.at("group");
246
0
        bool requested_all = category_name == "all";
247
0
        ej->Set("requested_thread_group", EasyJson::kObject);
248
0
        (*ej)["group_name"] = escape_for_html_to_string(category_name);
249
0
        (*ej)["requested_all"] = requested_all;
250
251
        // The critical section is as short as possible so as to minimize the delay
252
        // imposed on new threads that acquire the lock in write mode.
253
0
        std::vector<ThreadDescriptor> descriptors_to_print;
254
0
        if (!requested_all) {
255
0
            std::unique_lock<std::mutex> l(_lock);
256
0
            if (!_thread_categories.contains(category_name)) {
257
0
                return;
258
0
            }
259
0
            for (const auto& elem : _thread_categories.at(category_name)) {
260
0
                descriptors_to_print.emplace_back(elem.second);
261
0
            }
262
0
        } else {
263
0
            std::unique_lock<std::mutex> l(_lock);
264
0
            for (const auto& category : _thread_categories) {
265
0
                for (const auto& elem : category.second) {
266
0
                    descriptors_to_print.emplace_back(elem.second);
267
0
                }
268
0
            }
269
0
        }
270
271
0
        EasyJson found = (*ej).Set("found", EasyJson::kObject);
272
0
        EasyJson threads = found.Set("threads", EasyJson::kArray);
273
0
        for (const auto& desc : descriptors_to_print) {
274
0
            summarize_thread_descriptor(desc, &threads);
275
0
        }
276
0
    } else {
277
        // List all thread groups and the number of threads running in each.
278
0
        std::vector<std::pair<string, uint64_t>> thread_categories_info;
279
0
        uint64_t running;
280
0
        {
281
0
            std::unique_lock<std::mutex> l(_lock);
282
0
            running = _threads_running_metric;
283
0
            thread_categories_info.reserve(_thread_categories.size());
284
0
            for (const auto& category : _thread_categories) {
285
0
                thread_categories_info.emplace_back(category.first, category.second.size());
286
0
            }
287
288
0
            (*ej)["total_threads_running"] = running;
289
0
            EasyJson groups = ej->Set("groups", EasyJson::kArray);
290
0
            for (const auto& elem : thread_categories_info) {
291
0
                string category_arg;
292
0
                url_encode(elem.first, &category_arg);
293
0
                EasyJson group = groups.PushBack(EasyJson::kObject);
294
0
                group["encoded_group_name"] = category_arg;
295
0
                group["group_name"] = elem.first;
296
0
                group["threads_running"] = elem.second;
297
0
            }
298
0
        }
299
0
    }
300
0
}
301
302
void ThreadMgr::summarize_thread_descriptor(const ThreadMgr::ThreadDescriptor& desc,
303
0
                                            EasyJson* ej) const {
304
0
    ThreadStats stats;
305
0
    Status status = get_thread_stats(desc.thread_id(), &stats);
306
0
    if (!status.ok()) {
307
0
        LOG(WARNING) << "Could not get per-thread statistics: " << status.to_string();
308
0
    }
309
310
0
    EasyJson thread = ej->PushBack(EasyJson::kObject);
311
0
    thread["thread_name"] = desc.name();
312
0
    thread["user_sec"] = static_cast<double>(stats.user_ns) / 1e9;
313
0
    thread["kernel_sec"] = static_cast<double>(stats.kernel_ns) / 1e9;
314
0
    thread["iowait_sec"] = static_cast<double>(stats.iowait_ns) / 1e9;
315
0
}
316
317
2.66k
Thread::~Thread() {
318
2.66k
    if (_joinable) {
319
1.59k
        int ret = pthread_detach(_thread);
320
1.59k
        CHECK_EQ(ret, 0);
321
1.59k
    }
322
2.66k
}
323
324
214
void Thread::set_self_name(const std::string& name) {
325
214
    ThreadMgr::set_thread_name(name, current_thread_id());
326
214
}
327
328
#ifndef __APPLE__
329
0
void Thread::set_idle_sched() {
330
0
    ThreadMgr::set_idle_sched(current_thread_id());
331
0
}
332
333
0
void Thread::set_thread_nice_value() {
334
0
    ThreadMgr::set_thread_nice_value(current_thread_id());
335
0
}
336
#endif
337
338
1.07k
void Thread::join() {
339
1.07k
    static_cast<void>(ThreadJoiner(this).join());
340
1.07k
}
341
342
1.00k
int64_t Thread::tid() const {
343
1.00k
    int64_t t = base::subtle::Acquire_Load(&_tid);
344
1.00k
    if (t != PARENT_WAITING_TID) {
345
1.00k
        return _tid;
346
1.00k
    }
347
0
    return wait_for_tid();
348
1.00k
}
349
350
0
pthread_t Thread::pthread_id() const {
351
0
    return _thread;
352
0
}
353
354
2.68k
const std::string& Thread::name() const {
355
2.68k
    return _name;
356
2.68k
}
357
358
5.35k
const std::string& Thread::category() const {
359
5.35k
    return _category;
360
5.35k
}
361
362
0
std::string Thread::to_string() const {
363
0
    return strings::Substitute("Thread $0 (name: \"$1\", category: \"$2\")", tid(), _name,
364
0
                               _category);
365
0
}
366
367
9.47k
Thread* Thread::current_thread() {
368
9.47k
    return _tls;
369
9.47k
}
370
371
0
int64_t Thread::unique_thread_id() {
372
#ifdef __APPLE__
373
    uint64_t tid;
374
    pthread_threadid_np(pthread_self(), &tid);
375
    return tid;
376
#else
377
0
    return static_cast<int64_t>(pthread_self());
378
0
#endif
379
0
}
380
381
2.90k
int64_t Thread::current_thread_id() {
382
#ifdef __APPLE__
383
    uint64_t tid;
384
    pthread_threadid_np(nullptr, &tid);
385
    return tid;
386
#else
387
2.90k
    return syscall(SYS_gettid);
388
2.90k
#endif
389
2.90k
}
390
391
0
int64_t Thread::wait_for_tid() const {
392
0
    int loop_count = 0;
393
0
    while (true) {
394
0
        int64_t t = Acquire_Load(&_tid);
395
0
        if (t != PARENT_WAITING_TID) {
396
0
            return t;
397
0
        }
398
        // copied from boost::detail::yield
399
0
        int k = loop_count++;
400
0
        if (k < 32 || k & 1) {
401
0
            sched_yield();
402
0
        } else {
403
            // g++ -Wextra warns on {} or {0}
404
0
            struct timespec rqtp = {0, 0};
405
406
            // POSIX says that timespec has tv_sec and tv_nsec
407
            // But it doesn't guarantee order or placement
408
409
0
            rqtp.tv_sec = 0;
410
0
            rqtp.tv_nsec = 1000;
411
412
0
            nanosleep(&rqtp, 0);
413
0
        }
414
0
    }
415
0
}
416
417
Status Thread::start_thread(const std::string& category, const std::string& name,
418
                            const ThreadFunctor& functor, uint64_t flags,
419
2.69k
                            scoped_refptr<Thread>* holder) {
420
2.69k
    std::call_once(once, init_threadmgr);
421
422
    // Temporary reference for the duration of this function.
423
2.69k
    scoped_refptr<Thread> t(new Thread(category, name, functor));
424
425
    // Optional, and only set if the thread was successfully created.
426
    //
427
    // We have to set this before we even start the thread because it's
428
    // allowed for the thread functor to access 'holder'.
429
2.69k
    if (holder) {
430
1.08k
        *holder = t;
431
1.08k
    }
432
433
2.69k
    t->_tid = PARENT_WAITING_TID;
434
435
    // Add a reference count to the thread since SuperviseThread() needs to
436
    // access the thread object, and we have no guarantee that our caller
437
    // won't drop the reference as soon as we return. This is dereferenced
438
    // in FinishThread().
439
2.69k
    t->AddRef();
440
441
2.69k
    auto cleanup = MakeScopedCleanup([&]() {
442
        // If we failed to create the thread, we need to undo all of our prep work.
443
0
        t->_tid = INVALID_TID;
444
0
        t->Release();
445
0
    });
446
447
2.69k
    int ret = pthread_create(&t->_thread, nullptr, &Thread::supervise_thread, t.get());
448
2.69k
    if (ret) {
449
0
        return Status::RuntimeError("Could not create thread. (error {}) {}", ret, strerror(ret));
450
0
    }
451
452
    // The thread has been created and is now joinable.
453
    //
454
    // Why set this in the parent and not the child? Because only the parent
455
    // (or someone communicating with the parent) can join, so joinable must
456
    // be set before the parent returns.
457
2.69k
    t->_joinable = true;
458
2.69k
    cleanup.cancel();
459
460
2.69k
    VLOG_NOTICE << "Started thread " << t->tid() << " - " << category << ":" << name;
461
2.69k
    return Status::OK();
462
2.69k
}
463
464
2.69k
void* Thread::supervise_thread(void* arg) {
465
2.69k
    Thread* t = static_cast<Thread*>(arg);
466
2.69k
    int64_t system_tid = Thread::current_thread_id();
467
2.69k
    PCHECK(system_tid != -1);
468
469
    // Take an additional reference to the thread manager, which we'll need below.
470
2.69k
    ANNOTATE_IGNORE_SYNC_BEGIN();
471
2.69k
    std::shared_ptr<ThreadMgr> thread_mgr_ref = thread_manager;
472
2.69k
    ANNOTATE_IGNORE_SYNC_END();
473
474
    // Set up the TLS.
475
    //
476
    // We could store a scoped_refptr in the TLS itself, but as its
477
    // lifecycle is poorly defined, we'll use a bare pointer. We
478
    // already incremented the reference count in StartThread.
479
2.69k
    Thread::_tls = t;
480
481
    // Create thread context, there is no need to create it when func is executed.
482
2.69k
    ThreadLocalHandle::create_thread_local_if_not_exits();
483
484
    // Publish our tid to '_tid', which unblocks any callers waiting in
485
    // WaitForTid().
486
2.69k
    Release_Store(&t->_tid, system_tid);
487
488
2.69k
    std::string name = strings::Substitute("$0-$1", t->name(), system_tid);
489
2.69k
    thread_manager->set_thread_name(name, t->_tid);
490
2.69k
    thread_manager->add_thread(pthread_self(), name, t->category(), t->_tid);
491
492
    // FinishThread() is guaranteed to run (even if functor_ throws an
493
    // exception) because pthread_cleanup_push() creates a scoped object
494
    // whose destructor invokes the provided callback.
495
2.69k
    pthread_cleanup_push(&Thread::finish_thread, t);
496
2.69k
    t->_functor();
497
2.69k
    pthread_cleanup_pop(true);
498
499
2.69k
    return nullptr;
500
2.69k
}
501
502
2.66k
void Thread::finish_thread(void* arg) {
503
2.66k
    Thread* t = static_cast<Thread*>(arg);
504
505
    // We're here either because of the explicit pthread_cleanup_pop() in
506
    // SuperviseThread() or through pthread_exit(). In either case,
507
    // thread_manager is guaranteed to be live because thread_mgr_ref in
508
    // SuperviseThread() is still live.
509
2.66k
    thread_manager->remove_thread(pthread_self(), t->category());
510
511
    // Signal any Joiner that we're done.
512
2.66k
    t->_done.count_down();
513
514
2.66k
    VLOG_CRITICAL << "Ended thread " << t->_tid << " - " << t->category() << ":" << t->name();
515
2.66k
    t->Release();
516
    // NOTE: the above 'Release' call could be the last reference to 'this',
517
    // so 'this' could be destructed at this point. Do not add any code
518
    // following here!
519
520
2.66k
    ThreadLocalHandle::del_thread_local_if_count_is_zero();
521
2.66k
}
522
523
1
void Thread::init_threadmgr() {
524
1
    thread_manager.reset(new ThreadMgr());
525
1
}
526
527
ThreadJoiner::ThreadJoiner(Thread* thr)
528
        : _thread(CHECK_NOTNULL(thr)),
529
          _warn_after_ms(kDefaultWarnAfterMs),
530
          _warn_every_ms(kDefaultWarnEveryMs),
531
1.07k
          _give_up_after_ms(kDefaultGiveUpAfterMs) {}
532
533
1
ThreadJoiner& ThreadJoiner::warn_after_ms(int ms) {
534
1
    _warn_after_ms = ms;
535
1
    return *this;
536
1
}
537
538
1
ThreadJoiner& ThreadJoiner::warn_every_ms(int ms) {
539
1
    _warn_every_ms = ms;
540
1
    return *this;
541
1
}
542
543
1
ThreadJoiner& ThreadJoiner::give_up_after_ms(int ms) {
544
1
    _give_up_after_ms = ms;
545
1
    return *this;
546
1
}
547
548
1.07k
Status ThreadJoiner::join() {
549
1.07k
    if (Thread::current_thread() && Thread::current_thread()->tid() == _thread->tid()) {
550
1
        return Status::InvalidArgument("Can't join on own thread. (error {}) {}", -1,
551
1
                                       _thread->_name);
552
1
    }
553
554
    // Early exit: double join is a no-op.
555
1.07k
    if (!_thread->_joinable) {
556
1
        return Status::OK();
557
1
    }
558
559
1.07k
    int waited_ms = 0;
560
1.07k
    bool keep_trying = true;
561
1.08k
    while (keep_trying) {
562
1.08k
        if (waited_ms >= _warn_after_ms) {
563
10
            LOG(WARNING) << strings::Substitute("Waited for $0ms trying to join with $1 (tid $2)",
564
10
                                                waited_ms, _thread->_name, _thread->_tid);
565
10
        }
566
567
1.08k
        int remaining_before_giveup = std::numeric_limits<int>::max();
568
1.08k
        if (_give_up_after_ms != -1) {
569
1
            remaining_before_giveup = _give_up_after_ms - waited_ms;
570
1
        }
571
572
1.08k
        int remaining_before_next_warn = _warn_every_ms;
573
1.08k
        if (waited_ms < _warn_after_ms) {
574
1.07k
            remaining_before_next_warn = _warn_after_ms - waited_ms;
575
1.07k
        }
576
577
1.08k
        if (remaining_before_giveup < remaining_before_next_warn) {
578
1
            keep_trying = false;
579
1
        }
580
581
1.08k
        int wait_for = std::min(remaining_before_giveup, remaining_before_next_warn);
582
583
1.08k
        if (_thread->_done.wait_for(std::chrono::milliseconds(wait_for))) {
584
            // Unconditionally join before returning, to guarantee that any TLS
585
            // has been destroyed (pthread_key_create() destructors only run
586
            // after a pthread's user method has returned).
587
1.07k
            int ret = pthread_join(_thread->_thread, nullptr);
588
1.07k
            CHECK_EQ(ret, 0);
589
1.07k
            _thread->_joinable = false;
590
1.07k
            return Status::OK();
591
1.07k
        }
592
11
        waited_ms += wait_for;
593
11
    }
594
1
    return Status::Aborted("Timed out after {}ms joining on {}", waited_ms, _thread->_name);
595
1.07k
}
596
597
1
void register_thread_display_page(WebPageHandler* web_page_handler) {
598
1
    web_page_handler->register_template_page(
599
1
            "/threadz", "Threads",
600
1
            std::bind(&ThreadMgr::display_thread_callback, thread_manager.get(),
601
1
                      std::placeholders::_1, std::placeholders::_2),
602
1
            true);
603
1
}
604
} // namespace doris