#[cfg(any(target_os = "android", target_os = "linux"))]
use std::sync::atomic::{AtomicI32, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThreadClass {
Frame,
Background,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThreadClassOutcome {
Changed {
from: i32,
to: i32,
},
Kept {
nice: i32,
},
QosApplied,
Refused {
error: i32,
},
Unsupported,
}
pub const NO_THREAD_TUNING_VAR: &str = "FRUST_NO_THREAD_TUNING";
pub fn thread_tuning_enabled() -> bool {
!kill_switch(
option_env!("FRUST_NO_THREAD_TUNING"),
std::env::var(NO_THREAD_TUNING_VAR).ok().as_deref(),
)
}
fn kill_switch(compile_time: Option<&str>, runtime: Option<&str>) -> bool {
fn is_set_non_zero(value: Option<&str>) -> bool {
matches!(value, Some(v) if v != "0")
}
is_set_non_zero(compile_time) || is_set_non_zero(runtime)
}
pub const FRAME_NICE: i32 = -4;
pub const BACKGROUND_NICE_OFFSET: i32 = 10;
#[cfg_attr(not(any(target_os = "android", target_os = "linux")), allow(dead_code))]
const MAX_NICE: i32 = 19;
#[cfg_attr(not(any(target_os = "android", target_os = "linux")), allow(dead_code))]
pub(crate) fn frame_target(inherited: i32) -> Option<i32> {
(inherited > FRAME_NICE).then_some(FRAME_NICE)
}
#[cfg_attr(not(any(target_os = "android", target_os = "linux")), allow(dead_code))]
pub(crate) fn background_target(inherited: i32, reference: Option<i32>) -> Option<i32> {
let base = reference.unwrap_or(inherited);
let target = base
.saturating_add(BACKGROUND_NICE_OFFSET)
.max(inherited)
.min(MAX_NICE);
(target != inherited).then_some(target)
}
pub const BLOCKING_KEEP_ALIVE: std::time::Duration = std::time::Duration::from_secs(10);
pub fn runtime_policy_summary() -> String {
let reference = current_thread_nice_reference();
format!(
"background runtime thread policy: reference nice {}, blocking keep-alive {}s, tuning {}",
reference.map_or_else(|| "unknown".to_owned(), |n| n.to_string()),
BLOCKING_KEEP_ALIVE.as_secs(),
if thread_tuning_enabled() { "on" } else { "off" },
)
}
fn current_thread_nice_reference() -> Option<i32> {
#[cfg(any(target_os = "android", target_os = "linux"))]
{
recorded_reference()
}
#[cfg(not(any(target_os = "android", target_os = "linux")))]
{
None
}
}
#[cfg(any(target_os = "android", target_os = "linux"))]
const NO_REFERENCE: i32 = i32::MIN;
#[cfg(any(target_os = "android", target_os = "linux"))]
static REFERENCE_NICE: AtomicI32 = AtomicI32::new(NO_REFERENCE);
#[cfg_attr(target_family = "wasm", allow(dead_code))]
pub(crate) fn record_reference_thread() {
#[cfg(any(target_os = "android", target_os = "linux"))]
if let Ok(nice) = sys::current_nice() {
REFERENCE_NICE.store(nice, Ordering::Relaxed);
}
}
#[cfg(any(target_os = "android", target_os = "linux"))]
fn recorded_reference() -> Option<i32> {
let nice = REFERENCE_NICE.load(Ordering::Relaxed);
(nice != NO_REFERENCE).then_some(nice)
}
pub fn set_current_thread_class(class: ThreadClass) -> ThreadClassOutcome {
#[cfg(any(target_os = "android", target_os = "linux"))]
{
let inherited = match sys::current_nice() {
Ok(nice) => nice,
Err(error) => return ThreadClassOutcome::Refused { error },
};
let target = match class {
ThreadClass::Frame => frame_target(inherited),
ThreadClass::Background => background_target(inherited, recorded_reference()),
};
match target {
None => ThreadClassOutcome::Kept { nice: inherited },
Some(to) => match sys::set_nice(to) {
Ok(()) => ThreadClassOutcome::Changed {
from: inherited,
to,
},
Err(error) => ThreadClassOutcome::Refused { error },
},
}
}
#[cfg(target_vendor = "apple")]
{
let qos = match class {
ThreadClass::Frame => libc::qos_class_t::QOS_CLASS_USER_INTERACTIVE,
ThreadClass::Background => libc::qos_class_t::QOS_CLASS_UTILITY,
};
match sys::set_qos(qos) {
Ok(()) => ThreadClassOutcome::QosApplied,
Err(error) => ThreadClassOutcome::Refused { error },
}
}
#[cfg(not(any(target_os = "android", target_os = "linux", target_vendor = "apple")))]
{
let _ = class;
ThreadClassOutcome::Unsupported
}
}
pub fn current_thread_nice() -> Option<i32> {
#[cfg(any(target_os = "android", target_os = "linux"))]
{
sys::current_nice().ok()
}
#[cfg(not(any(target_os = "android", target_os = "linux")))]
{
None
}
}
#[cfg(any(target_os = "android", target_os = "linux"))]
mod sys {
fn tid() -> libc::id_t {
let tid = unsafe { libc::gettid() };
tid as libc::id_t
}
pub(super) fn current_nice() -> Result<i32, i32> {
let raw = unsafe {
libc::syscall(
libc::SYS_getpriority,
libc::PRIO_PROCESS as libc::c_long,
tid() as libc::c_long,
)
};
if raw < 0 {
Err(last_errno())
} else {
Ok(20 - raw as i32)
}
}
pub(super) fn set_nice(nice: i32) -> Result<(), i32> {
let rc = unsafe { libc::setpriority(libc::PRIO_PROCESS, tid(), nice) };
if rc == 0 { Ok(()) } else { Err(last_errno()) }
}
fn last_errno() -> i32 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
}
}
#[cfg(target_vendor = "apple")]
mod sys {
pub(super) fn set_qos(qos: libc::qos_class_t) -> Result<(), i32> {
let rc = unsafe { libc::pthread_set_qos_class_self_np(qos, 0) };
if rc == 0 { Ok(()) } else { Err(rc) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_target_keeps_an_inherited_value_at_or_above_display() {
assert_eq!(frame_target(-10), None, "a top-app UI thread's -10 is kept");
assert_eq!(frame_target(FRAME_NICE), None);
}
#[test]
fn frame_target_asks_for_display_when_the_inherited_value_is_worse() {
assert_eq!(frame_target(0), Some(FRAME_NICE));
assert_eq!(frame_target(10), Some(FRAME_NICE));
}
#[test]
fn background_target_sits_ten_steps_below_the_reference() {
assert_eq!(background_target(-10, Some(-10)), Some(0));
assert_eq!(background_target(0, Some(-10)), None);
}
#[test]
fn background_target_never_raises_priority() {
assert_eq!(background_target(15, Some(-10)), None);
}
#[test]
fn background_target_without_a_reference_is_relative_to_the_inherited_value() {
assert_eq!(background_target(0, None), Some(10));
assert_eq!(background_target(-4, None), Some(6));
}
#[test]
fn background_target_clamps_at_the_weakest_nice() {
assert_eq!(background_target(15, None), Some(MAX_NICE));
assert_eq!(background_target(MAX_NICE, None), None);
}
#[test]
fn kill_switch_parses_like_the_render_thread_switch() {
assert!(!kill_switch(None, None));
assert!(kill_switch(Some("1"), None));
assert!(kill_switch(None, Some("1")));
assert!(!kill_switch(Some("0"), None));
assert!(!kill_switch(None, Some("0")));
assert!(kill_switch(Some("0"), Some("1")));
}
#[cfg(any(target_os = "android", target_os = "linux"))]
fn on_spawned_thread<R: Send + 'static>(f: impl FnOnce() -> R + Send + 'static) -> R {
std::thread::spawn(f).join().expect("probe thread panicked")
}
#[cfg(any(target_os = "android", target_os = "linux"))]
#[test]
fn a_background_thread_runs_at_a_higher_nice_than_its_spawner() {
let spawner = current_thread_nice().expect("nice is readable on Linux");
let (outcome, after) = on_spawned_thread(|| {
let outcome = set_current_thread_class(ThreadClass::Background);
(outcome, current_thread_nice().expect("nice is readable"))
});
if spawner >= MAX_NICE {
assert_eq!(after, MAX_NICE, "nothing weaker than {MAX_NICE} exists");
return;
}
assert!(
after > spawner,
"background thread nice {after} must be higher than its spawner's {spawner} \
(outcome {outcome:?})"
);
}
#[cfg(any(target_os = "android", target_os = "linux"))]
#[test]
fn a_frame_thread_never_runs_below_its_inherited_priority() {
let spawner = current_thread_nice().expect("nice is readable on Linux");
let (outcome, after) = on_spawned_thread(|| {
let outcome = set_current_thread_class(ThreadClass::Frame);
(outcome, current_thread_nice().expect("nice is readable"))
});
assert!(
after <= spawner,
"frame thread nice {after} must not be weaker than the inherited {spawner} \
(outcome {outcome:?})"
);
match outcome {
ThreadClassOutcome::Kept { nice } => assert_eq!(nice, spawner),
ThreadClassOutcome::Changed { from, to } => {
assert_eq!(from, spawner);
assert_eq!(to, FRAME_NICE);
}
ThreadClassOutcome::Refused { .. } => assert_eq!(after, spawner),
other => panic!("unexpected outcome on Linux: {other:?}"),
}
}
}