Skip to content

Commit fe11cbf

Browse files
committed
Add bindings for Thermal Manager
https://developer.android.com/ndk/reference/group/thermal `AThermal` allows querying the current thermal (throttling) status, as well as forecasts of future thermal statuses to allow applications to respond and mitigate possible throttling in the (near) future.
1 parent 7811b58 commit fe11cbf

File tree

3 files changed

+333
-0
lines changed

3 files changed

+333
-0
lines changed

ndk/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Unreleased
22

33
- image_reader: Add `ImageReader::new_with_data_space()` constructor and `ImageReader::data_space()` getter from API level 34. (#474)
4+
- Add bindings for Thermal (`AThermalManager`). (#481)
45

56
# 0.9.0 (2024-04-26)
67

ndk/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ pub mod native_window;
2929
pub mod shared_memory;
3030
pub mod surface_texture;
3131
pub mod sync;
32+
pub mod thermal;
3233
pub mod trace;
3334
mod utils;

ndk/src/thermal.rs

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
//! Bindings for [`AThermalManager`]
2+
//!
3+
//! Structures and functions to access thermal status and register/unregister thermal status
4+
//! listener in native code.
5+
//!
6+
//! [`AThermalManager`]: https://developer.android.com/ndk/reference/group/thermal#athermalmanager
7+
#![cfg(feature = "api-level-30")]
8+
9+
#[cfg(doc)]
10+
use std::io::ErrorKind;
11+
use std::{io::Result, os::raw::c_void, ptr::NonNull};
12+
13+
use num_enum::{FromPrimitive, IntoPrimitive};
14+
15+
use crate::utils::abort_on_panic;
16+
17+
/// Workaround for <https://issuetracker.google.com/issues/358664965>. `status_t` should only
18+
/// contain negative error codes, but the underlying `AThermal` implementation freely passes
19+
/// positive error codes around. At least the expected errno values are doucmentated, and "somewhat
20+
/// implicitly" listed as positive values.
21+
fn status_to_io_result(status: i32) -> Result<()> {
22+
// Intentionally not imported in scope (and an identically-named function) to prevent
23+
// accidentally calling this function without negation.
24+
crate::utils::status_to_io_result(-status)
25+
}
26+
27+
/// Thermal status used in function [`ThermalManager::current_thermal_status()`] and
28+
/// [`ThermalStatusCallback`].
29+
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, FromPrimitive, IntoPrimitive)]
30+
#[repr(i32)]
31+
#[doc(alias = "AThermalStatus")]
32+
#[non_exhaustive]
33+
pub enum ThermalStatus {
34+
/// Error in thermal status.
35+
// TODO: Move to a Result?
36+
#[doc(alias = "ATHERMAL_STATUS_ERROR")]
37+
Error = ffi::AThermalStatus::ATHERMAL_STATUS_ERROR.0,
38+
/// Not under throttling.
39+
#[doc(alias = "ATHERMAL_STATUS_NONE")]
40+
None = ffi::AThermalStatus::ATHERMAL_STATUS_NONE.0,
41+
/// Light throttling where UX is not impacted.
42+
#[doc(alias = "ATHERMAL_STATUS_LIGHT")]
43+
Light = ffi::AThermalStatus::ATHERMAL_STATUS_LIGHT.0,
44+
/// Moderate throttling where UX is not largely impacted.
45+
#[doc(alias = "ATHERMAL_STATUS_MODERATE")]
46+
Moderate = ffi::AThermalStatus::ATHERMAL_STATUS_MODERATE.0,
47+
/// Severe throttling where UX is largely impacted.
48+
#[doc(alias = "ATHERMAL_STATUS_SEVERE")]
49+
Severe = ffi::AThermalStatus::ATHERMAL_STATUS_SEVERE.0,
50+
/// Platform has done everything to reduce power.
51+
#[doc(alias = "ATHERMAL_STATUS_CRITICAL")]
52+
Critical = ffi::AThermalStatus::ATHERMAL_STATUS_CRITICAL.0,
53+
/// Key components in platform are shutting down due to thermal condition. Device
54+
/// functionalities will be limited.
55+
#[doc(alias = "ATHERMAL_STATUS_EMERGENCY")]
56+
Emergency = ffi::AThermalStatus::ATHERMAL_STATUS_EMERGENCY.0,
57+
/// Need shutdown immediately.
58+
#[doc(alias = "ATHERMAL_STATUS_SHUTDOWN")]
59+
Shutdown = ffi::AThermalStatus::ATHERMAL_STATUS_SHUTDOWN.0,
60+
61+
#[doc(hidden)]
62+
#[num_enum(catch_all)]
63+
__Unknown(i32),
64+
}
65+
66+
impl From<ffi::AThermalStatus> for ThermalStatus {
67+
fn from(value: ffi::AThermalStatus) -> Self {
68+
value.0.into()
69+
}
70+
}
71+
72+
/// Prototype of the function that is called when thermal status changes. It's passed the updated
73+
/// thermal status as parameter.
74+
///
75+
/// # Warning
76+
/// [`ThermalManager`] is synchronized internally, and its lock is held while this callback is
77+
/// called. Interacting with [`ThermalManager`] inside this closure *will* result in a deadlock.
78+
#[doc(alias = "AThermal_StatusCallback")]
79+
pub type ThermalStatusCallback = Box<dyn FnMut(ThermalStatus) + Send>;
80+
81+
/// Token returned by [`ThermalManager::register_thermal_status_listener()`] for a given
82+
/// [`ThermalStatusCallback`].
83+
///
84+
/// Pass this to [`ThermalManager::unregister_thermal_status_listener()`] when you no longer wish to
85+
/// receive the callback.
86+
#[derive(Debug, PartialEq, Eq, Hash)]
87+
#[must_use = "Without this token the callback can no longer be unregistered and will leak Boxes"]
88+
pub struct ThermalStatusListenerToken {
89+
func: ffi::AThermal_StatusCallback,
90+
data: *mut ThermalStatusCallback,
91+
}
92+
93+
// SAFETY: (un)register_thermal_status_listener() is internally synchronized
94+
unsafe impl Send for ThermalStatusListenerToken {}
95+
unsafe impl Sync for ThermalStatusListenerToken {}
96+
97+
/// An opaque type representing a handle to a thermal manager. An instance of thermal manager must
98+
/// be acquired prior to using thermal status APIs. It will be freed automatically on [`drop()`]
99+
/// after use.
100+
///
101+
/// To use:
102+
/// - Create a new thermal manager instance by calling the [`ThermalManager::new()`] function.
103+
/// - Get current thermal status with [`ThermalManager::current_thermal_status()`].
104+
/// - Register a thermal status listener with [`ThermalManager::register_thermal_status_listener()`].
105+
/// - Unregister a thermal status listener with
106+
/// [`ThermalManager::unregister_thermal_status_listener()`].
107+
/// - Release the thermal manager instance with [`drop()`].
108+
#[derive(Debug, PartialEq, Eq, Hash)]
109+
#[doc(alias = "AThermalManager")]
110+
pub struct ThermalManager {
111+
ptr: NonNull<ffi::AThermalManager>,
112+
}
113+
114+
// SAFETY: All AThermalManager methods are internally synchronized
115+
unsafe impl Send for ThermalManager {}
116+
unsafe impl Sync for ThermalManager {}
117+
118+
impl ThermalManager {
119+
/// Acquire an instance of the thermal manager.
120+
///
121+
/// Returns [`None`] on failure.
122+
#[doc(alias = "AThermal_acquireManager")]
123+
pub fn new() -> Option<Self> {
124+
NonNull::new(unsafe { ffi::AThermal_acquireManager() }).map(|ptr| Self { ptr })
125+
}
126+
127+
/// Gets the current thermal status.
128+
///
129+
/// Returns current thermal status, [`ThermalStatus::Error`] on failure.
130+
// TODO: Result?
131+
#[doc(alias = "AThermal_getCurrentThermalStatus")]
132+
pub fn current_thermal_status(&self) -> ThermalStatus {
133+
unsafe { ffi::AThermal_getCurrentThermalStatus(self.ptr.as_ptr()) }.into()
134+
}
135+
136+
/// Register the thermal status listener for thermal status change.
137+
///
138+
/// Will leak [`Box`]es unless [`ThermalManager::unregister_thermal_status_listener()`] is
139+
/// called.
140+
// TODO: This API properly mutex-syncs the callbacks with the destructor! Meaning we can track
141+
// `Box`es in `self` and trivially `drop()` them _after_ calling `AThermal_releaseManager()`!
142+
///
143+
/// # Returns
144+
/// - [`ErrorKind::InvalidInput`] if the listener and data pointer were previously added and not removed.
145+
/// - [`ErrorKind::PermissionDenied`] if the required permission is not held.
146+
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
147+
#[doc(alias = "AThermal_registerThermalStatusListener")]
148+
pub fn register_thermal_status_listener(
149+
&self,
150+
callback: ThermalStatusCallback,
151+
) -> Result<ThermalStatusListenerToken> {
152+
let boxed = Box::new(callback);
153+
// This box is only freed when unregister() is called
154+
let data = Box::into_raw(boxed);
155+
156+
unsafe extern "C" fn thermal_status_callback(
157+
data: *mut c_void,
158+
status: ffi::AThermalStatus,
159+
) {
160+
abort_on_panic(|| {
161+
let func: *mut ThermalStatusCallback = data.cast();
162+
(*func)(status.into())
163+
})
164+
}
165+
166+
status_to_io_result(unsafe {
167+
ffi::AThermal_registerThermalStatusListener(
168+
self.ptr.as_ptr(),
169+
Some(thermal_status_callback),
170+
data.cast(),
171+
)
172+
})
173+
.map(|()| ThermalStatusListenerToken {
174+
func: Some(thermal_status_callback),
175+
data,
176+
})
177+
}
178+
179+
/// Unregister the thermal status listener previously resgistered.
180+
///
181+
/// # Returns
182+
/// - [`ErrorKind::InvalidInput`] if the listener and data pointer were not previously added.
183+
/// - [`ErrorKind::PermissionDenied`] if the required permission is not held.
184+
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
185+
#[doc(alias = "AThermal_unregisterThermalStatusListener")]
186+
pub fn unregister_thermal_status_listener(
187+
&self,
188+
token: ThermalStatusListenerToken,
189+
) -> Result<()> {
190+
status_to_io_result(unsafe {
191+
ffi::AThermal_unregisterThermalStatusListener(
192+
self.ptr.as_ptr(),
193+
token.func,
194+
token.data.cast(),
195+
)
196+
})?;
197+
let _ = unsafe { Box::from_raw(token.data) };
198+
Ok(())
199+
}
200+
201+
/// Provides an estimate of how much thermal headroom the device currently has before hitting
202+
/// severe throttling.
203+
///
204+
/// Note that this only attempts to track the headroom of slow-moving sensors, such as the
205+
/// skin temperature sensor. This means that there is no benefit to calling this function more
206+
/// frequently than about once per second, and attempted to call significantly more frequently
207+
/// may result in the function returning [`f32::NAN`].
208+
///
209+
/// In addition, in order to be able to provide an accurate forecast, the system does not
210+
/// attempt to forecast until it has multiple temperature samples from which to extrapolate.
211+
/// This should only take a few seconds from the time of the first call, but during this time,
212+
/// no forecasting will occur, and the current headroom will be returned regardless of the value
213+
/// of `forecast_seconds`.
214+
///
215+
/// The value returned is a non-negative float that represents how much of the thermal envelope
216+
/// is in use (or is forecasted to be in use). A value of `1.0` indicates that the device is
217+
/// (or will be) throttled at [`ThermalStatus::Severe`]. Such throttling can affect the CPU,
218+
/// GPU, and other subsystems. Values may exceed `1.0`, but there is no implied mapping to
219+
/// specific thermal levels beyond that point. This means that values greater than `1.0` may
220+
/// correspond to [`ThermalStatus::Severe`], but may also represent heavier throttling.
221+
///
222+
/// A value of `0.0` corresponds to a fixed distance from `1.0`, but does not correspond to any
223+
/// particular thermal status or temperature. Values on `(0.0, 1.0]` may be expected to scale
224+
/// linearly with temperature, though temperature changes over time are typically not linear.
225+
/// Negative values will be clamped to `0.0` before returning.
226+
///
227+
/// `forecast_seconds` specifies how many seconds into the future to forecast. Given that device
228+
/// conditions may change at any time, forecasts from further in the
229+
/// future will likely be less accurate than forecasts in the near future.
230+
////
231+
/// # Returns
232+
/// A value greater than equal to `0.0`, where `1.0` indicates the SEVERE throttling threshold,
233+
/// as described above. Returns [`f32::NAN`] if the device does not support this functionality
234+
/// or if this function is called significantly faster than once per second.
235+
#[cfg(feature = "api-level-31")]
236+
#[doc(alias = "AThermal_getThermalHeadroom")]
237+
pub fn thermal_headroom(
238+
&self,
239+
// TODO: Duration, even though it has a granularity of seconds?
240+
forecast_seconds: i32,
241+
) -> f32 {
242+
unsafe { ffi::AThermal_getThermalHeadroom(self.ptr.as_ptr(), forecast_seconds) }
243+
}
244+
245+
/// Gets the thermal headroom thresholds for all available thermal status.
246+
///
247+
/// A thermal status will only exist in output if the device manufacturer has the corresponding
248+
/// threshold defined for at least one of its slow-moving skin temperature sensors. If it's
249+
/// set, one should also expect to get it from [`ThermalManager::current_thermal_status()`] or
250+
/// [`ThermalStatusCallback`].
251+
///
252+
/// The headroom threshold is used to interpret the possible thermal throttling status
253+
/// based on the headroom prediction. For example, if the headroom threshold for
254+
/// [`ThermalStatus::Light`] is `0.7`, and a headroom prediction in `10s` returns `0.75` (or
255+
/// [`ThermalManager::thermal_headroom(10)`] = `0.75`), one can expect that in `10` seconds the
256+
/// system could be in lightly throttled state if the workload remains the same. The app can
257+
/// consider taking actions according to the nearest throttling status the difference between
258+
/// the headroom and the threshold.
259+
///
260+
/// For new devices it's guaranteed to have a single sensor, but for older devices with
261+
/// multiple sensors reporting different threshold values, the minimum threshold is taken to
262+
/// be conservative on predictions. Thus, when reading real-time headroom, it's not guaranteed
263+
/// that a real-time value of `0.75` (or [`ThermalManager::thermal_headroom(0)`] = `0.75`)
264+
/// exceeding the threshold of `0.7` above will always come with lightly throttled state (or
265+
/// [`ThermalManager::current_thermal_status()`] = [`ThermalStatus::Light`]) but it can be lower
266+
/// (or [`ThermalManager::current_thermal_status()`] = [`ThermalStatus::None`]). While it's
267+
/// always guaranteed that the device won't be throttled heavier than the unmet threshold's
268+
/// state, so a real-time headroom of `0.75` will never come with [`ThermalStatus::Moderate`]
269+
/// but always lower, and `0.65` will never come with [`ThermalStatus::Light`] but
270+
/// [`ThermalStatus::None`].
271+
///
272+
/// The returned list of thresholds is cached on first successful query and owned by the thermal
273+
/// manager, which will not change between calls to this function. The caller should only need
274+
/// to free the manager with [`drop()`].
275+
///
276+
/// # Returns
277+
/// - [`ErrorKind::InvalidInput`] if outThresholds or size_t is nullptr, or *outThresholds is not nullptr.
278+
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
279+
/// - [`ErrorKind::Unsupported`] if the feature is disabled by the current system.
280+
#[cfg(feature = "api-level-35")]
281+
#[doc(alias = "AThermal_getThermalHeadroomThresholds")]
282+
pub fn thermal_headroom_thresholds(
283+
&self,
284+
) -> Result<Option<impl ExactSizeIterator<Item = ThermalHeadroomThreshold> + '_>> {
285+
let mut out_thresholds = std::ptr::null();
286+
let mut out_size = 0;
287+
status_to_io_result(unsafe {
288+
ffi::AThermal_getThermalHeadroomThresholds(
289+
self.ptr.as_ptr(),
290+
&mut out_thresholds,
291+
&mut out_size,
292+
)
293+
})?;
294+
if out_thresholds.is_null() {
295+
return Ok(None);
296+
}
297+
Ok(Some(
298+
unsafe { std::slice::from_raw_parts(out_thresholds, out_size) }
299+
.iter()
300+
.map(|t| ThermalHeadroomThreshold {
301+
headroom: t.headroom,
302+
thermal_status: t.thermalStatus.into(),
303+
}),
304+
))
305+
}
306+
}
307+
308+
impl Drop for ThermalManager {
309+
/// Release the thermal manager pointer acquired via [`ThermalManager::new()`].
310+
#[doc(alias = "AThermal_releaseManager")]
311+
fn drop(&mut self) {
312+
unsafe { ffi::AThermal_releaseManager(self.ptr.as_ptr()) }
313+
}
314+
}
315+
316+
/// This struct defines an instance of headroom threshold value and its status.
317+
///
318+
/// The value should be monotonically non-decreasing as the thermal status increases. For
319+
/// [`ThermalStatus::Severe`], its headroom threshold is guaranteed to be `1.0`. For status below
320+
/// severe status, the value should be lower or equal to `1.0`, and for status above severe, the
321+
/// value should be larger or equal to `1.0`.
322+
///
323+
/// Also see [`ThermalManager::thermal_headroom()`] for explanation on headroom, and
324+
/// [`ThermalManager::thermal_headroom_thresholds()`] for how to use this.
325+
#[cfg(feature = "api-level-35")]
326+
#[derive(Clone, Copy, Debug, PartialEq)]
327+
#[doc(alias = "AThermalHeadroomThreshold")]
328+
pub struct ThermalHeadroomThreshold {
329+
headroom: f32,
330+
thermal_status: ThermalStatus,
331+
}

0 commit comments

Comments
 (0)