blob: a3456f18bf6bd90db0ac72ee80184d3407d38452 [file] [edit]
// Copyright 2018 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 "kernel/scheduler.h"
#include <assert.h>
#include <debug.h>
#include <inttypes.h>
#include <lib/arch/intrin.h>
#include <lib/counters.h>
#include <lib/kconcurrent/chainlock.h>
#include <lib/kconcurrent/chainlock_transaction.h>
#include <lib/ktrace.h>
#include <lib/sched/affine.h>
#include <lib/zircon-internal/macros.h>
#include <platform.h>
#include <stdio.h>
#include <string.h>
#include <zircon/errors.h>
#include <zircon/time.h>
#include <zircon/types.h>
#include <arch/interrupt.h>
#include <arch/ops.h>
#include <arch/thread.h>
#include <ffl/string.h>
#include <kernel/auto_preempt_disabler.h>
#include <kernel/cpu.h>
#include <kernel/event_limiter.h>
#include <kernel/lockdep.h>
#include <kernel/mp.h>
#include <kernel/percpu.h>
#include <kernel/scheduler_inline.h>
#include <kernel/scheduler_state.h>
#include <kernel/scheduler_tracing.h>
#include <kernel/thread.h>
#include <ktl/algorithm.h>
#include <ktl/forward.h>
#include <ktl/limits.h>
#include <ktl/new.h>
#include <ktl/span.h>
#include <ktl/tuple.h>
#include <ktl/utility.h>
#include <object/process_dispatcher.h>
#include <object/thread_dispatcher.h>
#include <vm/vm.h>
// TODO(https://fxbug.dev/322207536): Stop resetting start and finish times
// when unblocking once we solve races higher in the stack.
#include <lib/boot-options/boot-options.h>
#include <ktl/enforce.h>
using ffl::Round;
// Counts the number of times we set the preemption timer to fire at a point
// that's prior to the time at which the CPU last entered the scheduler. See
// also the comment where this counter is used.
KCOUNTER(counter_preempt_past, "scheduler.preempt_past")
// Counts the number of times we dequeue a deadline thread whose finish_time is
// earlier than its eligible_time.
KCOUNTER(counter_deadline_past, "scheduler.deadline_past")
// Counts the number of times a thread's effective profile or affinity changed
// while transitioning from READY to RUNNING.
KCOUNTER(counter_update_in_transition, "scheduler.update_in_transition")
// Counts the number of times we had to run FindTargetCpu again because our
// selected Target became in-active after we chose it.
KCOUNTER(counter_find_target_cpu_retries, "scheduler.find_target_cpu.retries")
// Counts the number of times the fair timeline was snapped forward to make a
// fair thread eligible to run.
KCOUNTER(counter_fair_timeline_snap_count, "scheduler.fair-timeline-snap.count")
// Counts the amount of variable time the fair timeline was snapped forward to
// make a fair thread eligible to run.
KCOUNTER(counter_fair_timeline_snap_time, "scheduler.fair-timeline-snap.time")
// Counts the number of times a task wakes up normally and is placed on the last
// CPU it ran on.
KCOUNTER(counter_wakeup_normal_local, "scheduler.wakeup.normal-local")
// Counts the number of times a task wakes up normally and is placed on a
// different CPU than the last CPU it ran on.
KCOUNTER(counter_wakeup_normal_migrated, "scheduler.wakeup.normal-migrated")
// Counts the number of times a task wakes up synchronously and is placed on the
// CPU the waker ran on.
KCOUNTER(counter_wakeup_synchronous_local, "scheduler.wakeup.synchronous-local")
// Counts the number of times a task wakes up synchronously and is placed on a
// different CPU than the CPU the waker ran on.
KCOUNTER(counter_wakeup_synchronous_migrated, "scheduler.wakeup.synchronous-migrated")
namespace {
// Utility operator to make expressions more succinct that update thread times
// and durations of basic types using the fixed-point counterparts.
constexpr zx_instant_mono_t& operator+=(zx_instant_mono_t& value, SchedDuration delta) {
value += delta.raw_value();
return value;
}
// Returns a delta value to additively update a predictor. Compares the given
// sample to the current value of the predictor and returns a delta such that
// the predictor either exponentially peaks or decays toward the sample. The
// rate of decay depends on the alpha parameter, while the rate of peaking
// depends on the beta parameter. The predictor is not permitted to become
// negative.
//
// A single-rate exponential moving average is updated as follows:
//
// Sn = Sn-1 + a * (Yn - Sn-1)
//
// This function updates the exponential moving average using potentially
// different rates for peak and decay:
//
// D = Yn - Sn-1
// [ Sn-1 + a * D if D < 0
// Sn = [
// [ Sn-1 + b * D if D >= 0
//
template <typename T, typename Alpha, typename Beta>
constexpr T PeakDecayDelta(T value, T sample, Alpha alpha, Beta beta) {
const T delta = sample - value;
return ktl::max<T>(delta >= 0 ? T{beta * delta} : T{alpha * delta}, -value);
}
// Reset this CPU's preemption timer to fire at |deadline|.
//
// |current_cpu| is the current CPU.
//
// |now| is the time that was latched when the CPU entered the scheduler.
//
// Must be called with interrupts disabled.
void PreemptReset(cpu_num_t current_cpu, zx_instant_mono_t now, zx_instant_mono_t deadline) {
ktrace::Scope trace =
LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "preempt_reset", ("now", now), ("deadline", deadline));
// Setting a preemption time that's prior to the point at which the CPU
// entered the scheduler indicates at worst a bug, or at best a wasted
// reschedule.
//
// What do we mean by wasted reschedule? When leaving the scheduler, if the
// preemption timer was set to a point prior to entering the scheduler, it
// will immediately fire once we've re-enabled interrupts. When it fires,
// we'll then re-enter the scheduler (provided that preemption is not
// disabled) without running the the current ask for any appreciable amount of
// time. Instead, we should have set the preemption timer to a point that's
// after the latched entry time and avoided an unnecessary interrupt and trip
// through the scheduler.
//
// TODO(https://fxbug.dev/42182770): For now, simply count the number of times
// this happens. Once we have eliminated all causes of "preemption time in
// the past" (whether they are bugs or simply missed optimization
// opportunities) replace this counter with a DEBUG_ASSERT.
//
// Aside from simple bugs, how can we end up with a preemption time that's
// earlier than the time at which we last entered the scheduler? Consider the
// case where the current thread, A, is blocking and the run queue contains
// just one thread, B, that's deadline scheduled and whose finish time has
// already elapsed. In other words, B's finish time is earlier than the time
// at which we last entered the scheduler. In this case, we'll end up
// scheduling B and setting the preemption timer to B's finish time. Longer
// term, we should consider resetting or reactivating all eligible tasks whose
// finish times would be earlier than the point at which we entered the
// scheduler. See also |counter_deadline_past|.
if (deadline < now) {
kcounter_add(counter_preempt_past, 1);
}
percpu::Get(current_cpu).timer_queue.PreemptReset(deadline);
}
} // anonymous namespace
bool Scheduler::EnableNewWakeupAccounting() {
#if EXPERIMENTAL_FORCE_NEW_WAKEUP_ACCOUNTING
return true;
#else
return BootOptions::Get()->enable_new_wakeup_accounting;
#endif // EXPERIMENTAL_FORCE_NEW_WAKEUP_ACCOUNTING
}
void Scheduler::CountUpdateInTransition() { counter_update_in_transition.Add(1); }
fxt::StringRef<fxt::RefType::kId> Scheduler::ToStringRef(Placement placement) {
using fxt::operator""_intern;
switch (placement) {
case Placement::Insertion:
return "Insertion"_intern;
case Placement::Adjustment:
return "Adjustment"_intern;
case Placement::Preemption:
return "Preemption"_intern;
case Placement::Migration:
return "Migration"_intern;
case Placement::Association:
return "Association"_intern;
default:
return "Invalid"_intern;
}
}
fxt::StringRef<fxt::RefType::kId> Scheduler::ToStringRef(PreemptType preempt_type) {
using fxt::operator""_intern;
switch (preempt_type) {
case PreemptType::Irq:
return "irq"_intern;
case PreemptType::FlushPending:
return "flush-pending"_intern;
case PreemptType::Reschedule:
return "reschedule"_intern;
default:
return "[unknown]"_intern;
}
}
// Records details about the threads entering/exiting the run queues for various
// CPUs, as well as which task on each CPU is currently active. These events are
// used for trace analysis to compute statistics about overall utilization,
// taking CPU affinity into account.
inline void Scheduler::TraceThreadQueueEvent(const fxt::InternedString& name,
const Thread* thread) const {
// Traces marking the end of a queue/dequeue operation have arguments encoded
// as follows:
//
// arg0[ 0..64] : TID
//
// arg1[ 0..15] : CPU availability mask.
// arg1[16..19] : CPU_ID of the affected queue.
// arg1[20..27] : Number of runnable tasks on this CPU after the queue event.
// arg1[28..28] : 1 == fair, 0 == deadline
// arg1[29..29] : 1 == eligible, 0 == ineligible
// arg1[30..30] : 1 == idle thread, 0 == normal thread
//
if constexpr (SCHEDULER_QUEUE_TRACING_ENABLED) {
const zx_instant_mono_t now = current_mono_time(); // TODO(johngro): plumb this in from above
const bool fair = IsFairThread(thread);
const bool eligible = fair || (thread->scheduler_state().start_time_ <= now);
const size_t cnt = fair_run_queue_.size() + critical_deadline_run_queue_.size() +
deadline_run_queue_.size() +
((active_thread_ && !active_thread_->IsIdle()) ? 1 : 0);
const uint64_t arg0 = thread->tid();
const uint64_t arg1 =
(thread->scheduler_state().GetEffectiveCpuMask(PeekActiveMask()) & 0xFFFF) |
(ktl::clamp<uint64_t>(this_cpu_, 0, 0xF) << 16) |
(ktl::clamp<uint64_t>(cnt, 0, 0xFF) << 20) | ((fair ? 1 : 0) << 28) |
((eligible ? 1 : 0) << 29) | ((thread->IsIdle() ? 1 : 0) << 30);
KTrace::Probe(KTrace::Context::Cpu, name, arg0, arg1);
}
}
void Scheduler::Dump(FILE* output_target, bool queue_state_only) {
// We're about to acquire the |queue_lock_| and fprintf some things.
// Depending on the FILE, calling fprintf may end up calling |DLog::Write|,
// which may call |Event::Signal|, which may re-enter the Scheduler. If that
// happens while we're holding the |queue_lock_| that would be bad.
// |DLog::Write| has a hack that allows it to defer the Signal operation when
// there's an active chain lock transaction. So even though we don't *need* a
// chain lock transaction, we establish one anyway in order to leverage the
// deferred Signal behavior and avoid a re-entrancy issue.
//
// TODO(https://fxbug.dev/331847876): Remove this hack once we have a better
// solution to scheduler re-entrancy issues.
const auto do_transaction =
[&]() TA_REQ(chainlock_transaction_token) -> ChainLockTransaction::Result<> {
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&queue_lock_, SOURCE_TAG};
const SchedTime now = CurrentTime();
const SchedTime variable_now = MonotonicToVariable(now);
SchedWeight min_weight{0};
SchedTime variable_eligible_time{SchedTime::Max()};
if (!fair_run_queue_.is_empty()) {
const Thread& root = *fair_run_queue_.croot();
AssertInScheduler(root);
min_weight =
ktl::min(active_thread_weight_, root.scheduler_state().subtree_invariants_.min_weight);
const Thread& front = fair_run_queue_.front();
AssertInScheduler(front);
variable_eligible_time = front.scheduler_state().start_time();
}
const SchedTime monotonic_eligible_time = VariableToMonotonic(variable_eligible_time);
const SchedUtilization min_utilization =
weight_total_ != 0 ? SchedUtilization{min_weight / weight_total_} : SchedUtilization{0};
fprintf(output_target,
"\tw_sum=%s w_min=%s T_v=%" PRId64 " count=%d ema_sum=%" PRId64
"\n\tU_{c,sum}=%s U_{v,min}=%s"
"\n\tnow=%" PRId64 " vnow=%" PRId64 " eligible=%" PRId64 " veligible=%" PRId64
"\n\tmono_ref=%" PRId64 " var_ref=%" PRId64 " slope=%s\n",
Format(weight_total_).c_str(), Format(min_weight).c_str(), fair_period_.raw_value(),
runnable_task_count_, total_expected_runtime_ns_.raw_value(),
Format(power_level_control_.normalized_deadline_utilization()).c_str(),
Format(min_utilization).c_str(), now.raw_value(), variable_now.raw_value(),
monotonic_eligible_time.raw_value(), variable_eligible_time.raw_value(),
fair_affine_transform_.monotonic_reference_time().raw_value(),
fair_affine_transform_.variable_reference_time().raw_value(),
Format(fair_affine_transform_.slope()).c_str());
if (queue_state_only) {
return ChainLockTransaction::Done;
}
const auto print_thread =
[output_target](const Scheduler* scheduler, const Thread& thread) TA_REQ(
scheduler->queue_lock_) TA_REQ_SHARED(thread.get_lock()) {
const SchedulerState& state = thread.scheduler_state();
const EffectiveProfile& ep = state.effective_profile();
const bool is_active = scheduler->active_thread_ == &thread;
if (IsFairThread(&thread)) {
// A fair thread that is the active thread has start and finish
// times on the monotonic timeline. All fair threads in run queues
// have start and finish times on the variable timeline. Display all
// start and finish times on the monotonic timeline for consistent
// presentation.
const SchedTime start_time =
is_active ? state.start_time_ : scheduler->VariableToMonotonic(state.start_time_);
const SchedTime finish_time =
is_active ? state.finish_time_ : scheduler->VariableToMonotonic(state.finish_time_);
fprintf(output_target,
"\t%s name=%s weight=%s start=%" PRId64 " finish=%" PRId64 " ts=%" PRId64
" ema=%" PRId64 " T_e=%" PRId64 "\n",
is_active ? "->" : " ", thread.name(), Format(ep.weight()).c_str(),
start_time.raw_value(), finish_time.raw_value(),
state.remaining_time_slice_ns().raw_value(),
state.expected_runtime_ns_.raw_value(), state.effective_period().raw_value());
} else {
fprintf(output_target,
"\t%s name=%s deadline=(%" PRId64 ", %" PRId64 ") start= %" PRId64
" finish= %" PRId64 " ts= %" PRId64 " ema= %" PRId64 " T_e= %" PRId64 " %s\n",
is_active ? "->" : " ", thread.name(), ep.deadline().capacity_ns.raw_value(),
ep.deadline().deadline_ns.raw_value(), state.start_time().raw_value(),
state.finish_time().raw_value(), state.time_slice_ns().raw_value(),
state.expected_runtime_ns_.raw_value(), state.effective_period().raw_value(),
IsCriticalDeadlineThread(&thread) ? "critical" : "");
}
};
if (active_thread_ != nullptr) {
AssertInScheduler(*active_thread_);
print_thread(this, *active_thread_);
}
for (const RunQueue* run_queue :
{&critical_deadline_run_queue_, &deadline_run_queue_, &fair_run_queue_}) {
for (const Thread& thread : *run_queue) {
AssertInScheduler(thread);
print_thread(this, thread);
}
}
return ChainLockTransaction::Done;
};
ChainLockTransaction::UntilDone(NoIrqSaveOption, CLT_TAG("Scheduler::Dump"), do_transaction);
}
void Scheduler::DumpActiveThread(FILE* output_target) {
// See comment in |Scheduler::Dump|.
//
// TODO(https://fxbug.dev/331847876): Remove this hack once we have a better
// solution to scheduler re-entrancy issues.
const auto do_transaction =
[&]() TA_REQ(chainlock_transaction_token) -> ChainLockTransaction::Result<> {
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&queue_lock_, SOURCE_TAG};
if (active_thread_ != nullptr) {
AssertInScheduler(*active_thread_);
fprintf(output_target, "thread: pid=%lu tid=%lu\n", active_thread_->pid(),
active_thread_->tid());
ThreadDispatcher* user_thread = active_thread_->user_thread();
if (user_thread != nullptr) {
ProcessDispatcher* process = user_thread->process();
char name[ZX_MAX_NAME_LEN]{};
[[maybe_unused]] zx_status_t status = process->get_name(name);
DEBUG_ASSERT(status == ZX_OK);
fprintf(output_target, "process: name=%s\n", name);
}
}
return ChainLockTransaction::Done;
};
ChainLockTransaction::UntilDone(NoIrqSaveOption, CLT_TAG("Scheduler::DumpActiveThread"),
do_transaction);
}
SchedWeight Scheduler::GetTotalWeight() const {
Guard<MonitoredSpinLock, IrqSave> guard{&queue_lock_, SOURCE_TAG};
return weight_total_;
}
size_t Scheduler::GetRunnableTasks() const {
Guard<MonitoredSpinLock, IrqSave> guard{&queue_lock_, SOURCE_TAG};
const int64_t total_runnable_tasks = runnable_task_count();
return static_cast<size_t>(total_runnable_tasks);
}
// Performs an augmented binary search for the task with the earliest finish
// time that also has a start time equal to or later than the given eligible
// time. An optional predicate may be supplied to filter candidates based on
// additional conditions.
//
// The tree is ordered by start time and is augmented by maintaining an
// additional invariant: each task node in the tree stores the minimum finish
// time of its descendents, including itself, in addition to its own start and
// finish time. The combination of these three values permits traversinng the
// tree along a perfect partition of minimum finish times with eligible start
// times.
//
// See fbl/wavl_tree_best_node_observer.h for an explanation of how the
// augmented invariant is maintained.
Thread* Scheduler::FindEarliestEligibleThread(RunQueue* run_queue, SchedTime eligible_time) {
return FindEarliestEligibleThread(run_queue, eligible_time,
[](const auto& iter) { return true; });
}
template <typename Predicate>
Thread* Scheduler::FindEarliestEligibleThread(RunQueue* run_queue, SchedTime eligible_time,
Predicate&& predicate) {
const auto scheduler_state = [this](const Thread& t)
TA_REQ(this->queue_lock_) -> const SchedulerState& {
AssertInScheduler(t);
return t.scheduler_state();
};
const auto subtree_state = [this](const Thread& t) TA_REQ(
this->queue_lock_) -> const SchedulerState::SubtreeInvariants& {
AssertInScheduler(t);
return t.scheduler_state().subtree_invariants_;
};
// Early out if there is no eligible thread.
if (run_queue->is_empty() || scheduler_state(run_queue->front()).start_time_ > eligible_time) {
return nullptr;
}
// Deduces either Predicate& or const Predicate&, preserving the const
// qualification of the predicate.
decltype(auto) accept = ktl::forward<Predicate>(predicate);
auto node = run_queue->root();
auto subtree = run_queue->end();
auto path = run_queue->end();
// Descend the tree, with |node| following the path from the root to a leaf,
// such that the path partitions the tree into two parts: the nodes on the
// left represent eligible tasks, while the nodes on the right represent tasks
// that are not eligible. Eligible tasks are both in the left partition and
// along the search path, tracked by |path|.
while (node) {
const SchedulerState& node_state = scheduler_state(*node);
if (node_state.start_time_ <= eligible_time) {
if (!path || scheduler_state(*path).finish_time_ > node_state.finish_time_) {
path = node;
}
if (auto left = node.left(); !subtree || (left && (subtree_state(*subtree).min_finish_time >
subtree_state(*left).min_finish_time))) {
subtree = left;
}
node = node.right();
} else {
node = node.left();
}
}
if (!subtree) {
return path && accept(path) ? path.CopyPointer() : nullptr;
}
const SchedTime subtree_min_finish_time = subtree_state(*subtree).min_finish_time;
if ((subtree_min_finish_time >= scheduler_state(*path).finish_time_) && accept(path)) {
return path.CopyPointer();
}
// Find the node with the earliest finish time among the descendants of the
// subtree with the smallest minimum finish time.
node = subtree;
do {
if ((subtree_min_finish_time == scheduler_state(*node).finish_time_) && accept(node)) {
return node.CopyPointer();
}
if (auto left = node.left();
left && (subtree_state(*node).min_finish_time == subtree_state(*left).min_finish_time)) {
node = left;
} else {
node = node.right();
}
} while (node);
return nullptr;
}
Scheduler* Scheduler::Get() { return Get(arch_curr_cpu_num()); }
Scheduler* Scheduler::Get(cpu_num_t cpu) { return &percpu::Get(cpu).scheduler; }
void Scheduler::InitializeThread(Thread* thread, const SchedulerState::BaseProfile& profile) {
new (&thread->scheduler_state()) SchedulerState{profile};
thread->scheduler_state().expected_runtime_ns_ =
profile.IsFair() ? kMinimumFairCapacity : profile.deadline.capacity_ns;
}
// Initialize the first thread to run on the current CPU. Called from
// thread_construct_first, this method will initialize the thread's scheduler
// state, then mark the thread as being "active" in its cpu's scheduler.
void Scheduler::InitializeFirstThread(Thread* thread) {
cpu_num_t current_cpu = arch_curr_cpu_num();
// Construct our scheduler state and assign a "priority"
InitializeThread(thread, SchedulerState::BaseProfile{HIGHEST_PRIORITY});
// Fill out other details about the thread, making sure to assign it to the
// current CPU with hard affinity.
SchedulerState& state = thread->scheduler_state();
state.state_ = THREAD_RUNNING;
state.curr_cpu_ = current_cpu;
state.last_cpu_ = current_cpu;
state.hard_affinity_ = cpu_num_to_mask(current_cpu);
// Finally, make sure that the thread is the active thread for the scheduler,
// and that the weight_total bookkeeping is accurate.
{
Scheduler* scheduler = Get(current_cpu);
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&scheduler->queue_lock_, SOURCE_TAG};
scheduler->AssertInScheduler(*thread);
SchedulerQueueState& queue_state = thread->scheduler_queue_state();
queue_state.OnInsert();
scheduler->active_thread_ = thread;
scheduler->weight_total_ = state.effective_profile_.weight();
scheduler->runnable_task_count_++;
scheduler->UpdateTotalExpectedRuntime(state.expected_runtime_ns_);
}
}
// Remove the impact of a CPUs first thread from the scheduler's bookkeeping.
//
// During initial startup, threads are not _really_ being scheduled, yet they
// can still do things like obtain locks and block, resulting in profile
// inheritance. In order to hold the scheduler's bookkeeping invariants, we
// assign these threads a fair weight, and include it in the total fair weight
// tracked by the scheduler instance. When the thread either becomes the idle
// thread (as the boot CPU first thread does), or exits (as secondary CPU first
// threads do), it is important that we remove this weight from the total
// bookkeeping. However, this is not as simple as just changing the thread's
// weight via ChangeWeight, as idle threads are special cases who contribute no
// weight to the total.
//
// So, this small method simply fixes up the bookkeeping before allowing the
// thread to move on to become the idle thread (boot CPU), or simply exiting
// (secondary CPU).
void Scheduler::RemoveFirstThread(Thread* thread) {
cpu_num_t current_cpu = arch_curr_cpu_num();
Scheduler* scheduler = Get(current_cpu);
SchedulerState& state = thread->scheduler_state();
// Since this is becoming an idle thread, it must have been one of the CPU's
// first threads. It should already be bound to this core with hard affinity.
// Assert this.
DEBUG_ASSERT(state.last_cpu_ == current_cpu);
DEBUG_ASSERT(state.curr_cpu_ == current_cpu);
DEBUG_ASSERT(state.hard_affinity_ == cpu_num_to_mask(current_cpu));
{
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&scheduler->queue_lock_, SOURCE_TAG};
scheduler->AssertInScheduler(*thread);
SchedulerQueueState& queue_state = thread->scheduler_queue_state();
// We are becoming the idle thread. We should currently be running with a
// fair (not deadline) profile, and we should not be holding any locks
// (therefore, we should not be inheriting any profile pressure).
DEBUG_ASSERT(state.base_profile_.IsFair());
DEBUG_ASSERT(state.inherited_profile_values_.total_weight == SchedWeight{0});
DEBUG_ASSERT(state.inherited_profile_values_.uncapped_utilization == SchedUtilization{0});
// We should also be the currently active thread on this core, but no
// longer. We are about to either exit, or "UnblockIdle".
DEBUG_ASSERT(scheduler->active_thread_ == thread);
DEBUG_ASSERT(scheduler->runnable_task_count_ > 0);
queue_state.OnRemove(INVALID_CPU); // Set active to false.
scheduler->active_thread_ = nullptr;
scheduler->weight_total_ -= state.effective_profile_.weight();
scheduler->runnable_task_count_--;
scheduler->UpdateTotalExpectedRuntime(-state.expected_runtime_ns_);
state.base_profile_.fair.weight = SchedulerState::ConvertPriorityToWeight(IDLE_PRIORITY);
state.effective_profile_.MarkBaseProfileChanged();
state.RecomputeEffectiveProfile();
}
}
// Removes the thread at the head of the first eligible run queue. If there is
// an eligible deadline thread, it takes precedence over available fair
// threads. If there is no eligible work, attempt to steal work from other busy
// CPUs.
Scheduler::DequeueResult Scheduler::DequeueThread(
SchedTime now, Guard<MonitoredSpinLock, NoIrqSave>& queue_guard) {
if (IsDeadlineThreadEligible(now)) {
return DequeueDeadlineThread(now);
}
if (likely(!fair_run_queue_.is_empty())) {
return DequeueFairThread(now);
}
// Release the queue lock while attempting to steal work, leaving IRQs
// disabled. Latch our scale up factor to use while determining whether or
// not we can steal a given thread before we drop our lock.
// TODO(eieio): Since the processing rate is CPU-local and not accessed across
// processors, it should be unnecessary to latch the value. Change the
// annotations to allow this.
DequeueResult result;
queue_guard.CallUnlocked([&, processing_rate = power_level_control_.processing_rate()] {
ChainLockTransaction::AssertActive();
result = StealWork(now, processing_rate);
});
if (result) {
return result;
}
return &percpu::Get(this_cpu_).idle_power_thread.thread();
}
// Attempts to steal work from other busy CPUs and move it to the local run
// queues. Returns a pointer to the stolen thread that is now associated with
// the local Scheduler instance, or nullptr is no work was stolen.
Scheduler::DequeueResult Scheduler::StealWork(SchedTime now, SchedProcessingRate processing_rate) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "StealWork");
const cpu_num_t current_cpu = this_cpu();
const cpu_mask_t current_cpu_mask = cpu_num_to_mask(current_cpu);
const cpu_mask_t active_cpu_mask = PeekActiveMask();
const CpuSearchSet& search_set = percpu::Get(current_cpu).search_set;
for (const auto& entry : search_set.const_iterator()) {
if (entry.cpu != current_cpu && active_cpu_mask & cpu_num_to_mask(entry.cpu)) {
Scheduler* const scheduler = Get(entry.cpu);
// Only steal across clusters if the target is above the load threshold.
if (cluster() != entry.cluster &&
scheduler->exported_queue_time_ns() <= kInterClusterThreshold) {
continue;
}
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&scheduler->queue_lock_, SOURCE_TAG};
// Check if the scheduler is still active. If it's not, ignore it because
// MigrateUnpinnedThreads will move the threads off of it.
if (!scheduler->IsSchedulerActiveLocked()) {
continue;
}
// TODO(b/430139320): Disabled until root cause of x64 pessimization is
// understood and addressed.
#if 0
// Only attempt to steal from CPUs that have more than one task, avoiding
// unnecessary overhead when a task was just added to an idle CPU but the
// CPU has not started running the new task (i.e. when this CPU wins the
// race to acquire the target CPU's queue lock after the singular task is
// queued).
if (scheduler->runnable_task_count() <= 1) {
continue;
}
#endif
// Note that in the lambdas below we will be making use of both the
// MarkHasSchedulerAccess (but only on |queue|) and
// MarkHasOwnedThreadAccess. We just acquired |queue|'s queue lock, and
// each thread we will be examining is a member of one of |queue|'s run
// queues (either fair or deadline). This should satisfy the requirements
// of both no-op checks.
// Returns true if the given thread can run on this CPU. Static analysis
// needs to be disabled, but this should be fine. We only need R/O access
// to the thread's scheduler state, which we have because we are holding
// the queue lock for a thread which belongs to that queue (see below).
const auto check_affinity = [current_cpu_mask,
active_cpu_mask](const Thread& thread) -> bool {
MarkHasOwnedThreadAccess(thread);
return current_cpu_mask & thread.scheduler_state().GetEffectiveCpuMask(active_cpu_mask);
};
// Common routine for stealing from a run queue.
const auto steal_from_queue = [current_cpu, check_affinity, scheduler, now](
RunQueue& run_queue,
const auto& predicate) -> DequeueResult {
ktrace::Scope trace_steal = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_steal");
DEBUG_ASSERT((&run_queue == &scheduler->fair_run_queue_) ||
(&run_queue == &scheduler->critical_deadline_run_queue_) ||
(&run_queue == &scheduler->deadline_run_queue_));
MarkHasSchedulerAccess(*scheduler);
const bool is_fair_run_queue = &run_queue == &scheduler->fair_run_queue_;
// Convert the monotonic eligible time to variable time and potentially
// snap the virtual timeline forward when stealing from the fair queue.
SchedTime eligible_time = now;
if (is_fair_run_queue) {
scheduler->MonotonicToVariableInPlace(eligible_time);
if (!run_queue.is_empty()) {
const Thread& earliest_thread = run_queue.front();
MarkHasOwnedThreadAccess(earliest_thread);
const SchedTime earliest_start = earliest_thread.scheduler_state().start_time();
if (eligible_time < earliest_start) {
counter_fair_timeline_snap_count.Add(1);
counter_fair_timeline_snap_time.Add(Round<int64_t>(earliest_start - eligible_time));
scheduler->fair_affine_transform_.Snap(now, earliest_start);
eligible_time = earliest_start;
}
}
}
Thread* thread =
scheduler->FindEarliestEligibleThread(&run_queue, eligible_time, predicate);
if (!thread) {
return nullptr;
}
MarkHasOwnedThreadAccess(*thread);
ktl::ignore = check_affinity; // Silence compiler when debug asserts are disabled.
DEBUG_ASSERT(check_affinity(*thread));
DEBUG_ASSERT(!thread->has_migrate_fn());
DEBUG_ASSERT(thread->disposition() == Disposition::Enqueued);
// Remove the thread from the source run queue and record that it was
// stolen by this CPU.
run_queue.erase(*thread);
scheduler->Remove(now, thread, current_cpu);
scheduler->TraceThreadQueueEvent("tqe_deque_steal_work"_intern, thread);
DEBUG_ASSERT(thread->disposition() == Disposition::Stolen);
if (is_fair_run_queue) {
const SchedulerState& state = const_cast<const Thread*>(thread)->scheduler_state();
const SchedTime mono_start_time = scheduler->VariableToMonotonic(state.start_time());
const SchedTime mono_finish_time = scheduler->VariableToMonotonic(state.finish_time());
return DequeueResult{thread, mono_start_time, mono_finish_time};
}
return thread;
};
// Returns true if the given thread in the run queue meets the criteria to
// run on this CPU. Don't attempt to steal any threads which are
// currently in the process of being scheduled.
const auto deadline_predicate = [check_affinity, processing_rate](const auto& iter) -> bool {
const Thread& thread = *iter;
MarkHasOwnedThreadAccess(thread);
const SchedulerState& state = thread.scheduler_state();
const EffectiveProfile& ep = state.effective_profile_;
const bool is_scheduleable = ep.deadline().utilization <= processing_rate;
return check_affinity(thread) && is_scheduleable && !thread.has_migrate_fn();
};
// Attempt to find a critical deadline thread that can run on this CPU.
if (DequeueResult result =
steal_from_queue(scheduler->critical_deadline_run_queue_, deadline_predicate)) {
return result;
}
// Attempt to find a deadline thread that can run on this CPU.
if (DequeueResult result =
steal_from_queue(scheduler->deadline_run_queue_, deadline_predicate)) {
return result;
}
// Returns true if the given thread in the run queue meets the criteria to
// run on this CPU.
const auto fair_predicate = [check_affinity](const auto& iter) -> bool {
const Thread& thread = *iter;
MarkHasOwnedThreadAccess(thread);
return check_affinity(thread) && !thread.has_migrate_fn();
};
// Attempt to find a fair thread that can run on this CPU.
if (DequeueResult result = steal_from_queue(scheduler->fair_run_queue_, fair_predicate)) {
return result;
}
}
}
return nullptr;
}
// Dequeues the eligible thread with the earliest virtual finish time. The
// caller must ensure that there is at least one thread in the queue.
Scheduler::DequeueResult Scheduler::DequeueFairThread(SchedTime monotonic_eligible_time) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "dequeue_fair_thread");
// Convert monotonic eligible time to variable time to compare with fair run
// queue values.
SchedTime variable_eligible_time =
fair_affine_transform_.MonotonicToVariable(monotonic_eligible_time);
// Use MarkHasOwnedThreadAccess to satisfy static annotations for shared
// access to the thread's COVs and exclusive access to the thread's SOVs. This
// is acceptable because the queue lock is required by this method and the
// thread is in a queue protectect by the same lock.
const auto& earliest_thread = fair_run_queue_.front();
MarkHasOwnedThreadAccess(earliest_thread);
// If there are no eligible fair threads, snap the variable timeline to the
// earliest start time.
const SchedTime earliest_start = earliest_thread.scheduler_state().start_time_;
if (variable_eligible_time < earliest_start) {
counter_fair_timeline_snap_count.Add(1);
counter_fair_timeline_snap_time.Add(Round<int64_t>(earliest_start - variable_eligible_time));
fair_affine_transform_.Snap(monotonic_eligible_time, earliest_start);
variable_eligible_time = earliest_start;
}
Thread* const eligible_thread =
FindEarliestEligibleThread(&fair_run_queue_, variable_eligible_time);
DEBUG_ASSERT_MSG(
eligible_thread != nullptr,
"vearliest_start=%" PRId64 ", veligible_time=%" PRId64 " , vstart_time=%" PRId64
", vfinish_time=%" PRId64 ", vmin_finish_time=%" PRId64 "!",
earliest_start.raw_value(), variable_eligible_time.raw_value(),
earliest_thread.scheduler_state().start_time_.raw_value(),
earliest_thread.scheduler_state().finish_time_.raw_value(),
earliest_thread.scheduler_state().subtree_invariants_.min_finish_time.raw_value());
// Same argument as before for the use of the no-op assert. We are holding
// the queue lock, and we just found this thread in the scheduler.
MarkHasOwnedThreadAccess(*eligible_thread);
fair_run_queue_.erase(*eligible_thread);
TraceThreadQueueEvent("tqe_deque_fair"_intern, eligible_thread);
DEBUG_ASSERT(eligible_thread->disposition() == Disposition::Associated);
const SchedulerState& state = const_cast<const Thread*>(eligible_thread)->scheduler_state();
const SchedTime mono_start_time_ns =
fair_affine_transform_.VariableToMonotonic(state.start_time());
const SchedTime mono_finish_time_ns =
fair_affine_transform_.VariableToMonotonic(state.finish_time());
trace = KTRACE_END_SCOPE(("var_start_time", state.start_time().raw_value()),
("var_finish_time", state.finish_time().raw_value()),
("mono_start_time", mono_start_time_ns.raw_value()),
("mono_finish_time", mono_finish_time_ns.raw_value()));
return DequeueResult{eligible_thread, mono_start_time_ns, mono_finish_time_ns};
}
// Dequeues the eligible thread with the earliest deadline. The caller must
// ensure that there is at least one eligible thread in either the critical
// deadline run queue or the deadline run queue.
Scheduler::DequeueResult Scheduler::DequeueDeadlineThread(SchedTime eligible_time) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "dequeue_deadline_thread");
DEBUG_ASSERT(!critical_deadline_run_queue_.is_empty() || !deadline_run_queue_.is_empty());
const Thread* critical_thread =
FindEarliestEligibleThread(&critical_deadline_run_queue_, eligible_time);
const bool is_oversubscribed =
power_level_control_.normalized_deadline_utilization() > kCpuUtilizationLimit;
const Thread* eligible_thread = nullptr;
if (is_oversubscribed && critical_thread) {
eligible_thread = critical_thread;
} else {
const Thread* normal_thread = FindEarliestEligibleThread(&deadline_run_queue_, eligible_time);
// There must be at least one eligible deadline thread, whether critical or
// normal.
DEBUG_ASSERT(critical_thread || normal_thread);
eligible_thread = EarliestDeadlineThread(critical_thread, normal_thread);
}
DEBUG_ASSERT_MSG(eligible_thread != nullptr, "eligible_time=%" PRId64, eligible_time.raw_value());
// Use MarkHasOwnedThreadAccess to satisfy static annotations for shared
// access to the thread's COVs and exclusive access to the thread's SOVs. This
// is acceptable because the queue lock is required by this method and the
// thread is in a queue protectect by the same lock.
MarkHasOwnedThreadAccess(*eligible_thread);
RunQueue* target_queue = IsCriticalDeadlineThread(eligible_thread) ? &critical_deadline_run_queue_
: &deadline_run_queue_;
target_queue->erase(*const_cast<Thread*>(eligible_thread));
TraceThreadQueueEvent("tqe_deque_deadline"_intern, eligible_thread);
DEBUG_ASSERT(eligible_thread->disposition() == Disposition::Associated);
const SchedulerState& state = eligible_thread->scheduler_state();
if (state.finish_time() <= eligible_time) {
kcounter_add(counter_deadline_past, 1);
}
trace =
KTRACE_END_SCOPE(("start time", state.start_time()), ("finish time", state.finish_time()));
return const_cast<Thread*>(eligible_thread);
}
Thread* Scheduler::FindEarlierFairThread(SchedTime eligible_time, SchedTime finish_time) {
// Convert monotonic to variable times to compare with fair run queue values.
const SchedTime variable_eligible_time = MonotonicToVariable(eligible_time);
const SchedTime variable_finish_time = MonotonicToVariable(finish_time);
Thread* const eligible_thread =
FindEarliestEligibleThread(&fair_run_queue_, variable_eligible_time);
if (eligible_thread != nullptr) {
// Use MarkHasOwnedThreadAccess to satisfy static annotations for shared
// access to the thread's COVs and exclusive access to the thread's SOVs.
// This is acceptable because the queue lock is required by this method and
// the thread is in a queue protectect by the same lock.
MarkHasOwnedThreadAccess(*eligible_thread);
if (const_cast<const Thread*>(eligible_thread)->scheduler_state().finish_time() <
variable_finish_time) {
return eligible_thread;
}
}
return nullptr;
}
Thread* Scheduler::FindEarlierDeadlineThread(SchedTime eligible_time,
const Thread* reference_thread) {
// The following logic governs how the scheduler finds an earlier deadline
// thread than the reference thread based on system load:
//
// 1. **Scenario A: Feasible Total Demand.** When the combined demand of both
// critical and general queues is within CPU capacity:
// a. If both queues contain eligible tasks, the scheduler selects the task
// with the earliest eligible deadline, regardless of which queue it
// resides in.
// b. If only one queue has eligible tasks, the earliest eligible deadline
// task from that queue is selected.
// 2. **Scenario B: Infeasible Total Demand (Oversubscription).** When total
// demand exceeds capacity, the system prioritizes the critical tier:
// a. If the critical deadline queue has eligible tasks, the scheduler
// selects the earliest eligible deadline task from this queue
// exclusively.
// b. The general deadline queue is only serviced if the critical deadline
// queue is empty or only has ineligible tasks.
//
// If the CPU is oversubscribed, an eligible critical deadline thread takes
// precedence over any eligible general deadline thread, including the
// reference thread. Otherwise, the scheduler selects the eligible deadline
// thread with the earliest deadline, regardless of which type, including the
// reference thread.
const Thread* critical_thread =
FindEarliestEligibleThread(&critical_deadline_run_queue_, eligible_time);
const bool is_oversubscribed =
power_level_control_.normalized_deadline_utilization() > kCpuUtilizationLimit;
const Thread* eligible_thread = nullptr;
if (is_oversubscribed && critical_thread) {
eligible_thread = critical_thread;
} else {
const Thread* normal_thread = FindEarliestEligibleThread(&deadline_run_queue_, eligible_time);
eligible_thread = EarliestDeadlineThread(critical_thread, normal_thread);
}
Thread* selected_thread = nullptr;
if (eligible_thread != nullptr) {
// Use MarkHasOwnedThreadAccess to satisfy static annotations for shared
// access to the thread's COVs and exclusive access to the thread's SOVs.
// This is acceptable because the queue lock is required by this method and
// the thread is in a queue protectect by the same lock.
MarkHasOwnedThreadAccess(*eligible_thread);
const bool eligible_is_critical = IsCriticalDeadlineThread(eligible_thread);
const bool reference_is_critical = IsCriticalDeadlineThread(reference_thread);
if (is_oversubscribed) {
if (eligible_is_critical && !reference_is_critical) {
selected_thread = const_cast<Thread*>(eligible_thread);
} else if (!eligible_is_critical && reference_is_critical) {
selected_thread = nullptr;
} else if (eligible_thread->scheduler_state().finish_time() <
reference_thread->scheduler_state().finish_time()) {
selected_thread = const_cast<Thread*>(eligible_thread);
}
} else if (eligible_thread->scheduler_state().finish_time() <
reference_thread->scheduler_state().finish_time()) {
selected_thread = const_cast<Thread*>(eligible_thread);
}
LOCAL_KTRACE_INSTANT(
QUEUE, "find_earlier_deadline", ("is_oversubscribed", is_oversubscribed),
("contender_name", eligible_thread->name()), ("contender_tid", eligible_thread->tid()),
("contender_finish_delta_ns",
SchedDuration{eligible_thread->scheduler_state().finish_time() - eligible_time}),
("contender_start_delta_ns",
SchedDuration{eligible_time - eligible_thread->scheduler_state().start_time()}),
("contender_remaining_slice_ns",
eligible_thread->scheduler_state().remaining_time_slice_ns()),
("contender_capacity_ns", eligible_thread->scheduler_state().time_slice_ns()),
("active_name", reference_thread->name()), ("active_tid", reference_thread->tid()),
("active_finish_delta_ns",
SchedDuration{reference_thread->scheduler_state().finish_time() - eligible_time}),
("active_start_delta_ns",
SchedDuration{eligible_time - reference_thread->scheduler_state().start_time()}),
("active_remaining_slice_ns",
reference_thread->scheduler_state().remaining_time_slice_ns()),
("active_capacity_ns", reference_thread->scheduler_state().time_slice_ns()),
("critical_state", (eligible_is_critical && reference_is_critical) ? "both"_intern
: eligible_is_critical ? "contender"_intern
: reference_is_critical ? "active"_intern
: "none"_intern),
("selected", selected_thread ? "contender"_intern : "active"_intern));
}
return selected_thread;
}
SchedTime Scheduler::GetNextEligibleTime() const {
const auto get_eligible_time = [](const RunQueue& queue) {
if (queue.is_empty()) {
return SchedTime{ZX_TIME_INFINITE};
}
const Thread& front = queue.front();
MarkHasOwnedThreadAccess(front);
return front.scheduler_state().start_time();
};
return ktl::min(get_eligible_time(critical_deadline_run_queue_),
get_eligible_time(deadline_run_queue_));
}
Scheduler::DequeueResult Scheduler::DequeueEarlierFairThread(SchedTime eligible_time,
SchedTime finish_time) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "dequeue_earlier_fair_thread");
Thread* const eligible_thread = FindEarlierFairThread(eligible_time, finish_time);
if (eligible_thread != nullptr) {
// Use MarkHasOwnedThreadAccess to satisfy static annotations for shared
// access to the thread's COVs and exclusive access to the thread's SOVs.
// This is acceptable because the queue lock is required by this method and
// the thread is in a queue protectect by the same lock.
MarkHasOwnedThreadAccess(*eligible_thread);
fair_run_queue_.erase(*eligible_thread);
TraceThreadQueueEvent("tqe_deque_earlier_fair"_intern, eligible_thread);
DEBUG_ASSERT(eligible_thread->disposition() == Disposition::Associated);
const SchedulerState& state = const_cast<const Thread*>(eligible_thread)->scheduler_state();
const SchedTime mono_start_time_ns =
fair_affine_transform_.VariableToMonotonic(state.start_time());
const SchedTime mono_finish_time_ns =
fair_affine_transform_.VariableToMonotonic(state.finish_time());
return DequeueResult{eligible_thread, mono_start_time_ns, mono_finish_time_ns};
}
return nullptr;
}
Scheduler::DequeueResult Scheduler::DequeueEarlierDeadlineThread(SchedTime eligible_time,
const Thread* reference_thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "dequeue_earlier_deadline_thread");
Thread* const eligible_thread = FindEarlierDeadlineThread(eligible_time, reference_thread);
if (eligible_thread != nullptr) {
// Use MarkHasOwnedThreadAccess to satisfy static annotations for shared
// access to the thread's COVs and exclusive access to the thread's SOVs.
// This is acceptable because the queue lock is required by this method and
// the thread is in a queue protected by the same lock.
MarkHasOwnedThreadAccess(*eligible_thread);
if (IsCriticalDeadlineThread(eligible_thread)) {
critical_deadline_run_queue_.erase(*eligible_thread);
} else {
deadline_run_queue_.erase(*eligible_thread);
}
TraceThreadQueueEvent("tqe_deque_earlier_deadline"_intern, eligible_thread);
DEBUG_ASSERT(eligible_thread->disposition() == Disposition::Associated);
}
return eligible_thread;
}
bool Scheduler::NeedsMigration(Thread* thread) {
const cpu_num_t current_cpu = arch_curr_cpu_num();
const cpu_mask_t current_cpu_mask = cpu_num_to_mask(current_cpu);
const cpu_mask_t active_mask = PeekActiveMask();
// Threads that are not ready should not be migrated.
if (thread->state() != THREAD_READY) {
return false;
}
// If we have no active CPUs, then we cannot migrate this thread, so return false.
if (active_mask == 0) {
return false;
}
// We need to migrate if the set of CPUs this thread can run on does not include the current CPU.
return (thread->scheduler_state().GetEffectiveCpuMask(active_mask) & current_cpu_mask) == 0;
}
Scheduler::DequeueResult Scheduler::EvaluateNextThread(
SchedTime now, Thread* current_thread, bool current_is_migrating,
Guard<MonitoredSpinLock, NoIrqSave>& queue_guard) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "find_thread");
// First things first. If we have pending power work, we choose the idle-power thread.
{
percpu& self = percpu::Get(this_cpu_);
if (IdlePowerThread::PendingPowerWorkPredicate pending_work =
self.idle_power_thread.pending_power_work()) {
// If the idle power thread is in the (offline, offline) state, CPU hot
// plugging attempted to block before restoring the idle power thread.
DEBUG_ASSERT(pending_work.current() != IdlePowerThread::State::Offline ||
pending_work.target() != IdlePowerThread::State::Offline);
return &self.idle_power_thread.thread();
}
}
const bool is_idle = current_thread->IsIdle();
const bool is_runnable = current_thread->state() == THREAD_READY;
const bool is_deadline = IsDeadlineThread(current_thread);
if (is_runnable && !is_idle && !current_is_migrating) {
// Start a new activation, if necessary, before comparing with other threads.
UpdateActivation(current_thread, now);
// Determine eligibility after updating the activation period.
const bool is_eligible = current_thread->scheduler_state().start_time() <= now;
if (is_deadline && !is_eligible) {
// Ineligible deadline threads must wait until they become eligible again.
return DequeueThread(now, queue_guard);
}
if (IsDeadlineThreadEligible(now)) {
if (is_deadline) {
// The current thread is deadline and eligible. Select the eligible
// thread with the earliest finish time, which may still be the current
// thread.
if (DequeueResult earlier_thread_result =
DequeueEarlierDeadlineThread(now, current_thread)) {
return earlier_thread_result;
}
// The current thread is eligible and has the earliest finish time.
return current_thread;
}
// The current thread is fair. An eligible deadline thread should preempt
// the current thread.
return DequeueDeadlineThread(now);
}
if (!is_deadline) {
if (is_eligible) {
if (IsFairThreadEligible(now)) {
// Select the eligible fair thread with the earliest finish time,
// which may still be the current thread.
const SchedTime finish_time = current_thread->scheduler_state().finish_time();
if (DequeueResult earlier_thread_result = DequeueEarlierFairThread(now, finish_time)) {
return earlier_thread_result;
}
}
// The current fair thread has the earliest eligible finish time.
return current_thread;
}
if (IsFairThreadEligible(now)) {
// Select an eligible fair thread.
return DequeueFairThread(now);
}
// No fair threads are currently eligible. Temporarily add the current
// thread to the run queue, before snapping the variable timeline to the
// earliest eligible time, to simplify selection in the updated timeline.
// Use Placement::Adjustment to avoid emitting inconsistent trace events.
QueueThread(current_thread, Placement::Adjustment);
// Snap the variable timeline to the earliest eligible time and select the
// thread with the earliest eligible finish time.
DequeueResult next_thread = DequeueFairThread(now);
DEBUG_ASSERT(next_thread);
// If the current thread is not selected to run again, it must be removed
// from the run queue to avoid being stolen during lock juggling. It will
// be returned to the run queue after the lock juggling is completed and
// before switching to the next thread.
if (next_thread.thread() != current_thread) {
MarkHasOwnedThreadAccess(*current_thread);
EraseFromQueue(current_thread);
}
return next_thread;
}
// The current deadline thread has remaining time and no eligible contender.
DEBUG_ASSERT(is_deadline);
DEBUG_ASSERT(is_eligible);
return current_thread;
}
// The thread is either no longer runnable, migrating, or the idle thread.
return DequeueThread(now, queue_guard);
}
// Utility type that tracks values related to the thread being placed when
// finding a target CPU for a waking or migrating thread.
class Scheduler::ThreadDemand {
public:
ThreadDemand(const ThreadDemand&) = default;
ThreadDemand& operator=(const ThreadDemand&) = default;
// Returns the thread demand parameters for the given thread and unblock type.
// When the unblock type is synchronous, values from the current thread are
// also considered.
static ThreadDemand Get(const Thread* woken_thread, UnblockType unblock_type)
TA_REQ_SHARED(woken_thread->get_lock()) {
const SchedTime woken_thread_finish_time = woken_thread->scheduler_state().finish_time();
const SchedUtilization woken_utilization =
IsDeadlineThread(woken_thread)
? woken_thread->scheduler_state().effective_profile().deadline().utilization
: SchedUtilization{0};
// Bandwidth demand is based only on the woken thread for the normal
// unblock type.
if (unblock_type == UnblockType::Normal) {
const ThreadDemand demand{woken_utilization,
SchedUtilization{0},
woken_thread_finish_time,
woken_thread->scheduler_state().last_cpu(),
INVALID_CPU,
IsFairThread(woken_thread)};
LOCAL_KTRACE_INSTANT(QUEUE, "ThreadDemand::Get",
("woken_thread_finish_time", woken_thread_finish_time),
("woken_thread_last_cpu", demand.woken_thread_last_cpu()),
("woken_thread_utilization", demand.woken_thread_utilization()),
("current_thread_utilization", demand.current_thread_utilization()));
return demand;
}
// Bandwidth demand depends on both the woken and current threads for the
// synchronous unblock type.
const Thread* current_thread = Thread::Current::Get();
Scheduler* scheduler = Scheduler::Get();
// Briefly acquire the queue lock for the current scheduler, where the
// current thread is running, to allow read-only access to the relevant
// scheduler state.
Guard<MonitoredSpinLock, NoIrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
scheduler->AssertInScheduler(*current_thread);
const SchedUtilization current_utilization =
IsDeadlineThread(current_thread)
? current_thread->scheduler_state().effective_profile().deadline().utilization
: SchedUtilization{0};
const ThreadDemand demand{woken_utilization,
current_utilization,
woken_thread_finish_time,
woken_thread->scheduler_state().last_cpu(),
current_thread->scheduler_state().curr_cpu(),
IsFairThread(woken_thread) && IsFairThread(current_thread)};
LOCAL_KTRACE_INSTANT(QUEUE, "ThreadDemand::Get",
("woken_thread_finish_time", woken_thread_finish_time),
("woken_thread_last_cpu", demand.woken_thread_last_cpu()),
("woken_thread_utilization", demand.woken_thread_utilization()),
("current_thread_utilization", demand.current_thread_utilization()));
return demand;
}
// Returns the effective demand the thread would place on the given cpu,
// accounting for cached demand by the woken thread.
constexpr SchedUtilization GetEffectiveUtilization(cpu_num_t cpu_num,
bool power_level_control_enabled) const {
// If power level control is enabled, and thus demand caching is being
// performed, the expiration of the woken thread's finish time is considered
// to avoid double counting demand cached on the last CPU it ran on. Note
// that the expiration of the finish time is compared with the current time
// here to maximize the accuracy of the prediction of the cached state,
// which could change in parallel with the overall CPU placement operation.
const bool woken_thread_demand_likely_cached = power_level_control_enabled &&
cpu_num == woken_thread_last_cpu() &&
woken_thread_finish_time() > CurrentTime();
// Return the additional demand that the woken thread would contribute to
// the given CPU, accounting for demand caching. The demand of the current
// thread, if contributing to the total demand, is already accounted for on
// the CPU it is running on.
const SchedUtilization total_utilization =
(!woken_thread_demand_likely_cached ? woken_thread_utilization() : SchedUtilization{0}) +
(cpu_num != current_thread_cpu() ? current_thread_utilization() : SchedUtilization{0});
return ktl::min(total_utilization, kThreadUtilizationMax);
}
constexpr cpu_num_t InitialCpu() const {
return current_thread_cpu_ != INVALID_CPU ? current_thread_cpu_ : woken_thread_last_cpu_;
}
constexpr SchedUtilization woken_thread_utilization() const { return woken_thread_utilization_; }
constexpr SchedUtilization current_thread_utilization() const {
return current_thread_utilization_;
}
constexpr SchedTime woken_thread_finish_time() const { return woken_thread_finish_time_; }
constexpr cpu_num_t woken_thread_last_cpu() const { return woken_thread_last_cpu_; }
constexpr cpu_num_t current_thread_cpu() const { return current_thread_cpu_; }
constexpr bool is_fair() const { return is_fair_; }
private:
constexpr ThreadDemand(SchedUtilization woken_thread_utilization,
SchedUtilization current_thread_utilization,
SchedTime woken_thread_finish_time, cpu_num_t woken_thread_last_cpu,
cpu_num_t current_thread_cpu, bool is_fair)
: woken_thread_utilization_{woken_thread_utilization},
current_thread_utilization_{current_thread_utilization},
woken_thread_finish_time_{woken_thread_finish_time},
woken_thread_last_cpu_{woken_thread_last_cpu},
current_thread_cpu_{current_thread_cpu},
is_fair_{is_fair} {}
SchedUtilization woken_thread_utilization_;
SchedUtilization current_thread_utilization_;
SchedTime woken_thread_finish_time_;
cpu_num_t woken_thread_last_cpu_;
cpu_num_t current_thread_cpu_;
bool is_fair_;
};
// Latches a potential candidate placement for the thread and essential values
// for making comparisons with other candidates. Dynamic values are exposed by
// each Scheduler instance as relaxed atomics to avoid queue lock contention,
// since these values are subject to change as soon as the queue lock is
// dropped, and the comparisons only require approximate consistency. Latching
// the values ensures that each comparison of a particular candidate with other
// candidates uses the same values.
class Scheduler::CandidatePlacement {
public:
CandidatePlacement() = default;
// Latches key values from the given CPU, demand, and power domain set.
static CandidatePlacement Evaluate(cpu_num_t cpu_num, const ThreadDemand& demand,
const PowerDomainSet& power_domain_set,
bool power_level_control_enabled) {
const Scheduler* scheduler = Scheduler::Get(cpu_num);
const SchedDuration cpu_queue_time_ns = scheduler->exported_queue_time_ns();
const SchedProcessingRate cpu_processing_rate = scheduler->exported_processing_rate();
const SchedProcessingRate cpu_max_processing_rate = scheduler->exported_max_processing_rate();
const SchedUtilization cpu_deadline_utilization = scheduler->exported_deadline_utilization();
// TODO(https://fxbug.dev/448195120): Factor in estimated utilization for
// variable/fair bandwidth workloads, modulated by the variable/fair
// bandwidth limit, if any. For now, just use constant/deadline bandwidth as
// the primary input for the requirement.
const SchedProcessingRate current_required_processing_rate{cpu_deadline_utilization};
// Start with the basic requirements common to all threads.
SchedProcessingRate new_required_processing_rate = current_required_processing_rate;
uint64_t estimated_power_delta_nw = 0;
// Add the bandwidth demand for the thread.
new_required_processing_rate +=
demand.GetEffectiveUtilization(cpu_num, power_level_control_enabled);
// Estimated energy cost comparisons are only necessary in heterogeneous /
// multi-domain systems. In strict SMP systems, where all CPUs have the same
// power / performance characteristics and operating point changes affect
// all CPUs simultaneously, only demand and processing rate requirements
// need to be considered for task placement and admission control. However,
// this condition may change to track and maintain deeper idle states in the
// future.
// TODO(eieio): Account for deeper idle states when making task placement
// decisions.
if (power_domain_set.count() > 1u) {
// Compute the power delta if there is a change in the required processing
// rate. The addition of deadline / constant bandwidth demand will always
// increase the required processing rate, but fair / variable bandwidth
// demand may or may not increase the required processing rate.
const SchedProcessingRate required_processing_rate_delta =
new_required_processing_rate - current_required_processing_rate;
if (required_processing_rate_delta != 0) {
// The current power cost for each individual processor in the power
// domain, including this candidate placement.
const uint64_t current_power_cost_nw_per_rate =
power_domain_set.LookupPowerCost(cpu_num, cpu_processing_rate);
// If the task fits on the candidate without changing the processing rate,
// estimate the power delta based only on the new demand. Otherwise, if the
// candidate will become oversubscribed at the current rate, a higher
// compatible rate must be selected, if one exists, and the estimated power
// delta must include the effect on the entire domain.
if (new_required_processing_rate <= cpu_processing_rate) {
estimated_power_delta_nw =
Round<uint64_t>(required_processing_rate_delta * current_power_cost_nw_per_rate);
} else {
const SchedUtilization domain_utilization =
power_domain_set.LookupTotalNormalizedUtilization(cpu_num);
const uint64_t new_power_cost_nw_per_rate =
power_domain_set.LookupPowerCost(cpu_num, new_required_processing_rate);
estimated_power_delta_nw = Round<uint64_t>(
(domain_utilization + required_processing_rate_delta) * new_power_cost_nw_per_rate -
domain_utilization * current_power_cost_nw_per_rate);
}
}
}
LOCAL_KTRACE_INSTANT(QUEUE, "evaluate", ("cpu", cpu_num), ("queue time", cpu_queue_time_ns),
("actual rate", cpu_processing_rate),
("max rate", cpu_max_processing_rate),
("current required rate", current_required_processing_rate),
("new required rate", new_required_processing_rate),
("estimated power delta nw", estimated_power_delta_nw));
return CandidatePlacement(cpu_num, scheduler->cluster(), cpu_queue_time_ns, cpu_processing_rate,
cpu_max_processing_rate, new_required_processing_rate,
cpu_deadline_utilization, estimated_power_delta_nw);
}
CandidatePlacement(const CandidatePlacement&) = default;
CandidatePlacement& operator=(const CandidatePlacement&) = default;
constexpr explicit operator bool() const { return is_valid_; }
// Returns true if the load balancing objectives and constraints are
// sufficiently met that candidate selection can stop on this candidate.
constexpr bool IsSufficient(const ThreadDemand& demand) const {
// If the estimated power is non-zero (i.e. there is an active energy model),
// don't stop searching early, even if the other criteria are met. This
// results in the search converging on the lowest power candidate that also
// meets the admission control criteria.
// TODO(https://fxbug.dev/448196664): Consider adding a configurable
// threshold to terminate the search if the estimated power cost is
// acceptably low.
const bool migration_unnecessary_condition =
estimated_power_delta_nw() == 0 && queue_time_ns() <= kIntraClusterThreshold;
// Bypass the migration condition for synchronous wakeups. For normal
// wakeups, the current thread's CPU will be INVALID_CPU to indicate that it
// should not affect the placement decision.
const bool synchronous_condition = cpu() == demand.current_thread_cpu();
// If this is a synchronous wake up or migration is unnecessary, then stop
// searching for a better placement if the demand is admissible.
return (synchronous_condition || migration_unnecessary_condition) && is_admissible();
}
// Returns true if this candidate is a better alternative than the current target.
constexpr bool IsBetterThan(const CandidatePlacement& current_target,
const ThreadDemand& demand) const {
LOCAL_KTRACE_INSTANT(QUEUE, "compare", ("Current queue time", current_target.queue_time_ns()),
("Current cluster", current_target.cluster()),
("Current deadline utilization", current_target.deadline_utilization()),
("Candidate queue time", queue_time_ns()),
("Candidate cluster", cluster()),
("Candidate deadline utilization", deadline_utilization()));
if (demand.is_fair()) {
// CPUs in the same logical cluster are considered equivalent in terms of
// cache affinity. Choose the least loaded among the members of a cluster.
if (cluster() == current_target.cluster()) {
return ktl::tuple{queue_time_ns(), deadline_utilization()} <
ktl::tuple{current_target.queue_time_ns(), current_target.deadline_utilization()};
}
// Only consider crossing cluster boundaries if the current candidate is
// above the threshold.
return current_target.queue_time_ns() > kInterClusterThreshold &&
queue_time_ns() < current_target.queue_time_ns();
}
ktl::tuple candidate_criteria{capacity_shortfall(), estimated_power_delta_nw(),
deadline_utilization(), queue_time_ns()};
ktl::tuple current_criteria{
current_target.capacity_shortfall(), current_target.estimated_power_delta_nw(),
current_target.deadline_utilization(), current_target.queue_time_ns()};
return candidate_criteria < current_criteria;
}
constexpr cpu_num_t cpu() const { return cpu_; }
constexpr size_t cluster() const { return cluster_; }
constexpr SchedDuration queue_time_ns() const { return queue_time_ns_; }
constexpr SchedProcessingRate processing_rate() const { return processing_rate_; }
constexpr SchedProcessingRate max_processing_rate() const { return max_processing_rate_; }
constexpr SchedProcessingRate required_processing_rate() const {
return required_processing_rate_;
}
constexpr SchedUtilization deadline_utilization() const { return deadline_utilization_; }
constexpr uint64_t estimated_power_delta_nw() const { return estimated_power_delta_nw_; }
constexpr bool is_admissible() const {
return required_processing_rate() <= max_processing_rate();
}
constexpr SchedProcessingRate capacity_shortfall() const {
return is_admissible() ? SchedProcessingRate{0}
: required_processing_rate() - max_processing_rate();
}
private:
constexpr CandidatePlacement(cpu_num_t cpu, size_t cluster, SchedDuration queue_time_ns,
SchedProcessingRate processing_rate,
SchedProcessingRate max_processing_rate,
SchedProcessingRate required_processing_rate,
SchedUtilization deadline_utilization,
uint64_t estimated_power_delta_nw)
: is_valid_{true},
cpu_{cpu},
cluster_{cluster},
queue_time_ns_{queue_time_ns},
processing_rate_{processing_rate},
max_processing_rate_{max_processing_rate},
required_processing_rate_{required_processing_rate},
deadline_utilization_{deadline_utilization},
estimated_power_delta_nw_{estimated_power_delta_nw} {}
bool is_valid_{false};
cpu_num_t cpu_{INVALID_CPU};
size_t cluster_{0};
SchedDuration queue_time_ns_{0};
SchedProcessingRate processing_rate_{0};
SchedProcessingRate max_processing_rate_{0};
SchedProcessingRate required_processing_rate_{0};
SchedUtilization deadline_utilization_{0};
uint64_t estimated_power_delta_nw_{0};
};
cpu_num_t Scheduler::FindTargetCpu(Thread* thread, UnblockType unblock_type) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "find_target");
// Determine the set of CPUs the thread is allowed to run on.
const cpu_num_t current_cpu = arch_curr_cpu_num();
const cpu_mask_t active_mask = PeekActiveMask();
const SchedulerState& thread_state = const_cast<const Thread*>(thread)->scheduler_state();
const cpu_mask_t available_mask = thread_state.GetEffectiveCpuMask(active_mask);
DEBUG_ASSERT_MSG(available_mask != 0,
"thread=%s affinity=%#x soft_affinity=%#x active=%#x "
"idle=%#x arch_ints_disabled=%d",
thread->name(), thread_state.hard_affinity(), thread_state.soft_affinity(),
active_mask, PeekIdleMask(), arch_ints_disabled());
LOCAL_KTRACE_INSTANT(DETAILED, "target_mask", ("online", mp_get_online_mask()),
("active", active_mask));
// Evaluate the bandwidth demand of the woken thread, which will include the
// bandwidth of the current thread for synchronous wakeups.
const ThreadDemand demand = ThreadDemand::Get(thread, unblock_type);
// Find a suitable target CPU, usually starting at the last CPU the task ran
// on, if any. Candidates are considered in order of best to worst potential
// cache affinity.
const cpu_num_t last_cpu = demand.InitialCpu();
const cpu_num_t starting_cpu = last_cpu != INVALID_CPU ? last_cpu : current_cpu;
const CpuSearchSet& search_set = percpu::Get(starting_cpu).search_set;
// Loop over the search set for CPU the task last ran on to find a suitable
// target.
CandidatePlacement target_queue;
{
// Acquire the queue lock of the current CPU to ensure that the power domain
// set for the current CPU doesn't change until the search is complete.
// TODO(https://fxbug.dev/448191466): Move the energy model into the ZBI and
// allocate power domain data structures only during early init, removing
// the need for locking or snapshotting.
Scheduler* scheduler = Get();
Guard<MonitoredSpinLock, NoIrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
const PowerDomainSet& power_domain_set = scheduler->power_level_control_.power_domain_set();
const auto power_level_control_enabled = scheduler->power_level_control_.is_enabled();
for (const auto& entry : search_set.const_iterator()) {
const cpu_num_t candidate_cpu = entry.cpu;
const bool candidate_available = available_mask & cpu_num_to_mask(candidate_cpu);
if (candidate_available) {
const CandidatePlacement candidate_queue = CandidatePlacement::Evaluate(
candidate_cpu, demand, power_domain_set, power_level_control_enabled);
if (!target_queue || candidate_queue.IsBetterThan(target_queue, demand)) {
target_queue = candidate_queue;
// Stop searching when the load balancing criteria have been sufficiently met.
if (target_queue.IsSufficient(demand)) {
break;
}
}
}
}
}
DEBUG_ASSERT(target_queue.cpu() != INVALID_CPU);
if (unblock_type == UnblockType::Normal) {
if (last_cpu == target_queue.cpu()) {
counter_wakeup_normal_local.Add(1);
} else {
counter_wakeup_normal_migrated.Add(1);
}
} else {
if (demand.current_thread_cpu() == target_queue.cpu()) {
counter_wakeup_synchronous_local.Add(1);
} else {
counter_wakeup_synchronous_migrated.Add(1);
}
}
trace = KTRACE_END_SCOPE(("last_cpu", last_cpu), ("target_cpu", target_queue.cpu()));
return target_queue.cpu();
}
void Scheduler::IncFindTargetCpuRetriesKcounter() { counter_find_target_cpu_retries.Add(1u); }
void Scheduler::ProcessSaveStateList() {
DEBUG_ASSERT(arch_ints_disabled());
// Move the save_state_list_ to a local, stack allocated list. This allows us to relinquish the
// queue_lock_ for the rest of the method.
Thread::SaveStateList local_save_state_list;
{
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&queue_lock_, SOURCE_TAG};
if (save_state_list_.is_empty()) {
return;
}
local_save_state_list = ktl::move(save_state_list_);
}
ChainLockTransaction& active_clt = ChainLockTransaction::ActiveRef();
active_clt.Restart(CLT_TAG("Scheduler::ProcessSaveStateList (restart)"));
cpu_mask_t cpus_to_reschedule_mask = 0;
while (!local_save_state_list.is_empty()) {
Thread* to_migrate = local_save_state_list.pop_front();
// Acquire the thread's lock without backing off using an explicit retry
// loop because the current thread's lock is held and ChainLock::Acquire
// asserts that no chain locks are currently held to avoid misuse in the
// general case. We also currently hold the special "sched token", meaning
// that we will win arbitration against any other chainlock transaction.
//
// This is safe to do here because we can guarantee that no other scheduler
// holds or is attempting to acquire the locks of any of the threads in this
// save_state_list_. The only context in which schedulers attempt to acquire
// the locks of threads in other schedulers is during the `StealWork`
// routine. However, that routine only looks for threads in the RunQueues
// (not save_state_lists) and also does not steal threads that have
// migration functions. Therefore, it is impossible for another scheduler to
// be fighting over this thread's lock.
while (!to_migrate->get_lock().TryAcquire(ChainLock::AllowFinalized::No,
ChainLock::RecordBackoff::No)) {
arch::Yield();
}
// We now have the thread's lock, so run the migrate function.
to_migrate->CallMigrateFnLocked(Thread::MigrateStage::Save);
// TODO(rudymathu): Threads placed in the save_state_list_ via an Unblock
// operation already performed a `FindTargetCpu` call. In a future
// optimization, we should be able to reuse that value here and avoid
// another CPU search, provided that the CPU is still online and load
// balancing constraints still hold.
const cpu_num_t target_cpu =
FindActiveSchedulerForThread(to_migrate, [](Thread* thread, Scheduler* target) {
MarkInFindActiveSchedulerForThreadCbk(*thread, *target);
target->Insert(CurrentTime(), thread, Placement::Migration);
});
cpus_to_reschedule_mask |= cpu_num_to_mask(target_cpu);
to_migrate->get_lock().Release();
}
ChainLockTransaction::Finalize();
// Issue reschedule IPIs to CPUs with migrated threads.
if (cpus_to_reschedule_mask) {
mp_reschedule(cpus_to_reschedule_mask, 0);
}
}
void Scheduler::UpdateEstimatedEnergyConsumption(Thread* current_thread,
SchedMonoTimeAndBootTicks now,
SchedDuration actual_runtime_ns) {
// Time in a low-power idle state should only accrue when running the idle
// thread. Always consume the processor idle time, even if an energy model is
// not set, to avoid accumulating excessive idle time and triggering the
// assert when an energy model is finally set.
const SchedDuration idle_processor_time_ns{TakeProcessorIdleTime()};
DEBUG_ASSERT_MSG(
idle_processor_time_ns <= actual_runtime_ns,
"idle_processor_time_ns=%" PRId64 " actual_runtime_ns=%" PRId64 " current_thread=%s",
idle_processor_time_ns.raw_value(), actual_runtime_ns.raw_value(), current_thread->name());
if (power_level_control_.is_enabled()) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "update_energy");
// Subtract any time the processor spent in the low-power idle state from the
// runtime to ensure that active vs. idle power consumption is attributed
// correctly. Processors can accumulate both active or idle power consumption,
// but threads, including the idle power thread, accumulate only active power
// consumption.
const SchedDuration active_processor_time_ns = actual_runtime_ns - idle_processor_time_ns;
using FractionalSeconds = ffl::Fixed<uint64_t, 20>;
using Energy = ffl::Fixed<uint64_t, 0>;
// This method can be called from another CPU during PI adjustments. Make sure
// to update the stats for the CPU the thread is associated with.
cpu_stats& stats = percpu::Get(this_cpu()).stats;
// The energy consumption over the active interval is computed as:
//
// E_active = P_active * dt_active
//
// P_active is the power coefficient of the current active power level.
const FractionalSeconds active_interval_s = active_processor_time_ns / ZX_SEC(1);
const uint64_t active_power_nw = power_level_control_.active_power_coefficient_nw();
const Energy active_energy_consumption_nj = active_power_nw * active_interval_s;
stats.active_energy_consumption_nj += active_energy_consumption_nj.raw_value();
current_thread->scheduler_state().estimated_energy_consumption_nj +=
active_energy_consumption_nj.raw_value();
// If the thread is dying, report its active energy concumption after the
// final update to ensure an accurate estimate based on its full runtime.
if (current_thread->scheduler_state().state() == THREAD_DEATH) {
KTRACE_INSTANT("kernel:power", "Thread Energy",
("estimated energy consumption (nJ)",
current_thread->scheduler_state().estimated_energy_consumption_nj));
}
// TODO(https://fxbug.dev/377583571): Select the correct power coefficient
// when deeper idle states are implemented. For now the max idle power
// coefficient corresponds to the most general arch idle state (e.g. WFI,
// halt).
if (idle_processor_time_ns > 0) {
const FractionalSeconds idle_interval_s = idle_processor_time_ns / ZX_SEC(1);
const uint64_t idle_power_nw = power_level_control_.max_idle_power_coefficient_nw();
const Energy idle_energy_consumption_nj = idle_power_nw * idle_interval_s;
stats.idle_energy_consumption_nj += idle_energy_consumption_nj.raw_value();
}
LOCAL_KTRACE_COUNTER_TIMESTAMP(
BANDWIDTH, "Energy (nJ)", now.boot_ticks, this_cpu(),
("CPU", stats.active_energy_consumption_nj + stats.idle_energy_consumption_nj));
trace = KTRACE_END_SCOPE(("active_processor_time_ns", active_processor_time_ns),
("idle_processor_time_ns", idle_processor_time_ns));
}
}
// CRITICAL: Maintain Coherency of Idle and Busy States
//
// Accurate CPU statistics depend on the atomic coherency of the following:
// 1. Time Samples: Current time and interval since last update.
// 2. Idle State: CPU flags (SetIdle, PeekIsIdle, PeekIdleMask).
// 3. Stats Members: cpu_stats (last_updated_instant, idle_time,
// normalized_busy_time).
// 4. Rate: The current rate of the CPU (processing_rate).
//
// ATOMICITY REQUIREMENT:
// All reads and writes of these variables must occur within a SINGLE
// acquisition of the queue lock. This ensures that external queries (e.g.,
// ZX_INFO_CPU_STATS) do not see inconsistent values or "negative progress"
// during projection.
//
// STATE TRANSITIONS, WORK STEALING, AND PROCESSING RATE:
// If the CPU transitions between idle and busy, or changes rate, stats must be
// updated carefully:
// 1. Initial Update: Always performed under the first lock acquisition to
// account for time elapsed since the last reschedule.
// 2. Transition Update: If the CPU's idle state changes, upon re-acquiring the
// lock, stats MUST be updated again using FRESH time samples.
// 3. Processing Rate Update: If the CPU's processing rate changes, upon
// re-acquiring the lock, stats MUST be updated again using FRESH time
// samples.
//
// Failure to re-sample time after re-acquiring the lock will result in
// "negative progress," as other CPUs may have already projected these values
// forward while the lock was released.
//
void Scheduler::UpdateCpuStatsSegment(SchedTime now, CpuStatsSegmentType stats_segment_type,
SchedProcessingRate segment_processing_rate) const {
percpu& percpu = percpu::Get(this_cpu());
const SchedDuration segment_runtime = now - percpu.stats.last_updated_instant;
DEBUG_ASSERT_MSG(segment_runtime >= 0,
"now=%" PRId64 " last_updated_instant=%" PRId64 " segment_runtime=%" PRId64,
now.raw_value(), percpu.stats.last_updated_instant, segment_runtime.raw_value());
ktl::atomic_ref{percpu.stats.last_updated_instant}.store(now.raw_value(),
ktl::memory_order_relaxed);
if (stats_segment_type == CpuStatsSegmentType::Idle) {
ktl::atomic_ref{percpu.stats.idle_time}.fetch_add(segment_runtime.raw_value(),
ktl::memory_order_relaxed);
} else {
ktl::atomic_ref{percpu.stats.normalized_busy_time}.fetch_add(
Round<int64_t>(segment_runtime * segment_processing_rate), ktl::memory_order_relaxed);
}
}
cpu_stats Scheduler::GetProjectedCpuStats(cpu_num_t cpu_num) {
DEBUG_ASSERT(cpu_num < percpu::processor_count());
const Scheduler* const scheduler = Get(cpu_num);
const percpu& percpu = percpu::Get(cpu_num);
Guard<MonitoredSpinLock, IrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
// Some CPU stats values are synchronized by the queue lock, while others are
// updated via unsynchronized relaxed atomic writes. Use well-defined-copy
// semantics to avoid formal data races when copying the unsynchronized CPU
// stats members.
cpu_stats cpu_stats;
concurrent::WellDefinedCopyFrom<concurrent::SyncOpt::None, alignof(decltype(cpu_stats))>(
&cpu_stats, &percpu.stats, sizeof(cpu_stats));
const SchedTime now = CurrentTime();
const SchedDuration duration_since_last_update_ns = now - cpu_stats.last_updated_instant;
DEBUG_ASSERT_MSG(
duration_since_last_update_ns >= 0,
"now=%" PRId64 " last_updated_instant=%" PRId64 " duration_since_last_update_ns=%" PRId64,
now.raw_value(), cpu_stats.last_updated_instant, duration_since_last_update_ns.raw_value());
if (PeekIsIdle(cpu_num)) {
cpu_stats.idle_time += duration_since_last_update_ns;
} else {
cpu_stats.normalized_busy_time += duration_since_last_update_ns * scheduler->processing_rate();
}
return cpu_stats;
}
void Scheduler::RescheduleCommon(Thread* const current_thread, EndTraceCallback end_outer_trace) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "reschedule_common");
DEBUG_ASSERT(current_thread == Thread::Current::Get());
const cpu_num_t current_cpu = arch_curr_cpu_num();
SchedulerState* const current_state = &current_thread->scheduler_state();
// No spinlocks should be held as we come into a reschedule operation. Only
// the current thread's lock should be held.
DEBUG_ASSERT_MSG(const uint32_t locks_held = arch_num_spinlocks_held();
locks_held == 0, "arch_num_spinlocks_held() == %u\n", locks_held);
DEBUG_ASSERT(!arch_blocking_disallowed());
DEBUG_ASSERT_MSG(const cpu_num_t tcpu = this_cpu();
current_cpu == tcpu, "current_cpu=%u this_cpu=%u", current_cpu, tcpu);
ChainLockTransaction& active_clt = ChainLockTransaction::ActiveRef();
active_clt.AssertNumLocksHeld(1);
CPU_STATS_INC(reschedules);
// Prepare for rescheduling by backing up the chain lock context and switching
// to the special "sched token" which we use during reschedule operations in
// order to guarantee that we will win arbitration with other active
// transactions which are not reschedule operations. We will restore this
// context at the end of the operation (whether we context switched or not).
const auto saved_transaction_state = ChainLockTransaction::SaveStateAndUseReservedToken();
current_thread->get_lock().SyncToken();
// Process all threads in the save_state_list_. We do this before performing any
// other reschedule operation.
ProcessSaveStateList();
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&queue_lock_, SOURCE_TAG};
// When calling into reschedule, the current thread is only allowed to be in a
// limited number of states, depending on where it came from.
//
// RUNNING : Thread is the currently active thread on this CPU.
// BLOCKED* : Thread has been inserted into a wait queue and is in the
// process of de-activating.
// SUSPENDED : Thread is in the process of suspending (but is not in any queue).
// DEAD : Thread is in the process of dying, this is its final reschedule.
//
AssertInScheduler(*current_thread);
DEBUG_ASSERT(current_thread->state() != THREAD_READY);
if (current_thread->state() == THREAD_RUNNING) {
DEBUG_ASSERT(current_thread->disposition() == Disposition::Associated);
current_thread->set_ready();
}
// Sample the current time after acquiring the queue lock to avoid large skews
// between accounting based on now and trace events that should occur
// approximately now.
const SchedMonoTimeAndBootTicks mono_and_boot_now = CurrentMonoTimeAndBootTicks();
const SchedTime now = mono_and_boot_now.mono_time;
// TODO(https://fxbug.dev/381899402): Find the root cause of small negative
// values in the actual runtime calculation.
const SchedDuration actual_runtime_ns =
ktl::max<SchedDuration>(now - current_state->last_started_running_, SchedDuration{0});
current_state->last_started_running_ = now;
const SchedDuration total_runtime_ns = now - start_of_current_time_slice_ns_;
current_state->runtime_ns_ += actual_runtime_ns;
current_thread->UpdateRuntimeStats(current_thread->state());
// CRITICAL: Maintain Coherency of Idle and Busy States
//
// Apply the following rule:
//
// 1. Initial Update: Always performed under the first lock acquisition to
// account for time elapsed since the last reschedule.
//
// See UpdateCpuStatsSegment for the full set of coherency rules.
//
// It is not yet known whether the CPU is transitioning to/from idle or the
// processing rate is changing. Simply advance the CPU stats to reflect the
// elapsed idle time or normalized busy time at the last known processing rate
// since the last update.
UpdateCpuStatsSegment(now, ToCpuStatsSegmentType(*current_thread), processing_rate());
// Update the energy consumption accumulators for the current task and
// processor.
UpdateEstimatedEnergyConsumption(current_thread, mono_and_boot_now, actual_runtime_ns);
// Update the used time slice before evaluating the next task. Scale the
// actual runtime of a deadline task by the relative performance of the CPU,
// effectively increasing the capacity of the task in proportion to the
// performance ratio. The remaining time slice may become negative due to
// scheduler overhead.
current_state->time_slice_used_ns_ +=
IsDeadlineThread(current_thread) ? ScaleDown(actual_runtime_ns) : actual_runtime_ns;
// Adjust the rate of the current thread when fair bandwidth demand changes.
if (IsThreadAdjustable(current_thread) && weight_total_ != scheduled_weight_total_ &&
current_state->remaining_time_slice_ns() > 0 && current_state->finish_time() > now) {
ktrace::Scope trace_adjust_rate =
LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "adjust_rate", ("total runtime", total_runtime_ns));
scheduled_weight_total_ = weight_total_;
AdjustFairBandwidth(current_thread, now);
target_preemption_time_ns_ = ktl::clamp<SchedTime>(
now + current_state->remaining_time_slice_ns(), now, current_state->finish_time());
}
// Use a small epsilon to avoid tripping the consistency check below due to
// rounding when scaling the time slice.
const SchedDuration time_slice_epsilon{100};
const bool timeslice_expired = now >= current_state->finish_time_ ||
current_state->remaining_time_slice_ns() <= time_slice_epsilon;
// Check the consistency of the target preemption time and the current time
// slice.
[[maybe_unused]] const auto& ep = current_state->effective_profile_;
DEBUG_ASSERT_MSG(
now < target_preemption_time_ns_ || timeslice_expired,
"cpu=%u capacity_ns=%" PRId64 " deadline_ns=%" PRId64
"\nfinish_time =%16" PRId64 "\nnow =%16" PRId64
"\ntarget_preemption_time_ns =%16" PRId64 "\nstart_of_current_time_slice_ns=%16" PRId64
"\ntotal_runtime_ns =%16" PRId64 "\nactual_runtime_ns =%16" PRId64
"\ntime_slice_ns =%16" PRId64 "\ntime_slice_used_ns =%16" PRId64
"\nremaining_time_slice_ns =%16" PRId64,
this_cpu(), IsDeadlineThread(current_thread) ? ep.deadline().capacity_ns.raw_value() : 0,
IsDeadlineThread(current_thread) ? ep.deadline().deadline_ns.raw_value() : 0,
current_state->finish_time_.raw_value(), now.raw_value(),
target_preemption_time_ns_.raw_value(), start_of_current_time_slice_ns_.raw_value(),
total_runtime_ns.raw_value(), actual_runtime_ns.raw_value(),
current_state->time_slice_ns_.raw_value(), current_state->time_slice_used_ns().raw_value(),
current_state->remaining_time_slice_ns().raw_value());
// If the current thread needs to be migrated, remove it for migration.
const bool current_is_migrating = NeedsMigration(current_thread);
if (current_is_migrating) {
Remove(now, current_thread);
}
// Select the next thread to run.
//
// WARNING: This method temporarily drops the queue lock during work stealing,
// when there are no threads in the run queue and the current thread is no
// longer in the running state. Do not split updates to different values
// across this call that need to be coherently read from another CPU and
// synchronized using the queue lock. Doing so creates an incomplete critical
// section that can result in incoherent reads of the values updated in
// different acquisitions of the queue lock.
DequeueResult next_thread_result =
EvaluateNextThread(now, current_thread, current_is_migrating, queue_guard);
DEBUG_ASSERT(next_thread_result);
DEBUG_ASSERT(!current_thread->scheduler_queue_state().in_queue());
// Flush pending preemptions.
mp_reschedule(current_thread->preemption_state().preempts_pending(), 0);
current_thread->preemption_state().preempts_pending_clear();
// If the next_thread is not the same as the current thread, we will need to
// (briefly) drop the scheduler's queue lock in order to acquire the next
// thread's lock exclusively. If we have selected the same thread as before,
// we can skip this step.
//
// If we do need to acquire next_thread's lock, we use the same chain lock
// token that was used to acquire current_thread's lock. We know this is safe
// because:
//
// TODO(johngro): THIS IS NOT SAFE - FIX THIS COMMENT
//
// This turned out to not be safe after all. The deadlock potential below is
// why we need to switch to the special "scheduler" token at the start of the
// operation.
//
// While there cannot be any cycles in this individual operation (trivial to
// show since there are two different locks here), deadlock is still possible.
//
// The flow is:
// 1) T0 is running on CPU X, and is locked entering a reschedule operation.
// 2) T1 is selected as the next thread to run, and is being locked
// exclusively (after dropping the scheduler's lock). At the same time:
// 3) T2 is running on CPU Y, and is blocking on an OWQ which currently has T0
// as the owner. T1 has been selected as the new owner.
// 4) T2 and the queue have been locked by the BAAO operation.
// 5) T1 is the start of the new owner chain and is locked.
// 6) The next step in the BAAO locking is to lock the old owner chain,
// starting from T0.
//
// So:
// X has T0 and wants T1.
// Y has T1 and wants T0.
//
// CPU Y is in a sequence which obeys the backoff protocol, so as long as its
// token value > X's token value, Y will back off and we will be OK. If not,
// however, we deadlock. The reschedule operation cannot backoff and drop
// T0's lock, so it is stuck trying to get T1 from Y.
//
// TODO(johngro): END TODO
//
// 1) current_thread is currently running. Because of this, we know that it
// is the target node of it's PI graph.
// 2) next thread is currently READY, which means it is also the target node
// of it's PI graph.
// 3) PI graphs always have exactly one target node.
// 4) Therefore, current and next cannot be members of the same graph, and can
// both be acquired unconditionally.
//
// WARNING: This block temporarily drops the queue lock to acquire the next
// thread's lock. Do not split updates to different values across this call
// that need to be coherently read from another CPU and synchronized using the
// queue lock. Doing so creates an incomplete critical section that can result
// in incoherent reads of the values updated in different acquisitions of the
// queue lock.
if (current_thread != next_thread_result.thread()) {
// The ChainLockTransaction we were in when we entered RescheduleCommon has
// already been finalized. We need to restart it in order to obtain new
// locks.
active_clt.Restart(CLT_TAG("Scheduler::RescheduleCommon (restart)"));
// Release the queue lock, acquire the next thread lock, and re-acquire the
// queue lock. While the queue lock is unlocked, a PI operation or affinity
// change can update the next thread's state, since these operations could
// already hold the next thread's lock while waiting for the queue lock.
// These operations are obligated to reschedule this CPU if they invalidate
// the next thread's state, such that a different thread should run or the
// current CPU is no longer in the affinity mask. However, since the next
// thread is no longer in the run queue, it cannot be discovered and stolen
// by another CPU while the queue lock is unlocked.
queue_guard.CallUnlocked(
[&next_thread_result]() TA_ACQ(next_thread_result.thread()->get_lock()) {
ChainLockTransaction::AssertActive();
// Acquire the lock without backing off using an explicit retry loop
// because the current thread's lock is held and ChainLock::Acquire
// asserts that no chain locks are currently held to avoid misuse in
// the general case.
while (!next_thread_result.thread()->get_lock().TryAcquire(
ChainLock::AllowFinalized::No, ChainLock::RecordBackoff::No)) {
arch::Yield();
}
});
// All of the locks needed to complete the reschedule are held.
ChainLockTransaction::Finalize();
}
// Update the state of the current and next thread.
Thread* const next_thread = next_thread_result.thread();
next_thread->get_lock().AssertHeld();
AssertInScheduler(*next_thread);
const SchedulerQueueState& current_queue_state = current_thread->scheduler_queue_state();
SchedulerState* const next_state = &next_thread->scheduler_state();
// Convert from variable time to monotonic time if necessary.
next_thread_result.thread()->get_lock().MarkHeld();
next_thread_result.UpdateThreadTimeline();
// If the next thread was just stolen from another CPU, finish associating it
// with this CPU and scheduler.
if (next_thread->disposition() == Disposition::Stolen) {
DEBUG_ASSERT(next_thread->state() == THREAD_READY);
DEBUG_ASSERT(next_state->curr_cpu() != current_cpu);
DEBUG_ASSERT(next_thread->scheduler_queue_state().stolen_by() == current_cpu);
DEBUG_ASSERT(!next_thread_result.is_updated_pending());
Insert(now, next_thread, Placement::Association);
}
DEBUG_ASSERT(next_thread->disposition() == Disposition::Associated);
if (current_thread != next_thread) {
// Update stall contributions of the current and next thread.
StallAccumulator::ApplyContextSwitch(current_thread, next_thread);
// If the current thread is not the idle thread and not migrating, return it
// to the run queue or remove its bookkeeping, depending on whether it is
// READY.
if (!current_thread->IsIdle() && !current_is_migrating) {
if (current_thread->state() == THREAD_READY) {
QueueThread(current_thread,
timeslice_expired ? Placement::Insertion : Placement::Preemption, now);
} else {
RemoveThreadLocked(now, current_thread);
}
}
}
next_thread->set_running();
next_state->last_cpu_ = current_cpu;
DEBUG_ASSERT(next_state->curr_cpu_ == current_cpu);
active_thread_ = next_thread;
// Keep track of the weight of the active thread, since it is not in the run
// queue contributing to the min_weight subtree invariant.
active_thread_weight_ = !next_thread->IsIdle() && IsFairThread(next_thread)
? next_state->effective_profile().weight()
: SchedWeight::Max();
// Handle any pending migration work.
next_thread->CallMigrateFnLocked(Thread::MigrateStage::Restore);
// Update the expected runtime of the current thread and the per-CPU total.
// Only update the thread and aggregate values if the current thread is still
// associated with this CPU or is no longer ready.
const bool current_is_associated =
!current_queue_state.active() || current_state->curr_cpu_ == current_cpu;
if (!current_thread->IsIdle() && current_is_associated &&
(timeslice_expired || current_thread != next_thread)) {
ktrace::Scope trace_update_ema = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "update_expected_runtime");
// Adjust the runtime for the relative performance of the CPU to account for
// different performance levels in the estimate. The relative performance
// scale is in the range (0.0, 1.0], such that the adjusted runtime is
// always less than or equal to the monotonic runtime.
const SchedDuration adjusted_total_runtime_ns = ScaleDown(total_runtime_ns);
current_state->banked_runtime_ns_ += adjusted_total_runtime_ns;
if (timeslice_expired || !current_queue_state.active()) {
const SchedDuration delta_ns =
PeakDecayDelta(current_state->expected_runtime_ns_, current_state->banked_runtime_ns_,
kExpectedRuntimeAlpha, kExpectedRuntimeBeta);
current_state->expected_runtime_ns_ += delta_ns;
current_state->banked_runtime_ns_ = SchedDuration{0};
// Adjust the aggregate value by the same amount. The adjustment is only
// necessary when the thread is still active on this CPU.
if (current_queue_state.active()) {
UpdateTotalExpectedRuntime(delta_ns);
}
}
}
if (power_level_control_.is_enabled()) {
// After the current thread has been potentially removed from this
// scheduler, clear or prune the bandwidth demand cache and re-evaluate the
// current power level.
const bool clear_cache = next_thread->IsIdle() && critical_deadline_run_queue_.is_empty() &&
deadline_run_queue_.is_empty();
const SchedUtilization utilization_to_remove = clear_cache
? bandwidth_reservation_cache_.Clear()
: bandwidth_reservation_cache_.Prune(now);
if (utilization_to_remove > 0) {
LOCAL_KTRACE_INSTANT(QUEUE, "clear/prune", ("utilization_to_remove", utilization_to_remove));
UpdateTotalDeadlineUtilization(-utilization_to_remove);
}
// Evaluate changing the processing rate when the required utilization
// moves outside the bounds of the current power level. All of the
// processors in the same domain must be re-evaluated to determine whether
// an actual rate change should occur.
if (power_level_control_.scheduler_control_enabled() &&
!power_level_control_.has_pending_request() &&
power_level_control_.processing_rate_should_change()) {
power_level_control_.ReevaluateCurrentPowerLevel();
}
// Send a pending power level request before updating the processing rate.
//
// WARNING: This call temporarily drops the queue lock when calling the
// fast-path power level controller. Do not split updates to different
// values across this call that need to be coherently read from another CPU
// and synchronized using the queue lock. Doing so creates an incomplete
// critical section that can result in incoherent reads of the values
// updated in different acquisitions of the queue lock.
if (const cpu_mask_t reschedule_mask =
power_level_control_.SendPendingPowerLevelRequest(queue_guard)) {
// Reschedule the other CPUs affected by the power level change. The
// current CPU is filtered out by mp_reschedule.
mp_reschedule(reschedule_mask, 0);
}
}
// Update the current processing rate only after any uses in the reschedule
// path above to ensure the scale is applied consistently over the interval
// between reschedules (i.e. not earlier than the requested update).
//
// Updating the processing rate also results in updating the target preemption
// time below when the current thread is deadline scheduled.
//
// TODO(eieio): Shed load when total utilization is above the processing rate.
const SchedProcessingRate previous_processing_rate = processing_rate();
const bool processing_rate_updated = UpdateProcessingRate(mono_and_boot_now.boot_ticks);
// CRITICAL: Maintain Coherency of Idle and Busy States
//
// Apply the following rules:
//
// 2. Transition Update: If the CPU's idle state changes, upon re-acquiring
// the lock, stats MUST be updated again using FRESH time samples.
// 3. Processing Rate Update: If the CPU's processing rate changes, upon
// re-acquiring the lock, stats MUST be updated again using FRESH time
// samples.
//
// See UpdateCpuStatsSegment for the full set of coherency rules.
//
// The queue lock has been released and re-acquired one or more times, each of
// which may have been interposed by another CPU sampling the critical values
// and forward projecting idle or normalized busy time. If there is transition
// to/from idle or the processing rate changed while non-idle, advance the CPU
// stats to cover the segment since the initial update.
SetIdle(next_thread->IsIdle());
if (next_thread->IsIdle() != current_thread->IsIdle() ||
(!current_thread->IsIdle() && processing_rate_updated)) {
UpdateCpuStatsSegment(CurrentTime(), ToCpuStatsSegmentType(*current_thread),
previous_processing_rate);
}
{
// Each path below must set the preemption time.
// TODO(eieio): Debug assert that the value set is >= now.
SchedTime preemption_time_ns = SchedTime::Min();
if (next_thread->IsIdle()) {
ktrace::Scope trace_stop_preemption = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "idle");
next_state->last_started_running_ = now;
// If there are no tasks to run in the future or there is idle/power work to
// perform, disable the preemption timer. Otherwise, set the preemption
// time to the earliest eligible time.
preemption_time_ns = target_preemption_time_ns_ =
percpu::Get(this_cpu_).idle_power_thread.pending_power_work()
? SchedTime(ZX_TIME_INFINITE)
: GetNextEligibleTime();
} else if (timeslice_expired || current_thread != next_thread) {
ktrace::Scope trace_start_preemption = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "next_slice");
// Re-compute the time slice and ideal preemption time for the new thread
// based on the latest state.
target_preemption_time_ns_ = NextThreadTimeslice(next_thread, now);
// Update the thread's runtime stats to record the amount of time that it spent in the run
// queue.
next_thread->UpdateRuntimeStats(next_thread->state());
next_state->last_started_running_ = now;
start_of_current_time_slice_ns_ = now;
scheduled_weight_total_ = weight_total_;
// Adjust the preemption time to account for a deadline thread becoming
// eligible before the current time slice expires.
preemption_time_ns = IsFairThread(next_thread)
? ClampToDeadline(target_preemption_time_ns_)
: ClampToEarlierDeadline(target_preemption_time_ns_, next_thread);
DEBUG_ASSERT(preemption_time_ns <= target_preemption_time_ns_);
trace_start_preemption =
KTRACE_END_SCOPE(("preemption_time", preemption_time_ns),
("target preemption time", target_preemption_time_ns_));
// Emit a flow end event to match the flow begin event emitted when the
// thread was enqueued. Emitting in this scope ensures that thread just
// came from the run queue (and is not the idle thread).
LOCAL_KTRACE_FLOW_END(FLOW, "sched_latency", next_state->flow_id(),
("tid", next_thread->tid()));
} else {
ktrace::Scope trace_continue = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "continue");
DEBUG_ASSERT(current_thread == next_thread);
// Update the target preemption time for consistency with the updated CPU
// processing rate.
// TODO(eieio): Eventually update the effective bandwidth for all threads.
if (processing_rate_updated && IsDeadlineThread(next_thread)) {
target_preemption_time_ns_ = NextThreadTimeslice(next_thread, now);
}
// The current thread should continue to run. A throttled deadline thread
// might become eligible before the current time slice expires. Figure out
// whether to set the preemption time earlier to switch to the newly
// eligible thread.
//
// The preemption time should be set earlier when either:
// * Current is a fair thread and a deadline thread will become eligible
// before its time slice expires.
// * Current is a deadline thread and a deadline thread with an earlier
// deadline will become eligible before its time slice expires.
//
// Note that the target preemption time remains set to the ideal
// preemption time for the current task, even if the preemption timer is set
// earlier. If a task that becomes eligible is stolen before the early
// preemption is handled, this logic will reset to the original target
// preemption time.
preemption_time_ns = IsFairThread(next_thread)
? ClampToDeadline(target_preemption_time_ns_)
: ClampToEarlierDeadline(target_preemption_time_ns_, next_thread);
DEBUG_ASSERT(preemption_time_ns <= target_preemption_time_ns_);
trace_continue = KTRACE_END_SCOPE(("preemption_time", preemption_time_ns),
("target preemption time", target_preemption_time_ns_));
}
// Make sure the bandwidth demand cache will get pruned if there are
// remaining valid entries.
if (power_level_control_.is_enabled()) {
preemption_time_ns = bandwidth_reservation_cache_.ClampToNextFinishTime(preemption_time_ns);
}
DEBUG_ASSERT(preemption_time_ns != SchedTime::Min());
PreemptReset(current_cpu, now.raw_value(), preemption_time_ns.raw_value());
// Assert that there is no path beside running the idle thread can leave the
// preemption timer unarmed. However, the preemption timer may or may not be
// armed when running the idle thread.
DEBUG_ASSERT(next_thread->IsIdle() || percpu::Get(current_cpu).timer_queue.PreemptArmed());
}
// Almost done, we need to handle the actual context switch (if any).
if (current_thread != next_thread) {
LOCAL_KTRACE_INSTANT(
DETAILED, "switch_threads", ("total threads", runnable_task_count()),
("total weight", weight_total_.raw_value()),
("current thread time slice", current_thread->scheduler_state().remaining_time_slice_ns()),
("next thread time slice", next_thread->scheduler_state().remaining_time_slice_ns()));
TraceThreadQueueEvent("tqe_astart"_intern, next_thread);
// Release queue lock before context switching.
queue_guard.Release();
// Finish the migration of the current thread, if pending.
if (current_is_migrating) {
current_thread->CallMigrateFnLocked(Thread::MigrateStage::Save);
const cpu_num_t target_cpu = FindActiveSchedulerForThread(
current_thread, [now, this](Thread* thread, Scheduler* target) {
MarkInFindActiveSchedulerForThreadCbk(*thread, *target);
DEBUG_ASSERT(target != this);
target->Insert(now, thread, Placement::Migration);
});
mp_reschedule(cpu_num_to_mask(target_cpu), 0);
}
// Trace the activation of the next thread before context switching.
SchedTraceContextSwitch(current_thread, next_thread, current_cpu);
// We invoke the context switch functions before context switching, so that
// they have a chance to correctly perform the actions required. Doing so
// after context switching may lead to an invalid CPU state.
current_thread->CallContextSwitchFnLocked();
next_thread->CallContextSwitchFnLocked();
// Notes about Thread aspace rules:
//
// Typically, it is only safe for the current thread to access its aspace
// member directly, as only a running thread can change its own aspace, and
// if a thread is running, then its process must also be alive and therefore
// its aspace must also be alive.
//
// Context switching is a bit of an edge case. The current thread is
// becoming the next thread. Both aspaces must still be alive (even if the
// current thread is in the process of becoming rescheduled for the very
// last time as it exits), and neither one can change its own aspace right
// now (they are not really running).
//
// Because of this, it should be OK for us to directly access the aspaces
// during the context switch, without needing to either check the thread's
// state, or add any references to the VmAstate object.
[&]() TA_NO_THREAD_SAFETY_ANALYSIS {
if (current_thread->aspace() != next_thread->aspace()) {
vmm_context_switch(current_thread->aspace(), next_thread->aspace());
}
}();
CPU_STATS_INC(context_switches);
// Prevent the scheduler durations from spanning the context switch.
// Some context switches do not resume within this method on the other
// thread, which results in unterminated durations. All of the callers
// with durations tail-call this method, so terminating the duration
// here should not cause significant inaccuracy of the outer duration.
trace.End();
if (end_outer_trace) {
end_outer_trace();
}
if constexpr (SCHEDULER_QUEUE_TRACING_ENABLED) {
const uint64_t arg0 = 0;
const uint64_t arg1 = (ktl::clamp<uint64_t>(this_cpu_, 0, 0xF) << 16);
KTrace::Probe(KTrace::Context::Cpu, "tqe_afinish"_intern, arg0, arg1);
}
// Time for the context switch. Before actually performing the switch,
// record the current thread as the "previous thread" for the next thread
// becoming scheduled. The next thread needs to know which thread preceded
// it in order to drop its lock before unwinding, and our current stack is
// not going to be available to it.
DEBUG_ASSERT(previous_thread_ == nullptr);
previous_thread_ = current_thread;
arch_context_switch(current_thread, next_thread);
// We made it to the other side of the switch. We have switched stacks, so
// the local variable meanings have become redefined.
//
// ++ The value of current_thread after the context switch is the value of
// next_thread before the switch.
// ++ The value of next_thread after the context switch is the thread that
// the new current_thread context-switched to the last time it ran
// through here. We don't even know if this thread exists now, so its
// value is junk.
// ++ The value of current_thread before the switch has been stashed in
// the new current thread's scheduler_state's previous thread member.
// If the new current thread has a restartable sequence registered, we
// need to signal the thread to check for the restartable sequence before
// it returns to userspace.
current_thread->SignalCheckRestartableSequenceIfNeeded();
// We need to drop the old current thread's lock, while continuing to hold
// the new current thread's lock as we unwind.
// Scheduler::LockHandoffInternal will take care of dropping the previous
// current_thread's lock, but at this point, the static analyzer is going to
// be extremely confused because it does not (and cannot) know about the
// stack-swap which just happened. It thinks that we are still holding
// next_thread's lock (and we are; it is now current thread) and that we
// need to drop it (it is wrong about this, we need to drop the previous
// current thread's lock). So, just lie to it. Tell it that we have dropped
// next_thread's lock using a no-op assert (we don't actually want to
// examine the state of the lock in any way, since next thread is no longer
// valid).
Scheduler::LockHandoffInternal(saved_transaction_state, current_thread);
next_thread->get_lock().MarkReleased();
} else {
// Restore the transaction state and drop the queue guard before unwinding.
ChainLockTransaction::RestoreState(saved_transaction_state);
current_thread->get_lock().SyncToken();
queue_guard.Release();
}
}
void Scheduler::TrampolineLockHandoff() {
ChainLockTransaction::AssertActive();
// Construct the fake chainlock transaction we will be restoring. It should:
//
// 1) Not attempt to register itself with the per-cpu data structure. There
// should already be a registered CLT, and attempting to instantiate a new
// one using the typical constructor would trigger an ASSERT. This should
// look like a transaction which had been sitting on a stack, but swapped
// out using PrepareForReschedule at some point in the past.
// 2) Report that there are currently two locks held (because there are).
// We have the previously running thread's lock, and the current (new)
// thread's lock held (obtained by Scheduler::RescheduleCommon).
// 3) Report that the transaction has been finalized, as it would have been
// had we come in via RescheduleCommon.
//
ChainLockTransaction transaction =
ChainLockTransaction::MakeBareTransaction(CLT_TAG("Trampoline"), 2, true);
// Assert that the current thread's lock was "acquired" here. It was
// actually acquired during the context switch from
// Scheduler::RescheduleCommon, but we now need to release it before letting
// the new thread run. The currently registered CLT should be using the
// scheduler token that was used to acquire the new thread's lock during
// reschedule common.
Thread* const current_thread = Thread::Current::Get();
current_thread->get_lock().AssertAcquired();
// Now perform our lock handoff. This will release our previous
// thread's lock, and restore the transaction we just instantiated as
// the active transaction, and replace the current thread's lock's
// token with the new non-scheduler token.
Scheduler::LockHandoffInternal(transaction.SaveStateInitial(), current_thread);
// Finally, we just need to release the current thread's lock and we
// are finished.
current_thread->get_lock().Release();
}
void Scheduler::LockHandoffInternal(SavedState saved_state, Thread* const current_thread) {
DEBUG_ASSERT(arch_ints_disabled());
Scheduler* scheduler = Scheduler::Get();
Thread* const previous_thread = scheduler->previous_thread_;
DEBUG_ASSERT(previous_thread != nullptr);
scheduler->previous_thread_ = nullptr;
previous_thread->get_lock().AssertAcquired();
ChainLockTransaction::Active()->AssertNumLocksHeld(2);
ChainLockTransaction::RestoreState(saved_state);
current_thread->get_lock().SyncToken();
previous_thread->get_lock().SyncToken();
previous_thread->get_lock().Release();
}
SchedDuration Scheduler::CalculateTimeslice(const Thread* thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "calculate_timeslice");
const SchedulerState& state = thread->scheduler_state();
const EffectiveProfile& ep = state.effective_profile_;
const SchedUtilization fair_utilization = ep.weight() / weight_total_;
const SchedDuration time_slice_ns = fair_period_ * fair_utilization;
trace = KTRACE_END_SCOPE(
("fair utilization", fair_utilization), ("time slice", time_slice_ns),
("weight", ep.weight().raw_value()),
("total weight", KTRACE_ANNOTATED_VALUE(AssertHeld(queue_lock_), weight_total_.raw_value())));
return time_slice_ns;
}
SchedTime Scheduler::ClampToDeadline(SchedTime completion_time) {
return ktl::min(completion_time, GetNextEligibleTime());
}
SchedTime Scheduler::ClampToEarlierDeadline(SchedTime completion_time,
const Thread* reference_thread) {
const Thread* const thread = FindEarlierDeadlineThread(completion_time, reference_thread);
if (thread != nullptr) {
AssertInScheduler(*thread);
return ktl::min(completion_time, thread->scheduler_state().start_time_);
}
return completion_time;
}
SchedTime Scheduler::NextThreadTimeslice(Thread* thread, SchedTime now) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "next_timeslice");
SchedulerState* const state = &thread->scheduler_state();
if (IsFairThread(thread)) {
state->time_slice_ns_ = CalculateTimeslice(thread);
}
// Calculate the deadline when the remaining time slice is completed. The
// time slice is maintained by the deadline queuing logic, no need to update
// it here. The target preemption time is based on the time slice scaled by
// the performance of the CPU and clamped to the deadline. This increases
// capacity on slower processors, however, bandwidth isolation is preserved
// because CPU selection attempts to keep scaled total capacity below one.
const SchedDuration scaled_time_slice_ns = IsDeadlineThread(thread)
? ScaleUp(state->remaining_time_slice_ns())
: state->remaining_time_slice_ns();
const SchedTime target_preemption_time_ns =
ktl::min<SchedTime>(now + scaled_time_slice_ns, state->finish_time_);
trace = KTRACE_END_SCOPE(("scaled time slice", scaled_time_slice_ns),
("target preemption time", target_preemption_time_ns));
return target_preemption_time_ns;
}
void Scheduler::UpdateActivation(Thread* thread, SchedTime now) {
DEBUG_ASSERT(thread->state() == THREAD_READY);
DEBUG_ASSERT(!thread->IsIdle());
SchedulerState* const state = &thread->scheduler_state();
EffectiveProfile& ep = state->effective_profile_;
ktrace::Scope update_trace = LOCAL_KTRACE_BEGIN_SCOPE(
DETAILED, "update", ("remaining timeslice before", state->remaining_time_slice_ns()));
// Determine how much time is left before the finish time. This might be less
// than the remaining time slice or negative if the thread blocked.
const SchedDuration time_until_finish_time_ns = state->finish_time_ - now;
if (time_until_finish_time_ns <= 0 || state->remaining_time_slice_ns() <= 0) {
// Start a new activation and update the period and capacity with the latest
// fair or deadline bandwidth parameters. Although the fair capacity is
// determined dynamically during reschedules by CalculateTimeslice, use a
// non-zero initial value here to ensure that multiple successive calls to
// UpdateActivation do not incorrectly start new activations with start
// times further into the future.
//
// For example, such a succession can occur when a fair thread wakes after
// its current activation has expired, is re-activated as it enters the run
// queue, and is later stolen and potentially re-activated again. Both wake
// and steal operations can occur after a thread's activation has expired
// and must start a new activation. Using a non-zero value for the new
// activation's capacity ensures that the second call to UpdateActivation
// during stealing does not enter this conditional block unless enough time
// has passed in the run queue for the finish time to expire.
const SchedDuration period_ns = IsFairThread(thread) ? fair_period_ : ep.deadline().deadline_ns;
const SchedDuration capacity_ns = IsFairThread(thread) ? period_ns : ep.deadline().capacity_ns;
const SchedTime period_finish_ns = state->start_time_ + period_ns;
state->start_time_ = now >= period_finish_ns ? now : period_finish_ns;
state->finish_time_ = state->start_time_ + period_ns;
state->time_slice_ns_ = capacity_ns;
state->time_slice_used_ns_ = SchedDuration{0};
state->activation_count_++;
}
DEBUG_ASSERT(state->remaining_time_slice_ns() > 0);
update_trace = KTRACE_END_SCOPE(
("remaining time slice after", state->remaining_time_slice_ns()),
("time until finish time", time_until_finish_time_ns), ("start time", state->start_time()),
("finish time", state->finish_time()),
("effective period", SchedDuration{state->finish_time() - state->start_time()}),
("eligible", state->start_time() <= now));
}
void Scheduler::QueueThread(Thread* thread, Placement placement, SchedTime now,
SchedDuration total_runtime_ns) {
ktrace::Scope trace =
LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "queue_thread", ("placement", ToStringRef(placement)));
DEBUG_ASSERT(thread->state() == THREAD_READY);
DEBUG_ASSERT(!thread->IsIdle());
DEBUG_ASSERT(placement != Placement::Association);
SchedulerState* const state = &thread->scheduler_state();
// Check the consistency of the start and finish times before adding the
// thread to the run queue.
// LINT.IfChange(scheduler_start_finish_time)
DEBUG_ASSERT_MSG(
state->start_time() < state->finish_time(),
"now=%" PRId64 " start=%" PRId64 " finish=%" PRId64 " remaining_time_slice=%" PRId64
" fair_period=%" PRId64 " mono_ref=%" PRId64 " var_ref=%" PRId64 " slope=%s\n",
now.raw_value(), state->start_time().raw_value(), state->finish_time().raw_value(),
state->remaining_time_slice_ns().raw_value(), fair_period_.raw_value(),
fair_affine_transform_.monotonic_reference_time().raw_value(),
fair_affine_transform_.variable_reference_time().raw_value(),
Format(fair_affine_transform_.slope()).c_str());
// LINT.ThenChange(//tools/testing/tefmocheck/string_in_log_check.go:scheduler_start_finish_time)
// Only update the enqueue time and emit a flow event if this is an insertion,
// preemption, or migration. In contrast, an adjustment only changes the queue
// position in the same queue due to a parameter change and should not perform
// these actions.
if (placement != Placement::Adjustment) {
if (placement == Placement::Migration) {
// Connect the flow into the previous queue to the new queue.
LOCAL_KTRACE_FLOW_STEP(FLOW, "sched_latency", state->flow_id());
} else {
// Reuse this member to track the time the thread enters the run queue. It
// is not read outside of the scheduler unless the thread state is
// THREAD_RUNNING.
state->last_started_running_ = now;
state->flow_id_ = NextFlowId();
LOCAL_KTRACE_FLOW_BEGIN(FLOW, "sched_latency", state->flow_id());
}
}
// Insert the thread into the appropriate run queue.
if (IsFairThread(thread)) {
// Convert start and finish times to variable time before inserting.
// TODO(eieio): Convert from standard period to local period.
MonotonicToVariableInPlace(state->start_time_);
MonotonicToVariableInPlace(state->finish_time_);
fair_run_queue_.insert(thread);
} else if (IsCriticalDeadlineThread(thread)) {
critical_deadline_run_queue_.insert(thread);
} else {
deadline_run_queue_.insert(thread);
}
if (placement != Placement::Adjustment) {
TraceThreadQueueEvent("tqe_enque"_intern, thread);
}
trace =
KTRACE_END_SCOPE(("start time", state->start_time_), ("finish time", state->finish_time_));
}
// Evaluates the fair bandwidth demand and updates the period scale, preventing fair bandwidth
// time slices from becoming too small, and causing thrashing, by ensuring that threads with the
// minimum weight in the queue get at least the minimum capacity.
//
// To guarantee that a fair bandwidth thread receives at least a minimum capacity, the following
// relationship must hold:
//
// w_min / w_total >= C_min / T_default
//
// Where:
//
// w_min is the minimum weight of threads in the run queue and the active thread.
// w_total is the total weight of the run queue.
// C_min is the minimum capacity.
// T_default is the default fair task period.
//
// If this relationship does not hold, a scale factor is applied to expand the fair task period
// and ensure that tasks with the minimum weight receive the minimum capacity:
//
// s = C_min / T_default * w_total / w_min
//
void Scheduler::UpdateFairBandwidthPeriod(SchedTime now) {
// Returns the minimum weight of the run queue, including the active thread.
const auto get_min_weight = [this]() TA_REQ(queue_lock_) {
if (!fair_run_queue_.is_empty()) {
const Thread& root = *fair_run_queue_.croot();
AssertInRunQueue(root);
return ktl::min(active_thread_weight_, root.scheduler_state().subtree_invariants_.min_weight);
}
return active_thread_weight_;
};
const SchedWeight min_weight = get_min_weight();
if (min_weight < weight_total_ * kMinFairUtilization) {
const SchedUtilization min_utilization = min_weight / weight_total_;
const Affine::Slope slope =
ktl::max<Affine::Slope>(min_utilization / kMinFairUtilization, Affine::kMinSlope);
if (fair_affine_transform_.slope() != slope) {
fair_affine_transform_.ChangeSlopeAtMonotonicTime(now, slope);
fair_period_ = kDefaultFairPeriod / slope;
reciprocal_fair_period_ = 1 / fair_period_;
}
} else {
if (fair_affine_transform_.slope() != Affine::kMaxSlope) {
fair_affine_transform_.ChangeSlopeAtMonotonicTime(now, Affine::kMaxSlope);
fair_period_ = kDefaultFairPeriod;
reciprocal_fair_period_ = kReciprocalDefaultFairPeriod;
}
}
}
void Scheduler::AdjustFairBandwidth(Thread* thread, SchedTime now) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "adjust_bandwidth");
SchedulerState& state = thread->scheduler_state();
DEBUG_ASSERT(IsFairThread(thread));
DEBUG_ASSERT(state.remaining_time_slice_ns() > 0 && state.finish_time() > now);
// Calculate the new remaining time slice from the current remaining time
// slice and the changes to period and utilization, such that lag is
// conserved, keeping the same finish time.
const SchedDuration time_until_finish_time_ns = state.finish_time() - now;
const SchedDuration period_delta = fair_period_ - state.effective_period();
const SchedDuration time_slice_ns = CalculateTimeslice(thread);
const SchedUtilization old_utilization = state.time_slice_ns() / state.effective_period();
const SchedUtilization new_utilization = time_slice_ns * reciprocal_fair_period_;
const SchedUtilization utilization_delta = new_utilization - old_utilization;
const SchedDuration old_remaining_time_slice_ns = state.remaining_time_slice_ns();
const SchedDuration new_remaining_time_slice =
old_remaining_time_slice_ns + utilization_delta * time_until_finish_time_ns;
// Update the dynamic parameters, clamping the remaining time slice to the end
// of the period.
state.time_slice_ns_ = time_slice_ns;
state.time_slice_used_ns_ =
time_slice_ns - ktl::min(new_remaining_time_slice, time_until_finish_time_ns);
state.start_time_ = state.finish_time() - fair_period_;
trace =
KTRACE_END_SCOPE(("r_i", old_remaining_time_slice_ns), ("r_i'", new_remaining_time_slice),
("r_{i,e}", state.remaining_time_slice_ns()), ("dU_i", utilization_delta),
("dT_i", period_delta), ("d_i", time_until_finish_time_ns),
("eligible", state.start_time() <= now));
}
void Scheduler::Insert(SchedTime now, Thread* thread, Placement placement) {
DEBUG_ASSERT(thread->state() == THREAD_READY);
DEBUG_ASSERT(!thread->IsIdle());
DEBUG_ASSERT(IsSchedulerActiveLocked());
SchedulerQueueState& queue_state = thread->scheduler_queue_state();
SchedulerState& state = thread->scheduler_state();
const EffectiveProfile& ep = state.effective_profile_;
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(
DETAILED, "insert", ("start time", state.start_time()), ("finish time", state.finish_time()),
("expired finish time", now >= state.finish_time()));
// Ensure insertion happens only once, even if Unblock is called multiple times.
if (queue_state.OnInsert()) {
// Insertion can happen from a different CPU. Set the thread's current
// CPU to the one this scheduler instance services.
state.curr_cpu_ = this_cpu();
UpdateTotalExpectedRuntime(state.expected_runtime_ns_);
if (IsFairThread(thread)) {
weight_total_ += ep.weight();
DEBUG_ASSERT(weight_total_ > 0);
UpdateFairBandwidthPeriod(now);
// If the thread is unblocking, set the start and finish time using the new
// period.
// TODO(https://fxbug.dev/322207536): Stop resetting start and finish times
// when unblocking once we solve races higher in the stack.
if (!EnableNewWakeupAccounting() && placement == Placement::Insertion) {
const SchedDuration period = state.effective_period();
state.start_time_ = now;
state.finish_time_ = now + period;
}
if (state.remaining_time_slice_ns() > 0 && state.finish_time() > now) {
AdjustFairBandwidth(thread, now);
// If we're unblocking, update the start and finish time again now that
// we've adjusted the fair bandwidth.
// TODO(https://fxbug.dev/322207536): Stop resetting start and finish
// times when unblocking once we solve races higher in the stack.
if (!EnableNewWakeupAccounting() && placement == Placement::Insertion) {
const SchedDuration period = state.effective_period();
state.start_time_ = now;
state.finish_time_ = now + period;
}
}
} else {
// Remove the thread from the bandwidth demand cache, if present, and
// subtract the any cached utilization that is still reflected in the
// total demand. This value may be different than the thread's current
// bandwidth demand due to bandwidth inheritance or a profile change since
// last blocking.
// TODO(eieio): Optimize based on placement type?
const SchedUtilization utilization_to_remove =
power_level_control_.is_enabled() ? bandwidth_reservation_cache_.Remove(thread->tid())
: SchedUtilization{0};
if (IsCriticalDeadlineThread(thread)) {
critical_deadline_utilization_ += ep.deadline().utilization;
if (critical_deadline_utilization_ > kCpuUtilizationLimit) {
// Use constinit to ensure that EventLimiter is trivially
// constructible, since that is required for local statics in the
// kernel.
static constinit EventLimiter<ZX_SEC(1)> oops_limiter;
if (oops_limiter.Ready()) {
dprintf(CRITICAL, "Critical deadline utilization %s exceeded limit %s on CPU %u\n",
Format(critical_deadline_utilization_).c_str(),
Format(kCpuUtilizationLimit).c_str(), this_cpu());
}
}
}
UpdateTotalDeadlineUtilization(ep.deadline().utilization - utilization_to_remove);
}
// Increase the processing rate when the required utilization increases
// beyond the current rate, accounting for current limits.
if (power_level_control_.is_enabled() && power_level_control_.scheduler_control_enabled() &&
power_level_control_.processing_rate_should_increase()) {
power_level_control_.PendPowerLevelRequestForRate(
power_level_control_.clamped_total_demand());
}
runnable_task_count_++;
DEBUG_ASSERT(runnable_task_count_ > 0);
TraceTotalRunnableThreads();
// Start a new activation, if necessary.
UpdateActivation(thread, now);
if (placement != Placement::Association) {
QueueThread(thread, placement, now);
} else {
// Connect the flow into the previous queue to the new queue.
LOCAL_KTRACE_FLOW_STEP(FLOW, "sched_latency", state.flow_id());
}
}
}
void Scheduler::Remove(SchedTime now, Thread* thread, cpu_num_t stolen_by) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "remove");
DEBUG_ASSERT(!thread->IsIdle());
SchedulerQueueState& queue_state = thread->scheduler_queue_state();
const SchedulerState& state = const_cast<const Thread*>(thread)->scheduler_state();
const EffectiveProfile& effective_profile = state.effective_profile_;
DEBUG_ASSERT(!queue_state.in_queue());
// Ensure that removal happens only once, even if Block() is called multiple times.
if (queue_state.OnRemove(stolen_by)) {
UpdateTotalExpectedRuntime(-state.expected_runtime_ns_);
if (IsFairThread(thread)) {
weight_total_ -= effective_profile.weight();
DEBUG_ASSERT(weight_total_ >= 0);
UpdateFairBandwidthPeriod(now);
} else {
// Add the thread to the bandwidth demand cache and adjust the total
// demand if there was an eviction of another active entry. If the thread
// is being stolen, simply remove its demand from the total demand, since
// its influence is migrating to another CPU.
SchedUtilization utilization_to_remove = effective_profile.deadline().utilization;
if (power_level_control_.is_enabled() && stolen_by == INVALID_CPU) {
utilization_to_remove = bandwidth_reservation_cache_.Add(thread->tid(), state.finish_time(),
utilization_to_remove);
}
if (IsCriticalDeadlineThread(thread)) {
critical_deadline_utilization_ -= effective_profile.deadline().utilization;
DEBUG_ASSERT(critical_deadline_utilization_ >= 0);
}
UpdateTotalDeadlineUtilization(-utilization_to_remove);
}
DEBUG_ASSERT(runnable_task_count_ > 0);
runnable_task_count_--;
TraceTotalRunnableThreads();
}
}
void Scheduler::RemoveThreadLocked(SchedTime now, Thread* thread) {
Remove(now, thread);
thread->scheduler_state().curr_cpu_ = INVALID_CPU;
}
void Scheduler::ValidateInvariantsUnconditional() const {
using ProfileDirtyFlag = SchedulerState::ProfileDirtyFlag;
auto ObserveFairThread = [&](const Thread& t) TA_REQ(this->queue_lock_,
chainlock_transaction_token) {
// We are holding the scheduler's queue lock, and observing thread which are
// either active, or members of our queues. Tell the static analyzer that
// it is OK for us to access a thread's effective profile in a read-only
// fashion, even though we don't explicitly hold the thread's lock.
[&t]() {
MarkHasOwnedThreadAccess(t);
const auto& ss = t.scheduler_state();
const auto& ep = ss.effective_profile();
ASSERT_MSG(ep.IsFair(), "Fair thread %" PRIu64 " has non-fair effective profile", t.tid());
}();
// If we have dirty tracking enabled for effective profiles (only enabled in
// builds with extra scheduler state validation enabled) we can try to make
// some extra checks.
//
// 1) If we know that the current base profile is clean, we can assert that
// the base profile is fair.
// 2) If we know that the current IPVs are clean, we can assert that the IPV
// utilization is zero.
//
// In order to safely observe these things, however, we need to be holding
// the thread's lock. Threads can change their base profile or IPVs (and
// the flags which indicate if they are dirty or nor) while only holding
// their lock. They do not need to be holding the scheduler's queue lock if
// the thread happens to be running or ready.
//
// So, _try_ to get the thread's lock if we don't already hold it, and if we
// succeed, go ahead a perform our checks. If we fail to get the lock for
// any reason, just skip the checks this time.
if constexpr (EffectiveProfile::kDirtyTrackingEnabled) {
auto ExtraChecks = [&t]() TA_REQ(t.get_lock()) -> void {
const auto& ss = t.scheduler_state();
const auto& ep = ss.effective_profile();
const auto& bp = ss.base_profile_;
const auto& ipv = ss.inherited_profile_values_;
if (!(ep.dirty_flags() & ProfileDirtyFlag::BaseDirty)) {
ASSERT_MSG(bp.IsFair(), "Fair thread %" PRIu64 " has clean, but non-fair, base profile",
t.tid());
}
if (!(ep.dirty_flags() & ProfileDirtyFlag::InheritedDirty)) {
ASSERT_MSG(ipv.uncapped_utilization == SchedUtilization{0},
"Fair thread %" PRIu64
" has clean IPV, but non-zero inherited utilization (%" PRId64 ")",
t.tid(), ipv.uncapped_utilization.raw_value());
}
};
if (t.get_lock().is_held()) {
t.get_lock().MarkHeld();
ExtraChecks();
} else if (t.get_lock().TryAcquire(ChainLock::AllowFinalized::Yes)) {
ExtraChecks();
t.get_lock().Release();
}
}
};
auto ObserveDeadlineThread = [&](const Thread& t) TA_REQ(this->queue_lock_,
chainlock_transaction_token) {
// See above for the locking rules for accessing the pieces of effective profile.
[&t]() {
MarkHasOwnedThreadAccess(t);
const auto& ss = t.scheduler_state();
const auto& ep = ss.effective_profile();
ASSERT_MSG(ep.IsDeadline(), "Deadline thread %" PRIu64 " has non-deadline effective profile",
t.tid());
}();
if constexpr (EffectiveProfile::kDirtyTrackingEnabled) {
auto ExtraChecks = [&t]() TA_REQ(t.get_lock()) -> void {
const auto& ss = t.scheduler_state();
const auto& ep = ss.effective_profile();
const auto& bp = ss.base_profile_;
const auto& ipv = ss.inherited_profile_values_;
if (ep.dirty_flags() == ProfileDirtyFlag::Clean) {
ASSERT_MSG(
bp.IsDeadline() || (ipv.uncapped_utilization > SchedUtilization{0}),
"Deadline thread %" PRIu64
" has a clean effective profile, but neither a deadline base profile (%s), nor a "
"non-zero inherited utilization (%" PRId64 ")",
t.tid(), bp.IsFair() ? "Fair" : "Deadline", ipv.uncapped_utilization.raw_value());
}
};
if (t.get_lock().is_held()) {
t.get_lock().MarkHeld();
ExtraChecks();
} else if (t.get_lock().TryAcquire(ChainLock::AllowFinalized::Yes)) {
ExtraChecks();
t.get_lock().Release();
}
}
};
for (const auto& t : fair_run_queue_) {
ObserveFairThread(t);
}
for (const auto& t : critical_deadline_run_queue_) {
ObserveDeadlineThread(t);
}
for (const auto& t : deadline_run_queue_) {
ObserveDeadlineThread(t);
}
ASSERT(active_thread_ != nullptr);
const Thread& active_thread = *active_thread_;
MarkHasOwnedThreadAccess(active_thread); // This should be safe, see above.
if (active_thread.scheduler_state().effective_profile().IsFair()) {
ObserveFairThread(active_thread);
} else {
ASSERT(active_thread.scheduler_state().effective_profile().IsDeadline());
ObserveDeadlineThread(active_thread);
}
}
void Scheduler::Block(Thread* const current_thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_block");
DEBUG_ASSERT(current_thread == Thread::Current::Get());
current_thread->canary().Assert();
DEBUG_ASSERT(current_thread->state() != THREAD_RUNNING);
Scheduler::Get()->RescheduleCommon(current_thread, trace.Completer());
}
Scheduler::RescheduleTargets Scheduler::UnblockCommon(Thread* thread, SchedTime now,
UnblockType unblock_type) {
thread->canary().Assert();
cpu_num_t target_cpu = INVALID_CPU;
cpu_mask_t cpus_to_reschedule = 0;
while (true) {
// TODO(rudymathu): This target_cpu should be stashed in the thread prior to
// adding it to the save_state_list_, as that would allow us to bypass CPU
// selection when processing the list so long as the CPU is still online.
target_cpu = FindTargetCpu(thread, unblock_type);
const cpu_num_t last_cpu = thread->scheduler_state().last_cpu();
const bool needs_migration = (last_cpu != INVALID_CPU && target_cpu != last_cpu &&
thread->has_migrate_fn() && !thread->migrate_pending());
if (needs_migration) {
target_cpu = last_cpu;
}
Scheduler* const target = Get(target_cpu);
Guard<MonitoredSpinLock, NoIrqSave> target_queue_guard{&target->queue_lock_, SOURCE_TAG};
if (target->IsSchedulerActiveLocked()) {
SchedTraceWakeup(thread, target_cpu);
thread->set_ready();
thread->UpdateRuntimeStats(thread->state());
if (needs_migration) {
MarkHasOwnedThreadAccess(*thread);
target->save_state_list_.push_front(thread);
} else {
thread->scheduler_state().curr_cpu_ = target_cpu;
target->AssertInScheduler(*thread);
target->Insert(now, thread);
// If a power level change was requested during insertion, attempt to
// handle it immediately with the fast path and update any affected
// CPUs. If the fast path is not supported, the request will be left
// pending on the target CPU and flushed during its reschedule.
cpus_to_reschedule =
target->power_level_control_.SendPendingPowerLevelRequest(target_queue_guard);
}
break;
}
IncFindTargetCpuRetriesKcounter();
}
return {.target_cpu = target_cpu, .cpus_to_reschedule = cpus_to_reschedule};
}
void Scheduler::Unblock(Thread* thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_unblock");
ChainLockTransaction::ActiveRef().AssertFinalized();
const SchedTime now = CurrentTime();
RescheduleTargets targets = UnblockCommon(thread, now, UnblockType::Normal);
thread->get_lock().Release();
trace.End();
RescheduleMask(targets.Mask());
}
void Scheduler::Unblock(Thread::UnblockList list) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_unblock_list");
ChainLockTransaction::ActiveRef().AssertFinalized();
cpu_mask_t reschedule_mask = 0;
const SchedTime now = CurrentTime();
Thread* thread;
while ((thread = list.pop_back()) != nullptr) {
DEBUG_ASSERT(!thread->IsIdle());
thread->get_lock().AssertAcquired();
RescheduleTargets targets = UnblockCommon(thread, now, UnblockType::Normal);
reschedule_mask |= targets.Mask();
thread->get_lock().Release();
}
trace.End();
RescheduleMask(reschedule_mask);
}
void Scheduler::UnblockSynchronous(Thread* thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_unblock_sync");
ChainLockTransaction::ActiveRef().AssertFinalized();
const SchedTime now = CurrentTime();
RescheduleTargets targets = UnblockCommon(thread, now, UnblockType::Synchronous);
thread->get_lock().Release();
trace.End();
// If there was a power level change we need to account for, reschedule those CPUs. But do not
// reschedule the target CPU, if it is the current CPU.
if (targets.target_cpu == arch_curr_cpu_num()) {
RescheduleMask(targets.cpus_to_reschedule);
} else {
RescheduleMask(targets.Mask());
}
}
void Scheduler::UnblockIdle(Thread* thread) {
SchedulerState* const state = &thread->scheduler_state();
DEBUG_ASSERT(thread->IsIdle());
DEBUG_ASSERT(ktl::popcount(state->hard_affinity_) == 1);
const cpu_num_t target_cpu = lowest_cpu_set(state->hard_affinity_);
Scheduler* const target_scheduler = Get(target_cpu);
{
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&target_scheduler->queue_lock_, SOURCE_TAG};
thread->set_ready();
state->curr_cpu_ = target_cpu;
target_scheduler->AssertInScheduler(*thread);
thread->scheduler_queue_state().OnInsert();
}
}
void Scheduler::Yield(Thread* const current_thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_yield");
DEBUG_ASSERT(current_thread == Thread::Current::Get());
DEBUG_ASSERT(!current_thread->IsIdle());
if (IsFairThread(current_thread)) {
Scheduler& current_scheduler = *Get();
SchedulerState& current_state = current_thread->scheduler_state();
// If the thread is running in an eligible activation, expire the time slice
// to push it into the next activation, potentially allowing other eligible
// threads to run. However, if the start time is in the future, this thread
// is running because there were no other eligible fair threads to run and
// the eligible time was snapped forward. In that case, avoid pushing the
// activation further into the future.
const SchedTime now = CurrentTime();
if (current_state.start_time() < now) {
current_state.time_slice_used_ns_ = current_state.time_slice_ns_;
current_scheduler.RescheduleCommon(current_thread, trace.Completer());
}
}
}
void Scheduler::Preempt(PreemptType preempt_type) {
Thread* current_thread = Thread::Current::Get();
SingleChainLockGuard thread_guard{IrqSaveOption, current_thread->get_lock(),
CLT_TAG("Scheduler::Preempt")};
PreemptLocked(current_thread, preempt_type);
}
void Scheduler::PreemptLocked(Thread* current_thread, PreemptType preempt_type) {
ktrace::Scope trace =
LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_preempt", ("type", ToStringRef(preempt_type)));
const cpu_num_t current_cpu = arch_curr_cpu_num();
// If any spinlocks are held, we can't immediately reschedule. Instead, send
// this CPU a reschedule IPI that will fire once the last spinlock has been
// released and interrupts have been re-enabled.
if (arch_num_spinlocks_held() > 0) {
PreemptionState& preemption_state = current_thread->preemption_state();
preemption_state.preempts_pending_add(cpu_num_to_mask(current_cpu));
mp_reschedule_self();
return;
}
SchedulerState& current_state = current_thread->scheduler_state();
DEBUG_ASSERT(current_state.curr_cpu_ == current_cpu);
DEBUG_ASSERT(current_state.last_cpu_ == current_state.curr_cpu_);
DEBUG_ASSERT(current_thread->state() == THREAD_RUNNING);
Get()->RescheduleCommon(current_thread, trace.Completer());
}
void Scheduler::Reschedule(Thread* const current_thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_reschedule");
DEBUG_ASSERT(current_thread == Thread::Current::Get());
SchedulerState* const current_state = &current_thread->scheduler_state();
const cpu_num_t current_cpu = arch_curr_cpu_num();
const bool preempt_enabled = current_thread->preemption_state().EvaluateTimesliceExtension();
// Pend the preemption rather than rescheduling if preemption is disabled.
if (!preempt_enabled) {
current_thread->preemption_state().preempts_pending_add(cpu_num_to_mask(current_cpu));
return;
}
// If any spinlocks are held, we can't immediately reschedule. Instead, send
// this CPU a reschedule IPI that will fire once the last spinlock has been
// released and interrupts have been re-enabled.
if (arch_num_spinlocks_held() > 0) {
current_thread->preemption_state().preempts_pending_add(cpu_num_to_mask(current_cpu));
mp_reschedule_self();
return;
}
DEBUG_ASSERT(current_state->curr_cpu_ == current_cpu);
DEBUG_ASSERT(current_state->last_cpu_ == current_state->curr_cpu_);
Get()->RescheduleCommon(current_thread, trace.Completer());
}
void Scheduler::RescheduleInternal(Thread* const current_thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_resched_internal");
Get()->RescheduleCommon(current_thread, trace.Completer());
}
void Scheduler::MigrateUnpinnedThreads() {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_migrate_unpinned");
const cpu_num_t current_cpu = arch_curr_cpu_num();
const cpu_mask_t current_cpu_mask = cpu_num_to_mask(current_cpu);
cpu_mask_t cpus_to_reschedule_mask = 0;
// Flag this scheduler as being in the process de-activating. If a thread
// with a migration function unblocks and needs to become READY while we are
// in the middle of MigrateUnpinnedThreads, we will allow it to join this
// CPU's ready-queue knowing that we are going to immediately call the
// thread's migration function (on this CPU, as required) before reassigning
// the thread to a different CPU.
//
// Note: Because of the nature of the global thread-lock, this race is not
// currently possible (Threads who are blocked cannot unblock while we hold
// the global thread lock, which we currently do). Once the thread lock has
// been removed, however, this race will become an important edge case to
// handle.
const SchedTime now = CurrentTime();
Scheduler* const current = Get(current_cpu);
current->cpu_deactivating_.store(true, ktl::memory_order_release);
// Flag this CPU as being inactive. This will prevent new threads from being
// added to this CPU's scheduler's run queues while we migrate the existing
// threads off this scheduler.
//
// Note that this does _not_ prevent FindTargetCpu from _selecting_ a given
// scheduler as potential target for thread placement, but it _does_ guarantee
// that by the time we attempt to actually add the thread to the chosen
// scheduler's queue, we will have had a chance to check the flag once again,
// this time safely from with the chosen scheduler's queue lock.
Scheduler::SetCurrCpuActive(false);
// Call all migrate functions for threads last run on the current CPU who are
// not currently READY. The next time these threads unblock or wake up, the
// will be assigned to a different CPU, and we will have already called their
// migrate function for them. When we are finished with this pass, the only
// threads who have a migrate function still to be called should only exist in
// this scheduler's READY queue, because:
//
// 1) New threads or threads who have not recently run on this CPU cannot be
// assigned to this CPU (because we cleared our active flag)
// 2a) Threads who were not ready but needed to have their migration function
// called were found and had their migration function called during
// CallMigrateFnForCpu --- OR ---
// 2b) The non-ready thread with a migration function unblocked and joined the
// ready queue of this scheduler while it was being deactivated. We will
// call their migration functions next as we move all of the READY threads
// off of this scheduler and over to a different one.
//
Thread::CallMigrateFnForCpu(current_cpu);
// There should no longer be any non-ready threads who need to run a
// migration function on this CPU, and there should not be any new one until
// we set this CPU back to being active. We can clear the transitory
// "cpu_deactivating_" flag now.
DEBUG_ASSERT(current->cpu_deactivating_.load(ktl::memory_order_relaxed));
current->cpu_deactivating_.store(false, ktl::memory_order_release);
// Now move any READY threads who can be moved over to a temporary list,
// flagging them as in the process of currently migrating. Also move all
// of the threads in the save_state_list into a temporary list, as they
// need to be migrated as well.
Thread::UnblockList migrating_deadline_threads;
Thread::UnblockList migrating_fair_threads;
Thread::SaveStateList local_save_state_list;
sched::Affine affine_transform;
{
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&current->queue_lock_, SOURCE_TAG};
// Save the current affine transform to update fair threads as they are migrated.
affine_transform = current->fair_affine_transform_;
if (!current->save_state_list_.is_empty()) {
local_save_state_list = ktl::move(current->save_state_list_);
}
auto CreateMoveList =
[current, current_cpu_mask](Thread::UnblockList& thread_list, RunQueue& run_queue)
TA_REQ(current->queue_lock_) {
(void)current; // We cannot annotate 'current' with [[maybe_unused]] in the capture
// list, so suppress the warning using the old school void-cast trick.
DEBUG_ASSERT((&run_queue == &current->fair_run_queue_) ||
(&run_queue == &current->critical_deadline_run_queue_) ||
(&run_queue == &current->deadline_run_queue_));
for (RunQueue::iterator iter = run_queue.begin(); iter.IsValid();) {
Thread& consider = *(iter++);
// The no-op version of this assertion should be sufficient here. We
// are holding the scheduler's queue lock, and iterating over the
// scheduler's run queues. All of the threads in those queues are
// clearly owned by this scheduler right now.
MarkHasOwnedThreadAccess(consider);
const SchedulerState& ss = const_cast<const Thread&>(consider).scheduler_state();
// If the thread can run on any other CPU, add it to our list of threads to migrate.
if (ss.hard_affinity_ != current_cpu_mask) {
thread_list.push_back(&consider);
}
}
};
CreateMoveList(migrating_fair_threads, current->fair_run_queue_);
CreateMoveList(migrating_deadline_threads, current->critical_deadline_run_queue_);
CreateMoveList(migrating_deadline_threads, current->deadline_run_queue_);
}
// OK, now that we have dropped our queue lock, go over our list of migrating
// threads and finish the migration process. Don't make any assumptions about
// the fair/deadline nature of the threads as we migrate. Since we dropped
// the scheduler's queue lock, it is possible that these threads have changed
// effective priority since being remove from their old scheduler, but before
// arriving at a new one.
auto MigrateThread = [current, now, &affine_transform](
Thread* thread, bool update_timeline,
const fxt::InternedString& trace_tag) -> cpu_num_t {
SingleChainLockGuard guard{IrqSaveOption, thread->get_lock(),
CLT_TAG("Scheduler::MigrateUnpinnedThreads")};
{
// Removing a thread from a scheduler requires queue_lock acquisition.
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&current->queue_lock_, SOURCE_TAG};
// Even though we dropped the queue lock between when we accumulated the list of threads to
// move, and did not hold the thread lock prior to this point, this thread should still be
// associated with this scheduler because the scheduler was marked inactive above. This means
// that any attempt to steal work from this scheduler should fail.
DEBUG_ASSERT(thread->scheduler_state().curr_cpu() == current->this_cpu());
MarkHasOwnedThreadAccess(*thread);
current->TraceThreadQueueEvent(trace_tag, thread);
current->EraseFromQueue(thread);
current->RemoveThreadLocked(now, thread);
}
// Threads removed from the fair run queue must be transformed to the monotonic timeline
// before migrating.
if (update_timeline) {
DEBUG_ASSERT(IsFairThread(thread));
affine_transform.VariableToMonotonicInPlace(thread->scheduler_state().start_time_);
affine_transform.VariableToMonotonicInPlace(thread->scheduler_state().finish_time_);
}
// Call the Save stage of the thread's migration function as it leaves
// this CPU.
thread->CallMigrateFnLocked(Thread::MigrateStage::Save);
const cpu_num_t target_cpu =
FindActiveSchedulerForThread(thread, [current, now](Thread* thread, Scheduler* target) {
// Finish the transition and add the thread to the new target queue. The
// Restore stage of the migration function will be called on the new CPU the
// next time the thread becomes scheduled.
MarkInFindActiveSchedulerForThreadCbk(*thread, *target);
(void)current; // We cannot annotate 'current' with [[maybe_unused]] in the capture
// list, so suppress the warning using the old school void-cast trick.
DEBUG_ASSERT(target != current);
target->Insert(now, thread, Placement::Migration);
});
return target_cpu;
};
while (!migrating_fair_threads.is_empty()) {
Thread* thread = migrating_fair_threads.pop_front();
const cpu_num_t target_cpu =
MigrateThread(thread, /*update_timeline=*/true, "tqe_deque_migrate_unpinned_fair"_intern);
cpus_to_reschedule_mask |= cpu_num_to_mask(target_cpu);
}
while (!migrating_deadline_threads.is_empty()) {
Thread* thread = migrating_deadline_threads.pop_front();
const cpu_num_t target_cpu = MigrateThread(thread, /*update_timeline=*/false,
"tqe_deque_migrate_unpinned_deadline"_intern);
cpus_to_reschedule_mask |= cpu_num_to_mask(target_cpu);
}
while (!local_save_state_list.is_empty()) {
Thread* thread = local_save_state_list.pop_front();
const cpu_num_t target_cpu = MigrateThread(thread, /*update_timeline=*/false,
"tqe_deque_migrate_unpinned_save_state"_intern);
cpus_to_reschedule_mask |= cpu_num_to_mask(target_cpu);
}
trace.End();
RescheduleMask(cpus_to_reschedule_mask);
}
void Scheduler::TimerTick(SchedTime now) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_timer_tick");
Thread::Current::preemption_state().PreemptSetPending();
}
void Scheduler::InitializeProcessingRate(SchedProcessingRate scale) TA_NO_THREAD_SAFETY_ANALYSIS {
// Since this happens early in boot, before the scheduler is actually running,
// acquiring the queue lock is unnecessary.
power_level_control_.TopologySetDefaultProcessingRate(scale);
exported_processing_rate_ = scale;
exported_max_processing_rate_ = power_level_control_.clamped_max_processing_rate();
}
void Scheduler::UpdateProcessingRates(ktl::span<zx_cpu_performance_info_t> info) {
DEBUG_ASSERT(info.size() <= percpu::processor_count());
InterruptDisableGuard irqd;
cpu_num_t cpus_to_reschedule_mask = 0;
for (auto& entry : info) {
DEBUG_ASSERT(entry.logical_cpu_number <= percpu::processor_count());
cpus_to_reschedule_mask |= cpu_num_to_mask(entry.logical_cpu_number);
Scheduler* scheduler = Scheduler::Get(entry.logical_cpu_number);
Guard<MonitoredSpinLock, NoIrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
// TODO(eieio): Apply a minimum value threshold.
scheduler->power_level_control_.UserSetProcessingRate(
ToSchedProcessingRate(entry.performance_scale));
// Return the original performance scale (i.e. processing rate).
entry.performance_scale = ToUserPerformanceScale(scheduler->processing_rate());
}
RescheduleMask(cpus_to_reschedule_mask);
}
void Scheduler::GetPerformanceScales(ktl::span<zx_cpu_performance_info_t> info) {
DEBUG_ASSERT(info.size() <= percpu::processor_count());
for (cpu_num_t i = 0; i < info.size(); i++) {
Scheduler* scheduler = Scheduler::Get(i);
Guard<MonitoredSpinLock, IrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
info[i].logical_cpu_number = i;
info[i].performance_scale =
ToUserPerformanceScale(scheduler->power_level_control_.updated_processing_rate());
}
}
void Scheduler::GetDefaultPerformanceScales(ktl::span<zx_cpu_performance_info_t> info) {
DEBUG_ASSERT(info.size() <= percpu::processor_count());
for (cpu_num_t i = 0; i < info.size(); i++) {
Scheduler* scheduler = Scheduler::Get(i);
Guard<MonitoredSpinLock, IrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
info[i].logical_cpu_number = i;
info[i].performance_scale =
ToUserPerformanceScale(scheduler->power_level_control_.max_processing_rate());
}
}
void Scheduler::UpdateProcessingLimits(ktl::span<zx_cpu_perf_limit_t> limits) {
DEBUG_ASSERT(limits.size() <= percpu::processor_count());
InterruptDisableGuard interrupts_disabled;
// Set the limits for each CPU in the given limits array, keeping track of one
// CPU per domain to evaluate the domain's active power level.
cpu_mask_t cpus_for_handled_domains = 0;
cpu_mask_t cpus_to_evaluate_updates = 0;
for (auto& entry : limits) {
switch (entry.limit_type) {
case ZX_CPU_PERF_LIMIT_TYPE_RATE: {
Scheduler* scheduler = Scheduler::Get(entry.logical_cpu_number);
Guard<MonitoredSpinLock, NoIrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
using power_management::PowerLevel;
const SchedProcessingRate min =
PowerLevel::ToProcessingRate(entry.min, PowerLevel::kUserProcessingRateScale);
const SchedProcessingRate max =
PowerLevel::ToProcessingRate(entry.max, PowerLevel::kUserProcessingRateScale);
scheduler->power_level_control_.UserSetProcessingRateLimits(min, max);
// A rate change only needs to be evaluated for one active CPU in each domain.
const cpu_mask_t cpu_mask = cpu_num_to_mask(entry.logical_cpu_number);
const bool is_domain_handled = cpus_for_handled_domains & cpu_mask;
if (scheduler->IsSchedulerActiveLocked() && scheduler->power_level_control_.is_enabled() &&
!is_domain_handled) {
cpus_to_evaluate_updates |= cpu_mask;
const cpu_mask_t domain_cpu_mask = scheduler->power_level_control_.domain()->cpus();
cpus_for_handled_domains |= domain_cpu_mask;
}
} break;
default:
break;
}
}
// Reevaluate the power level for each updated domain.
cpu_mask_t cpu_mask = cpus_to_evaluate_updates;
for (cpu_num_t cpu = remove_cpu_from_mask(cpu_mask); cpu != INVALID_CPU;
cpu = remove_cpu_from_mask(cpu_mask)) {
Scheduler* scheduler = Scheduler::Get(cpu);
Guard<MonitoredSpinLock, NoIrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
scheduler->power_level_control_.ReevaluateCurrentPowerLevel();
}
RescheduleMask(cpus_to_evaluate_updates);
}
void Scheduler::GetProcessingLimits(ktl::span<zx_cpu_perf_limit_t> limits) {
DEBUG_ASSERT(limits.size() <= percpu::processor_count());
for (cpu_num_t i = 0; i < limits.size(); i++) {
Scheduler* scheduler = Scheduler::Get(i);
Guard<MonitoredSpinLock, IrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
limits[i].logical_cpu_number = i;
limits[i].limit_type = ZX_CPU_PERF_LIMIT_TYPE_RATE;
using power_management::PowerLevel;
limits[i].min =
PowerLevel::FromProcessingRate(scheduler->power_level_control_.processing_rate_limit_min(),
PowerLevel::kUserProcessingRateScale);
limits[i].max =
PowerLevel::FromProcessingRate(scheduler->power_level_control_.processing_rate_limit_max(),
PowerLevel::kUserProcessingRateScale);
}
}
cpu_mask_t Scheduler::SetCpuAffinity(Thread& thread, cpu_mask_t affinity,
AffinityType affinity_type) {
DEBUG_ASSERT_MSG(
const cpu_mask_t active_mask = PeekActiveMask();
affinity_type == AffinityType::Soft || (affinity & active_mask) != 0,
"Attempted to set affinity mask to %#x, which has no overlap of active CPUs %#x.", affinity,
active_mask);
// Utility to set the correct affinity mask based on the affinity type. Uses a
// function pointer conversion (i.e. +) to avoid a bug with static annotations
// on some lambdas, where the compiler complains about not holding the lambda
// as a capability.
const auto set_affinity = +[](Thread& thread, cpu_mask_t affinity, AffinityType affinity_type)
TA_REQ(thread.get_lock()) -> cpu_mask_t {
switch (affinity_type) {
case AffinityType::Hard:
ktl::swap(thread.scheduler_state().hard_affinity_, affinity);
break;
case AffinityType::Soft:
ktl::swap(thread.scheduler_state().soft_affinity_, affinity);
break;
}
return affinity;
};
const auto do_transaction =
[&]() TA_REQ(chainlock_transaction_token) -> ChainLockTransaction::Result<cpu_mask_t> {
thread.get_lock().AcquireFirstInChain();
// Mutating the affinity mask in the scheduler state requires holding both
// the thread lock and the container lock, if any.
//
// TODO(johngro): It is really easy to mess this requirement up. Is there a
// better way to statically assert these requirements?
//
// TODO(eieio): Why does the wait queue lock need to be held when updating
// the affinity mask of a blocked thread? Only scheduling needs to read the
// affinity mask and make decisions based on it, and even then only tracing
// needs to read the affinity mask while holding only the queue lock. How
// are wait queues different than the no container case wrt affinity?
// The thread container is a scheduler.
if ((thread.state() == THREAD_RUNNING) || (thread.state() == THREAD_READY)) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_migrate");
ChainLockTransaction::Finalize();
SchedulerState& state = thread.scheduler_state();
DEBUG_ASSERT(state.curr_cpu() != INVALID_CPU);
const cpu_mask_t thread_cpu_mask = cpu_num_to_mask(state.curr_cpu());
cpu_mask_t cpus_to_reschedule_mask = 0;
bool find_new_target_cpu = false;
cpu_mask_t previous_affinity = 0;
// Use a lambda to prevent the AssertInScheduler from leaking out of the
// scope of the queue lock guard. Static lock assertions can leak out of
// nested scopes, including conditionals and loops.
[&]() TA_REQ(thread.get_lock()) {
Scheduler& scheduler = *Scheduler::Get(state.curr_cpu());
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&scheduler.queue_lock(), SOURCE_TAG};
scheduler.AssertInScheduler(thread);
SchedulerQueueState& queue_state = thread.scheduler_queue_state();
const Disposition disposition = queue_state.disposition();
// Set the affinity before determining if the thread needs to move.
previous_affinity = set_affinity(thread, affinity, affinity_type);
const cpu_mask_t effective_cpu_mask = state.GetEffectiveCpuMask(PeekActiveMask());
const bool stale_curr_cpu = (thread_cpu_mask & effective_cpu_mask) == 0;
if (thread.state() == THREAD_RUNNING) {
DEBUG_ASSERT(disposition == Disposition::Associated);
if (stale_curr_cpu) {
cpus_to_reschedule_mask |= thread_cpu_mask;
}
} else {
DEBUG_ASSERT(thread.state() == THREAD_READY);
DEBUG_ASSERT(disposition != Disposition::Unassociated);
if (disposition == Disposition::Associated) {
// The thread is transitioning to RUNNING: curr_cpu is correct.
CountUpdateInTransition();
if (stale_curr_cpu) {
cpus_to_reschedule_mask |= thread_cpu_mask;
}
} else if (disposition == Disposition::Stolen) {
// The thread is being stolen: curr_cpu is incorrect, use stolen_by.
CountUpdateInTransition();
DEBUG_ASSERT(queue_state.stolen_by() != INVALID_CPU);
const cpu_mask_t stealing_cpu_mask = cpu_num_to_mask(queue_state.stolen_by());
const bool stale_stealing_cpu = (stealing_cpu_mask & effective_cpu_mask) == 0;
if (stale_stealing_cpu) {
cpus_to_reschedule_mask |= stealing_cpu_mask;
}
} else {
// The READY thread is waiting in the run queue: curr_cpu is correct.
DEBUG_ASSERT(disposition == Disposition::Enqueued);
if (stale_curr_cpu) {
scheduler.EraseFromQueue(&thread);
scheduler.RemoveThreadLocked(CurrentTime(), &thread);
// Execute the migrate function inline if the thread is on the local
// CPU. Otherwise, add it to the save state list of the last CPU it
// ran on and reschedule that CPU to process the list.
if (thread.has_migrate_fn() && !thread.migrate_pending() &&
state.last_cpu() != INVALID_CPU && state.last_cpu() != arch_curr_cpu_num()) {
scheduler.save_state_list_.push_front(&thread);
cpus_to_reschedule_mask |= thread_cpu_mask;
} else {
find_new_target_cpu = true;
}
}
}
}
}();
// Find a new target CPU for the thread.
if (find_new_target_cpu) {
DEBUG_ASSERT(thread.state() == THREAD_READY);
// Call the migration function if the thread is migrating away from this
// CPU. This is a no-op if there is no migration function or the Save
// state has already been invoked.
if (state.last_cpu() == arch_curr_cpu_num()) {
thread.CallMigrateFnLocked(Thread::MigrateStage::Save);
}
// Find a new CPU for the thread and add it to the queue.
const cpu_num_t target_cpu =
FindActiveSchedulerForThread(&thread, [](Thread* thread, Scheduler* target) {
MarkInFindActiveSchedulerForThreadCbk(*thread, *target);
target->Insert(CurrentTime(), thread, Placement::Migration);
});
// Reschedule both CPUs to handle the run queue changes.
cpus_to_reschedule_mask |= cpu_num_to_mask(target_cpu) | thread_cpu_mask;
}
trace.End();
thread.get_lock().Release();
RescheduleMask(cpus_to_reschedule_mask);
return previous_affinity;
}
// The thread container is a wait queue.
// TODO(eieio): Why do we need to synchronize the affinity mask with the
// wait queue?
if (thread.state() == THREAD_BLOCKED) {
WaitQueueBase* wait_queue = thread.wait_queue_state().blocking_wait_queue_;
DEBUG_ASSERT(wait_queue != nullptr);
if (!wait_queue->get_lock().AcquireOrBackoff()) {
thread.get_lock().Release();
return ChainLockTransaction::Action::Backoff;
}
ChainLockTransaction::Finalize();
const cpu_mask_t previous_affinity = set_affinity(thread, affinity, affinity_type);
wait_queue->get_lock().Release();
thread.get_lock().Release();
return previous_affinity;
}
// The thread has no container (e.g. SLEEPING, SUSPENDED).
ChainLockTransaction::Finalize();
const cpu_mask_t previous_affinity = set_affinity(thread, affinity, affinity_type);
thread.get_lock().Release();
return previous_affinity;
};
return ChainLockTransaction::UntilDone(IrqSaveOption, CLT_TAG("Scheduler::SetCpuAffinity"),
do_transaction);
}
bool Scheduler::RequestPowerLevelForTesting(uint8_t power_level) {
InterruptDisableGuard interrupts_disabled;
bool need_reschedule;
{
Guard<MonitoredSpinLock, NoIrqSave> guard{&queue_lock_, SOURCE_TAG};
need_reschedule = power_level_control_.IsValidActivePowerLevel(power_level) &&
power_level_control_.PendPowerLevelRequest(power_level);
}
if (need_reschedule) {
RescheduleMask(cpu_num_to_mask(this_cpu()));
}
return need_reschedule;
}
bool Scheduler::PowerLevelControl::UserSetProcessingRateLimits(SchedProcessingRate min,
SchedProcessingRate max) {
AssertHeld(scheduler().queue_lock());
processing_rate_limit_min_ = ktl::clamp(min, SchedProcessingRate{0}, SchedProcessingRate{1});
processing_rate_limit_max_ = ktl::clamp(max, SchedProcessingRate{0}, SchedProcessingRate{1});
scheduler().exported_max_processing_rate_ = clamped_max_processing_rate();
scheduler().exported_clamped_deadline_utilization_ = clamped_deadline_utilization();
scheduler().exported_clamped_fair_utilization_ = clamped_fair_demand();
scheduler().exported_clamped_total_utilization_ = clamped_total_demand();
return processing_rate_should_change();
}
bool Scheduler::PowerLevelControl::PendPowerLevelRequest(uint8_t power_level) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(QUEUE, "sched_req_power_level", ("cpu", cpu()),
("power_level", power_level));
// If there is already a pending request, it can be updated without issuing a reschedule,
// since this update raced with dispatch of the previous request and won.
const bool had_pending_request = pending_update_request_.has_value();
if (auto request = power_state_.RequestTransition(cpu(), power_level)) {
pending_update_request_ = request;
}
return !had_pending_request && pending_update_request_.has_value();
}
bool Scheduler::PowerLevelControl::PendPowerLevelRequestForRate(
SchedProcessingRate processing_rate) {
if (domain()) {
if (const power_management::PowerLevel* power_level =
domain()->model().FindActivePowerLevelForRate(processing_rate)) {
return PendPowerLevelRequest(power_level->level());
}
}
return false;
}
void Scheduler::PowerLevelControl::ReevaluateCurrentPowerLevel() {
SchedUtilization max_clamped_demand{0};
cpu_mask_t domain_cpus = domain() ? domain()->cpus() : 0;
while (domain_cpus != 0) {
const cpu_num_t cpu_num = remove_cpu_from_mask(domain_cpus);
max_clamped_demand = ktl::max(
max_clamped_demand, percpu::Get(cpu_num).scheduler.exported_clamped_total_utilization());
}
LOCAL_KTRACE_INSTANT(QUEUE, "ReevaluateCurrentPowerLevel",
("max_clamped_utilization", max_clamped_demand),
("preceding_processing_rate", preceding_target_processing_rate()),
("processing_rate", target_processing_rate()));
if ((has_preceding_target_power_level() &&
max_clamped_demand <= preceding_target_processing_rate()) ||
max_clamped_demand > target_processing_rate()) {
PendPowerLevelRequestForRate(max_clamped_demand);
}
}
cpu_mask_t Scheduler::PowerLevelControl::SendPendingPowerLevelRequest(
Guard<MonitoredSpinLock, NoIrqSave>& queue_guard) {
cpu_mask_t cpus_to_reschedule_mask = 0;
if (domain() && pending_update_request_.has_value() && power_state_.is_serving()) {
// Use swap to move the pending request into the local optional, leaving the
// member optional empty. Using ktl::move is insufficient, since moving from
// an optional does not leave it empty, it just leaves the underlying value
// in the post-moved state which is a no-op for a POD structure.
ktl::optional<power_management::PowerLevelUpdateRequest> request;
request.swap(pending_update_request_);
if (request.has_value()) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(
QUEUE, "sched_opp_request_fast", ("domain_id", request->domain_id),
("control_argument", request->control_argument));
queue_guard.CallUnlocked([&] {
if (const zx::result<cpu_mask_t> result = domain()->controller()->Post(*request);
result.is_ok()) {
cpus_to_reschedule_mask = result.value();
} else {
KERNEL_OOPS("Failed to set OPP: domain_id=%" PRIu32 " control_argument=%" PRIu64 ": %d\n",
request->domain_id, request->control_argument, result.status_value());
}
});
trace = KTRACE_END_SCOPE(("cpus_to_reschedule_mask", cpus_to_reschedule_mask));
}
}
return cpus_to_reschedule_mask;
}
void Scheduler::SetPowerDomainSet(PowerDomainSet power_domain_set) {
percpu::ForEach([&power_domain_set](cpu_num_t cpu_num, percpu* percpu) {
percpu->scheduler.ExchangePowerDomainSet(power_domain_set);
});
}
zx::result<cpu_mask_t> Scheduler::UpdatePowerLevel(uint32_t domain_id, uint64_t controller_id,
power_management::ControlInterface interface,
uint64_t arg) {
// Get a ref to the power domain using the power domain set for the current
// CPU.
fbl::RefPtr<PowerDomain> domain;
{
Scheduler* scheduler = Get();
Guard<MonitoredSpinLock, IrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
const PowerDomainSet& power_domain_set = scheduler->power_level_control_.power_domain_set();
domain = fbl::RefPtr{power_domain_set.FindByDomainId(domain_id)};
}
if (!domain) {
return zx::error(ZX_ERR_NOT_FOUND);
}
if (domain->controller()->id() != controller_id ||
domain->controller()->control_interface() != interface) {
return zx::error(ZX_ERR_NOT_FOUND);
}
ktl::optional<uint8_t> power_level = domain->model().FindPowerLevel(interface, arg);
if (!power_level.has_value()) {
return zx::error(ZX_ERR_NOT_FOUND);
}
cpu_mask_t domain_cpus = domain->cpus();
cpu_mask_t cpus_to_reschedule_mask = 0;
while (domain_cpus != 0) {
const cpu_num_t cpu_num = remove_cpu_from_mask(domain_cpus);
Scheduler* scheduler = Get(cpu_num);
if (scheduler->UpdateActivePowerLevel(*power_level).is_ok()) {
cpus_to_reschedule_mask |= cpu_num_to_mask(cpu_num);
}
}
return zx::ok(cpus_to_reschedule_mask);
}