0133: pw_wakelock#
Status: Open for Comments Intent Approved Last Call Accepted Rejected
Proposal Date: 2025-01-31
CL: pwrev/263535
Authors: Jiaming (Charlie) Wang, Ben Lawson
Facilitator: Unassigned
Rejection rationale#
Wake locks are an anti-pattern in software development, especially so in embedded systems. MCUs typically do not need wake locks because the scheduler does not suspend the system in the middle of tasks like Android does. MCUs also suspend/resume faster than more complex systems, so the benefit of keeping the system awake when no task is running is minimal or non-existent. Finally, wake locks tend to be viral once introduced due to all tasks seemingly needing to be wrapped in a wake lock, so we are wary of promoting them.
For now, we think it is more appropriate for subsystems like
pw_bluetooth_sapphire
to provide a custom API for wake lock logic that is
likely to only be used when the subsystem is running on a big OS like Android
or Fuchsia. Once we have some more concrete example code, we may reconsider
power management APIs in Pigweed.
Summary#
This SEED proposes the creation of a new pw_wakelock
module that provides a
portable interface for creating, holding, and destroying wakelocks. A wakelock
is a handle that, when active, prevents the system from going to a low power
state.
Motivation#
Bluetooth is a major wakeup source on embedded projects and systems typically
need to stay awake while Bluetooth procedures are active. While this can be done
with a crude timeout upon packet activity, tight integration with the Bluetooth stack
enables more effective power management. Accordingly, pw_bluetooth_sapphire
needs to integrate with the power framework on Fuchsia and future customer
platforms. This needs to be done in a portable way that enables consistent
power management behavior across platforms.
Beyond the Sapphire module, wakelocks are a common requirement in battery powered embedded projects so we expect users to find a wakelock abstraction useful. Additionally, developers need a way to write unit tests for code that uses wakelocks without invoking real power management APIs. This indicates the need for an abstraction layer.
Requirements#
pw_wakelock should provide an API for creating, holding, and releasing a wakelock.
The wakelock API should support unit testing.
The wakelock API should not overfit to any existing power management API.
The wakelock API should at least be compatible with Fuchsia, Linux, and Zephyr.
pw_wakelock backends should be able to adopt an existing platform-specific wakelock. This enables better integration with the rest of the system.
Investigation into operating system power APIs#
User space power management in Linux#
Wakelock#
Wakelock allows processes in user space to prevent CPUs from going into low power state by writing a lock name to a file descriptor. The wakelock implementation was initially shipped with Android without first landing in the Linux mainline, and upstreaming it was controversial.
Key APIs from userspace are as follows:
Acquire a wakelock#
Write wakelock name to /sys/power/wake_lock
.
Acquire a wakelock with timeout#
Write wakelock name along with a timeout (in nanoseconds) to
/sys/power/wake_lock
.
Release a wakelock#
Write wakelock name to /sys/power/wake_unlock
.
Power management in Fuchsia#
See Fuchsia power framework for detailed information on Fuchsia power concepts.
Power element and power level#
Power Element is the abstraction that represents either a hardware device or a higher level software component. Power elements are composed of discrete power levels, which can be a range of performance levels. Managed elements have their power level dictated by the Power Broker. Unmanaged elements cannot have dependencies and simply report their current power level.
Dependencies in power elements#
The power level of one power element can have a dependency with the power level in another power element. Fulfillment policies for power level dependencies are strong fulfillment and weak fulfillment. Strong fulfillment means the power level is activated, while weak fulfillment does not control the power level. A dependency can be strongly-fulfilled only if its required element is managed, whereas dependencies on managed and unmanaged elements may be weakly-fulfilled.
Leases#
A component can take a lease at a certain power level in a power element owned
by the component. A lease is a grant for a managed element to have all of its
dependencies for a given power level fulfilled. A component can also acquire a
lease without explicitly configuring a power element or its dependencies. The
`` fuchsia.power.system/ActivityGovernor.AcquireWakeLease`` API returns a
LeaseToken
that prevents system suspension. The lease is transferable
across components and drivers using FIDL messages. Here
is the latest example code for taking a wake lease directly from the System
Activity Governor. The timed power lease is not available as of now and
requires a custom implementation.
Power management in Zephyr#
Power State Locks in Zephyr provide a mechanism to explicitly prevent the system from transitioning into certain power states. This functionality is useful when a component needs to ensure the system remains in a specific power state, effectively acting similar to a wakelock in Linux.
Power API#
Zephyr provides System Power Management which is responsible for controlling the overall power state of the CPU or the entire System-on-Chip (SoC). This involves transitioning the system between different power states, each characterized by varying levels of power consumption and wakeup latency.
Processes / Applications can utilize the following Power Management Policy APIs to prevent the SoC going into certain power states, to keep the CPU along with certain devices in the right power level.
pm_policy_state_lock_get()#
void pm_policy_state_lock_get(enum pm_state state, uint8_t substate_id)
Increase a power state lock counter.
A power state will not be allowed on the first call of
pm_policy_state_lock_get()
. Subsequent calls will just increase a reference
count, thus meaning this API can be safely used concurrently. A state will be
allowed again after pm_policy_state_lock_put()
is called as many times as
pm_policy_state_lock_get()
.
Note that the PM_STATE_ACTIVE state is always allowed, so calling this API with PM_STATE_ACTIVE will have no effect.
pm_policy_state_lock_is_active()#
bool pm_policy_state_lock_is_active(enum pm_state state, uint8_t substate_id)
Check if a power state lock is active (not allowed).
pm_policy_state_lock_put()#
void pm_policy_state_lock_put(enum pm_state state, uint8_t substate_id)
Decrease a power state lock counter.
Power states in Zephyr#
Zephyr supports the following power states: active, runtime idle, suspend to idle, standby, suspend to ram, suspend to disk, and soft off. See the documentation for detailed descriptions.
Design#
We aim to minimize the API surface to maximize portability. For example, we are not adding an API with timeout parameters, power levels, or diagnostic features (other than names). Tokenization is also optional.
The WakelockProvider
interface is implemented by backends and has a single
method for vending wakelocks. A Wakelock
is a simple movable class that
calls a function on destruction. This function can be used by backends to
unlock the wakelock.
A helper macro PW_WAKELOCK_ACQUIRE
is provided that optionally tokenizes
the wakelock name parameter depending on the configuration.
Testability is also a primary goal of this design. By using the
WakelockProvider
interface, a fake can be dependency injected into the code
under test.
API#
#define PW_WAKELOCK_ACQUIRE(provider, name) \
provider.Acquire(PW_WAKELOCK_TOKEN_EXPR(name))
class WakelockProvider {
public:
virtual ~WakelockProvider() = default;
virtual Result<Wakelock> Acquire(PW_WAKELOCK_TOKEN_TYPE name) = 0;
};
class Wakelock final {
public:
Wakelock();
Wakelock(pw::Function<void()>);
Wakelock(Wakelock&&) = default;
Wakelock& operator=(Wakelock&&) = default;
Wakelock(const Wakelock&) = delete;
Wakelock& operator=(const Wakelock&) = delete;
~Wakelock() {
if (unlock_fn_) {
unlock_fn_();
}
}
private:
pw::Function<void()> unlock_fn_ = nullptr;
};
Example usage#
Status MyFunction(WakelockProvider& provider) {
PW_TRY_ASSIGN(Wakelock lock, PW_WAKELOCK_ACQUIRE(provider, "hello_world"));
PW_LOG_INFO("Hello World");
}
int main() {
LinuxWakelockProvider provider;
if (!MyFunction(provider).ok()) {
return 1;
}
return 0;
}
Testing#
A fake WakelockProvider
implementation will be created with the following
API:
class FakeWakelockProvider final : WakelockProvider {
public:
Result<Wakelock> Acquire(PW_WAKELOCK_TOKEN_TYPE name) override {
if (!status_.ok()) {
return status_;
}
wakelock_count_++;
return Wakelock([this](){ wakelock_count_--; });
}
uint16_t wakelock_count() const { return wakelock_count_; }
void set_acquire_status(Status status) { status_ = status; }
private:
uint16_t wakelock_count_ = 0;
Status status_ = PW_STATUS_OK;
};
Tokenization#
The tokenized configuration will set PW_WAKELOCK_TOKEN_TYPE
to
pw_tokenizer_Token
and PW_WAKELOCK_TOKEN_EXPR
to
PW_TOKENIZE_STRING_EXPR
. By default, these macros will be set to const
char*
and no-op, respectively.
Backends#
No-op#
There is no reasonable basic backend possible, but we can provide a default no-op backend that always succeeds and has an empty implementation.
Linux#
#define WAKE_LOCK_PATH "/sys/power/wake_lock"
#define WAKE_UNLOCK_PATH "/sys/power/wake_unlock"
class LinuxWakelockProvider final {
public:
// Note: if name is tokenized, we can convert the token into a base64
// string.
Result<Wakelock> Acquire(const char* name) override {
int wake_lock_fd = open(WAKE_LOCK_PATH, O_WRONLY|O_APPEND);
if (wake_lock_fd < 0) {
PW_LOG_WARN("Unable to open %s, err:%s",
WAKE_LOCK_PATH, std::strerror(errno));
if (errno == ENOENT) {
PW_LOG_WARN("No wake lock support");
}
return Status::Unavailable();
}
// Acquire the wakelock
int ret = write(wake_lock_fd, name, strlen(name));
close(wake_lock_fd);
if (ret < 0) {
PW_LOG_ERROR("Failed to acquire wakelock %d %s", ret,
strerror(errno));
return Status::Unavailable();
}
return Wakelock([name](){ LinuxWakelockProvider::Release(name); });
}
static void Release(const char* name) {
int wake_unlock_fd = open(WAKE_UNLOCK_PATH, O_WRONLY|O_APPEND);
if (wake_unlock_fd < 0) {
PW_LOG_WARN("Unable to open %s, err:%s",
WAKE_UNLOCK_PATH, std::strerror(errno));
return Status::Unavailable();
}
// Release the wakelock
int ret = write(wake_unlock_fd, name, strlen(name));
close(wake_unlock_fd);
if (ret < 0) {
PW_LOG_ERROR("Failed to release wakelock %d %s", ret, strerror(errno));
return;
}
}
};
Fuchsia#
The Fuchsia backend will be initialized with the client end of ActivityGovernor.
FuchsiaWakelockProvider::Acquire
will be implemented by making a
synchronous call to ActivityGovernor.AcquireWakeLease()
for the first wakelock to obtain a LeaseToken
and setting a lock counter to
1. The lock counter will be incremented for additional Acquire
calls. The
lock counter will be decremented when a Wakelock
is destroyed. When the
number of active Wakelock
objects is 0, the LeaseToken
will be
destroyed. A non-portable method FuchsiaWakelockProvider::Adopt
will
support creating a Wakelock
from an existing LeaseToken
. This will be
used in FIDL clients and servers before passing a received Wakelock
to
portable code.
Zephyr#
ZephyrWakelockProvider::Acquire
will call pm_policy_state_lock_get()
with a configurable power state. The default implementation should keep the
system in PM_STATE_ACTIVE
. On destruction of the Wakelock
,
pm_policy_state_lock_put()
will be called.
Alternatives#
NativeWakelock#
We originally considered the Class/NativeClass pattern, but is difficult to use with unit tests.
/// Acquire a wakelock object. Returns a Result<Wakelock>.
/// PW_HANDLE_WAKELOCK_ACQUIRE is implemented by the backend.
#define PW_WAKELOCK_ACQUIRE(name) \
PW_HANDLE_WAKELOCK_ACQUIRE(name, __FILE__, __LINE__)
// Wakelock interface (using the Class/NativeClass pattern)
class Wakelock final {
public:
Wakelock() : native_wakelock_(*this) {}
~Wakelock();
Wakelock(Wakelock&& other);
Wakelock& operator=(Wakelock&& other);
/// Returns the inner `NativeWakelock` containing backend-specific state.
/// Only non-portable code should call these methods.
backend::NativeWakelock& native_wakelock() { return native_wakelock_; }
const backend::NativeWakelock& native_wakelock() const { return native_wakelock_; }
private:
backend::NativeWakelock native_wakelock_;
};
Custom wakelock API in pw_bluetooth_sapphire#
If this proposal is not accepted, we will need to make a custom wakelock
abstraction layer inside pw_bluetooth_sapphire
that will likely look similar
to this proposal.