// 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 #include #include #include #include #include #include #include "bthread/bthread.h" #include "bthread/task_group.h" namespace { // Counter incremented by priority bthreads to verify execution std::atomic g_priority_count(0); // Mutex + set for collecting executed tids to verify no loss std::mutex g_tid_mutex; std::set g_executed_ids; void reset_globals() { g_priority_count.store(0); std::lock_guard lk(g_tid_mutex); g_executed_ids.clear(); } struct TaskArg { int id; }; void* priority_task_fn(void* arg) { TaskArg* ta = static_cast(arg); g_priority_count.fetch_add(1, std::memory_order_relaxed); { std::lock_guard 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 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 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 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(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 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 resume_count(0); struct EDArg { int ed_index; int n_children; std::atomic* resume_count; }; auto ed_fn = [](void* arg) -> void* { EDArg* ea = static_cast(arg); bthread::TaskMeta* meta = bthread::TaskGroup::address_meta(bthread_self()); meta->priority_index = ea->ed_index; std::vector 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 ed_args(NUM_EDS); std::vector 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 lk(g_tid_mutex); ASSERT_EQ((size_t)TOTAL, g_executed_ids.size()); } } // namespace