Skip to content

Commit

Permalink
Add bindings for Thermal Manager
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
MarijnS95 committed Aug 3, 2024
1 parent 7811b58 commit 6793250
Show file tree
Hide file tree
Showing 2 changed files with 310 additions and 0 deletions.
1 change: 1 addition & 0 deletions ndk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ pub mod native_window;
pub mod shared_memory;
pub mod surface_texture;
pub mod sync;
pub mod thermal;
pub mod trace;
mod utils;
309 changes: 309 additions & 0 deletions ndk/src/thermal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
//! Bindings for [`AThermalManager`]
//!
//! Structures and functions to access thermal status and register/unregister thermal status
//! listener in native code.
//!
//! [`AThermalManager`]: https://developer.android.com/ndk/reference/group/thermal#athermalmanager
#![cfg(feature = "api-level-30")]

#[cfg(doc)]
use std::io::ErrorKind;
use std::{io::Result, os::raw::c_void, ptr::NonNull};

use num_enum::{FromPrimitive, IntoPrimitive};

use crate::utils::{abort_on_panic, status_to_io_result};

/// Thermal status used in function [`ThermalManager::current_thermal_status()`] and
/// [`ThermalStatusCallback`].
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, FromPrimitive, IntoPrimitive)]
#[repr(i32)]
#[doc(alias = "AThermalStatus")]
#[non_exhaustive]
pub enum ThermalStatus {
/// Error in thermal status.
// TODO: Move to a Result?
#[doc(alias = "ATHERMAL_STATUS_ERROR")]
Error = ffi::AThermalStatus::ATHERMAL_STATUS_ERROR.0,
/// Not under throttling.
#[doc(alias = "ATHERMAL_STATUS_NONE")]
None = ffi::AThermalStatus::ATHERMAL_STATUS_NONE.0,
/// Light throttling where UX is not impacted.
#[doc(alias = "ATHERMAL_STATUS_LIGHT")]
Light = ffi::AThermalStatus::ATHERMAL_STATUS_LIGHT.0,
/// Moderate throttling where UX is not largely impacted.
#[doc(alias = "ATHERMAL_STATUS_MODERATE")]
Moderate = ffi::AThermalStatus::ATHERMAL_STATUS_MODERATE.0,
/// Severe throttling where UX is largely impacted.
#[doc(alias = "ATHERMAL_STATUS_SEVERE")]
Severe = ffi::AThermalStatus::ATHERMAL_STATUS_SEVERE.0,
/// Platform has done everything to reduce power.
#[doc(alias = "ATHERMAL_STATUS_CRITICAL")]
Critical = ffi::AThermalStatus::ATHERMAL_STATUS_CRITICAL.0,
/// Key components in platform are shutting down due to thermal condition. Device
/// functionalities will be limited.
#[doc(alias = "ATHERMAL_STATUS_EMERGENCY")]
Emergency = ffi::AThermalStatus::ATHERMAL_STATUS_EMERGENCY.0,
/// Need shutdown immediately.
#[doc(alias = "ATHERMAL_STATUS_SHUTDOWN")]
Shutdown = ffi::AThermalStatus::ATHERMAL_STATUS_SHUTDOWN.0,

#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(i32),
}

impl From<ffi::AThermalStatus> for ThermalStatus {
fn from(value: ffi::AThermalStatus) -> Self {
value.0.into()
}
}

/// Prototype of the function that is called when thermal status changes. It's passed the updated
/// thermal status as parameter.
#[doc(alias = "AThermal_StatusCallback")]
// TODO: SendSync? What thread does this run on?
pub type ThermalStatusCallback = Box<dyn FnMut(ThermalStatus)>;

/// Token returned by [`ThermalManager::register_thermal_status_listener()`] for a given
/// [`ThermalStatusCallback`].
///
/// Pass this to [`ThermalManager::unregister_thermal_status_listener()`] when you no longer wish to
/// receive the callback.
#[derive(Debug)]
#[must_use = "Without this token the callback can no longer be unregistered and will leak Boxes"]
// TODO: SendSync if this can be (de)registered across threads (on different instances even)?
pub struct ThermalStatusListenerToken {
func: ffi::AThermal_StatusCallback,
data: *mut ThermalStatusCallback,
}

/// An opaque type representing a handle to a thermal manager. An instance of thermal manager must
/// be acquired prior to using thermal status APIs. It will be freed automatically on [`drop()`]
/// after use.
///
/// To use:
/// - Create a new thermal manager instance by calling the [`ThermalManager::new()`] function.
/// - Get current thermal status with [`ThermalManager::current_thermal_status()`].
/// - Register a thermal status listener with [`ThermalManager::register_thermal_status_listener()`].
/// - Unregister a thermal status listener with
/// [`ThermalManager::unregister_thermal_status_listener()`].
/// - Release the thermal manager instance with [`drop()`].
#[derive(Debug)]
#[doc(alias = "AThermalManager")]
pub struct ThermalManager {
ptr: NonNull<ffi::AThermalManager>,
}

impl ThermalManager {
/// Acquire an instance of the thermal manager.
///
/// Returns [`None`] on failure.
#[doc(alias = "AThermal_acquireManager")]
pub fn new() -> Option<Self> {
NonNull::new(unsafe { ffi::AThermal_acquireManager() }).map(|ptr| Self { ptr })
}

/// Gets the current thermal status.
///
/// Returns current thermal status, [`ThermalStatus::Error`] on failure.
// TODO: Result?
#[doc(alias = "AThermal_getCurrentThermalStatus")]
pub fn current_thermal_status(&self) -> ThermalStatus {
unsafe { ffi::AThermal_getCurrentThermalStatus(self.ptr.as_ptr()) }.into()
}

/// Register the thermal status listener for thermal status change.
///
/// Will leak [`Box`]es unless [`ThermalManager::unregister_thermal_status_listener()`] is
/// called.
///
/// # Returns
/// - [`ErrorKind::InvalidInput`] if the listener and data pointer were previously added and not removed.
/// - [`ErrorKind::PermissionDenied`] if the required permission is not held.
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
#[doc(alias = "AThermal_registerThermalStatusListener")]
pub fn register_thermal_status_listener(
&self,
callback: ThermalStatusCallback,
) -> Result<ThermalStatusListenerToken> {
let boxed = Box::new(callback);
// This box is only freed when unregister() is called
let data = Box::into_raw(boxed);

unsafe extern "C" fn thermal_status_callback(
data: *mut c_void,
status: ffi::AThermalStatus,
) {
abort_on_panic(|| {
let func: *mut ThermalStatusCallback = data.cast();
(*func)(status.into())
})
}

status_to_io_result(unsafe {
ffi::AThermal_registerThermalStatusListener(
self.ptr.as_ptr(),
Some(thermal_status_callback),
data.cast(),
)
})
.map(|()| ThermalStatusListenerToken {
func: Some(thermal_status_callback),
data,
})
}

/// Unregister the thermal status listener previously resgistered.
///
/// # Returns
/// - [`ErrorKind::InvalidInput`] if the listener and data pointer were not previously added.
/// - [`ErrorKind::PermissionDenied`] if the required permission is not held.
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
#[doc(alias = "AThermal_unregisterThermalStatusListener")]
pub fn unregister_thermal_status_listener(
&self,
token: ThermalStatusListenerToken,
) -> Result<()> {
status_to_io_result(unsafe {
ffi::AThermal_unregisterThermalStatusListener(
self.ptr.as_ptr(),
token.func,
token.data.cast(),
)
})?;
let _ = unsafe { Box::from_raw(token.data) };
Ok(())
}

/// Provides an estimate of how much thermal headroom the device currently has before hitting
/// severe throttling.
///
/// Note that this only attempts to track the headroom of slow-moving sensors, such as the
/// skin temperature sensor. This means that there is no benefit to calling this function more
/// frequently than about once per second, and attempted to call significantly more frequently
/// may result in the function returning [`f32::NAN`].
///
/// In addition, in order to be able to provide an accurate forecast, the system does not
/// attempt to forecast until it has multiple temperature samples from which to extrapolate.
/// This should only take a few seconds from the time of the first call, but during this time,
/// no forecasting will occur, and the current headroom will be returned regardless of the value
/// of `forecast_seconds`.
///
/// The value returned is a non-negative float that represents how much of the thermal envelope
/// is in use (or is forecasted to be in use). A value of `1.0` indicates that the device is
/// (or will be) throttled at [`ThermalStatus::Severe`]. Such throttling can affect the CPU,
/// GPU, and other subsystems. Values may exceed `1.0`, but there is no implied mapping to
/// specific thermal levels beyond that point. This means that values greater than `1.0` may
/// correspond to [`ThermalStatus::Severe`], but may also represent heavier throttling.
///
/// A value of `0.0` corresponds to a fixed distance from `1.0`, but does not correspond to any
/// particular thermal status or temperature. Values on `(0.0, 1.0]` may be expected to scale
/// linearly with temperature, though temperature changes over time are typically not linear.
/// Negative values will be clamped to `0.0` before returning.
///
/// `forecast_seconds` specifies how many seconds into the future to forecast. Given that device
/// conditions may change at any time, forecasts from further in the
/// future will likely be less accurate than forecasts in the near future.
////
/// # Returns
/// A value greater than equal to `0.0`, where `1.0` indicates the SEVERE throttling threshold,
/// as described above. Returns [`f32::NAN`] if the device does not support this functionality
/// or if this function is called significantly faster than once per second.
#[cfg(feature = "api-level-31")]
#[doc(alias = "AThermal_getThermalHeadroom")]
pub fn thermal_headroom(
&self,
// TODO: Duration, even though it has a granularity of seconds?
forecast_seconds: i32,
) -> f32 {
unsafe { ffi::AThermal_getThermalHeadroom(self.ptr.as_ptr(), forecast_seconds) }
}

/// Gets the thermal headroom thresholds for all available thermal status.
///
/// A thermal status will only exist in output if the device manufacturer has the corresponding
/// threshold defined for at least one of its slow-moving skin temperature sensors. If it's
/// set, one should also expect to get it from [`ThermalManager::current_thermal_status()`] or
/// [`ThermalStatusCallback`].
///
/// The headroom threshold is used to interpret the possible thermal throttling status
/// based on the headroom prediction. For example, if the headroom threshold for
/// [`ThermalStatus::Light`] is `0.7`, and a headroom prediction in `10s` returns `0.75` (or
/// [`ThermalManager::thermal_headroom(10)`] = `0.75`), one can expect that in `10` seconds the
/// system could be in lightly throttled state if the workload remains the same. The app can
/// consider taking actions according to the nearest throttling status the difference between
/// the headroom and the threshold.
///
/// For new devices it's guaranteed to have a single sensor, but for older devices with
/// multiple sensors reporting different threshold values, the minimum threshold is taken to
/// be conservative on predictions. Thus, when reading real-time headroom, it's not guaranteed
/// that a real-time value of `0.75` (or [`ThermalManager::thermal_headroom(0)`] = `0.75`)
/// exceeding the threshold of `0.7` above will always come with lightly throttled state (or
/// [`ThermalManager::current_thermal_status()`] = [`ThermalStatus::Light`]) but it can be lower
/// (or [`ThermalManager::current_thermal_status()`] = [`ThermalStatus::None`]). While it's
/// always guaranteed that the device won't be throttled heavier than the unmet threshold's
/// state, so a real-time headroom of `0.75` will never come with [`ThermalStatus::Moderate`]
/// but always lower, and `0.65` will never come with [`ThermalStatus::Light`] but
/// [`ThermalStatus::None`].
///
/// The returned list of thresholds is cached on first successful query and owned by the thermal
/// manager, which will not change between calls to this function. The caller should only need
/// to free the manager with [`drop()`].
///
/// # Returns
/// - [`ErrorKind::InvalidInput`] if outThresholds or size_t is nullptr, or *outThresholds is not nullptr.
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
/// - [`ErrorKind::Unsupported`] if the feature is disabled by the current system.
#[cfg(feature = "api-level-35")]
#[doc(alias = "AThermal_getThermalHeadroomThresholds")]
pub fn thermal_headroom_thresholds(
&self,
) -> Result<Option<impl ExactSizeIterator<Item = ThermalHeadroomThreshold> + '_>> {
let mut out_thresholds = std::ptr::null();
let mut out_size = 0;
status_to_io_result(unsafe {
ffi::AThermal_getThermalHeadroomThresholds(
self.ptr.as_ptr(),
&mut out_thresholds,
&mut out_size,
)
})?;
if out_thresholds.is_null() {
return Ok(None);
}
Ok(Some(
unsafe { std::slice::from_raw_parts(out_thresholds, out_size) }
.iter()
.map(|t| ThermalHeadroomThreshold {
headroom: t.headroom,
thermal_status: t.thermalStatus.into(),
}),
))
}
}

impl Drop for ThermalManager {
/// Release the thermal manager pointer acquired via [`ThermalManager::new()`].
#[doc(alias = "AThermal_releaseManager")]
fn drop(&mut self) {
unsafe { ffi::AThermal_releaseManager(self.ptr.as_ptr()) }
}
}

/// This struct defines an instance of headroom threshold value and its status.
///
/// The value should be monotonically non-decreasing as the thermal status increases. For
/// [`ThermalStatus::Severe`], its headroom threshold is guaranteed to be `1.0`. For status below
/// severe status, the value should be lower or equal to `1.0`, and for status above severe, the
/// value should be larger or equal to `1.0`.
///
/// Also see [`ThermalManager::thermal_headroom()`] for explanation on headroom, and
/// [`ThermalManager::thermal_headroom_thresholds()`] for how to use this.
#[cfg(feature = "api-level-35")]
#[derive(Clone, Copy, Debug, PartialEq)]
#[doc(alias = "AThermalHeadroomThreshold")]
pub struct ThermalHeadroomThreshold {
headroom: f32,
thermal_status: ThermalStatus,
}

0 comments on commit 6793250

Please sign in to comment.