blob: bc7ba4a380e79aa71aaa51a16ad5078b43070501 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#include <lib/fit/defer.h>
#include <lib/fit/function.h>
#include <lib/unittest/unittest.h>
#include <lib/zircon-internal/macros.h>
#include <platform.h>
#include <zircon/types.h>
#include <new>
#include <fbl/alloc_checker.h>
#include <fbl/macros.h>
#include <kernel/auto_preempt_disabler.h>
#include <kernel/owned_wait_queue.h>
#include <kernel/scheduler.h>
#include <kernel/thread.h>
#include <kernel/wait.h>
#include <ktl/algorithm.h>
#include <ktl/array.h>
#include <ktl/atomic.h>
#include <ktl/iterator.h>
#include <ktl/limits.h>
#include <ktl/type_traits.h>
#include <ktl/unique_ptr.h>
#include "tests.h"
#include <ktl/enforce.h>
namespace {
constexpr int TEST_LOWEST_PRIORITY = LOWEST_PRIORITY + 1;
constexpr int TEST_HIGHEST_PRIORITY = HIGHEST_PRIORITY;
constexpr int TEST_DEFAULT_PRIORITY = DEFAULT_PRIORITY;
constexpr int TEST_PRIORTY_COUNT = TEST_HIGHEST_PRIORITY - TEST_LOWEST_PRIORITY;
class TestThread; // fwd decl
// An RAII style helper which lets us auto boost the priority of our test thread
// to maximum, but return it to whatever it was when the test ends. Many of
// these tests need to rely on timing in order to control the order with which
// threads time out of various wait queues. Since we don't have deterministic
// control over timing in our tests, we rely on our high priority test thread
// being scheduled and pre-empting all other threads when it's timer goes off in
// order to reduce the chances of timing related flake in the tests.
class AutoPrioBooster {
public:
AutoPrioBooster() {
Thread* t = Thread::Current::Get();
initial_base_prio_ = t->scheduler_state().base_priority();
t->SetPriority(TEST_HIGHEST_PRIORITY);
}
~AutoPrioBooster() { Thread::Current::Get()->SetPriority(initial_base_prio_); }
DISALLOW_COPY_ASSIGN_AND_MOVE(AutoPrioBooster);
private:
int initial_base_prio_;
};
// A small helper which creates diffierent distributions of numbers which can be
// used for things like determining priority order, or release order, for the
// various tests.
struct DistroSpec {
enum class Type { ASCENDING, DESCENDING, SAME, RANDOM, SHUFFLE };
constexpr DistroSpec(Type t, uint32_t o, uint64_t s = 0) : type(t), offset(o), seed(s) {}
const Type type;
const uint32_t offset;
const uint64_t seed;
};
void CreateDistribution(uint32_t* data, uint32_t N, const DistroSpec& spec) {
DEBUG_ASSERT(data);
uint64_t prng = spec.seed;
switch (spec.type) {
// Create an ascending sequence from [0, N] offset by spec.offset
case DistroSpec::Type::ASCENDING:
for (uint32_t i = 0; i < N; ++i) {
data[i] = i + spec.offset;
}
break;
// Create a descending sequence from (N, 0] offset by spec.offset
case DistroSpec::Type::DESCENDING:
for (uint32_t i = 0; i < N; ++i) {
data[i] = static_cast<uint32_t>(N - i - 1 + spec.offset);
}
break;
// Set all of the values to just offset.
case DistroSpec::Type::SAME:
for (uint32_t i = 0; i < N; ++i) {
data[i] = spec.offset;
}
break;
// Set all of the values to a random number on the range [0, N) + offset
case DistroSpec::Type::RANDOM:
for (uint32_t i = 0; i < N; ++i) {
data[i] = spec.offset + (rand_r(&prng) % N);
}
break;
// Create a range of values from [0, N) + offset, but shuffle the order of
// those values in the set.
case DistroSpec::Type::SHUFFLE:
// Start by filling our array with a illegal sentinel value (N will do
// the job just fine), then foreach i in the range [0, num_links) pick a
// random position in the output to put i, and linearly probe until we
// find the first unused position in order to shuffle. Finally, offset
// by 'offset' and we should be done.
for (uint32_t i = 0; i < N; ++i) {
data[i] = N;
}
for (uint32_t i = 0; i < N; ++i) {
uint32_t pos = (rand_r(&prng) % N);
while (data[pos] != N) {
pos = (pos + 1) % N;
}
data[pos] = i;
}
for (uint32_t i = 0; i < N; ++i) {
data[i] += spec.offset;
}
break;
}
}
template <typename DATA_TYPE, size_t N>
void CreateDistribution(DATA_TYPE (&data)[N], const DistroSpec& spec) {
static_assert(ktl::is_same<DATA_TYPE, int32_t>::value || ktl::is_same<DATA_TYPE, uint32_t>::value,
"CreateDistribution only operates on 32 bit integer types!");
static_assert(N <= ktl::numeric_limits<uint32_t>::max(),
"CreateDistribution array size must be expressible using a 32 bit unsigned int");
CreateDistribution(reinterpret_cast<uint32_t*>(data), static_cast<uint32_t>(N), spec);
}
// A simple barrier class which can be waited on by multiple threads. Used to
// stall test threads at various parts of their execution in order to sequence
// things in a deterministic fashion.
class Barrier {
public:
constexpr Barrier(bool signaled = false) : signaled_{signaled} {}
~Barrier() {
Guard<MonitoredSpinLock, IrqSave> guard{ThreadLock::Get(), SOURCE_TAG};
ASSERT(queue_.IsEmpty());
}
void Signal(bool state) {
bool expected = !state;
if (signaled_.compare_exchange_strong(expected, state) && state) {
Guard<MonitoredSpinLock, IrqSave> guard{ThreadLock::Get(), SOURCE_TAG};
queue_.WakeAll(ZX_OK);
}
}
void Wait(Deadline deadline = Deadline::infinite()) {
if (state()) {
return;
}
Guard<MonitoredSpinLock, IrqSave> guard{ThreadLock::Get(), SOURCE_TAG};
if (state()) {
return;
}
queue_.Block(deadline, Interruptible::Yes);
}
bool state() const { return signaled_.load(); }
private:
ktl::atomic<bool> signaled_;
WaitQueue queue_;
};
// Helper wrapper for an owned wait queue which manages grabbing and releasing
// the thread lock at appropriate times for us. Mostly, this is just about
// saving some typing.
class LockedOwnedWaitQueue : public OwnedWaitQueue {
public:
constexpr LockedOwnedWaitQueue() = default;
DISALLOW_COPY_ASSIGN_AND_MOVE(LockedOwnedWaitQueue);
void ReleaseAllThreads() TA_EXCL(thread_lock) {
AnnotatedAutoPreemptDisabler aapd;
Guard<MonitoredSpinLock, IrqSave> guard{ThreadLock::Get(), SOURCE_TAG};
OwnedWaitQueue::WakeThreads(ktl::numeric_limits<uint32_t>::max());
}
void ReleaseOneThread() TA_EXCL(thread_lock) {
AnnotatedAutoPreemptDisabler aapd;
Guard<MonitoredSpinLock, IrqSave> guard{ThreadLock::Get(), SOURCE_TAG};
auto hook = [](Thread*, void*) { return Hook::Action::SelectAndAssignOwner; };
OwnedWaitQueue::WakeThreads(1u, {hook, nullptr});
}
void AssignOwner(TestThread* thread) TA_EXCL(thread_lock);
};
// LoopIterPrinter
// A small RAII style class which helps us to print out where a loop iterator
// is when a test fails and bails out. Note: loop iterator types must be
// convertible to int64_t.
template <typename T>
class LoopIterPrinter {
public:
constexpr LoopIterPrinter(const char* field_name, T iter_val)
: field_name_(field_name), iter_val_(iter_val) {}
~LoopIterPrinter() {
if (field_name_ != nullptr) {
printf("Test failed with %s == %ld\n", field_name_, static_cast<int64_t>(iter_val_));
}
}
DISALLOW_COPY_ASSIGN_AND_MOVE(LoopIterPrinter);
void cancel() { field_name_ = nullptr; }
private:
const char* field_name_;
T iter_val_;
};
#define PRINT_LOOP_ITER(_var_name) LoopIterPrinter print_##_var_name(#_var_name, _var_name)
// The core test thread object. We use this object to build various graphs of
// priority inheritance chains, and then evaluate that the effective priorities
// of the threads involved in the graph are what we expect them to be after
// various mutations of the graph have taken place.
class TestThread {
public:
enum class State : uint32_t {
INITIAL,
CREATED,
WAITING_TO_START,
STARTED,
WAITING_FOR_SHUTDOWN,
SHUTDOWN,
};
enum class Condition : uint32_t {
BLOCKED,
WAITING_FOR_SHUTDOWN,
};
TestThread() = default;
~TestThread() { Reset(); }
DISALLOW_COPY_ASSIGN_AND_MOVE(TestThread);
// Reset the barrier at the start of a test in order to prevent threads from
// exiting after they have completed their operation..
static void ResetShutdownBarrier() { allow_shutdown_.Signal(false); }
// Clear the barrier and allow shutdown.
static void ClearShutdownBarrier() { allow_shutdown_.Signal(true); }
static Barrier& allow_shutdown() { return allow_shutdown_; }
// Create a thread, settings its entry point and initial base priority in
// the process, but do not start it yet.
bool Create(int initial_base_priority);
// Start the thread, have it do nothing but wait to be allowed to exit.
bool DoStall();
// Start the thread and have it block on an owned wait queue, declaring the
// specified test thread to be the owner of that queue in the process.
bool BlockOnOwnedQueue(OwnedWaitQueue* owned_wq, TestThread* owner,
Deadline timeout = Deadline::infinite());
// Directly take ownership of the specified wait queue using AssignOwner.
bool TakeOwnership(OwnedWaitQueue* owned_wq);
// Change base the priority of the thread
bool SetBasePriority(int base_prio) {
BEGIN_TEST;
ASSERT_NONNULL(thread_);
ASSERT_EQ(state(), State::STARTED);
ASSERT_GE(base_prio, TEST_LOWEST_PRIORITY);
ASSERT_LT(base_prio, TEST_HIGHEST_PRIORITY);
thread_->SetPriority(base_prio);
END_TEST;
}
// Reset the thread back to its initial state. If |explicit_kill| is true,
// then do not wait for the thread to exit normally if it has been started.
// Simply send it the kill signal.
bool Reset(bool explicit_kill = false);
int inherited_priority() const {
if (thread_ == nullptr) {
return -2;
}
Guard<MonitoredSpinLock, IrqSave> guard{ThreadLock::Get(), SOURCE_TAG};
return thread_->scheduler_state().inherited_priority();
}
int effective_priority() const {
if (thread_ == nullptr) {
return -2;
}
Guard<MonitoredSpinLock, IrqSave> guard{ThreadLock::Get(), SOURCE_TAG};
return thread_->scheduler_state().effective_priority();
}
int base_priority() const {
if (thread_ == nullptr) {
return -2;
}
Guard<MonitoredSpinLock, IrqSave> guard{ThreadLock::Get(), SOURCE_TAG};
return thread_->scheduler_state().base_priority();
}
State state() const { return state_.load(); }
thread_state tstate() const {
if (thread_ == nullptr) {
return thread_state::THREAD_DEATH;
}
Guard<MonitoredSpinLock, IrqSave> guard{ThreadLock::Get(), SOURCE_TAG};
return thread_->state();
}
template <Condition condition>
bool WaitFor();
private:
// Test threads in the various tests use lambdas in order to store their
// customized test operations. In order to allow these lambda's to capture
// context from their local scope, but not need to use the heap in order to
// allocate the storage for the scope, we need to know the worst case
// capture storage requirements across all of these tests. Armed with this
// knowledge, we can use a fit::inline_function to pre-allocate storage in
// the TestThread object for the worst case lambda we will encounter in the
// test suite.
//
// Currently, this bound is 6 pointer's worth of storage. If this grows in
// the future, this constexpr bound should be updated to match the new worst
// case storage requirement.
static constexpr size_t kMaxOpLambdaCaptureStorageBytes = sizeof(void*) * 6;
friend class LockedOwnedWaitQueue;
int ThreadEntry();
static Barrier allow_shutdown_;
Thread* thread_ = nullptr;
ktl::atomic<State> state_{State::INITIAL};
fit::inline_function<void(void), kMaxOpLambdaCaptureStorageBytes> op_;
};
Barrier TestThread::allow_shutdown_;
bool TestThread::Create(int initial_base_priority) {
BEGIN_TEST;
ASSERT_NULL(thread_);
ASSERT_EQ(state(), State::INITIAL);
ASSERT_GE(initial_base_priority, TEST_LOWEST_PRIORITY);
ASSERT_LT(initial_base_priority, TEST_HIGHEST_PRIORITY);
thread_ = Thread::Create(
"pi_test_thread",
[](void* ctx) -> int { return reinterpret_cast<TestThread*>(ctx)->ThreadEntry(); },
reinterpret_cast<void*>(this), initial_base_priority);
ASSERT_NONNULL(thread_);
state_.store(State::CREATED);
END_TEST;
}
bool TestThread::DoStall() {
BEGIN_TEST;
ASSERT_EQ(state(), State::CREATED);
ASSERT_FALSE(static_cast<bool>(op_));
op_ = []() {};
state_.store(State::WAITING_TO_START);
thread_->Resume();
ASSERT_TRUE(WaitFor<Condition::BLOCKED>());
END_TEST;
}
bool TestThread::BlockOnOwnedQueue(OwnedWaitQueue* owned_wq, TestThread* owner, Deadline timeout) {
BEGIN_TEST;
ASSERT_EQ(state(), State::CREATED);
ASSERT_FALSE(static_cast<bool>(op_));
op_ = [owned_wq, owner_thrd = owner ? owner->thread_ : nullptr, timeout]() {
AnnotatedAutoPreemptDisabler aapd;
Guard<MonitoredSpinLock, IrqSave> guard{ThreadLock::Get(), SOURCE_TAG};
owned_wq->BlockAndAssignOwner(timeout, owner_thrd, ResourceOwnership::Normal,
Interruptible::Yes);
};
state_.store(State::WAITING_TO_START);
thread_->Resume();
ASSERT_TRUE(WaitFor<Condition::BLOCKED>());
END_TEST;
}
bool TestThread::Reset(bool explicit_kill) {
BEGIN_TEST;
// If we are explicitly killing the thread as part of the test, then we
// should not expect the shutdown barrier to be cleared.
if (!explicit_kill) {
EXPECT_TRUE(allow_shutdown_.state());
}
constexpr zx_duration_t join_timeout = ZX_MSEC(500);
switch (state()) {
case State::INITIAL:
break;
case State::CREATED:
// Created but not started? thread_forget seems to be the proper way to
// cleanup a thread which was never started.
ASSERT(thread_ != nullptr);
thread_->Forget();
thread_ = nullptr;
break;
case State::WAITING_TO_START:
case State::STARTED:
case State::WAITING_FOR_SHUTDOWN:
case State::SHUTDOWN:
// If we are explicitly killing the thread, send it the kill signal now.
if (explicit_kill) {
thread_->Kill();
}
// Hopefully, the thread is on it's way to termination as we speak.
// Attempt to join it. If this fails, print a warning and then kill it.
ASSERT(thread_ != nullptr);
int ret_code;
zx_status_t res = thread_->Join(&ret_code, current_time() + join_timeout);
if (res != ZX_OK) {
printf("Failed to join thread %p (res %d); attempting to kill\n", thread_, res);
// If we have already sent the kill signal to the thread and failed,
// there is no point in trying to do so gain.
if (!explicit_kill) {
thread_->Kill();
res = thread_->Join(&ret_code, current_time() + join_timeout);
}
if (res != ZX_OK) {
panic("Failed to stop thread during PI tests!! (res = %d)\n", res);
}
}
thread_ = nullptr;
}
state_.store(State::INITIAL);
op_ = nullptr;
ASSERT_NULL(thread_);
END_TEST;
}
int TestThread::ThreadEntry() {
if (!static_cast<bool>(op_) || (state() != State::WAITING_TO_START)) {
return -1;
}
state_.store(State::STARTED);
op_();
state_.store(State::WAITING_FOR_SHUTDOWN);
allow_shutdown_.Wait();
state_.store(State::SHUTDOWN);
op_ = nullptr;
return 0;
}
template <TestThread::Condition condition>
bool TestThread::WaitFor() {
BEGIN_TEST;
constexpr zx_duration_t timeout = ZX_SEC(10);
constexpr zx_duration_t poll_interval = ZX_USEC(100);
zx_time_t deadline = current_time() + timeout;
while (true) {
if constexpr (condition == Condition::BLOCKED) {
thread_state cur_state = tstate();
if (cur_state == THREAD_BLOCKED) {
break;
}
if (cur_state != THREAD_RUNNING) {
ASSERT_EQ(THREAD_READY, cur_state);
}
} else {
static_assert(condition == Condition::WAITING_FOR_SHUTDOWN);
if (state() == State::WAITING_FOR_SHUTDOWN) {
break;
}
}
zx_time_t now = current_time();
ASSERT_LT(now, deadline);
Thread::Current::SleepRelative(poll_interval);
}
END_TEST;
}
void LockedOwnedWaitQueue::AssignOwner(TestThread* thread) {
AnnotatedAutoPreemptDisabler aapd;
Guard<MonitoredSpinLock, IrqSave> guard{ThreadLock::Get(), SOURCE_TAG};
OwnedWaitQueue::AssignOwner(thread ? thread->thread_ : nullptr);
}
bool pi_test_basic() {
BEGIN_TEST;
enum class ReleaseMethod { WAKE = 0, TIMEOUT, KILL };
AutoPrioBooster pboost;
constexpr zx_duration_t TIMEOUT_RELEASE_DURATION = ZX_MSEC(200);
constexpr int END_PRIO = TEST_DEFAULT_PRIORITY;
constexpr int PRIO_DELTAS[] = {-1, 0, 1};
constexpr ReleaseMethod REL_METHODS[] = {ReleaseMethod::WAKE, ReleaseMethod::TIMEOUT,
ReleaseMethod::KILL};
for (auto prio_delta : PRIO_DELTAS) {
for (auto rel_method : REL_METHODS) {
PRINT_LOOP_ITER(prio_delta);
PRINT_LOOP_ITER(rel_method);
bool retry_test;
do {
retry_test = false;
LockedOwnedWaitQueue owq;
TestThread pressure_thread;
TestThread blocking_thread;
auto cleanup = fit::defer([&]() {
TestThread::ClearShutdownBarrier();
owq.ReleaseAllThreads();
pressure_thread.Reset();
blocking_thread.Reset();
});
int pressure_prio = END_PRIO + prio_delta;
int expected_prio = (prio_delta > 0) ? pressure_prio : END_PRIO;
// Make sure that our default barriers have been reset to their proper
// initial states.
TestThread::ResetShutdownBarrier();
// Create 2 threads, one which will sit at the end of the priority
// chain, and one which will exert priority pressure on the end of the
// chain.
ASSERT_TRUE(blocking_thread.Create(END_PRIO));
ASSERT_TRUE(pressure_thread.Create(pressure_prio));
// Start the first thread, wait for it to block, and verify that it's
// priority is correct (it should not be changed).
ASSERT_TRUE(blocking_thread.DoStall());
ASSERT_EQ(TEST_DEFAULT_PRIORITY, blocking_thread.effective_priority());
// Start the second thread, and have it block on the owned wait queue,
// and declare the blocking thread to be the owner of the queue at the
// same time. Then check to be sure that the effective priority of the
// blocking thread matches what we expect to see.
Deadline timeout = (rel_method == ReleaseMethod::TIMEOUT)
? Deadline::after(TIMEOUT_RELEASE_DURATION)
: Deadline::infinite();
ASSERT_TRUE(pressure_thread.BlockOnOwnedQueue(&owq, &blocking_thread, timeout));
// Observe the effective priority of the blocking thread, then observe
// the state of the thread applying pressure. If this is the TIMEOUT
// test, the thread *must* still be blocked on |owq| (not timed out yet)
// in order for the test to be considered valid. If the thread managed
// to unblock before we could observe its effective priority, just try
// again.
int observed_priority = blocking_thread.effective_priority();
if (rel_method == ReleaseMethod::TIMEOUT) {
retry_test = (pressure_thread.tstate() != thread_state::THREAD_BLOCKED) ||
(pressure_thread.state() == TestThread::State::WAITING_FOR_SHUTDOWN);
}
// Only assert this if we managed to observe the blocked thread's
// effective priority while the pressure thread was still applying
// pressure.
if (!retry_test) {
ASSERT_EQ(expected_prio, observed_priority);
}
// Finally, release the thread from the owned wait queue based on
// the release method we are testing. We will either explicitly
// wake it up, let it time out, or kill the thread outright.
//
// Then, verify that the priority drops back down to what we
// expected.
switch (rel_method) {
case ReleaseMethod::WAKE:
owq.ReleaseAllThreads();
break;
case ReleaseMethod::TIMEOUT:
// Wait until the pressure thread times out and has exited.
ASSERT_TRUE(pressure_thread.WaitFor<TestThread::Condition::WAITING_FOR_SHUTDOWN>());
break;
case ReleaseMethod::KILL:
pressure_thread.Reset(true);
break;
}
ASSERT_EQ(TEST_DEFAULT_PRIORITY, blocking_thread.effective_priority());
} while (retry_test);
print_prio_delta.cancel();
print_rel_method.cancel();
}
}
END_TEST;
}
bool pi_test_changing_priority() {
BEGIN_TEST;
AutoPrioBooster pboost;
LockedOwnedWaitQueue owq;
TestThread pressure_thread;
TestThread blocking_thread;
auto cleanup = fit::defer([&]() {
TestThread::ClearShutdownBarrier();
owq.ReleaseAllThreads();
pressure_thread.Reset();
blocking_thread.Reset();
});
// Make sure that our default barriers have been reset to their proper
// initial states.
TestThread::ResetShutdownBarrier();
// Create our threads.
ASSERT_TRUE(blocking_thread.Create(TEST_DEFAULT_PRIORITY));
ASSERT_TRUE(pressure_thread.Create(TEST_LOWEST_PRIORITY));
// Start the first thread, wait for it to block, and verify that it's
// priority is correct (it should not be changed).
ASSERT_TRUE(blocking_thread.DoStall());
ASSERT_EQ(TEST_DEFAULT_PRIORITY, blocking_thread.effective_priority());
// Block the second thread behind the first.
ASSERT_TRUE(pressure_thread.BlockOnOwnedQueue(&owq, &blocking_thread));
ASSERT_EQ(TEST_DEFAULT_PRIORITY, blocking_thread.effective_priority());
// Run up and down through a bunch of priorities
for (int ascending = TEST_LOWEST_PRIORITY; ascending < TEST_HIGHEST_PRIORITY; ++ascending) {
PRINT_LOOP_ITER(ascending);
int expected = ktl::max(ascending, TEST_DEFAULT_PRIORITY);
ASSERT_TRUE(pressure_thread.SetBasePriority(ascending));
ASSERT_EQ(expected, blocking_thread.effective_priority());
print_ascending.cancel();
}
for (int descending = TEST_HIGHEST_PRIORITY - 1; descending >= TEST_LOWEST_PRIORITY;
--descending) {
PRINT_LOOP_ITER(descending);
int expected = ktl::max(descending, TEST_DEFAULT_PRIORITY);
ASSERT_TRUE(pressure_thread.SetBasePriority(descending));
ASSERT_EQ(expected, blocking_thread.effective_priority());
print_descending.cancel();
}
// Release the pressure thread, validate that the priority is what we
// started with and we are done.
owq.ReleaseAllThreads();
ASSERT_EQ(TEST_DEFAULT_PRIORITY, blocking_thread.effective_priority());
END_TEST;
}
template <uint32_t CHAIN_LEN>
bool pi_test_chain() {
static_assert(CHAIN_LEN >= 2, "Must have at least 2 nodes to form a PI chain");
static_assert(CHAIN_LEN < TEST_PRIORTY_COUNT,
"Cannot create a chain which would result in a thread being created at "
"TEST_HIGHEST_PRIORITY");
BEGIN_TEST;
fbl::AllocChecker ac;
enum class ReleaseOrder : uint64_t { ASCENDING = 0, DESCENDING };
AutoPrioBooster pboost;
TestThread threads[CHAIN_LEN];
struct Link {
LockedOwnedWaitQueue queue;
bool active = false;
};
auto links = ktl::make_unique<ktl::array<Link, CHAIN_LEN - 1>>(&ac);
ASSERT_TRUE(ac.check());
const DistroSpec PRIORITY_GENERATORS[] = {
{DistroSpec::Type::ASCENDING, TEST_LOWEST_PRIORITY},
{DistroSpec::Type::DESCENDING, TEST_LOWEST_PRIORITY},
{DistroSpec::Type::SAME, TEST_DEFAULT_PRIORITY},
{DistroSpec::Type::RANDOM, TEST_LOWEST_PRIORITY, 0xa064eba4bf1b5087},
{DistroSpec::Type::RANDOM, TEST_LOWEST_PRIORITY, 0x87251211471cb789},
{DistroSpec::Type::SHUFFLE, TEST_LOWEST_PRIORITY, 0xbd6f3bfe33d51c8e},
{DistroSpec::Type::SHUFFLE, TEST_LOWEST_PRIORITY, 0x857ce1aa3209ecc7},
};
const DistroSpec RELEASE_ORDERS[]{
{DistroSpec::Type::ASCENDING, 0},
{DistroSpec::Type::DESCENDING, 0},
{DistroSpec::Type::SHUFFLE, 0, 0xac8d4a8ed016caf0},
{DistroSpec::Type::SHUFFLE, 0, 0xb51e76ca5cf20875},
};
for (uint32_t pgen_ndx = 0; pgen_ndx < ktl::size(PRIORITY_GENERATORS); ++pgen_ndx) {
PRINT_LOOP_ITER(pgen_ndx);
// Generate the priority map for this pass.
int prio_map[ktl::size(threads)];
CreateDistribution(prio_map, PRIORITY_GENERATORS[pgen_ndx]);
for (uint32_t ro_ndx = 0; ro_ndx < ktl::size(RELEASE_ORDERS); ++ro_ndx) {
PRINT_LOOP_ITER(ro_ndx);
// Generate the order in which we will release the links for this
// pass
uint32_t release_ordering[CHAIN_LEN - 1];
CreateDistribution(release_ordering, RELEASE_ORDERS[ro_ndx]);
auto cleanup = fit::defer([&]() {
TestThread::ClearShutdownBarrier();
for (auto& l : *links) {
l.queue.ReleaseAllThreads();
}
for (auto& t : threads) {
t.Reset();
}
});
// Lambda used to validate the current thread priorities.
auto ValidatePriorities = [&]() -> bool {
BEGIN_TEST;
int expected_prio = -1;
for (uint32_t tndx = ktl::size(threads); tndx-- > 0;) {
PRINT_LOOP_ITER(tndx);
// All threads should either be created, started or waiting for
// shutdown. If they are merely created, they have no effective
// priority to evaluate at the moment, so just skip them.
const auto& t = threads[tndx];
const TestThread::State cur_state = t.state();
if (cur_state == TestThread::State::CREATED) {
print_tndx.cancel();
continue;
}
if (cur_state != TestThread::State::WAITING_FOR_SHUTDOWN) {
ASSERT_EQ(TestThread::State::STARTED, cur_state);
}
// If the link behind us in the chain does not exist, or exists
// but is not currently active, then reset the expected priority
// pressure. Otherwise, the expected priority should be the
// priority of the maximum of the base priorities we have
// traversed so far.
ASSERT_LT(tndx, ktl::size(prio_map));
if ((tndx >= links->size()) || !(*links)[tndx].active) {
expected_prio = prio_map[tndx];
} else {
expected_prio = ktl::max(expected_prio, prio_map[tndx]);
}
ASSERT_EQ(expected_prio, t.effective_priority());
print_tndx.cancel();
}
END_TEST;
};
// Make sure that our default barriers have been reset to their proper
// initial states.
TestThread::ResetShutdownBarrier();
// Create our threads.
for (uint32_t tndx = 0; tndx < ktl::size(threads); ++tndx) {
ASSERT_LT(tndx, ktl::size(prio_map));
PRINT_LOOP_ITER(tndx);
ASSERT_TRUE(threads[tndx].Create(prio_map[tndx]));
print_tndx.cancel();
}
// Start the head of the chain, wait for it to block, then verify that its
// priority is correct (it should not be changed).
auto& chain_head = threads[0];
ASSERT_TRUE(chain_head.DoStall());
ASSERT_TRUE(ValidatePriorities());
// Start each of the threads in the chain one at a time. Make sure that the
// pressure of the threads in the chain is properly transmitted each time.
for (uint32_t tndx = 1; tndx < ktl::size(threads); ++tndx) {
PRINT_LOOP_ITER(tndx);
auto& link = (*links)[tndx - 1];
ASSERT_TRUE(threads[tndx].BlockOnOwnedQueue(&link.queue, &threads[tndx - 1]));
link.active = true;
ASSERT_TRUE(ValidatePriorities());
print_tndx.cancel();
}
// Tear down the chain according to the release ordering for this
// pass. Make sure that the priority properly relaxes for each of
// the threads as we do so.
for (auto link_ndx : release_ordering) {
PRINT_LOOP_ITER(link_ndx);
ASSERT_LT(link_ndx, links->size());
auto& link = (*links)[link_ndx];
link.queue.ReleaseAllThreads();
link.active = false;
ASSERT_TRUE(ValidatePriorities());
print_link_ndx.cancel();
}
print_ro_ndx.cancel();
}
print_pgen_ndx.cancel();
}
END_TEST;
}
template <uint32_t WAITER_CNT>
bool pi_test_multi_waiter() {
static_assert(WAITER_CNT >= 2, "Must have at least 2 waiters in the multi-waiter test");
static_assert(WAITER_CNT < TEST_PRIORTY_COUNT,
"Multi waiter test must have fewer waiters than priority levels");
BEGIN_TEST;
fbl::AllocChecker ac;
AutoPrioBooster pboost;
LockedOwnedWaitQueue blocking_queue;
TestThread blocking_thread;
struct Waiter {
TestThread thread;
bool is_waiting = false;
int prio = 0;
};
auto waiters = ktl::make_unique<ktl::array<Waiter, WAITER_CNT>>(&ac);
ASSERT_TRUE(ac.check());
const int BLOCKING_THREAD_PRIO[] = {TEST_LOWEST_PRIORITY, TEST_DEFAULT_PRIORITY,
TEST_HIGHEST_PRIORITY - 1};
const DistroSpec PRIORITY_GENERATORS[] = {
{DistroSpec::Type::ASCENDING, TEST_LOWEST_PRIORITY},
{DistroSpec::Type::DESCENDING, TEST_LOWEST_PRIORITY},
{DistroSpec::Type::SAME, TEST_DEFAULT_PRIORITY},
{DistroSpec::Type::RANDOM, TEST_LOWEST_PRIORITY, 0xa064eba4bf1b5087},
{DistroSpec::Type::RANDOM, TEST_LOWEST_PRIORITY, 0x87251211471cb789},
{DistroSpec::Type::SHUFFLE, TEST_LOWEST_PRIORITY, 0xbd6f3bfe33d51c8e},
{DistroSpec::Type::SHUFFLE, TEST_LOWEST_PRIORITY, 0x857ce1aa3209ecc7},
};
for (auto bt_prio : BLOCKING_THREAD_PRIO) {
PRINT_LOOP_ITER(bt_prio);
for (uint32_t pgen_ndx = 0; pgen_ndx < ktl::size(PRIORITY_GENERATORS); ++pgen_ndx) {
PRINT_LOOP_ITER(pgen_ndx);
// At the end of the tests, success or failure, be sure to clean up.
auto cleanup = fit::defer([&]() {
TestThread::ClearShutdownBarrier();
blocking_queue.ReleaseAllThreads();
blocking_thread.Reset();
for (auto& w : *waiters) {
w.thread.Reset();
}
});
// Make sure that our barriers have been reset.
TestThread::ResetShutdownBarrier();
// Generate the priority map for this pass.
int prio_map[WAITER_CNT];
CreateDistribution(prio_map, PRIORITY_GENERATORS[pgen_ndx]);
// Create all of the threads.
ASSERT_TRUE(blocking_thread.Create(bt_prio));
for (uint32_t waiter_ndx = 0; waiter_ndx < waiters->size(); ++waiter_ndx) {
PRINT_LOOP_ITER(waiter_ndx);
auto& w = (*waiters)[waiter_ndx];
w.prio = prio_map[waiter_ndx];
ASSERT_TRUE(w.thread.Create(w.prio));
print_waiter_ndx.cancel();
}
// Define a small lambda we will use to validate the expected priorities of
// each of our threads.
TestThread* current_owner = &blocking_thread;
auto ValidatePriorities = [&]() -> bool {
BEGIN_TEST;
// All threads in the test who are not the current owner should have
// their effective priority be equal to their base priority
if (&blocking_thread != current_owner) {
ASSERT_EQ(bt_prio, blocking_thread.effective_priority());
}
for (uint32_t waiter_ndx = 0; waiter_ndx < waiters->size(); ++waiter_ndx) {
PRINT_LOOP_ITER(waiter_ndx);
auto& w = (*waiters)[waiter_ndx];
if (&w.thread != current_owner) {
ASSERT_EQ(prio_map[waiter_ndx], w.thread.effective_priority());
}
print_waiter_ndx.cancel();
}
// The current owner (if any) should have the max priority across all of
// the waiters, and its own base priority.
ASSERT_NONNULL(current_owner);
int expected_prio = current_owner->base_priority();
for (const auto& w : *waiters) {
if (w.is_waiting && (expected_prio < w.prio)) {
expected_prio = w.prio;
}
}
ASSERT_EQ(expected_prio, current_owner->effective_priority());
END_TEST;
};
// Start the blocking thread.
ASSERT_TRUE(blocking_thread.DoStall());
ASSERT_TRUE(ValidatePriorities());
// Start each of the threads and have them block on the blocking_queue,
// declaring blocking_thread to be the owner as they go. Verify that the
// blocking thread has the priority of the highest priority thread who is
// currently waiting.
for (uint32_t waiter_ndx = 0; waiter_ndx < waiters->size(); ++waiter_ndx) {
PRINT_LOOP_ITER(waiter_ndx);
auto& w = (*waiters)[waiter_ndx];
ASSERT_TRUE(w.thread.BlockOnOwnedQueue(&blocking_queue, current_owner));
w.is_waiting = true;
ASSERT_TRUE(ValidatePriorities());
print_waiter_ndx.cancel();
}
// Now wake the threads, one at a time, assigning ownership to the thread
// which was woken each time. Note that we should not be assuming which
// thread is going to be woken. We will need to request that a thread be
// woken, then figure out after the fact which one was.
for (uint32_t tndx = 0; tndx < waiters->size(); ++tndx) {
PRINT_LOOP_ITER(tndx);
blocking_queue.ReleaseOneThread();
TestThread* new_owner = nullptr;
zx_time_t deadline = current_time() + ZX_SEC(10);
while (current_time() < deadline) {
for (auto& w : *waiters) {
// If the waiter's is_waiting flag is set, but the thread has
// reached the WAITING_FOR_SHUTDOWN state, then we know that
// this was a thread which was just woken.
if (w.is_waiting && (w.thread.state() == TestThread::State::WAITING_FOR_SHUTDOWN)) {
new_owner = &w.thread;
w.is_waiting = false;
break;
}
}
if (new_owner != nullptr) {
break;
}
Thread::Current::SleepRelative(ZX_USEC(100));
}
// Sanity checks. Make sure that the new owner exists, and is not the
// same as the old owner. Also make sure that none of the other threads
// have been released but have not been recognized yet.
ASSERT_NONNULL(new_owner);
ASSERT_NE(new_owner, current_owner);
for (auto& w : *waiters) {
if (w.is_waiting) {
ASSERT_EQ(TestThread::State::STARTED, w.thread.state());
} else {
ASSERT_EQ(TestThread::State::WAITING_FOR_SHUTDOWN, w.thread.state());
}
}
current_owner = new_owner;
// Validate our priorities.
ASSERT_TRUE(ValidatePriorities());
print_tndx.cancel();
}
print_pgen_ndx.cancel();
}
print_bt_prio.cancel();
}
END_TEST;
}
template <uint32_t QUEUE_CNT>
bool pi_test_multi_owned_queues() {
static_assert(QUEUE_CNT >= 2, "Must have at least 2 owned queues in the multi-waiter test");
static_assert(QUEUE_CNT < TEST_PRIORTY_COUNT,
"Multi waiter test must have fewer owned queues than priority levels");
BEGIN_TEST;
fbl::AllocChecker ac;
AutoPrioBooster pboost;
TestThread blocking_thread;
struct Waiter {
TestThread thread;
LockedOwnedWaitQueue queue;
bool is_waiting = false;
int prio = 0;
};
auto queues = ktl::make_unique<ktl::array<Waiter, QUEUE_CNT>>(&ac);
ASSERT_TRUE(ac.check());
const int BLOCKING_THREAD_PRIO[] = {TEST_LOWEST_PRIORITY, TEST_DEFAULT_PRIORITY,
TEST_HIGHEST_PRIORITY - 1};
const DistroSpec PRIORITY_GENERATORS[] = {
{DistroSpec::Type::ASCENDING, TEST_LOWEST_PRIORITY},
{DistroSpec::Type::DESCENDING, TEST_LOWEST_PRIORITY},
{DistroSpec::Type::SAME, TEST_DEFAULT_PRIORITY},
{DistroSpec::Type::RANDOM, TEST_LOWEST_PRIORITY, 0xef900a44da89a82d},
{DistroSpec::Type::RANDOM, TEST_LOWEST_PRIORITY, 0xb89e3b7442b95a1c},
{DistroSpec::Type::SHUFFLE, TEST_LOWEST_PRIORITY, 0xa23574c4fb9b0a10},
{DistroSpec::Type::SHUFFLE, TEST_LOWEST_PRIORITY, 0x06ec82d4ade8efba},
};
for (auto bt_prio : BLOCKING_THREAD_PRIO) {
PRINT_LOOP_ITER(bt_prio);
for (uint32_t pgen_ndx = 0; pgen_ndx < ktl::size(PRIORITY_GENERATORS); ++pgen_ndx) {
PRINT_LOOP_ITER(pgen_ndx);
// At the end of the tests, success or failure, be sure to clean up.
auto cleanup = fit::defer([&]() {
TestThread::ClearShutdownBarrier();
blocking_thread.Reset();
for (auto& q : *queues) {
q.queue.ReleaseAllThreads();
}
for (auto& q : *queues) {
q.thread.Reset();
}
});
// Make sure that our barriers have been reset.
TestThread::ResetShutdownBarrier();
// Generate the priority map for this pass.
int prio_map[QUEUE_CNT];
CreateDistribution(prio_map, PRIORITY_GENERATORS[pgen_ndx]);
// Create all of the threads.
ASSERT_TRUE(blocking_thread.Create(bt_prio));
for (uint32_t queue_ndx = 0; queue_ndx < queues->size(); ++queue_ndx) {
PRINT_LOOP_ITER(queue_ndx);
auto& q = (*queues)[queue_ndx];
q.prio = prio_map[queue_ndx];
ASSERT_TRUE(q.thread.Create(q.prio));
print_queue_ndx.cancel();
}
// Define a small lambda we will use to validate the expected priorities of
// each of our threads.
auto ValidatePriorities = [&]() -> bool {
BEGIN_TEST;
// Each of the queue threads should simply have their base
// priority. Verify this while we compute the maximum priority
// across all of the threads who are still applying pressure to
// the blocking thread.
int max_pressure = -1;
for (const auto& q : *queues) {
ASSERT_EQ(q.prio, q.thread.effective_priority());
if (q.is_waiting) {
max_pressure = ktl::max(max_pressure, q.prio);
}
}
for (uint32_t queue_ndx = 0; queue_ndx < queues->size(); ++queue_ndx) {
PRINT_LOOP_ITER(queue_ndx);
const auto& q = (*queues)[queue_ndx];
ASSERT_EQ(q.prio, q.thread.effective_priority());
if (q.is_waiting) {
max_pressure = ktl::max(max_pressure, q.prio);
}
print_queue_ndx.cancel();
}
// Now that we know the pressure which is being applied to the
// blocking thread, verify its effective priority.
int expected_prio = ktl::max(max_pressure, bt_prio);
ASSERT_EQ(expected_prio, blocking_thread.effective_priority());
END_TEST;
};
// Start the blocking thread.
ASSERT_TRUE(blocking_thread.DoStall());
ASSERT_TRUE(ValidatePriorities());
// Start each of the threads and have them block on their associated
// queue, declaring blocking_thread to be the owner of their queue
// as they go. Validate priorities at each step.
for (uint32_t queue_ndx = 0; queue_ndx < queues->size(); ++queue_ndx) {
PRINT_LOOP_ITER(queue_ndx);
auto& q = (*queues)[queue_ndx];
ASSERT_TRUE(q.thread.BlockOnOwnedQueue(&q.queue, &blocking_thread));
q.is_waiting = true;
ASSERT_TRUE(ValidatePriorities());
print_queue_ndx.cancel();
}
// Now wake the threads, one at a time, verifying priorities as we
// go.
for (uint32_t queue_ndx = 0; queue_ndx < queues->size(); ++queue_ndx) {
PRINT_LOOP_ITER(queue_ndx);
auto& q = (*queues)[queue_ndx];
q.queue.ReleaseOneThread();
q.is_waiting = false;
ASSERT_TRUE(ValidatePriorities());
print_queue_ndx.cancel();
}
print_pgen_ndx.cancel();
}
print_bt_prio.cancel();
}
END_TEST;
}
template <uint32_t CYCLE_LEN>
bool pi_test_cycle() {
static_assert(CYCLE_LEN >= 2, "Must have at least 2 nodes to form a PI cycle");
static_assert(CYCLE_LEN < TEST_PRIORTY_COUNT,
"Cannot create a cycle which would result in a thread being created at "
"TEST_HIGHEST_PRIORITY");
BEGIN_TEST;
fbl::AllocChecker ac;
AutoPrioBooster pboost;
// Deliberately create a cycle and make sure that we don't hang or otherwise
// exhibit bad behavior.
struct Link {
TestThread thread;
LockedOwnedWaitQueue link;
};
auto nodes = ktl::make_unique<ktl::array<Link, CYCLE_LEN>>(&ac);
ASSERT_TRUE(ac.check());
// At the end of the tests, success or failure, be sure to clean up.
auto cleanup = fit::defer([&]() {
TestThread::ClearShutdownBarrier();
for (auto& n : *nodes) {
n.link.ReleaseAllThreads();
}
for (auto& n : *nodes) {
n.thread.Reset();
}
});
// Create the priorities we will assign to each thread.
int prio_map[CYCLE_LEN];
CreateDistribution(prio_map, {DistroSpec::Type::ASCENDING, TEST_LOWEST_PRIORITY});
// Create each thread
for (uint32_t tndx = 0; tndx < nodes->size(); ++tndx) {
PRINT_LOOP_ITER(tndx);
ASSERT_TRUE((*nodes)[tndx].thread.Create(prio_map[tndx]));
print_tndx.cancel();
}
// Let each thread run, blocking it on its own link and declaring the next
// thread in the list to be the owner of the link. When we hit the last
// thread, we form a cycle. Our threads are in ascending priority order, so
// we should not see any PI ripple until the final link has been made. At
// that point, all of the threads in the test should have the priority of
// the final thread.
for (uint32_t tndx = 0; tndx < nodes->size(); ++tndx) {
PRINT_LOOP_ITER(tndx);
auto owner_thread = &(*nodes)[(tndx + 1) % nodes->size()].thread;
auto link_ptr = &(*nodes)[tndx].link;
ASSERT_TRUE((*nodes)[tndx].thread.BlockOnOwnedQueue(link_ptr, owner_thread));
for (uint32_t validation_ndx = 0; validation_ndx <= tndx; ++validation_ndx) {
PRINT_LOOP_ITER(validation_ndx);
int expected_prio = prio_map[(tndx == (nodes->size() - 1)) ? tndx : validation_ndx];
ASSERT_EQ(expected_prio, (*nodes)[validation_ndx].thread.effective_priority());
print_validation_ndx.cancel();
}
print_tndx.cancel();
}
END_TEST;
}
// Exercise the specific failure tracked down during the investigation of fxbug.dev/33934
//
// There are a few different ways that this situation can be forced to happen.
// See the bug writeup for details.
bool pi_test_zx4153() {
BEGIN_TEST;
AutoPrioBooster pboost;
// Repro of this involves 2 threads and 2 wait queues involved in a PI
// cycle. The simplest repro is as follows.
//
// Let T1.prio == 16
// Let T2.prio == 17
//
// 1) Block T1 on Q2 and declare T2 to be the owner of Q2
// 2) Block T2 on Q1 and declare T1 to be the owner of Q1. T1 and T2 now
// form a cycle. The inherited priority of the cycle is now 17.
// 3) Raise T1's priority to 20. The cycle priority is now up to 20.
// 4) Lower T1's priority back down to 16. The cycle priority remains at
// 20. It cannot relax until the cycle is broken.
// 5) Break the cycle by declaring Q1 to have no owner. Do not wake T1.
//
// If the bookkeeping error found in fxbug.dev/33934 was still around, the effect
// would be...
//
// 1) T1 no longer feels pressure from Q1. T1's effective priority drops
// from 20 to 16 (its base priority)
// 2) T1 is the only waiter on Q2. Q2's pressure drops from 20 -> 16
// 3) The pressure applied to T2 drops from 20 -> 16. T2's effective
// priority is now 17 (its base priority).
// 4) T2 is the only waiter on Q1. Q1's pressure drops from 20 -> 17
// 5) Q1's owner is still mistakenly set to T1. T1 receives Q1's pressure,
// and its inherited priority goes from -1 -> 17.
// 6) Q1 now owns no queues, but has inherited priority. This should be
// impossible, and triggers the assert.
//
TestThread T1, T2;
LockedOwnedWaitQueue Q1, Q2;
// At the end of the tests, success or failure, be sure to clean up.
auto cleanup = fit::defer([&]() {
TestThread::ClearShutdownBarrier();
Q1.ReleaseAllThreads();
Q2.ReleaseAllThreads();
T1.Reset();
T2.Reset();
});
constexpr int kT1InitialPrio = 16;
constexpr int kT2InitialPrio = 17;
constexpr int kT1BoostPrio = 20;
// Create the threads.
ASSERT_TRUE(T1.Create(kT1InitialPrio));
ASSERT_TRUE(T2.Create(kT2InitialPrio));
ASSERT_EQ(T1.base_priority(), kT1InitialPrio);
ASSERT_EQ(T1.inherited_priority(), -1);
ASSERT_EQ(T1.effective_priority(), kT1InitialPrio);
ASSERT_EQ(T2.base_priority(), kT2InitialPrio);
ASSERT_EQ(T2.inherited_priority(), -1);
ASSERT_EQ(T2.effective_priority(), kT2InitialPrio);
// Form the cycle, verify the priorities
ASSERT_TRUE(T1.BlockOnOwnedQueue(&Q2, &T2));
ASSERT_TRUE(T2.BlockOnOwnedQueue(&Q1, &T1));
ASSERT_EQ(T1.base_priority(), kT1InitialPrio);
ASSERT_EQ(T1.inherited_priority(), kT2InitialPrio);
ASSERT_EQ(T1.effective_priority(), kT2InitialPrio);
ASSERT_EQ(T2.base_priority(), kT2InitialPrio);
ASSERT_EQ(T2.inherited_priority(), kT2InitialPrio);
ASSERT_EQ(T2.effective_priority(), kT2InitialPrio);
// Boost T1's priority.
ASSERT_TRUE(T1.SetBasePriority(kT1BoostPrio));
ASSERT_EQ(T1.base_priority(), kT1BoostPrio);
ASSERT_EQ(T1.inherited_priority(), kT1BoostPrio);
ASSERT_EQ(T1.effective_priority(), kT1BoostPrio);
ASSERT_EQ(T2.base_priority(), kT2InitialPrio);
ASSERT_EQ(T2.inherited_priority(), kT1BoostPrio);
ASSERT_EQ(T2.effective_priority(), kT1BoostPrio);
// Relax T1's priority. The cycle's priority cannot relax yet.
ASSERT_TRUE(T1.SetBasePriority(kT1InitialPrio));
ASSERT_EQ(T1.base_priority(), kT1InitialPrio);
ASSERT_EQ(T1.inherited_priority(), kT1BoostPrio);
ASSERT_EQ(T1.effective_priority(), kT1BoostPrio);
ASSERT_EQ(T2.base_priority(), kT2InitialPrio);
ASSERT_EQ(T2.inherited_priority(), kT1BoostPrio);
ASSERT_EQ(T2.effective_priority(), kT1BoostPrio);
// Release ownership of Q1, breaking the cycle. T2 should feel the pressure
// from T1, but T1 should not be inheriting any priority anymore.
Q1.AssignOwner(nullptr);
ASSERT_EQ(T1.base_priority(), kT1InitialPrio);
ASSERT_EQ(T1.inherited_priority(), -1);
ASSERT_EQ(T1.effective_priority(), kT1InitialPrio);
ASSERT_EQ(T2.base_priority(), kT2InitialPrio);
ASSERT_EQ(T2.inherited_priority(), kT1InitialPrio);
ASSERT_EQ(T2.effective_priority(), kT2InitialPrio);
// Success! Let the cleanup AutoCall do its job.
END_TEST;
}
} // namespace
UNITTEST_START_TESTCASE(pi_tests)
UNITTEST("basic", pi_test_basic)
UNITTEST("changing priority", pi_test_changing_priority)
UNITTEST("long chains", pi_test_chain<29>)
UNITTEST("multiple waiters", pi_test_multi_waiter<29>)
UNITTEST("multiple owned queues", pi_test_multi_owned_queues<29>)
UNITTEST("cycles", pi_test_cycle<29>)
UNITTEST("fxbug.dev/33934", pi_test_zx4153)
UNITTEST_END_TESTCASE(pi_tests, "pi", "Priority inheritance tests for OwnedWaitQueues")