blob: 4481807a92738afd32171c1fe9a948dfcaa17f6e [file]
// Copyright 2026 The Pigweed Authors
//
// Licensed 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
//
// https://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.
// This is a stress test for pw_async2 dispatchers and tasks. Do NOT use this as
// an example for how to use pw_async2! Instead, visit
// https://pigweed.dev/pw_async2/ for pw_async2 examples and a codelab.
//
// This test focuses on the core pw_async2 dispatcher implementation:
// dispatcher, tasks and wakers. It uses NotifiedDispatcher.
//
// The test consists of N threads and one driver thread. Each of the N threads
// runs a dispatcher. The driver thread generates events that:
//
// - wake a task on one of the dispatchers, which in turn wakes other tasks,
// - allocate and post a task to a dispatcher,
// - deregister and free a task, or
// - destroy and recreate a thread's dispatcher.
#define PW_LOG_MODULE_NAME "pw_async2 test"
#define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
#include <atomic>
#include <mutex>
#include <random>
#include "pw_allocator/libc_allocator.h"
#include "pw_assert/check.h"
#include "pw_async2/notified_dispatcher.h"
#include "pw_async2/task.h"
#include "pw_containers/dynamic_queue.h"
#include "pw_containers/dynamic_vector.h"
#include "pw_log/log.h"
#include "pw_sync/lock_annotations.h"
#include "pw_sync/mutex.h"
#include "pw_sync/thread_notification.h"
#include "pw_thread/test_thread_context.h"
#include "pw_thread/thread.h"
#include "pw_thread/yield.h"
#include "pw_unit_test/framework.h"
namespace {
constexpr bool kEnableVerboseLogging = false;
constexpr size_t kNumThreads = 4;
constexpr size_t kIterations = 20'000;
constexpr int kRandomSeed = 42;
class WakerTask;
class TestState;
enum class Event {
kWakeRandomTask,
kPostNewTask,
kDeregisterRandomTask,
kDestroyRandomDispatcher,
kStopTest,
};
constexpr const char* EventToString(Event event) {
switch (event) {
case Event::kWakeRandomTask:
return "kWakeRandomTask";
case Event::kPostNewTask:
return "kPostNewTask";
case Event::kDeregisterRandomTask:
return "kDeregisterRandomTask";
case Event::kDestroyRandomDispatcher:
return "kDestroyRandomDispatcher";
case Event::kStopTest:
return "kStopTest";
}
}
// A task that runs a `Dispatcher` on a dedicated thread and executes queued
// `Event`s.
class EventHandler : public pw::async2::Task {
public:
// Constructs an event handler with the given ID.
EventHandler(TestState& test_state, size_t id)
: pw::async2::Task(PW_ASYNC_TASK_NAME("EventHandler")),
test_state_(test_state),
events_(allocator_),
id_(id) {}
// Starts the thread that runs the dispatcher.
void Start() {
thread_ = pw::Thread(context_.options(), [this] { Run(); });
}
// Queues an event to be executed by this handler and wakes it.
void HandleEvent(Event type);
// Notifies the thread to stop and waits for it to finish.
void Halt();
private:
void Run();
// Pulls an event from this handler's event queue and executes it.
pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
TestState& test_state_;
pw::thread::test::TestThreadContext context_;
pw::Thread thread_;
pw::sync::ThreadNotification notification_;
std::optional<pw::async2::NotifiedDispatcher> dispatcher_;
pw::allocator::LibCAllocator allocator_;
pw::sync::Mutex mutex_;
pw::DynamicQueue<Event> events_ PW_GUARDED_BY(mutex_);
const size_t id_;
pw::async2::Waker waker_;
bool terminate_ = false;
};
// Manages the shared state of the dispatcher stress test.
class TestState {
public:
// Initializes the test state, including the event handlers.
TestState();
void StartTest() {
for (EventHandler& handler : handlers_) {
handler.Start();
}
}
// Signals all threads to stop and waits for them to terminate.
void HaltTest() PW_LOCKS_EXCLUDED(mutex_);
// Queues an event for a specific handler.
void HandleEvent(Event type, size_t handler_id) PW_LOCKS_EXCLUDED(mutex_);
void RecordEventHandled() {
events_handled_.fetch_add(1, std::memory_order_relaxed);
}
unsigned EventsHandled() const {
return events_handled_.load(std::memory_order_relaxed);
}
// Cleans up memory for waker tasks that have completed.
void GarbageCollectTasks() PW_LOCKS_EXCLUDED(mutex_);
// Called from any thread to randomly wake a pending `WakerTask`.
void WakeRandomTask() PW_LOCKS_EXCLUDED(mutex_);
// Used by an EventHandler to register a newly allocated `WakerTask`.
// The dispatcher remains valid for this call since the EventHandler controls
// its lifetime.
void RegisterWakerTask(const pw::SharedPtr<WakerTask>& task)
PW_LOCKS_EXCLUDED(mutex_);
// Randomly selects a registered `WakerTask` and deregisters it.
void DeregisterRandomWakerTask(pw::async2::Task& skip_this_task)
PW_LOCKS_EXCLUDED(mutex_);
private:
std::array<EventHandler, kNumThreads> handlers_;
pw::allocator::LibCAllocator allocator_;
pw::sync::Mutex mutex_;
std::mt19937 rng_ PW_GUARDED_BY(mutex_);
pw::DynamicVector<pw::SharedPtr<WakerTask>, size_t> all_waker_tasks_
PW_GUARDED_BY(mutex_);
std::atomic<unsigned> events_handled_ = 0;
};
// A simple task that can be awoken, which in turn wakes other random tasks.
class WakerTask : public pw::async2::Task {
public:
explicit WakerTask(TestState& test_state)
: pw::async2::Task(PW_ASYNC_TASK_NAME("WakerTask")),
test_state_(test_state) {}
// Advances the task, randomly waking another task and yielding.
pw::async2::Poll<> DoPend(pw::async2::Context& cx) override {
test_state_.WakeRandomTask();
PW_ASYNC_STORE_WAKER(cx, waker_, "WakerTask");
return pw::async2::Pending();
}
// Awakes this task so it will be polled again.
void Wake() { waker_.Wake(); }
private:
TestState& test_state_;
pw::async2::Waker waker_;
};
void EventHandler::HandleEvent(Event type) {
{
std::lock_guard lock(mutex_);
PW_CHECK(events_.try_push(type));
}
waker_.Wake();
}
void EventHandler::Halt() {
notification_.release();
thread_.join();
}
void EventHandler::Run() {
while (true) {
// Run() and DoPend() run on the same thread, so no locking is needed to
// set dispatcher_, and dispatcher_ will always be set when DoPend() is
// running.
dispatcher_.emplace(notification_);
dispatcher_->Post(*this);
// Destroy the dispatcher when this task returns Ready().
while (true) {
dispatcher_->RunUntilStalled();
if (terminate_) {
PW_LOG_DEBUG("Destroying dispatcher %zu", id_);
dispatcher_.reset();
return;
}
if (!IsRegistered()) {
// EventHandler returned Ready(), so destroy the dispatcher
PW_LOG_DEBUG("Destroying and reinstantiating dispatcher %zu", id_);
dispatcher_.emplace(notification_);
dispatcher_->Post(*this);
test_state_.GarbageCollectTasks();
continue;
}
// Block until the dispatcher is woken to avoid busy waiting
notification_.acquire();
}
}
}
pw::async2::Poll<> EventHandler::DoPend(pw::async2::Context& cx) {
Event event;
// Pop the next event from the queue.
{
std::lock_guard lock(mutex_);
if (events_.empty()) {
PW_ASYNC_STORE_WAKER(cx, waker_, "EventHandler");
return pw::async2::Pending();
};
event = events_.front();
events_.pop();
}
PW_LOG_DEBUG("Handler %zu/%zu executing event %s",
id_ + 1,
kNumThreads,
EventToString(event));
test_state_.RecordEventHandled();
// Handle the event.
switch (event) {
case Event::kPostNewTask:
PW_DCHECK(dispatcher_);
test_state_.RegisterWakerTask(
dispatcher_->Post<WakerTask>(allocator_, test_state_));
break;
case Event::kWakeRandomTask:
test_state_.WakeRandomTask();
break;
case Event::kDeregisterRandomTask:
test_state_.DeregisterRandomWakerTask(*this);
break;
case Event::kDestroyRandomDispatcher:
// The dispatcher is destroyed when the EventHandler is deregistered.
return pw::async2::Ready();
case Event::kStopTest:
terminate_ = true;
return pw::async2::Ready();
}
// Allow other tasks to run
cx.ReEnqueue();
return pw::async2::Pending();
}
// Initialize array of handlers with IDs 0-kHandlers-1 using
// std::index_sequence.
template <size_t... kIndices>
std::array<EventHandler, sizeof...(kIndices)> InitializeHandlers(
TestState& test_state, std::index_sequence<kIndices...>) {
return {EventHandler(test_state, kIndices)...};
}
template <size_t kHandlers = kNumThreads>
std::array<EventHandler, kNumThreads> InitializeHandlers(
TestState& test_state) {
return InitializeHandlers(test_state, std::make_index_sequence<kHandlers>{});
}
TestState::TestState()
: handlers_{InitializeHandlers(*this)},
rng_(kRandomSeed),
all_waker_tasks_(allocator_) {}
void TestState::HaltTest() {
for (size_t i = 0; i < kNumThreads; ++i) {
HandleEvent(Event::kStopTest, i);
}
for (auto& handler : handlers_) {
handler.Halt();
}
}
void TestState::HandleEvent(Event type, size_t handler_id) {
std::lock_guard lock(mutex_);
handlers_[handler_id].HandleEvent(type);
}
void TestState::GarbageCollectTasks() {
size_t i = 0;
while (true) {
pw::SharedPtr<WakerTask> task_to_delete;
{
std::lock_guard lock(mutex_);
if (i >= all_waker_tasks_.size()) {
break;
}
if (!all_waker_tasks_[i]->IsRegistered()) {
task_to_delete = std::move(all_waker_tasks_[i]);
std::swap(all_waker_tasks_[i], all_waker_tasks_.back());
all_waker_tasks_.pop_back();
} else {
i++;
}
}
}
}
void TestState::WakeRandomTask() {
pw::SharedPtr<WakerTask> task;
{ // Lock to get a task to wake.
std::lock_guard lock(mutex_);
if (all_waker_tasks_.empty()) {
return; // Ignore this event; there are no tasks to wake.
}
if (rng_() % 8 == 0) {
return; // Skip the wake
}
const size_t index = rng_() % all_waker_tasks_.size();
task = all_waker_tasks_[index];
if (kEnableVerboseLogging) {
PW_LOG_DEBUG(
"Waking task at index %zu/%zu", index + 1, all_waker_tasks_.size());
}
}
task->Wake();
}
void TestState::RegisterWakerTask(const pw::SharedPtr<WakerTask>& task) {
PW_DCHECK(task != nullptr);
std::lock_guard lock(mutex_);
all_waker_tasks_.push_back(task);
PW_LOG_DEBUG("Posted task %zu", all_waker_tasks_.size());
}
void TestState::DeregisterRandomWakerTask(pw::async2::Task& skip_this_task) {
pw::SharedPtr<WakerTask> task;
{
std::lock_guard lock(mutex_);
// If this is the only task, return.
if (all_waker_tasks_.size() <= 1u) {
return;
}
size_t index = rng_() % all_waker_tasks_.size();
if (all_waker_tasks_[index].get() == &skip_this_task) {
// Don't deregister the calling task!
index += 1;
if (index >= all_waker_tasks_.size()) {
index = 0;
}
}
task = all_waker_tasks_[index];
std::swap(all_waker_tasks_[index], all_waker_tasks_.back());
all_waker_tasks_.pop_back();
PW_LOG_DEBUG(
"Deregistering task %zu/%zu", index + 1, all_waker_tasks_.size());
}
task->Deregister();
}
constexpr int kWakeProbability = 50;
constexpr int kPostProbability = 30;
constexpr int kDeregisterProbability = 15;
constexpr int kDestroyProbability = 5;
static_assert(kWakeProbability + kPostProbability + kDeregisterProbability +
kDestroyProbability ==
100);
TEST(MultiThreadedTest, StressTest) {
PW_LOG_INFO("Starting stress test with %zu threads for %zu iterations",
kNumThreads,
kIterations);
std::mt19937 rng{kRandomSeed};
TestState test_state;
test_state.StartTest();
for (size_t i = 0; i < kIterations; ++i) {
const size_t handler_id = static_cast<size_t>(rng() % kNumThreads);
const int rand_val = rng() % 100;
Event type;
if (rand_val < kWakeProbability) {
type = Event::kWakeRandomTask;
} else if (rand_val < kWakeProbability + kPostProbability) {
type = Event::kPostNewTask;
} else if (rand_val <
kWakeProbability + kPostProbability + kDeregisterProbability) {
type = Event::kDeregisterRandomTask;
} else {
type = Event::kDestroyRandomDispatcher;
}
if (kEnableVerboseLogging) {
PW_LOG_DEBUG("[%zu/%zu] %s -> handler %zu/%zu",
i + 1,
kIterations,
EventToString(type),
handler_id + 1,
kNumThreads);
}
test_state.HandleEvent(type, handler_id);
pw::this_thread::yield();
}
test_state.HaltTest();
// Total number of events handled, plus a kStopTest event for each thread.
EXPECT_EQ(test_state.EventsHandled(), kIterations + kNumThreads);
PW_LOG_INFO("Finished stress test!");
}
} // namespace