Coverage Report

Created: 2025-04-26 12:00

/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 "absl/strings/substitute.h"
55
#include "common/config.h"
56
#include "common/logging.h"
57
#include "gutil/atomicops.h"
58
#include "gutil/dynamic_annotations.h"
59
#include "gutil/stringprintf.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
3
    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
4.94k
        ThreadDescriptor() {}
120
        ThreadDescriptor(std::string category, std::string name, int64_t thread_id)
121
4.94k
                : _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
6.70k
void ThreadMgr::set_thread_name(const std::string& name, int64_t tid) {
158
6.70k
    if (tid == getpid()) {
159
0
        return;
160
0
    }
161
#ifdef __APPLE__
162
    int err = pthread_setname_np(name.c_str());
163
#else
164
6.70k
    int err = prctl(PR_SET_NAME, name.c_str());
165
6.70k
#endif
166
6.70k
    if (err < 0 && errno != EPERM) {
167
0
        LOG(ERROR) << "set_thread_name";
168
0
    }
169
6.70k
}
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
4.94k
                           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
4.94k
    ANNOTATE_IGNORE_SYNC_BEGIN();
219
4.94k
    debug::ScopedTSANIgnoreReadsAndWrites ignore_tsan;
220
4.94k
    {
221
4.94k
        std::unique_lock<std::mutex> l(_lock);
222
4.94k
        _thread_categories[category][pthread_id] = ThreadDescriptor(category, name, tid);
223
4.94k
        _threads_running_metric++;
224
4.94k
        _threads_started_metric++;
225
4.94k
    }
226
4.94k
    ANNOTATE_IGNORE_SYNC_END();
227
4.94k
}
228
229
3.82k
void ThreadMgr::remove_thread(const pthread_t& pthread_id, const std::string& category) {
230
3.82k
    ANNOTATE_IGNORE_SYNC_BEGIN();
231
3.82k
    debug::ScopedTSANIgnoreReadsAndWrites ignore_tsan;
232
3.82k
    {
233
3.82k
        std::unique_lock<std::mutex> l(_lock);
234
3.82k
        auto category_it = _thread_categories.find(category);
235
3.82k
        DCHECK(category_it != _thread_categories.end());
236
3.82k
        category_it->second.erase(pthread_id);
237
3.82k
        _threads_running_metric--;
238
3.82k
    }
239
3.82k
    ANNOTATE_IGNORE_SYNC_END();
240
3.82k
}
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
3.82k
Thread::~Thread() {
318
3.82k
    if (_joinable) {
319
2.70k
        int ret = pthread_detach(_thread);
320
2.70k
        CHECK_EQ(ret, 0);
321
2.70k
    }
322
3.82k
}
323
324
1.77k
void Thread::set_self_name(const std::string& name) {
325
1.77k
    ThreadMgr::set_thread_name(name, current_thread_id());
326
1.77k
}
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.11k
void Thread::join() {
339
1.11k
    static_cast<void>(ThreadJoiner(this).join());
340
1.11k
}
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
4.94k
const std::string& Thread::name() const {
355
4.94k
    return _name;
356
4.94k
}
357
358
8.76k
const std::string& Thread::category() const {
359
8.76k
    return _category;
360
8.76k
}
361
362
0
std::string Thread::to_string() const {
363
0
    return absl::Substitute("Thread $0 (name: \"$1\", category: \"$2\")", tid(), _name, _category);
364
0
}
365
366
13.0k
Thread* Thread::current_thread() {
367
13.0k
    return _tls;
368
13.0k
}
369
370
0
int64_t Thread::unique_thread_id() {
371
#ifdef __APPLE__
372
    uint64_t tid;
373
    pthread_threadid_np(pthread_self(), &tid);
374
    return tid;
375
#else
376
0
    return static_cast<int64_t>(pthread_self());
377
0
#endif
378
0
}
379
380
6.70k
int64_t Thread::current_thread_id() {
381
#ifdef __APPLE__
382
    uint64_t tid;
383
    pthread_threadid_np(nullptr, &tid);
384
    return tid;
385
#else
386
6.70k
    return syscall(SYS_gettid);
387
6.70k
#endif
388
6.70k
}
389
390
0
int64_t Thread::wait_for_tid() const {
391
0
    int loop_count = 0;
392
0
    while (true) {
393
0
        int64_t t = Acquire_Load(&_tid);
394
0
        if (t != PARENT_WAITING_TID) {
395
0
            return t;
396
0
        }
397
        // copied from boost::detail::yield
398
0
        int k = loop_count++;
399
0
        if (k < 32 || k & 1) {
400
0
            sched_yield();
401
0
        } else {
402
            // g++ -Wextra warns on {} or {0}
403
0
            struct timespec rqtp = {0, 0};
404
405
            // POSIX says that timespec has tv_sec and tv_nsec
406
            // But it doesn't guarantee order or placement
407
408
0
            rqtp.tv_sec = 0;
409
0
            rqtp.tv_nsec = 1000;
410
411
0
            nanosleep(&rqtp, 0);
412
0
        }
413
0
    }
414
0
}
415
416
Status Thread::start_thread(const std::string& category, const std::string& name,
417
                            const ThreadFunctor& functor, uint64_t flags,
418
4.94k
                            scoped_refptr<Thread>* holder) {
419
4.94k
    std::call_once(once, init_threadmgr);
420
421
    // Temporary reference for the duration of this function.
422
4.94k
    scoped_refptr<Thread> t(new Thread(category, name, functor));
423
424
    // Optional, and only set if the thread was successfully created.
425
    //
426
    // We have to set this before we even start the thread because it's
427
    // allowed for the thread functor to access 'holder'.
428
4.94k
    if (holder) {
429
1.18k
        *holder = t;
430
1.18k
    }
431
432
4.94k
    t->_tid = PARENT_WAITING_TID;
433
434
    // Add a reference count to the thread since SuperviseThread() needs to
435
    // access the thread object, and we have no guarantee that our caller
436
    // won't drop the reference as soon as we return. This is dereferenced
437
    // in FinishThread().
438
4.94k
    t->AddRef();
439
440
4.94k
    auto cleanup = MakeScopedCleanup([&]() {
441
        // If we failed to create the thread, we need to undo all of our prep work.
442
0
        t->_tid = INVALID_TID;
443
0
        t->Release();
444
0
    });
445
446
4.94k
    int ret = pthread_create(&t->_thread, nullptr, &Thread::supervise_thread, t.get());
447
4.94k
    if (ret) {
448
0
        return Status::RuntimeError("Could not create thread. (error {}) {}", ret, strerror(ret));
449
0
    }
450
451
    // The thread has been created and is now joinable.
452
    //
453
    // Why set this in the parent and not the child? Because only the parent
454
    // (or someone communicating with the parent) can join, so joinable must
455
    // be set before the parent returns.
456
4.94k
    t->_joinable = true;
457
4.94k
    cleanup.cancel();
458
459
4.94k
    VLOG_NOTICE << "Started thread " << t->tid() << " - " << category << ":" << name;
460
4.94k
    return Status::OK();
461
4.94k
}
462
463
4.93k
void* Thread::supervise_thread(void* arg) {
464
4.93k
    Thread* t = static_cast<Thread*>(arg);
465
4.93k
    int64_t system_tid = Thread::current_thread_id();
466
4.93k
    PCHECK(system_tid != -1);
467
468
    // Take an additional reference to the thread manager, which we'll need below.
469
4.93k
    ANNOTATE_IGNORE_SYNC_BEGIN();
470
4.93k
    std::shared_ptr<ThreadMgr> thread_mgr_ref = thread_manager;
471
4.93k
    ANNOTATE_IGNORE_SYNC_END();
472
473
    // Set up the TLS.
474
    //
475
    // We could store a scoped_refptr in the TLS itself, but as its
476
    // lifecycle is poorly defined, we'll use a bare pointer. We
477
    // already incremented the reference count in StartThread.
478
4.93k
    Thread::_tls = t;
479
480
    // Create thread context, there is no need to create it when func is executed.
481
4.93k
    ThreadLocalHandle::create_thread_local_if_not_exits();
482
483
    // Publish our tid to '_tid', which unblocks any callers waiting in
484
    // WaitForTid().
485
4.93k
    Release_Store(&t->_tid, system_tid);
486
487
4.93k
    std::string name = absl::Substitute("$0-$1", t->name(), system_tid);
488
4.93k
    thread_manager->set_thread_name(name, t->_tid);
489
4.93k
    thread_manager->add_thread(pthread_self(), name, t->category(), t->_tid);
490
491
    // FinishThread() is guaranteed to run (even if functor_ throws an
492
    // exception) because pthread_cleanup_push() creates a scoped object
493
    // whose destructor invokes the provided callback.
494
4.93k
    pthread_cleanup_push(&Thread::finish_thread, t);
495
4.93k
    t->_functor();
496
4.93k
    pthread_cleanup_pop(true);
497
498
4.93k
    return nullptr;
499
4.93k
}
500
501
3.82k
void Thread::finish_thread(void* arg) {
502
3.82k
    Thread* t = static_cast<Thread*>(arg);
503
504
    // We're here either because of the explicit pthread_cleanup_pop() in
505
    // SuperviseThread() or through pthread_exit(). In either case,
506
    // thread_manager is guaranteed to be live because thread_mgr_ref in
507
    // SuperviseThread() is still live.
508
3.82k
    thread_manager->remove_thread(pthread_self(), t->category());
509
510
    // Signal any Joiner that we're done.
511
3.82k
    t->_done.count_down();
512
513
3.82k
    VLOG_CRITICAL << "Ended thread " << t->_tid << " - " << t->category() << ":" << t->name();
514
3.82k
    t->Release();
515
    // NOTE: the above 'Release' call could be the last reference to 'this',
516
    // so 'this' could be destructed at this point. Do not add any code
517
    // following here!
518
519
3.82k
    ThreadLocalHandle::del_thread_local_if_count_is_zero();
520
3.82k
}
521
522
3
void Thread::init_threadmgr() {
523
3
    thread_manager.reset(new ThreadMgr());
524
3
}
525
526
ThreadJoiner::ThreadJoiner(Thread* thr)
527
        : _thread(CHECK_NOTNULL(thr)),
528
          _warn_after_ms(kDefaultWarnAfterMs),
529
          _warn_every_ms(kDefaultWarnEveryMs),
530
1.12k
          _give_up_after_ms(kDefaultGiveUpAfterMs) {}
531
532
1
ThreadJoiner& ThreadJoiner::warn_after_ms(int ms) {
533
1
    _warn_after_ms = ms;
534
1
    return *this;
535
1
}
536
537
1
ThreadJoiner& ThreadJoiner::warn_every_ms(int ms) {
538
1
    _warn_every_ms = ms;
539
1
    return *this;
540
1
}
541
542
1
ThreadJoiner& ThreadJoiner::give_up_after_ms(int ms) {
543
1
    _give_up_after_ms = ms;
544
1
    return *this;
545
1
}
546
547
1.12k
Status ThreadJoiner::join() {
548
1.12k
    if (Thread::current_thread() && Thread::current_thread()->tid() == _thread->tid()) {
549
1
        return Status::InvalidArgument("Can't join on own thread. (error {}) {}", -1,
550
1
                                       _thread->_name);
551
1
    }
552
553
    // Early exit: double join is a no-op.
554
1.12k
    if (!_thread->_joinable) {
555
1
        return Status::OK();
556
1
    }
557
558
1.12k
    int waited_ms = 0;
559
1.12k
    bool keep_trying = true;
560
1.13k
    while (keep_trying) {
561
1.13k
        if (waited_ms >= _warn_after_ms) {
562
10
            LOG(WARNING) << absl::Substitute("Waited for $0ms trying to join with $1 (tid $2)",
563
10
                                             waited_ms, _thread->_name, _thread->_tid);
564
10
        }
565
566
1.13k
        int remaining_before_giveup = std::numeric_limits<int>::max();
567
1.13k
        if (_give_up_after_ms != -1) {
568
1
            remaining_before_giveup = _give_up_after_ms - waited_ms;
569
1
        }
570
571
1.13k
        int remaining_before_next_warn = _warn_every_ms;
572
1.13k
        if (waited_ms < _warn_after_ms) {
573
1.12k
            remaining_before_next_warn = _warn_after_ms - waited_ms;
574
1.12k
        }
575
576
1.13k
        if (remaining_before_giveup < remaining_before_next_warn) {
577
1
            keep_trying = false;
578
1
        }
579
580
1.13k
        int wait_for = std::min(remaining_before_giveup, remaining_before_next_warn);
581
582
1.13k
        if (_thread->_done.wait_for(std::chrono::milliseconds(wait_for))) {
583
            // Unconditionally join before returning, to guarantee that any TLS
584
            // has been destroyed (pthread_key_create() destructors only run
585
            // after a pthread's user method has returned).
586
1.12k
            int ret = pthread_join(_thread->_thread, nullptr);
587
1.12k
            CHECK_EQ(ret, 0);
588
1.12k
            _thread->_joinable = false;
589
1.12k
            return Status::OK();
590
1.12k
        }
591
11
        waited_ms += wait_for;
592
11
    }
593
1
    return Status::Aborted("Timed out after {}ms joining on {}", waited_ms, _thread->_name);
594
1.12k
}
595
596
3
void register_thread_display_page(WebPageHandler* web_page_handler) {
597
3
    web_page_handler->register_template_page(
598
3
            "/threadz", "Threads",
599
3
            std::bind(&ThreadMgr::display_thread_callback, thread_manager.get(),
600
3
                      std::placeholders::_1, std::placeholders::_2),
601
3
            true);
602
3
}
603
} // namespace doris