| // 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/kconcurrent/chainlock_transaction.h> |
| #include <lib/unittest/unittest.h> |
| #include <lib/zircon-internal/macros.h> |
| #include <lib/zx/time.h> |
| #include <platform.h> |
| #include <zircon/types.h> |
| |
| #include <new> |
| |
| #include <fbl/alloc_checker.h> |
| #include <fbl/macros.h> |
| #include <fbl/ref_counted.h> |
| #include <fbl/ref_ptr.h> |
| #include <kernel/auto_preempt_disabler.h> |
| #include <kernel/event.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 SchedWeight TEST_LOWEST_WEIGHT = |
| SchedulerState::ConvertPriorityToWeight(LOWEST_PRIORITY + 1); |
| constexpr SchedWeight TEST_HIGHEST_WEIGHT = |
| SchedulerState::ConvertPriorityToWeight(HIGHEST_PRIORITY); |
| constexpr SchedWeight TEST_DEFAULT_WEIGHT = |
| SchedulerState::ConvertPriorityToWeight(DEFAULT_PRIORITY); |
| constexpr SchedWeight TEST_EPSILON_WEIGHT{ffl::FromRatio<int64_t>(1, SchedWeight::Format::Power)}; |
| |
| class TestThread; // fwd decl |
| |
| enum class InheritableProfile { No, Yes }; |
| |
| // An RAII style helper which automatically assigns a deadline profile (with a |
| // short deadline and high utilization)to a thread, restoring the base profile |
| // automatically 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 AutoProfileBooster { |
| public: |
| AutoProfileBooster() : initial_base_profile_(Thread::Current::Get()->SnapshotBaseProfile()) { |
| constexpr SchedUtilization utilization = SchedUtilization{90} / SchedUtilization{100}; |
| constexpr SchedDuration deadline{ZX_USEC(200)}; |
| const SchedulerState::BaseProfile new_base_profile{SchedDeadlineParams{utilization, deadline}}; |
| Thread::Current::Get()->SetBaseProfile(new_base_profile); |
| } |
| |
| ~AutoProfileBooster() { Thread::Current::Get()->SetBaseProfile(initial_base_profile_); } |
| |
| DISALLOW_COPY_ASSIGN_AND_MOVE(AutoProfileBooster); |
| |
| private: |
| const SchedulerState::BaseProfile initial_base_profile_; |
| }; |
| |
| // A small helper which creates different permutations of an input array based |
| // on a distribution method, and optional random seed. Used for things like |
| // determining which profiles will be assigned to which test threads, or which |
| // order threads will be released from the blocked state during various tests. |
| class DistroSpec { |
| public: |
| enum class Type { ASCENDING, DESCENDING, RANDOM, SHUFFLE }; |
| constexpr DistroSpec(Type t, uint64_t s = 0) : type_(t), seed_(s) {} |
| |
| template <typename MemberType, size_t N> |
| void Apply(const ktl::array<MemberType, N>& in, ktl::array<MemberType, N>& out) const { |
| uint64_t prng = seed_; |
| switch (type_) { |
| case DistroSpec::Type::ASCENDING: |
| for (size_t i = 0; i < N; ++i) { |
| out[i] = in[i]; |
| } |
| break; |
| |
| case DistroSpec::Type::DESCENDING: |
| for (size_t i = 0; i < N; ++i) { |
| out[i] = in[in.size() - i - 1]; |
| } |
| break; |
| |
| case DistroSpec::Type::RANDOM: |
| for (auto& item : out) { |
| item = in[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 shuffle order array with a illegal sentinel |
| // value (N will do the job just fine), then foreach i in the range [0, |
| // N) pick a random position in the output to put i, and linearly probe |
| // until we find the first unused position in order to shuffle. |
| ktl::array<size_t, N> order; |
| for (size_t i = 0; i < N; ++i) { |
| order[i] = N; |
| } |
| |
| for (size_t i = 0; i < N; ++i) { |
| size_t pos = (rand_r(&prng) % N); |
| while (order[pos] != N) { |
| pos = (pos + 1) % N; |
| } |
| order[pos] = i; |
| } |
| |
| // Finally, produce our output from our input, permuting using the |
| // shuffle order. |
| for (size_t i = 0; i < N; ++i) { |
| out[i] = in[order[i]]; |
| } |
| break; |
| } |
| } |
| |
| private: |
| const Type type_; |
| const uint64_t seed_; |
| }; |
| |
| struct ExpectedEffectiveProfile { |
| struct { |
| SchedDiscipline discipline{SchedDiscipline::Fair}; |
| SchedWeight fair_weight{0}; |
| SchedDeadlineParams deadline; |
| } base; |
| |
| SchedulerState::InheritedProfileValues ipvs; |
| }; |
| } // namespace |
| |
| namespace unittest { |
| class ThreadEffectiveProfileObserver { |
| public: |
| void Observe(const Thread& t) { observed_profile_ = t.SnapshotEffectiveProfile(); } |
| |
| bool VerifyExpectedEffectiveProfile(const ExpectedEffectiveProfile& eep) { |
| BEGIN_TEST; |
| |
| bool expected_fair = (eep.base.discipline == SchedDiscipline::Fair) && |
| (eep.ipvs.uncapped_utilization == SchedUtilization{0}); |
| ASSERT_EQ(expected_fair, observed_profile_.IsFair()); |
| |
| if (observed_profile_.IsFair()) { |
| SchedWeight expected = eep.base.fair_weight + eep.ipvs.total_weight; |
| EXPECT_EQ(expected.raw_value(), observed_profile_.fair.weight.raw_value()); |
| } else { |
| SchedUtilization effective_utilization = eep.ipvs.uncapped_utilization; |
| SchedDuration effective_deadline = eep.ipvs.min_deadline; |
| |
| if (eep.base.discipline == SchedDiscipline::Deadline) { |
| effective_utilization += eep.base.deadline.utilization; |
| effective_deadline = ktl::min(effective_deadline, eep.base.deadline.deadline_ns); |
| } |
| effective_utilization = ktl::min(effective_utilization, SchedUtilization{1}); |
| |
| SchedDeadlineParams expected{effective_utilization, effective_deadline}; |
| EXPECT_EQ(expected.capacity_ns.raw_value(), |
| observed_profile_.deadline.capacity_ns.raw_value()); |
| EXPECT_EQ(expected.deadline_ns.raw_value(), |
| observed_profile_.deadline.deadline_ns.raw_value()); |
| EXPECT_EQ(expected.utilization.raw_value(), |
| observed_profile_.deadline.utilization.raw_value()); |
| } |
| |
| END_TEST; |
| } |
| |
| private: |
| SchedulerState::EffectiveProfile observed_profile_; |
| }; |
| } // namespace unittest |
| |
| namespace { |
| |
| class Profile : public fbl::RefCounted<Profile> { |
| public: |
| virtual ~Profile() = default; |
| virtual void Apply(Thread& thread) = 0; |
| virtual void SetExpectedBaseProfile(ExpectedEffectiveProfile& eep) = 0; |
| virtual void AccumulateExpectedPressure(ExpectedEffectiveProfile& eep) = 0; |
| virtual size_t DebugPrint(char* buf, size_t space) = 0; |
| |
| protected: |
| Profile() = default; |
| }; |
| |
| class FairProfile : public Profile { |
| public: |
| static fbl::RefPtr<Profile> Create(SchedWeight weight, InheritableProfile inheritable) { |
| fbl::AllocChecker ac; |
| FairProfile* profile = new (&ac) FairProfile(weight, inheritable); |
| if (ac.check()) { |
| return fbl::AdoptRef(profile); |
| } |
| return nullptr; |
| } |
| |
| void Apply(Thread& thread) override { |
| thread.SetBaseProfile( |
| SchedulerState::BaseProfile{weight_, (inheritable_ == InheritableProfile::Yes)}); |
| } |
| |
| void SetExpectedBaseProfile(ExpectedEffectiveProfile& eep) override { |
| eep.base.discipline = SchedDiscipline::Fair; |
| eep.base.fair_weight = weight_; |
| eep.ipvs = SchedulerState::InheritedProfileValues{}; |
| } |
| |
| void AccumulateExpectedPressure(ExpectedEffectiveProfile& eep) override { |
| if (inheritable_ == InheritableProfile::Yes) { |
| eep.ipvs.total_weight += weight_; |
| } |
| } |
| |
| size_t DebugPrint(char* buf, size_t space) override { |
| return snprintf(buf, space, "[weight %ld]", weight_.raw_value()); |
| } |
| |
| private: |
| FairProfile(SchedWeight weight, InheritableProfile inheritable) |
| : weight_(weight), inheritable_(inheritable) { |
| ASSERT(static_cast<uint64_t>(weight_.raw_value()) != 0xFFFFFFFFFFFF0000); |
| } |
| |
| const SchedWeight weight_; |
| const InheritableProfile inheritable_; |
| }; |
| |
| class DeadlineProfile : public Profile { |
| public: |
| static fbl::RefPtr<Profile> Create(zx_duration_t capacity, zx_duration_t deadline) { |
| fbl::AllocChecker ac; |
| DeadlineProfile* profile = |
| new (&ac) DeadlineProfile(SchedDuration{capacity}, SchedDuration{deadline}); |
| if (ac.check()) { |
| return fbl::AdoptRef(profile); |
| } |
| return nullptr; |
| } |
| |
| void Apply(Thread& thread) override { |
| thread.SetBaseProfile(SchedulerState::BaseProfile{sched_params_}); |
| } |
| |
| void SetExpectedBaseProfile(ExpectedEffectiveProfile& eep) override { |
| eep.base.discipline = SchedDiscipline::Deadline; |
| eep.base.deadline = sched_params_; |
| eep.ipvs = SchedulerState::InheritedProfileValues{}; |
| } |
| |
| void AccumulateExpectedPressure(ExpectedEffectiveProfile& eep) override { |
| eep.ipvs.uncapped_utilization += sched_params_.utilization; |
| eep.ipvs.min_deadline = ktl::min(eep.ipvs.min_deadline, sched_params_.deadline_ns); |
| } |
| |
| size_t DebugPrint(char* buf, size_t space) override { |
| return snprintf(buf, space, "[capacity %ld deadline %ld]", |
| sched_params_.capacity_ns.raw_value(), sched_params_.deadline_ns.raw_value()); |
| } |
| |
| const SchedDeadlineParams& sched_params() const { return sched_params_; } |
| |
| private: |
| DeadlineProfile(SchedDuration capacity, SchedDuration deadline) |
| : sched_params_(capacity, deadline) { |
| DEBUG_ASSERT(capacity <= deadline); |
| } |
| |
| const SchedDeadlineParams sched_params_; |
| }; |
| |
| // 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() { |
| AnnotatedAutoEagerReschedDisabler eager_resched_disabler; |
| OwnedWaitQueue::WakeThreads(ktl::numeric_limits<uint32_t>::max()); |
| } |
| |
| void ReleaseOneThread() { |
| AnnotatedAutoEagerReschedDisabler eager_resched_disabler; |
| OwnedWaitQueue::WakeThreadAndAssignOwner(); |
| } |
| }; |
| |
| // 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) { |
| return; |
| } |
| |
| char buffer[256]; |
| size_t offset = 0; |
| |
| offset += snprintf(buffer + offset, ktl::size(buffer) - offset, |
| "Test failed with %s == ", field_name_); |
| |
| if constexpr (ktl::is_same_v<T, fbl::RefPtr<Profile>>) { |
| offset += iter_val_->DebugPrint(buffer + offset, ktl::size(buffer) - offset); |
| } else { |
| offset += snprintf(buffer + offset, ktl::size(buffer) - offset, "%ld", |
| static_cast<int64_t>(iter_val_)); |
| } |
| |
| printf("%s\n", buffer); |
| } |
| |
| 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_.Unsignal(); } |
| |
| // Clear the barrier and allow shutdown. |
| static void ClearShutdownBarrier() { allow_shutdown_.Signal(); } |
| |
| static Event& allow_shutdown() { return allow_shutdown_; } |
| |
| // Create a thread, settings its entry point and initial profile in |
| // the process, but do not start it yet. |
| bool Create(fbl::RefPtr<Profile> initial_profile); |
| |
| // 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 standard wait queue. |
| bool BlockOnWaitQueue(WaitQueue* wq, zx::duration relative_timeout = zx::duration::infinite()) |
| TA_EXCL(chainlock_transaction_token); |
| |
| // 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 BlockOnOwnedWaitQueue(OwnedWaitQueue* owned_wq, TestThread* owner, |
| zx::duration relative_timeout = zx::duration::infinite()); |
| |
| // Directly take ownership of the specified wait queue using AssignOwner. |
| bool TakeOwnership(OwnedWaitQueue* owned_wq); |
| |
| // 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); |
| |
| State state() const { return state_.load(); } |
| Profile* initial_profile() const { return initial_profile_.get(); } |
| Thread& thread() const { |
| DEBUG_ASSERT(thread_ != nullptr); |
| return *thread_; |
| } |
| |
| thread_state tstate() const { |
| if (thread_ == nullptr) { |
| return thread_state::THREAD_DEATH; |
| } |
| |
| SingletonChainLockGuardIrqSave guard{thread_->get_lock(), |
| CLT_TAG("TestThread::tstate (pi_tests)")}; |
| 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 inline Event allow_shutdown_{}; |
| |
| Thread* thread_ = nullptr; |
| ktl::atomic<State> state_{State::INITIAL}; |
| fit::inline_function<void(void), kMaxOpLambdaCaptureStorageBytes> op_; |
| fbl::RefPtr<Profile> initial_profile_; |
| }; |
| |
| bool TestThread::Create(fbl::RefPtr<Profile> initial_profile) { |
| BEGIN_TEST; |
| |
| ASSERT_NULL(thread_); |
| ASSERT_NULL(initial_profile_); |
| ASSERT_EQ(state(), State::INITIAL); |
| |
| initial_profile_ = initial_profile; |
| thread_ = Thread::Create( |
| "pi_test_thread", |
| [](void* ctx) -> int { return reinterpret_cast<TestThread*>(ctx)->ThreadEntry(); }, |
| reinterpret_cast<void*>(this), DEFAULT_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::BlockOnWaitQueue(WaitQueue* wq, zx::duration relative_timeout) { |
| BEGIN_TEST; |
| ASSERT_EQ(state(), State::CREATED); |
| ASSERT_FALSE(static_cast<bool>(op_)); |
| |
| op_ = [wq, relative_timeout]() TA_EXCL(chainlock_transaction_token) { |
| Deadline timeout = (relative_timeout == zx::duration::infinite()) |
| ? Deadline::infinite() |
| : Deadline::after(relative_timeout.get()); |
| |
| ChainLockTransactionPreemptDisableAndIrqSave clt{ |
| CLT_TAG("TestThread::BlockOnWaitQueue (pi_tests)")}; |
| for (;; clt.Relax()) { |
| Thread* const current_thread = Thread::Current::Get(); |
| ktl::array locks{¤t_thread->get_lock(), &wq->get_lock()}; |
| ChainLock::LockResult res = AcquireChainLockSet(locks); |
| |
| if (res == ChainLock::LockResult::kBackoff) { |
| continue; |
| } |
| |
| DEBUG_ASSERT(res == ChainLock::LockResult::kOk); |
| clt.Finalize(); |
| |
| current_thread->get_lock().AssertAcquired(); |
| wq->get_lock().AssertAcquired(); |
| wq->Block(current_thread, timeout, Interruptible::Yes); |
| current_thread->get_lock().Release(); |
| break; |
| } |
| }; |
| |
| state_.store(State::WAITING_TO_START); |
| thread_->Resume(); |
| |
| ASSERT_TRUE(WaitFor<Condition::BLOCKED>()); |
| |
| END_TEST; |
| } |
| |
| bool TestThread::BlockOnOwnedWaitQueue(OwnedWaitQueue* owned_wq, TestThread* owner, |
| zx::duration relative_timeout) { |
| BEGIN_TEST; |
| ASSERT_EQ(state(), State::CREATED); |
| ASSERT_FALSE(static_cast<bool>(op_)); |
| |
| op_ = [owned_wq, owner_thrd = owner ? owner->thread_ : nullptr, relative_timeout]() { |
| AnnotatedAutoEagerReschedDisabler eager_resched_disabler; |
| |
| Deadline timeout = (relative_timeout == zx::duration::infinite()) |
| ? Deadline::infinite() |
| : Deadline::after(relative_timeout.get()); |
| |
| 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_.is_signaled()); |
| } |
| |
| 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(); |
| } |
| |
| // The thread should be on its way to termination as we speak. Attempt to |
| // join it with a relatively short timeout. If this fails, print a |
| // warning and try again with an infinite timeout. Why try with a short |
| // timeout and then an infinite timeout? We might be running in an |
| // emulated or virtualized environment and things may take a lot longer |
| // that they otherwise would. By timing out quickly and printing an |
| // warning, we can hopefully make it easier for a developer to figure out |
| // what's going on in the case where the second join hangs forever. |
| constexpr zx_duration_t timeout = ZX_MSEC(500); |
| ASSERT(thread_ != nullptr); |
| int ret_code; |
| const Deadline join_deadline = Deadline::after(timeout); |
| zx_status_t res = thread_->Join(&ret_code, join_deadline.when()); |
| if (res == ZX_ERR_TIMED_OUT) { |
| printf("Timed out while joining thread %p, retrying with infinite timeout\n", thread_); |
| res = thread_->Join(&ret_code, ZX_TIME_INFINITE); |
| } |
| if (res != ZX_OK) { |
| panic("join of thread %p failed with %d\n", thread_, res); |
| } |
| thread_ = nullptr; |
| } |
| |
| state_.store(State::INITIAL); |
| op_ = nullptr; |
| initial_profile_ = nullptr; |
| ASSERT_NULL(thread_); |
| |
| END_TEST; |
| } |
| |
| int TestThread::ThreadEntry() { |
| if (!static_cast<bool>(op_) || (state() != State::WAITING_TO_START)) { |
| return -1; |
| } |
| |
| initial_profile_->Apply(*thread_); |
| state_.store(State::STARTED); |
| op_(); |
| state_.store(State::WAITING_FOR_SHUTDOWN); |
| // Do not block on the allow shutdown event if we have received the kill |
| // signal. Zircon events are non-interruptible. |
| if ((thread_->signals() & THREAD_SIGNAL_KILL) == 0) { |
| 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; |
| } |
| |
| bool pi_test_basic() { |
| BEGIN_TEST; |
| |
| AutoProfileBooster pboost; |
| enum class ReleaseMethod { WAKE = 0, TIMEOUT, KILL }; |
| constexpr ReleaseMethod REL_METHODS[] = {ReleaseMethod::WAKE, ReleaseMethod::TIMEOUT, |
| ReleaseMethod::KILL}; |
| constexpr zx::duration TIMEOUT_RELEASE_DURATION = zx::msec(10); |
| constexpr uint32_t RETRY_LIMIT = 100; |
| |
| // create the array of profiles we will use during the test, then verify that |
| // all of them were successfully allocated before proceeding. |
| const ktl::array profiles = { |
| FairProfile::Create(TEST_DEFAULT_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT + TEST_EPSILON_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT - TEST_EPSILON_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT, InheritableProfile::No), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT + TEST_EPSILON_WEIGHT, InheritableProfile::No), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT - TEST_EPSILON_WEIGHT, InheritableProfile::No), |
| DeadlineProfile::Create(ZX_MSEC(2), ZX_MSEC(5)), |
| DeadlineProfile::Create(ZX_USEC(200), ZX_MSEC(1)), |
| }; |
| |
| for (auto& profile : profiles) { |
| ASSERT_NONNULL(profile); |
| } |
| |
| // Test every combination of profiles in a test where one thread waits while |
| // another thread blocks behind it, applying profile pressure. Validate that |
| // the receiving thread has the proper effective profile after receiving |
| // pressure, and that the effective profile relaxes back to the initial |
| // profile after the thread applying pressure ceases to do so for each of the |
| // various release methods. |
| for (auto& blocking_profile : profiles) { |
| for (auto& pressure_profile : profiles) { |
| for (auto rel_method : REL_METHODS) { |
| PRINT_LOOP_ITER(blocking_profile); |
| PRINT_LOOP_ITER(pressure_profile); |
| PRINT_LOOP_ITER(rel_method); |
| |
| uint32_t retry_count = 0; |
| bool retry_test; |
| do { |
| retry_test = false; |
| |
| LockedOwnedWaitQueue owq; |
| TestThread pressure_thread; |
| TestThread blocking_thread; |
| ExpectedEffectiveProfile expected_profile; |
| unittest::ThreadEffectiveProfileObserver observer; |
| |
| 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 2 threads, each with the appropriate profile. |
| ASSERT_TRUE(blocking_thread.Create(blocking_profile)); |
| ASSERT_TRUE(pressure_thread.Create(pressure_profile)); |
| |
| // Start the first thread, wait for it to block, and verify that it's |
| // profile is correct (it should not be changed). |
| ASSERT_TRUE(blocking_thread.DoStall()); |
| blocking_profile->SetExpectedBaseProfile(expected_profile); |
| observer.Observe(blocking_thread.thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| |
| // 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. |
| zx::duration relative_timeout = (rel_method == ReleaseMethod::TIMEOUT) |
| ? TIMEOUT_RELEASE_DURATION |
| : zx::duration::infinite(); |
| ASSERT_TRUE( |
| pressure_thread.BlockOnOwnedWaitQueue(&owq, &blocking_thread, relative_timeout)); |
| |
| // Observe the effective profile 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. |
| observer.Observe(blocking_thread.thread()); |
| if (rel_method == ReleaseMethod::TIMEOUT) { |
| retry_test = (pressure_thread.tstate() != thread_state::THREAD_BLOCKED) || |
| (pressure_thread.state() == TestThread::State::WAITING_FOR_SHUTDOWN); |
| } |
| |
| // Only verify this if we managed to observe the blocked thread's |
| // effective profile while the pressure thread was still applying |
| // pressure. |
| if (!retry_test) { |
| pressure_profile->AccumulateExpectedPressure(expected_profile); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| } |
| |
| // 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 profile drops back down to what it |
| // was initially. |
| 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; |
| } |
| |
| blocking_profile->SetExpectedBaseProfile(expected_profile); |
| observer.Observe(blocking_thread.thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| } while (retry_test && (++retry_count < RETRY_LIMIT)); |
| |
| ASSERT_FALSE(retry_test, "Failed timeout race too many times!"); |
| |
| print_blocking_profile.cancel(); |
| print_pressure_profile.cancel(); |
| print_rel_method.cancel(); |
| } |
| } |
| } |
| |
| END_TEST; |
| } |
| |
| bool pi_test_changing_priority() { |
| BEGIN_TEST; |
| |
| AutoProfileBooster pboost; |
| LockedOwnedWaitQueue owq; |
| TestThread pressure_thread; |
| TestThread blocking_thread; |
| |
| auto cleanup = fit::defer([&]() { |
| TestThread::ClearShutdownBarrier(); |
| owq.ReleaseAllThreads(); |
| pressure_thread.Reset(); |
| blocking_thread.Reset(); |
| }); |
| |
| const ktl::array profiles = { |
| FairProfile::Create(TEST_DEFAULT_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT + TEST_EPSILON_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT - TEST_EPSILON_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_LOWEST_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_HIGHEST_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT, InheritableProfile::No), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT + TEST_EPSILON_WEIGHT, InheritableProfile::No), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT - TEST_EPSILON_WEIGHT, InheritableProfile::No), |
| FairProfile::Create(TEST_LOWEST_WEIGHT, InheritableProfile::No), |
| FairProfile::Create(TEST_HIGHEST_WEIGHT, InheritableProfile::No), |
| DeadlineProfile::Create(ZX_MSEC(2), ZX_MSEC(5)), |
| DeadlineProfile::Create(ZX_USEC(200), ZX_MSEC(1)), |
| }; |
| |
| for (auto& profile : profiles) { |
| ASSERT_NONNULL(profile); |
| } |
| |
| ExpectedEffectiveProfile expected_profile; |
| unittest::ThreadEffectiveProfileObserver observer; |
| |
| // Make sure that our default barriers have been reset to their proper |
| // initial states. |
| TestThread::ResetShutdownBarrier(); |
| |
| // Create our threads. |
| ASSERT_TRUE(blocking_thread.Create(profiles[0])); |
| ASSERT_TRUE(pressure_thread.Create(profiles[0])); |
| |
| // Start the first thread, wait for it to block, and verify that it's |
| // profile is correct (it should not be changed). |
| ASSERT_TRUE(blocking_thread.DoStall()); |
| blocking_thread.initial_profile()->SetExpectedBaseProfile(expected_profile); |
| observer.Observe(blocking_thread.thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| |
| // Block the second thread behind the first. |
| ASSERT_TRUE(pressure_thread.BlockOnOwnedWaitQueue(&owq, &blocking_thread)); |
| pressure_thread.initial_profile()->AccumulateExpectedPressure(expected_profile); |
| observer.Observe(blocking_thread.thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| |
| // Changing the pressure thread's profile to a number of different profiles, |
| // verifying that the pressure felt by the blocking thread changes |
| // appropriately as we do. |
| for (auto profile : profiles) { |
| PRINT_LOOP_ITER(profile); |
| |
| profile->Apply(pressure_thread.thread()); |
| observer.Observe(blocking_thread.thread()); |
| |
| blocking_thread.initial_profile()->SetExpectedBaseProfile(expected_profile); |
| profile->AccumulateExpectedPressure(expected_profile); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| |
| print_profile.cancel(); |
| } |
| |
| // Release the pressure thread, validate that the priority is what we |
| // started with and we are done. |
| owq.ReleaseAllThreads(); |
| blocking_thread.initial_profile()->SetExpectedBaseProfile(expected_profile); |
| observer.Observe(blocking_thread.thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| |
| END_TEST; |
| } |
| |
| // A simple smoke test to make sure that we can change a thread's base priority |
| // when it is blocked in a normal wait queue (instead of an owned wait queue) |
| bool pi_test_changing_priority_in_wait_queue() { |
| BEGIN_TEST; |
| |
| AutoProfileBooster pboost; |
| WaitQueue wq; |
| TestThread thread; |
| |
| auto cleanup = fit::defer([&]() { |
| TestThread::ClearShutdownBarrier(); |
| wq.WakeAll(ZX_OK); |
| thread.Reset(); |
| }); |
| |
| const ktl::array profiles = { |
| FairProfile::Create(TEST_DEFAULT_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT + TEST_EPSILON_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT - TEST_EPSILON_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_LOWEST_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_HIGHEST_WEIGHT, InheritableProfile::Yes), |
| DeadlineProfile::Create(ZX_MSEC(2), ZX_MSEC(5)), |
| DeadlineProfile::Create(ZX_USEC(200), ZX_MSEC(1)), |
| }; |
| |
| for (auto& profile : profiles) { |
| ASSERT_NONNULL(profile); |
| } |
| |
| ExpectedEffectiveProfile expected_profile; |
| unittest::ThreadEffectiveProfileObserver observer; |
| |
| // Make sure that our default barriers have been reset to their proper |
| // initial states. |
| TestThread::ResetShutdownBarrier(); |
| |
| // Create our thread and have it block in our wait queue. |
| ASSERT_TRUE(thread.Create(profiles[0])); |
| ASSERT_TRUE(thread.BlockOnWaitQueue(&wq)); |
| thread.initial_profile()->SetExpectedBaseProfile(expected_profile); |
| observer.Observe(thread.thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| |
| // Changing the thread's base profile while it is blocked in the queue. Its |
| // effective profile should always match the base profile we set. |
| for (auto profile : profiles) { |
| PRINT_LOOP_ITER(profile); |
| |
| profile->Apply(thread.thread()); |
| profile->SetExpectedBaseProfile(expected_profile); |
| observer.Observe(thread.thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| |
| print_profile.cancel(); |
| } |
| |
| END_TEST; |
| } |
| |
| bool pi_test_chain() { |
| BEGIN_TEST; |
| |
| enum class ReleaseOrder : uint64_t { ASCENDING = 0, DESCENDING }; |
| struct Link { |
| LockedOwnedWaitQueue queue; |
| bool active = false; |
| }; |
| |
| const ktl::array profile_deck{ |
| FairProfile::Create(TEST_DEFAULT_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_LOWEST_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_HIGHEST_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT, InheritableProfile::No), |
| FairProfile::Create(TEST_LOWEST_WEIGHT, InheritableProfile::No), |
| FairProfile::Create(TEST_HIGHEST_WEIGHT, InheritableProfile::No), |
| DeadlineProfile::Create(ZX_MSEC(2), ZX_MSEC(5)), |
| DeadlineProfile::Create(ZX_USEC(200), ZX_MSEC(1)), |
| }; |
| constexpr size_t CHAIN_LEN = profile_deck.size(); |
| |
| const ktl::array PRIORITY_GENERATORS = { |
| DistroSpec{DistroSpec::Type::ASCENDING}, |
| DistroSpec{DistroSpec::Type::DESCENDING}, |
| DistroSpec{DistroSpec::Type::SHUFFLE, 0xbd6f3bfe33d51c8e}, |
| DistroSpec{DistroSpec::Type::SHUFFLE, 0x857ce1aa3209ecc7}, |
| }; |
| |
| const ktl::array RELEASE_ORDERS = { |
| DistroSpec{DistroSpec::Type::ASCENDING}, |
| DistroSpec{DistroSpec::Type::DESCENDING}, |
| DistroSpec{DistroSpec::Type::SHUFFLE, 0xac8d4a8ed016caf0}, |
| DistroSpec{DistroSpec::Type::SHUFFLE, 0xb51e76ca5cf20875}, |
| }; |
| |
| ktl::array<TestThread, CHAIN_LEN> threads; |
| ktl::array<Link, CHAIN_LEN - 1> links; |
| ktl::array<size_t, CHAIN_LEN - 1> release_deck; |
| |
| for (auto& profile : profile_deck) { |
| ASSERT_NONNULL(profile); |
| } |
| |
| for (size_t i = 0; i < ktl::size(release_deck); ++i) { |
| release_deck[i] = i; |
| } |
| |
| AutoProfileBooster pboost; |
| for (uint32_t pgen_ndx = 0; pgen_ndx < ktl::size(PRIORITY_GENERATORS); ++pgen_ndx) { |
| PRINT_LOOP_ITER(pgen_ndx); |
| |
| // Generate the profile order to use for this pass. |
| ktl::array<fbl::RefPtr<Profile>, CHAIN_LEN> profiles; |
| PRIORITY_GENERATORS[pgen_ndx].Apply(profile_deck, profiles); |
| |
| 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 |
| decltype(release_deck) release_ordering; |
| RELEASE_ORDERS[ro_ndx].Apply(release_deck, release_ordering); |
| |
| auto cleanup = fit::defer([&]() { |
| TestThread::ClearShutdownBarrier(); |
| for (auto& l : links) { |
| l.queue.ReleaseAllThreads(); |
| } |
| for (auto& t : threads) { |
| t.Reset(); |
| } |
| }); |
| |
| // Lambda used to validate the effective profiles of the threads currently |
| // involved in the chain. |
| auto ValidatePriorities = [&]() -> bool { |
| BEGIN_TEST; |
| |
| for (size_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); |
| } |
| |
| // The effective profile of this thread should be its base profile, |
| // plus all of the profile pressure received from the actively linked |
| // thread. |
| ExpectedEffectiveProfile expected_profile; |
| threads[tndx].initial_profile()->SetExpectedBaseProfile(expected_profile); |
| for (size_t i = tndx; (i < ktl::size(links)) && (links[i].active); ++i) { |
| ASSERT_LT(i + 1, ktl::size(threads)); |
| threads[i + 1].initial_profile()->AccumulateExpectedPressure(expected_profile); |
| } |
| |
| unittest::ThreadEffectiveProfileObserver observer; |
| observer.Observe(threads[tndx].thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| |
| print_tndx.cancel(); |
| } |
| |
| END_TEST; |
| }; |
| |
| // Make sure that our default barriers have been reset to their proper |
| // initial states. |
| TestThread::ResetShutdownBarrier(); |
| |
| // Create our threads. |
| static_assert(ktl::size(threads) == ktl::size(profiles)); |
| for (uint32_t tndx = 0; tndx < ktl::size(threads); ++tndx) { |
| PRINT_LOOP_ITER(tndx); |
| ASSERT_TRUE(threads[tndx].Create(profiles[tndx])); |
| print_tndx.cancel(); |
| } |
| |
| // Start the head of the chain, wait for it to block, then verify that its |
| // profile 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].BlockOnOwnedWaitQueue(&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, ktl::size(links)); |
| 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; |
| } |
| |
| bool pi_test_multi_waiter() { |
| BEGIN_TEST; |
| AutoProfileBooster pboost; |
| |
| const ktl::array profile_deck{ |
| FairProfile::Create(TEST_DEFAULT_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_LOWEST_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_HIGHEST_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT, InheritableProfile::No), |
| FairProfile::Create(TEST_LOWEST_WEIGHT, InheritableProfile::No), |
| FairProfile::Create(TEST_HIGHEST_WEIGHT, InheritableProfile::No), |
| DeadlineProfile::Create(ZX_MSEC(2), ZX_MSEC(5)), |
| DeadlineProfile::Create(ZX_USEC(200), ZX_MSEC(1)), |
| }; |
| constexpr size_t WAITER_CNT = profile_deck.size(); |
| |
| const ktl::array PRIORITY_GENERATORS = { |
| DistroSpec{DistroSpec::Type::ASCENDING}, |
| DistroSpec{DistroSpec::Type::DESCENDING}, |
| DistroSpec{DistroSpec::Type::RANDOM, 0x87251211471cb789}, |
| DistroSpec{DistroSpec::Type::SHUFFLE, 0x857ce1aa3209ecc7}, |
| }; |
| |
| LockedOwnedWaitQueue blocking_queue; |
| TestThread blocking_thread; |
| struct Waiter { |
| TestThread thread; |
| bool started = false; |
| bool is_waiting = false; |
| |
| void Reset() { |
| thread.Reset(); |
| started = false; |
| is_waiting = false; |
| } |
| }; |
| ktl::array<Waiter, WAITER_CNT> waiters; |
| |
| for (auto bt_profile : profile_deck) { |
| PRINT_LOOP_ITER(bt_profile); |
| |
| 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.Reset(); |
| } |
| }); |
| |
| // Make sure that our barriers have been reset. |
| TestThread::ResetShutdownBarrier(); |
| |
| // Select the profiles to apply to the waiter threads during this pass. |
| ktl::array<fbl::RefPtr<Profile>, WAITER_CNT> profiles; |
| PRIORITY_GENERATORS[pgen_ndx].Apply(profile_deck, profiles); |
| |
| // Create all of the threads. |
| ASSERT_TRUE(blocking_thread.Create(bt_profile)); |
| for (uint32_t waiter_ndx = 0; waiter_ndx < ktl::size(waiters); ++waiter_ndx) { |
| PRINT_LOOP_ITER(waiter_ndx); |
| |
| static_assert(ktl::size(waiters) == ktl::size(profiles)); |
| ASSERT_TRUE(waiters[waiter_ndx].thread.Create(profiles[waiter_ndx])); |
| |
| 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; |
| |
| ExpectedEffectiveProfile expected_profile; |
| unittest::ThreadEffectiveProfileObserver observer; |
| |
| // The expected profile for the current owner of the OWQ should be its |
| // base profile, plus all of the pressure from each of the waiting |
| // threads. |
| ASSERT_NONNULL(current_owner); |
| current_owner->initial_profile()->SetExpectedBaseProfile(expected_profile); |
| for (const auto& waiter : waiters) { |
| if (waiter.is_waiting) { |
| waiter.thread.initial_profile()->AccumulateExpectedPressure(expected_profile); |
| } |
| } |
| |
| observer.Observe(current_owner->thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| |
| // Every waiter thread which has started (waiting or not) should be |
| // running with its initial profile, unless it happens to currently be |
| // the owner of the OWQ. |
| for (size_t waiter_ndx = 0; waiter_ndx < ktl::size(waiters); ++waiter_ndx) { |
| PRINT_LOOP_ITER(waiter_ndx); |
| |
| const Waiter& waiter = waiters[waiter_ndx]; |
| if (waiter.started && (&waiter.thread != current_owner)) { |
| waiter.thread.initial_profile()->SetExpectedBaseProfile(expected_profile); |
| observer.Observe(waiter.thread.thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| } |
| |
| print_waiter_ndx.cancel(); |
| } |
| |
| 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 all of |
| // the threads' effective profiles as we go. |
| for (uint32_t waiter_ndx = 0; waiter_ndx < ktl::size(waiters); ++waiter_ndx) { |
| PRINT_LOOP_ITER(waiter_ndx); |
| |
| auto& w = waiters[waiter_ndx]; |
| ASSERT_TRUE(w.thread.BlockOnOwnedWaitQueue(&blocking_queue, current_owner)); |
| w.started = true; |
| 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 < ktl::size(waiters); ++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 profiles. |
| ASSERT_TRUE(ValidatePriorities()); |
| |
| print_tndx.cancel(); |
| } |
| |
| print_pgen_ndx.cancel(); |
| } |
| print_bt_profile.cancel(); |
| } |
| |
| END_TEST; |
| } |
| |
| bool pi_test_multi_owned_queues() { |
| BEGIN_TEST; |
| AutoProfileBooster pboost; |
| |
| const ktl::array profile_deck{ |
| FairProfile::Create(TEST_DEFAULT_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_LOWEST_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_HIGHEST_WEIGHT, InheritableProfile::Yes), |
| FairProfile::Create(TEST_DEFAULT_WEIGHT, InheritableProfile::No), |
| FairProfile::Create(TEST_LOWEST_WEIGHT, InheritableProfile::No), |
| FairProfile::Create(TEST_HIGHEST_WEIGHT, InheritableProfile::No), |
| DeadlineProfile::Create(ZX_MSEC(2), ZX_MSEC(5)), |
| DeadlineProfile::Create(ZX_USEC(200), ZX_MSEC(1)), |
| }; |
| constexpr size_t QUEUE_CNT = profile_deck.size(); |
| |
| struct Waiter { |
| TestThread thread; |
| LockedOwnedWaitQueue queue; |
| bool is_started = false; |
| bool is_waiting = false; |
| |
| void Reset() { |
| queue.ReleaseAllThreads(); |
| thread.Reset(); |
| is_started = false; |
| is_waiting = false; |
| } |
| }; |
| |
| TestThread blocking_thread; |
| ktl::array<Waiter, QUEUE_CNT> queues; |
| |
| const ktl::array PRIORITY_GENERATORS = { |
| DistroSpec{DistroSpec::Type::ASCENDING}, |
| DistroSpec{DistroSpec::Type::DESCENDING}, |
| DistroSpec{DistroSpec::Type::RANDOM, 0xb89e3b7442b95a1c}, |
| DistroSpec{DistroSpec::Type::SHUFFLE, 0x06ec82d4ade8efba}, |
| }; |
| |
| for (auto bt_profile : profile_deck) { |
| PRINT_LOOP_ITER(bt_profile); |
| |
| 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.Reset(); |
| } |
| }); |
| |
| // Make sure that our barriers have been reset. |
| TestThread::ResetShutdownBarrier(); |
| |
| // Select the profiles to apply to the waiter threads during this pass. |
| ktl::array<fbl::RefPtr<Profile>, QUEUE_CNT> profiles; |
| PRIORITY_GENERATORS[pgen_ndx].Apply(profile_deck, profiles); |
| |
| // Create all of the threads. |
| ASSERT_TRUE(blocking_thread.Create(bt_profile)); |
| for (uint32_t queue_ndx = 0; queue_ndx < ktl::size(queues); ++queue_ndx) { |
| PRINT_LOOP_ITER(queue_ndx); |
| ASSERT_TRUE(queues[queue_ndx].thread.Create(profiles[queue_ndx])); |
| 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; |
| |
| ExpectedEffectiveProfile expected_profile; |
| unittest::ThreadEffectiveProfileObserver observer; |
| |
| // Each of the started queue threads (waiting or not) should simply have their |
| // base profile. Verify this. |
| for (uint32_t queue_ndx = 0; queue_ndx < ktl::size(queues); ++queue_ndx) { |
| PRINT_LOOP_ITER(queue_ndx); |
| const auto& q = queues[queue_ndx]; |
| |
| if (q.is_started) { |
| q.thread.initial_profile()->SetExpectedBaseProfile(expected_profile); |
| observer.Observe(q.thread.thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| } |
| |
| print_queue_ndx.cancel(); |
| } |
| |
| // The effective profile of the blocking_thread should be the |
| // combination of its base profile, in addition to all of the currently |
| // blocked threads whose OWQs are owned by the blocking thread. |
| blocking_thread.initial_profile()->SetExpectedBaseProfile(expected_profile); |
| for (const auto& q : queues) { |
| if (q.is_waiting) { |
| q.thread.initial_profile()->AccumulateExpectedPressure(expected_profile); |
| } |
| } |
| |
| observer.Observe(blocking_thread.thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| |
| 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 < ktl::size(queues); ++queue_ndx) { |
| PRINT_LOOP_ITER(queue_ndx); |
| |
| auto& q = queues[queue_ndx]; |
| ASSERT_TRUE(q.thread.BlockOnOwnedWaitQueue(&q.queue, &blocking_thread)); |
| q.is_started = true; |
| 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 < ktl::size(queues); ++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_profile.cancel(); |
| } |
| |
| END_TEST; |
| } |
| |
| template <InheritableProfile kInheritableProfiles> |
| bool pi_test_cycle() { |
| BEGIN_TEST; |
| AutoProfileBooster pboost; |
| |
| // Deliberately create a cycle and make sure that we don't hang or otherwise |
| // exhibit bad behavior. |
| struct Link { |
| TestThread thread; |
| LockedOwnedWaitQueue link; |
| fbl::RefPtr<Profile> profile; |
| }; |
| |
| static constexpr size_t CYCLE_LEN = 4; |
| ktl::array<Link, CYCLE_LEN> nodes; |
| |
| // 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 each of the profiles and assign them to each of the threads. |
| for (uint32_t tndx = 0; tndx < ktl::size(nodes); ++tndx) { |
| PRINT_LOOP_ITER(tndx); |
| |
| SchedWeight tgt_weight = TEST_LOWEST_WEIGHT + (tndx * TEST_EPSILON_WEIGHT); |
| ASSERT_LE(tgt_weight.raw_value(), TEST_HIGHEST_WEIGHT.raw_value()); |
| nodes[tndx].profile = FairProfile::Create(tgt_weight, kInheritableProfiles); |
| ASSERT_NONNULL(nodes[tndx].profile); |
| ASSERT_TRUE(nodes[tndx].thread.Create(nodes[tndx].profile)); |
| |
| 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 attempt to form a cycle. |
| // |
| // As of today, the OwnedWaitQueue code will refuse to create the cycle and |
| // will "fix" the problem by allowing the thread to block, but declaring the |
| // owner of the OWQ the thread is blocking in to be no one. Our threads are |
| // in ascending priority order, so we should not see any changes to the |
| // effective priority of any of the threads. |
| // |
| // Eventually, however, the Block operation will completely fail instead of |
| // allowing the cycle to come into existence. It will not change the owner, |
| // nor will it block the thread in question. Instead, it will return an error. |
| // This test will need to be updated when that day arrives. |
| for (size_t tndx = 0; tndx < ktl::size(nodes); ++tndx) { |
| PRINT_LOOP_ITER(tndx); |
| |
| TestThread& owner_thread = nodes[(tndx + 1) % ktl::size(nodes)].thread; |
| LockedOwnedWaitQueue& link = nodes[tndx].link; |
| ASSERT_TRUE(nodes[tndx].thread.BlockOnOwnedWaitQueue(&link, &owner_thread)); |
| |
| for (size_t validation_ndx = 0; validation_ndx <= tndx; ++validation_ndx) { |
| PRINT_LOOP_ITER(validation_ndx); |
| |
| // The profile of each link in the chain should be the combination of its |
| // base profile and all of the threads in the chain which are blocked |
| // behind it. |
| ExpectedEffectiveProfile expected_profile; |
| nodes[validation_ndx].thread.initial_profile()->SetExpectedBaseProfile(expected_profile); |
| for (size_t i = 1; i <= validation_ndx; ++i) { |
| TestThread& thread = nodes[validation_ndx - i].thread; |
| thread.initial_profile()->AccumulateExpectedPressure(expected_profile); |
| } |
| |
| unittest::ThreadEffectiveProfileObserver observer; |
| observer.Observe(nodes[validation_ndx].thread.thread()); |
| ASSERT_TRUE(observer.VerifyExpectedEffectiveProfile(expected_profile)); |
| |
| // Every OWQ in the test vector should be owned by the thread after it in |
| // the sequence, except for the last OWQ. We tried to assign it to be |
| // owned by the first thread when we tried to create the cycle, but the |
| // implementation should have refused and left the queue with now owner. |
| auto ObserveOwner = [](OwnedWaitQueue& queue) -> const Thread* { |
| SingletonChainLockGuardIrqSave guard{queue.get_lock(), CLT_TAG("pi_test_cycle")}; |
| return queue.owner(); |
| }; |
| |
| const Thread* const expected_owner = |
| (tndx < (nodes.size() - 1)) ? &(nodes[tndx + 1].thread.thread()) : nullptr; |
| const Thread* const observed_owner = ObserveOwner(nodes[tndx].link); |
| ASSERT_EQ(expected_owner, observed_owner); |
| |
| print_validation_ndx.cancel(); |
| } |
| |
| print_tndx.cancel(); |
| } |
| |
| END_TEST; |
| } |
| |
| bool bug_42182770_regression() { |
| BEGIN_TEST; |
| |
| // Set up and test a situation which mimics the one encountered in during bug 42182770. |
| // Basically, we want to: |
| // |
| // 1) Block a thread A which has a fair profile in an owned wait queue W. Be |
| // sure to test both inheritable and non-inheritable profiles. |
| // 2) Observe that the start time, finish time, and time slice of W (the |
| // "dynamic" IPVs) remain zero (the initial defaults) A has blocked in W, |
| // and it was the first thread to block, but its profile is |
| // non-inheritable, therefore the dynamic parameters remain undefined. |
| // 3) Block a thread B, which has an inheritable deadline profile in W. |
| // 4) Observe that W's IPVs have taken on B's dynamic scheduler profile values. |
| // 5) Unblock B from W. Observe that W still has IPV storage allocated to it, |
| // but that the static parameters (total weight, uncapped utilization, min |
| // deadline) have returned to their default values. If extra scheduler |
| // invariant validation is turned on, the static parameters should have |
| // returned to their defaults as well, otherwise they should remain |
| // unchanged. |
| // 6) Unblock A from W. After this operation, W should no longer have any |
| // inherited profile value storage allocated to it as it no longer has any |
| // waiting threads. |
| |
| constexpr ktl::array kInheritableOptions = {InheritableProfile::No, InheritableProfile::Yes}; |
| for (const InheritableProfile allow_inherit : kInheritableOptions) { |
| // Create the queue, profiles, and threads we will need to run the test. |
| fbl::AllocChecker ac; |
| ktl::unique_ptr<LockedOwnedWaitQueue> owq = ktl::make_unique<LockedOwnedWaitQueue>(&ac); |
| ASSERT_TRUE(ac.check()); |
| |
| fbl::RefPtr<Profile> fair_profile = FairProfile::Create(TEST_DEFAULT_WEIGHT, allow_inherit); |
| fbl::RefPtr<Profile> deadline_profile = DeadlineProfile::Create(ZX_MSEC(2), ZX_MSEC(5)); |
| ASSERT_NONNULL(fair_profile); |
| ASSERT_NONNULL(deadline_profile); |
| |
| const SchedWeight inheritable_weight = |
| (allow_inherit == InheritableProfile::Yes) ? TEST_DEFAULT_WEIGHT : SchedWeight{0}; |
| |
| TestThread fair_thread, deadline_thread; |
| ASSERT_TRUE(fair_thread.Create(fair_profile)); |
| ASSERT_TRUE(deadline_thread.Create(deadline_profile)); |
| |
| // Make sure that our default barriers have been reset to their proper initial |
| // states, and that we will properly release threads from their blocked state |
| // if anything goes wrong during the test and we bail out. |
| TestThread::ResetShutdownBarrier(); |
| auto cleanup = fit::defer([&]() { |
| TestThread::ClearShutdownBarrier(); |
| owq->ReleaseAllThreads(); |
| fair_thread.Reset(); |
| deadline_thread.Reset(); |
| }); |
| |
| // Verify that the OWQ has no inherited profile storage allocated to it. This |
| // should always be the case when there are no waiters. |
| EXPECT_NULL(owq->inherited_scheduler_state_storage()); |
| |
| // Now, block the fair thread on the queue, and verify the IPV state. There |
| // should be storage allocated at this point in time, but because the |
| // blocked thread's profile is not inheritable, there should be no inherited |
| // utilization in the queue's values. |
| ASSERT_TRUE(fair_thread.BlockOnOwnedWaitQueue(owq.get(), nullptr)); |
| ASSERT_NONNULL(owq->inherited_scheduler_state_storage()); |
| { |
| const SchedulerState::WaitQueueInheritedSchedulerState& iss = |
| *owq->inherited_scheduler_state_storage(); |
| EXPECT_EQ(iss.ipvs.total_weight.raw_value(), inheritable_weight.raw_value()); |
| EXPECT_EQ(iss.ipvs.uncapped_utilization.raw_value(), SchedUtilization{0}.raw_value()); |
| EXPECT_EQ(iss.ipvs.min_deadline.raw_value(), SchedDuration::Max().raw_value()); |
| EXPECT_EQ(iss.start_time.raw_value(), SchedTime{0}.raw_value()); |
| EXPECT_EQ(iss.finish_time.raw_value(), SchedTime{0}.raw_value()); |
| EXPECT_EQ(iss.time_slice_ns.raw_value(), SchedDuration{0}.raw_value()); |
| } |
| |
| // Next, block the deadline thread. After this, the queue should have |
| // inherited the blocked thread's utilization, as well as its dynamic |
| // parameters since it is the only "consequential" thread blocked in the |
| // queue. |
| ASSERT_TRUE(deadline_thread.BlockOnOwnedWaitQueue(owq.get(), nullptr)); |
| ASSERT_NONNULL(owq->inherited_scheduler_state_storage()); |
| { |
| const SchedDeadlineParams& params = |
| static_cast<DeadlineProfile*>(deadline_profile.get())->sched_params(); |
| const SchedulerState::WaitQueueInheritedSchedulerState& iss = |
| *owq->inherited_scheduler_state_storage(); |
| EXPECT_EQ(iss.ipvs.total_weight.raw_value(), inheritable_weight.raw_value()); |
| EXPECT_EQ(iss.ipvs.uncapped_utilization.raw_value(), params.utilization.raw_value()); |
| EXPECT_EQ(iss.ipvs.min_deadline.raw_value(), params.deadline_ns.raw_value()); |
| EXPECT_NE(iss.start_time.raw_value(), SchedTime{0}.raw_value()); |
| EXPECT_NE(iss.finish_time.raw_value(), SchedTime{0}.raw_value()); |
| EXPECT_NE(iss.time_slice_ns.raw_value(), SchedDuration{0}.raw_value()); |
| } |
| |
| // Wake the deadline thread from the owned wait queue. We cannot 100% control |
| // which thread the will be woken if we simply use WakeOne as the choice of |
| // which thread to wake is part of the scheduler's logic, and not something we |
| // can guarantee over time. |
| // |
| // Instead, we can force the thread we want to wake by sending it either a |
| // suspend, or kill signal (in this case, we are going to send it the kill |
| // signal). |
| // |
| // Once the thread has woken and exited, observe that the OWQ's IPVs have gone |
| // back to no inherited weight or utilization. If we have extra scheduler |
| // invariant validation turned on, we should see the dynamic parameters get |
| // reset as well (even though they are not normally reset as they are now |
| // considered to be undefined. |
| ASSERT_TRUE(deadline_thread.Reset(true)); |
| { |
| const SchedulerState::WaitQueueInheritedSchedulerState& iss = |
| *owq->inherited_scheduler_state_storage(); |
| EXPECT_EQ(iss.ipvs.total_weight.raw_value(), inheritable_weight.raw_value()); |
| EXPECT_EQ(iss.ipvs.uncapped_utilization.raw_value(), SchedUtilization{0}.raw_value()); |
| EXPECT_EQ(iss.ipvs.min_deadline.raw_value(), SchedDuration::Max().raw_value()); |
| if constexpr (kSchedulerExtraInvariantValidation) { |
| EXPECT_EQ(iss.start_time.raw_value(), SchedTime{0}.raw_value()); |
| EXPECT_EQ(iss.finish_time.raw_value(), SchedTime{0}.raw_value()); |
| EXPECT_EQ(iss.time_slice_ns.raw_value(), SchedDuration{0}.raw_value()); |
| } |
| } |
| |
| // Finally, release the fair thread, confirm that the IPV storage no longer |
| // exists in the OWQ, and shutdown the test. |
| TestThread::ClearShutdownBarrier(); |
| owq->ReleaseAllThreads(); |
| ASSERT_TRUE(fair_thread.Reset()); |
| ASSERT_NULL(owq->inherited_scheduler_state_storage()); |
| } |
| |
| END_TEST; |
| } |
| |
| } // namespace |
| |
| UNITTEST_START_TESTCASE(pi_tests) |
| UNITTEST("basic", pi_test_basic) |
| UNITTEST("changing priority", pi_test_changing_priority) |
| UNITTEST("changing priority in WaitQueue", pi_test_changing_priority_in_wait_queue) |
| UNITTEST("chains", pi_test_chain) |
| UNITTEST("multiple waiters", pi_test_multi_waiter) |
| UNITTEST("multiple owned queues", pi_test_multi_owned_queues) |
| UNITTEST("cycles (inheritable)", pi_test_cycle<InheritableProfile::Yes>) |
| UNITTEST("cycles (non-inheritable)", pi_test_cycle<InheritableProfile::No>) |
| UNITTEST("b/42182770 regression test", bug_42182770_regression) |
| UNITTEST_END_TESTCASE(pi_tests, "pi", "Priority inheritance tests for OwnedWaitQueues") |