Skip to content

Commit

Permalink
Add bindings for Performance Hint Manager
Browse files Browse the repository at this point in the history
https://developer.android.com/ndk/reference/group/a-performance-hint

Performance Hint Manager allows apps to inform the system about
workloads running on threads to more accurately allocate power for them.
After the fact, applications should report back how long their operation
actually ended up taking.

The new work duration API (driven by `AWorkDuration`) also includes
GPU timings.
  • Loading branch information
MarijnS95 committed Aug 3, 2024
1 parent e4a0b94 commit 808ce8a
Show file tree
Hide file tree
Showing 4 changed files with 333 additions and 0 deletions.
1 change: 1 addition & 0 deletions ndk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Unreleased

- image_reader: Add `ImageReader::new_with_data_space()` constructor and `ImageReader::data_space()` getter from API level 34. (#474)
- Add bindings for Performance Hint manager (`APerformanceHintManager`, `APerformanceHintSession`, `AWorkDuration`). (#480)

# 0.9.0 (2024-04-26)

Expand Down
1 change: 1 addition & 0 deletions ndk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ api-level-31 = ["api-level-30"]
api-level-32 = ["api-level-31"]
api-level-33 = ["api-level-32"]
api-level-34 = ["api-level-33"]
api-level-35 = ["api-level-34"]

test = ["ffi/test", "jni", "all"]

Expand Down
1 change: 1 addition & 0 deletions ndk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub mod media;
pub mod media_error;
pub mod native_activity;
pub mod native_window;
pub mod performance_hint;
pub mod shared_memory;
pub mod surface_texture;
pub mod sync;
Expand Down
330 changes: 330 additions & 0 deletions ndk/src/performance_hint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
//! Bindings for [`APerformanceHintManager`] and [`APerformanceHintSession`]
//!
//! `APerformanceHint` allows apps to create performance hint sessions for groups of
//! threads, and provide hints to the system about the workload of those threads, to help the
//! system more accurately allocate power for them. It is the NDK counterpart to the [Java
//! PerformanceHintManager SDK API].
//!
//! [`APerformanceHintManager`]: https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehintmanager
//! [`APerformanceHintSession`]: https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehintsession
//! [Java PerformanceHintManager SDK API]: https://developer.android.com/reference/android/os/PerformanceHintManager
#![cfg(feature = "api-level-33")]

#[cfg(doc)]
use std::io::ErrorKind;
use std::{io::Result, ptr::NonNull, time::Duration};

use crate::utils::status_to_io_result;

/// An opaque type representing a handle to a performance hint manager.
// TODO: The original docs say"It must be released after use." and the constructor always creates a
// new object, but no release() function appears to be available:
// https://issuetracker.google.com/issues/357214902
///
/// To use:
/// - Obtain the performance hint manager instance by calling [`PerformanceHintManager::new()`].
/// - Create a [`PerformanceHintSession`] with [`PerformanceHintManager::new()`].
/// - Get the preferred update rate with [`PerformanceHintManager::preferred_update_rate()`].
#[derive(Debug)]
#[doc(alias = "APerformanceHintManager")]
pub struct PerformanceHintManager {
ptr: NonNull<ffi::APerformanceHintManager>,
}

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

/// Creates a session for the given set of threads and sets their initial target work duration.
///
/// # Parameters
/// - `thread_ids`: The list of threads to be associated with this session. They must be part of
/// this process' thread group.
/// - `initial_target_work_duration`: The target duration for the new session. This must be
/// positive if using the work duration API, or [`Duration::ZERO`] otherwise.
#[doc(alias = "APerformanceHint_createSession")]
pub fn create_session(
&self,
thread_ids: &[i32],
initial_target_work_duration: Duration,
) -> Option<PerformanceHintSession> {
NonNull::new(unsafe {
ffi::APerformanceHint_createSession(
self.ptr.as_ptr(),
thread_ids.as_ptr(),
thread_ids.len(),
initial_target_work_duration
.as_nanos()
.try_into()
.expect("Duration should be convertible to i64 nanoseconds"),
)
})
.map(|ptr| PerformanceHintSession { ptr })
}

/// Get preferred update rate information for this device.
///
/// Returns the preferred update rate supported by device software.
#[doc(alias = "APerformanceHint_getPreferredUpdateRateNanos")]
pub fn preferred_update_rate(&self) -> Duration {
Duration::from_nanos(unsafe {
ffi::APerformanceHint_getPreferredUpdateRateNanos(self.ptr.as_ptr())
.try_into()
.expect("getPreferredUpdateRateNanos should not return negative")
})
}
}

/// An opaque type representing a handle to a performance hint session. A session can only be
/// acquired from a [`PerformanceHintManager`] with [`PerformanceHintManager::create_session()`].
/// It will be freed automatically on [`drop()`] after use.
///
/// A Session represents a group of threads with an inter-related workload such that hints for their
/// performance should be considered as a unit. The threads in a given session should be long-lived
/// and not created or destroyed dynamically.
///
/// The work duration API can be used with periodic workloads to dynamically adjust
/// thread performance and keep the work on schedule while optimizing the available
/// power budget. When using the work duration API, the starting target duration
/// should be specified while creating the session, and can later be adjusted with
/// [`PerformanceHintSession::update_target_work_duration()`]. While using the work duration API,
/// the client is expected to call [`PerformanceHintSession::report_actual_work_duration()`] each
/// cycle to report the actual time taken to complete to the system.
///
/// All timings should be from [`ffi::CLOCK_MONOTONIC`].
#[derive(Debug)]
#[doc(alias = "APerformanceHintSession")]
pub struct PerformanceHintSession {
ptr: NonNull<ffi::APerformanceHintSession>,
}

impl PerformanceHintSession {
/// Updates this session's target duration for each cycle of work.
///
/// `target_duration` is the new desired duration.
///
/// # Returns
/// - [`ErrorKind::InvalidInput`] if `target_duration` is not positive.
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
#[doc(alias = "APerformanceHint_updateTargetWorkDuration")]
pub fn update_target_work_duration(&self, target_duration: Duration) -> Result<()> {
status_to_io_result(unsafe {
ffi::APerformanceHint_updateTargetWorkDuration(
self.ptr.as_ptr(),
target_duration
.as_nanos()
.try_into()
.expect("Duration should be convertible to i64 nanoseconds"),
)
})
}

/// Reports the actual duration for the last cycle of work.
///
/// The system will attempt to adjust the scheduling and performance of the threads within the
/// thread group to bring the actual duration close to the target duration.
///
/// `actual_duration` is the duration of time the thread group took to complete its last
/// task.
///
/// # Returns
/// - [`ErrorKind::InvalidInput`] if `actual_duration` is not positive.
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
#[doc(alias = "APerformanceHint_reportActualWorkDuration")]
pub fn report_actual_work_duration(&self, actual_duration: Duration) -> Result<()> {
status_to_io_result(unsafe {
ffi::APerformanceHint_reportActualWorkDuration(
self.ptr.as_ptr(),
actual_duration
.as_nanos()
.try_into()
.expect("Duration should be convertible to i64 nanoseconds"),
)
})
}

/// Set a list of threads to the performance hint session. This operation will replace the
/// current list of threads with the given list of threads.
///
/// `thread_ids` is the list of threads to be associated with this session. They must be part of
/// this app's thread group.
///
/// # Returns
/// - [`ErrorKind::InvalidInput`] if the list of thread ids is empty or if any of the thread ids
/// are not part of the thread group.
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
/// - [`ErrorKind::PermissionDenied`] if any thread id doesn't belong to the application.
#[cfg(feature = "api-level-34")]
#[doc(alias = "APerformanceHint_setThreads")]
pub fn set_threads(&self, thread_ids: &[i32]) -> Result<()> {
status_to_io_result(unsafe {
ffi::APerformanceHint_setThreads(
self.ptr.as_ptr(),
thread_ids.as_ptr(),
thread_ids.len(),
)
})
}

/// This tells the session that these threads can be safely scheduled to prefer power efficiency
/// over performance.
///
/// `enabled` is the flag which sets whether this session will use power-efficient scheduling.
///
/// # Returns
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
#[cfg(feature = "api-level-35")]
#[doc(alias = "APerformanceHint_setPreferPowerEfficiency")]
pub fn set_prefer_power_efficiency(&self, enabled: bool) -> Result<()> {
status_to_io_result(unsafe {
ffi::APerformanceHint_setPreferPowerEfficiency(self.ptr.as_ptr(), enabled)
})
}

/// Reports the durations for the last cycle of work.
///
/// The system will attempt to adjust the scheduling and performance of the threads within the
/// thread group to bring the actual duration close to the target duration.
///
/// # Parameters
/// - `work_duration` is the [`WorkDuration`] structure of times the thread group took to
/// complete its last task breaking down into different components.
///
/// The work period start timestamp and actual total duration must be greater than zero.
///
/// The actual CPU and GPU durations must be greater than or equal to [`Duration::ZERO`], and
/// at least one of them must be greater than [`Duration::ZERO`]. When one of them is equal to
/// [`Duration::ZERO`], it means that type of work was not measured for this workload.
///
/// # Returns
/// - [`ErrorKind::BrokenPipe`] if communication with the system service has failed.
#[cfg(feature = "api-level-35")]
#[doc(alias = "APerformanceHint_reportActualWorkDuration2")]
pub fn report_actual_work_duration2(&self, work_duration: &WorkDuration) -> Result<()> {
status_to_io_result(unsafe {
ffi::APerformanceHint_reportActualWorkDuration2(
self.ptr.as_ptr(),
work_duration.ptr.as_ptr(),
)
})
}
}

impl Drop for PerformanceHintSession {
/// Release the performance hint manager pointer acquired via
/// [`PerformanceHintManager::create_session()`].
#[doc(alias = "APerformanceHint_closeSession")]
fn drop(&mut self) {
unsafe { ffi::APerformanceHint_closeSession(self.ptr.as_ptr()) }
}
}

#[cfg(feature = "api-level-35")]
#[derive(Debug)]
#[doc(alias = "AWorkDuration")]
pub struct WorkDuration {
ptr: NonNull<ffi::AWorkDuration>,
}

#[cfg(feature = "api-level-35")]
impl WorkDuration {
/// Creates a new [`WorkDuration`]. When the client finishes using [`WorkDuration`], it will
/// automatically be released on [`drop()`].
#[doc(alias = "AWorkDuration_create")]
pub fn new() -> Self {
Self {
ptr: NonNull::new(unsafe { ffi::AWorkDuration_create() })
.expect("AWorkDuration_create should not return NULL"),
}
}

/// Sets the work period start timestamp in nanoseconds.
///
/// `work_period_start_timestamp` is the work period start timestamp based on
/// [`ffi::CLOCK_MONOTONIC`] about when the work starts. This timestamp must be greater than
/// [`Duration::ZERO`].
#[doc(alias = "AWorkDuration_setWorkPeriodStartTimestampNanos")]
pub fn set_work_period_start_timestamp(&self, work_period_start_timestamp: Duration) {
unsafe {
ffi::AWorkDuration_setWorkPeriodStartTimestampNanos(
self.ptr.as_ptr(),
work_period_start_timestamp
.as_nanos()
.try_into()
.expect("Duration should be convertible to i64 nanoseconds"),
)
}
}

/// Sets the actual total work duration in nanoseconds.
///
/// `actual_total_duration` is the actual total work duration. This number must be greater
/// than [`Duration::ZERO`].
#[doc(alias = "AWorkDuration_setActualTotalDurationNanos")]
pub fn set_actual_total_duration(&self, actual_total_duration: Duration) {
unsafe {
ffi::AWorkDuration_setActualTotalDurationNanos(
self.ptr.as_ptr(),
actual_total_duration
.as_nanos()
.try_into()
.expect("Duration should be convertible to i64 nanoseconds"),
)
}
}

/// Sets the actual CPU work duration in nanoseconds.
///
/// `actual_cpu_duration` is the actual CPU work duration. If it is equal to
/// [`Duration::ZERO`], that means the CPU was not measured.
#[doc(alias = "AWorkDuration_setActualCpuDurationNanos")]
pub fn set_actual_cpu_duration(&self, actual_cpu_duration: Duration) {
unsafe {
ffi::AWorkDuration_setActualCpuDurationNanos(
self.ptr.as_ptr(),
actual_cpu_duration
.as_nanos()
.try_into()
.expect("Duration should be convertible to i64 nanoseconds"),
)
}
}

/// Sets the actual GPU work duration in nanoseconds.
///
/// `actual_gpu_duration` is the actual GPU work duration. If it is equal to
/// [`Duration::ZERO`], that means the GPU was not measured.
#[doc(alias = "AWorkDuration_setActualGpuDurationNanos")]
pub fn set_actual_gpu_duration(&self, actual_gpu_duration: Duration) {
unsafe {
ffi::AWorkDuration_setActualGpuDurationNanos(
self.ptr.as_ptr(),
actual_gpu_duration
.as_nanos()
.try_into()
.expect("Duration should be convertible to i64 nanoseconds"),
)
}
}
}

// TODO: Does this make sense?
impl Default for WorkDuration {
#[doc(alias = "AWorkDuration_create")]
fn default() -> Self {
Self::new()
}
}

impl Drop for WorkDuration {
/// Destroys [`WorkDuration`] and free all resources associated to it.
#[doc(alias = "AWorkDuration_release")]
fn drop(&mut self) {
unsafe { ffi::AWorkDuration_release(self.ptr.as_ptr()) }
}
}

0 comments on commit 808ce8a

Please sign in to comment.