.. _seed-0133:
==================
0133: pw_wakelock
==================
.. seed::
:number: 133
:name: pw_wakelock
:status: Rejected
:proposal_date: 2025-01-31
:cl: 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
===
.. code-block:: c++
#define PW_WAKELOCK_ACQUIRE(provider, name) \
provider.Acquire(PW_WAKELOCK_TOKEN_EXPR(name))
class WakelockProvider {
public:
virtual ~WakelockProvider() = default;
virtual Result Acquire(PW_WAKELOCK_TOKEN_TYPE name) = 0;
};
class Wakelock final {
public:
Wakelock();
Wakelock(pw::Function);
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 unlock_fn_ = nullptr;
};
Example usage
=============
.. code-block:: c++
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:
.. code-block:: c++
class FakeWakelockProvider final : WakelockProvider {
public:
Result 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
-----
.. code-block:: c++
#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 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.
.. code-block:: c++
/// Acquire a wakelock object. Returns a Result.
/// 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.