blob: 8072742b31a04b291accb76e1683815881364ac9 [file] [edit]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SRC_DEVICES_BIN_DRIVER_RUNTIME_THREAD_POOL_H_
#define SRC_DEVICES_BIN_DRIVER_RUNTIME_THREAD_POOL_H_
#include <lib/async-loop/cpp/loop.h>
#include <zircon/types.h>
#include <string_view>
#include <fbl/intrusive_wavl_tree.h>
#include "dispatcher.h"
#include "dispatcher_internals.h"
namespace driver_runtime {
class ThreadPool : public fbl::WAVLTreeContainable<std::unique_ptr<ThreadPool>> {
public:
// The default pool is for the dispatchers with no specified scheduler role.
static constexpr std::string_view kNoSchedulerRole = "";
explicit ThreadPool(std::string_view scheduler_role = kNoSchedulerRole, bool unmanaged = false)
: scheduler_role_(scheduler_role),
is_unmanaged_(unmanaged),
config_(MakeConfig(this, scheduler_role)),
loop_(&config_) {}
// Required to instantiate fbl::DefaultKeyedObjectTraits.
std::string GetKey() const { return scheduler_role_; }
// Starts a new thread on the thread pool unconditionally.
zx_status_t AddThread();
// Decrements the number of required threads. Currently this doesn't spin down the extra thread
// but for now that is ok since more often than not it can be used by another dispatcher on the
// thread-pool. If it is not used, there will simply be one more thread than needed.
// TODO(https://fxbug.dev/326266527): Use a timer to spin down un-necessary thread.
zx_status_t OnDispatcherSealed();
// Updates the number of threads needed in the thread pool. Starts a new thread if needed.
zx_status_t OnDispatcherAdded(Dispatcher& dispatcher);
// Updates the number of threads needed in the thread pool.
void OnDispatcherRemoved(Dispatcher& dispatcher);
// Requests the profile provider set the role profile.
zx_status_t SetRoleProfile();
// Resets to 0 threads.
// Must only be called when there are no outstanding dispatchers.
// Must not be called from within a driver_runtime managed thread as that will result in a
// deadlock.
void Reset();
// Stores |irq| which has been unbound.
// This is avoid destroying the irq wrapper immediately after unbinding, as it's possible
// another thread in the thread pool has already pulled an irq packet
// from the port and may attempt to call the irq handler.
void CacheUnboundIrq(std::unique_ptr<AsyncIrq> irq);
// Updates the thread tracking and checks whether to garbage collect the current generation of
// irqs.
void OnThreadWakeup();
// Returns the number of threads that have been started on |loop_|.
uint32_t num_threads() const {
fbl::AutoLock al(&lock_);
return num_threads_;
}
uint32_t thread_limit() const {
fbl::AutoLock al(&lock_);
return thread_limit_;
}
zx_status_t set_thread_limit(uint32_t max_threads) {
fbl::AutoLock al(&lock_);
if (max_threads < num_threads_) {
return ZX_ERR_OUT_OF_RANGE;
}
thread_limit_ = max_threads;
return ZX_OK;
}
uint32_t scheduler_role_options() const {
fbl::AutoLock al(&lock_);
return scheduler_role_options_;
}
zx_status_t set_scheduler_role_options(uint32_t options) {
// reject unknown options
if ((options & ~FDF_SCHEDULER_ROLE_OPTION_NO_SYNC_CALLS) != 0) {
return ZX_ERR_INVALID_ARGS;
}
fbl::AutoLock al(&lock_);
// don't allow setting no-sync-calls if there's already an allow-sync-calls dispatcher.
if ((options & FDF_SCHEDULER_ROLE_OPTION_NO_SYNC_CALLS) && allow_sync_call_dispatchers_ != 0) {
return ZX_ERR_NOT_SUPPORTED;
}
scheduler_role_options_ = options;
return ZX_OK;
}
bool ScanThreadsForStalls();
uint32_t num_dispatchers() const {
fbl::AutoLock al(&lock_);
return num_dispatchers_;
}
bool is_unmanaged() const { return is_unmanaged_; }
std::string_view scheduler_role() const { return scheduler_role_; }
async::Loop* loop() { return &loop_; }
static constexpr uint32_t kDefaultThreadLimit = 20;
private:
// This stores irqs to avoid destroying them immediately after unbinding.
// Even though unbinding an irq will clear all irq packets on a port,
// it's possible another thread in the thread pool has already pulled an irq packet
// from the port and may attempt to call the irq handler.
//
// It is safe to destroy a cached irq once we can determine that all threads
// have woken up at least once since the irq was unbound.
class CachedIrqs {
public:
// Adds an unbound irq to the cached irqs.
void AddIrqLocked(std::unique_ptr<AsyncIrq> irq) __TA_REQUIRES(&lock_);
void NewThreadWakeupLocked(uint32_t total_number_threads) __TA_REQUIRES(&lock_);
// The coordinator can compare the current generation id to a thread's stored generation id to
// see if the thread wakeup has not yet been tracked, in which case |NewThreadWakeupLocked|
// should be called.
uint32_t cur_generation_id() { return cur_generation_id_.load(); }
private:
using List = fbl::DoublyLinkedList<std::unique_ptr<AsyncIrq>, fbl::DefaultObjectTag,
fbl::SizeOrder::Constant>;
void IncrementGenerationId() __TA_REQUIRES(&lock_) {
if (cur_generation_id_.fetch_add(1) == UINT32_MAX) {
// |fetch_add| returns the value before adding. Avoid using 0 for a new generation id,
// since new threads may be spawned with default generation id 0.
cur_generation_id_++;
}
}
// The current generation of cached irqs to be garbage collected once all threads wakeup.
List cur_generation_ __TA_GUARDED(&lock_);
// These are the irqs that were unbound after we already tracked a thread wakeup for the
// current generation.
List next_generation_ __TA_GUARDED(&lock_);
// The number of threads that have woken up since the irqs in the |cur_generation_| list was
// populated.
uint32_t threads_wakeup_count_ __TA_GUARDED(&lock_) = 0;
// This is not locked for reads, so that threads do not need to deal with lock contention if
// there are no cached irqs.
std::atomic<uint32_t> cur_generation_id_ = 0;
};
static constexpr async_loop_config_t MakeConfig(ThreadPool* self,
std::string_view scheduler_role) {
async_loop_config_t config = kAsyncLoopConfigNeverAttachToThread;
config.irq_support = true;
config.data = self;
// Add a thread wakeup handler.
config.prologue = [](async_loop_t* loop, void* data) {
ThreadPool* thread_pool = static_cast<ThreadPool*>(data);
thread_pool->ThreadWakeupPrologue();
};
config.epilogue = [](async_loop_t* loop, void* data) {
ThreadPool* thread_pool = static_cast<ThreadPool*>(data);
thread_pool->ThreadWakeupEpilogue();
};
return config;
}
// Function that runs for every thread wakeup before any handler is called.
void ThreadWakeupPrologue();
// Function that runs for every thread wakeup after any handler is called.
void ThreadWakeupEpilogue();
// The actual current limit on the number of threads we'll spawn, based on the number and
// types of dispatchers as well as the user-settable limit.
// The heuristic is basically, whichever is the lowest of:
// - up to one thread for every dispatcher that allows sync calls, plus one thread for
// all other dispatchers (which should never block).
// - one thread for every dispatcher.
// - the user-settable |thread_limit_|.
uint32_t MaxThreadsLocked() const __TA_REQUIRES(&lock_) {
return std::min({allow_sync_call_dispatchers_ + 1, num_dispatchers_, thread_limit_});
}
// Starts a new thread on the thread pool unconditionally. The caller should check if
// we're not at maximum with |MaxThreadsLocked|.
zx_status_t AddThreadLocked() __TA_REQUIRES(&lock_);
std::string scheduler_role_;
mutable fbl::Mutex lock_;
// Options that affect the kinds of dispatchers that can be created on this thread pool.
// This is guarded by the lock because we have to prevent the precondition specified by
// these options from being violated while they're being set.
uint32_t scheduler_role_options_ __TA_GUARDED(&lock_) = 0;
// Tracks the number of dispatchers which have sync calls allowed. We want to only spawn enough
// threads needed so that every sync call dispatcher can have a thread to itself, at most.
// See |MaxThreadsLocked| for more info.
uint32_t allow_sync_call_dispatchers_ __TA_GUARDED(&lock_) = 0;
// Tracks the number of threads we've spawned via |loop_|.
uint32_t num_threads_ __TA_GUARDED(&lock_) = 0;
// Total number of threads we will spawn.
// TODO(https://fxbug.dev/42085539): We are clamping number_threads_ to 10 to avoid spawning too
// many threads. Technically this can result in a deadlock scenario in a very complex driver
// host. We need better support for dynamically starting threads as necessary.
uint32_t thread_limit_ __TA_GUARDED(&lock_) = kDefaultThreadLimit;
// A unique_ptr to each active thread's task entry time slot, used to tell when we've run
// out of un-stalled threads and should spawn another.
std::vector<std::pair<zx_koid_t, std::atomic_int64_t*>> thread_entry_time_slots_
__TA_GUARDED(&lock_);
// True if we've already attempted to spawn a new thread in response to the current thread
// stall. This prevents us from constantly warning when we're at max threads and there's a
// persistent stall.
bool stalled_ __TA_GUARDED(&lock_) = false;
// Total number of threads which have entered a driver. When this number matches num_threads_,
// we start polling.
std::atomic<uint32_t> threads_entered_ = 0;
uint32_t num_dispatchers_ __TA_GUARDED(&lock_) = 0;
bool is_unmanaged_;
// Stores unbound irqs which will be garbage collected at a later time.
CachedIrqs cached_irqs_;
async_loop_config_t config_;
// |loop_| must be declared last, to ensure that the loop shuts down before
// other members are destructed.
async::Loop loop_;
};
} // namespace driver_runtime
#endif // SRC_DEVICES_BIN_DRIVER_RUNTIME_THREAD_POOL_H_