| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| #include <gtest/gtest.h> |
| #include <gflags/gflags.h> |
| #include <atomic> |
| #include <vector> |
| #include <set> |
| #include <mutex> |
| #include <sched.h> |
| #include "bthread/bthread.h" |
| #include "bthread/task_group.h" |
| |
| namespace { |
| |
| // Counter incremented by priority bthreads to verify execution |
| std::atomic<int> g_priority_count(0); |
| // Mutex + set for collecting executed tids to verify no loss |
| std::mutex g_tid_mutex; |
| std::set<int> g_executed_ids; |
| |
| void reset_globals() { |
| g_priority_count.store(0); |
| std::lock_guard<std::mutex> lk(g_tid_mutex); |
| g_executed_ids.clear(); |
| } |
| |
| struct TaskArg { |
| int id; |
| }; |
| |
| void* priority_task_fn(void* arg) { |
| TaskArg* ta = static_cast<TaskArg*>(arg); |
| g_priority_count.fetch_add(1, std::memory_order_relaxed); |
| { |
| std::lock_guard<std::mutex> lk(g_tid_mutex); |
| g_executed_ids.insert(ta->id); |
| } |
| delete ta; |
| return NULL; |
| } |
| |
| void* normal_task_fn(void* /*arg*/) { |
| // Just a normal task that does nothing, used as a filler |
| bthread_usleep(1000); |
| return NULL; |
| } |
| |
| class PriorityQueueTest : public ::testing::Test { |
| protected: |
| static void SetUpTestSuite() { |
| google::SetCommandLineOption("enable_bthread_priority_queue", "true"); |
| google::SetCommandLineOption("event_dispatcher_num", "4"); |
| } |
| void SetUp() override { |
| reset_globals(); |
| } |
| }; |
| |
| // Test 1: End-to-end priority task submission and execution. |
| // Multiple producers submit priority tasks, verify all tasks are executed. |
| TEST_F(PriorityQueueTest, e2e_priority_tasks_all_executed) { |
| const int N = 200; |
| |
| bthread_attr_t attr = BTHREAD_ATTR_NORMAL; |
| attr.flags |= BTHREAD_GLOBAL_PRIORITY; |
| |
| std::vector<bthread_t> tids(N); |
| for (int i = 0; i < N; ++i) { |
| TaskArg* arg = new TaskArg{i}; |
| ASSERT_EQ(0, bthread_start_background(&tids[i], &attr, |
| priority_task_fn, arg)); |
| } |
| |
| for (int i = 0; i < N; ++i) { |
| bthread_join(tids[i], NULL); |
| } |
| |
| ASSERT_EQ(N, g_priority_count.load()); |
| std::lock_guard<std::mutex> lk(g_tid_mutex); |
| ASSERT_EQ((size_t)N, g_executed_ids.size()); |
| for (int i = 0; i < N; ++i) { |
| ASSERT_TRUE(g_executed_ids.count(i)) << "Missing task id=" << i; |
| } |
| } |
| |
| // Test 2: Mix of priority and normal tasks, all complete correctly. |
| TEST_F(PriorityQueueTest, mixed_priority_and_normal_tasks) { |
| const int N_PRIORITY = 100; |
| const int N_NORMAL = 100; |
| |
| bthread_attr_t priority_attr = BTHREAD_ATTR_NORMAL; |
| priority_attr.flags |= BTHREAD_GLOBAL_PRIORITY; |
| |
| std::vector<bthread_t> tids; |
| tids.reserve(N_PRIORITY + N_NORMAL); |
| |
| for (int i = 0; i < N_PRIORITY + N_NORMAL; ++i) { |
| bthread_t tid; |
| if (i % 2 == 0 && (i / 2) < N_PRIORITY) { |
| TaskArg* arg = new TaskArg{i / 2}; |
| ASSERT_EQ(0, bthread_start_background(&tid, &priority_attr, |
| priority_task_fn, arg)); |
| } else { |
| ASSERT_EQ(0, bthread_start_background(&tid, NULL, |
| normal_task_fn, NULL)); |
| } |
| tids.push_back(tid); |
| } |
| |
| for (auto tid : tids) { |
| bthread_join(tid, NULL); |
| } |
| |
| ASSERT_EQ(N_PRIORITY, g_priority_count.load()); |
| } |
| |
| // Test 3: start_foreground (bthread_start_urgent) with GLOBAL_PRIORITY. |
| // Simulates ED calling StartInputEvent: ED bthread calls start_urgent, |
| // gets preempted into PQ via priority_to_run, child runs and ends, |
| // ending_sched steals ED from PQ to resume. |
| TEST_F(PriorityQueueTest, start_foreground_priority_to_run) { |
| const int N = 200; |
| |
| struct EDSimArg { |
| int n_tasks; |
| }; |
| EDSimArg ed_arg{N}; |
| |
| auto ed_fn = [](void* arg) -> void* { |
| EDSimArg* ea = static_cast<EDSimArg*>(arg); |
| bthread::TaskMeta* meta = |
| bthread::TaskGroup::address_meta(bthread_self()); |
| meta->priority_index = 0; |
| |
| for (int i = 0; i < ea->n_tasks; ++i) { |
| TaskArg* ta = new TaskArg{i}; |
| bthread_t child; |
| bthread_start_urgent(&child, NULL, priority_task_fn, ta); |
| } |
| return NULL; |
| }; |
| |
| bthread_attr_t priority_attr = BTHREAD_ATTR_NORMAL; |
| priority_attr.flags |= BTHREAD_GLOBAL_PRIORITY; |
| |
| bthread_t ed_tid; |
| ASSERT_EQ(0, bthread_start_background(&ed_tid, &priority_attr, |
| ed_fn, &ed_arg)); |
| bthread_join(ed_tid, NULL); |
| |
| ASSERT_EQ(N, g_priority_count.load()); |
| std::lock_guard<std::mutex> lk(g_tid_mutex); |
| ASSERT_EQ((size_t)N, g_executed_ids.size()); |
| } |
| |
| // Test 4: Multiple ED-like bthreads concurrently calling start_urgent. |
| // Verifies PQ correctness under concurrent preemption from multiple EDs. |
| TEST_F(PriorityQueueTest, multiple_eds_concurrent_preempt) { |
| const int NUM_EDS = 4; |
| const int TASKS_PER_ED = 50; |
| const int TOTAL = NUM_EDS * TASKS_PER_ED; |
| std::atomic<int> resume_count(0); |
| |
| struct EDArg { |
| int ed_index; |
| int n_children; |
| std::atomic<int>* resume_count; |
| }; |
| |
| auto ed_fn = [](void* arg) -> void* { |
| EDArg* ea = static_cast<EDArg*>(arg); |
| bthread::TaskMeta* meta = |
| bthread::TaskGroup::address_meta(bthread_self()); |
| meta->priority_index = ea->ed_index; |
| |
| std::vector<bthread_t> children; |
| children.reserve(ea->n_children); |
| for (int i = 0; i < ea->n_children; ++i) { |
| int id = ea->ed_index * ea->n_children + i; |
| TaskArg* ta = new TaskArg{id}; |
| bthread_t child; |
| bthread_start_urgent(&child, NULL, priority_task_fn, ta); |
| children.push_back(child); |
| ea->resume_count->fetch_add(1, std::memory_order_relaxed); |
| } |
| for (auto c : children) { |
| bthread_join(c, NULL); |
| } |
| return NULL; |
| }; |
| |
| bthread_attr_t priority_attr = BTHREAD_ATTR_NORMAL; |
| priority_attr.flags |= BTHREAD_GLOBAL_PRIORITY; |
| |
| std::vector<EDArg> ed_args(NUM_EDS); |
| std::vector<bthread_t> ed_tids(NUM_EDS); |
| for (int i = 0; i < NUM_EDS; ++i) { |
| ed_args[i] = {i, TASKS_PER_ED, &resume_count}; |
| ASSERT_EQ(0, bthread_start_background(&ed_tids[i], &priority_attr, |
| ed_fn, &ed_args[i])); |
| } |
| |
| for (int i = 0; i < NUM_EDS; ++i) { |
| bthread_join(ed_tids[i], NULL); |
| } |
| |
| ASSERT_EQ(TOTAL, g_priority_count.load()); |
| ASSERT_EQ(TOTAL, resume_count.load()); |
| std::lock_guard<std::mutex> lk(g_tid_mutex); |
| ASSERT_EQ((size_t)TOTAL, g_executed_ids.size()); |
| } |
| |
| } // namespace |