use std::sync::{Condvar, Mutex, MutexGuard, OnceLock};
use std::time::{Duration, Instant};
pub const GRACE: Duration = Duration::from_millis(300);
struct Inner {
active: usize,
background: bool,
idle_since: Option<Instant>,
switches: u64,
last_switch_us: u64,
quiet: bool,
}
struct Manager {
inner: Mutex<Inner>,
wake: Condvar,
log: Box<dyn Fn(&str) + Send + Sync>,
}
static MANAGER: OnceLock<Manager> = OnceLock::new();
impl Manager {
fn lock(&self) -> MutexGuard<'_, Inner> {
self.inner.lock().unwrap_or_else(|e| e.into_inner())
}
fn switch(&self, inner: &mut Inner, background: bool, why: &str, say: bool) {
let started = Instant::now();
let result = platform::set(background);
let took = started.elapsed().as_micros() as u64;
inner.background = background;
inner.switches += 1;
inner.last_switch_us = took;
let to = if background { "background" } else { "normal" };
match result {
Ok(()) if !say => {}
Ok(()) => (self.log)(&format!("priority: {to} ({why}) in {took} µs")),
Err(e) => (self.log)(&format!("priority: could not switch to {to}: {e}")),
}
}
}
pub fn manage(log: impl Fn(&str) + Send + Sync + 'static) -> bool {
if !platform::SUPPORTED {
log("priority: not switched on this platform (an idle daemon uses no CPU)");
return false;
}
let fresh = MANAGER.set(Manager {
inner: Mutex::new(Inner {
active: 0,
background: false,
idle_since: Some(Instant::now()),
switches: 0,
last_switch_us: 0,
quiet: false,
}),
wake: Condvar::new(),
log: Box::new(log),
});
if fresh.is_err() {
return true;
}
let manager = MANAGER.get().expect("just set");
{
let mut inner = manager.lock();
manager.switch(&mut inner, true, "idle at start", true);
}
std::thread::Builder::new()
.name("semlith-priority".to_string())
.spawn(|| settle(MANAGER.get().expect("set before the thread")))
.map(|_| true)
.unwrap_or(false)
}
fn settle(manager: &'static Manager) {
let mut inner = manager.lock();
loop {
match (inner.active, inner.background, inner.idle_since) {
(0, false, Some(at)) => {
let left = GRACE.saturating_sub(at.elapsed());
if left.is_zero() {
let say = !inner.quiet;
inner.quiet = false;
manager.switch(&mut inner, true, "idle", say);
} else {
inner = manager
.wake
.wait_timeout(inner, left)
.unwrap_or_else(|e| e.into_inner())
.0;
}
}
_ => {
inner = manager.wake.wait(inner).unwrap_or_else(|e| e.into_inner());
}
}
}
}
#[must_use = "the priority drops the moment the guard does"]
pub struct Embedding(bool);
pub fn embedding() -> Embedding {
hold(false)
}
pub fn request() -> Embedding {
hold(true)
}
fn hold(request: bool) -> Embedding {
let Some(manager) = MANAGER.get() else {
return Embedding(false);
};
let mut inner = manager.lock();
inner.active += 1;
inner.idle_since = None;
if inner.background {
inner.quiet = request;
manager.switch(&mut inner, false, "embedding", !request);
} else if !request && inner.quiet {
inner.quiet = false;
(manager.log)(&format!(
"priority: normal (embedding) in {} µs",
inner.last_switch_us
));
}
Embedding(true)
}
impl Drop for Embedding {
fn drop(&mut self) {
if !self.0 {
return;
}
let Some(manager) = MANAGER.get() else {
return;
};
let mut inner = manager.lock();
inner.active = inner.active.saturating_sub(1);
if inner.active == 0 {
inner.idle_since = Some(Instant::now());
manager.wake.notify_all();
}
}
}
pub fn snapshot() -> serde_json::Value {
let threads = INDEXING_THREADS.load(std::sync::atomic::Ordering::Relaxed);
match MANAGER.get() {
None => serde_json::json!({
"managed": false,
"state": "normal",
"embedding": 0,
"switches": 0,
"indexing_threads": threads,
"indexing_class": platform::INDEXING_CLASS,
}),
Some(manager) => {
let inner = manager.lock();
serde_json::json!({
"managed": true,
"state": if inner.background { "background" } else { "normal" },
"embedding": inner.active,
"switches": inner.switches,
"last_switch_us": inner.last_switch_us,
"indexing_threads": threads,
"indexing_class": platform::INDEXING_CLASS,
})
}
}
}
static INDEXING_THREADS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
#[must_use = "the thread's class is restored the moment the guard drops"]
pub struct IndexingThread(Option<platform::Saved>);
pub fn indexing_thread() -> IndexingThread {
if MANAGER.get().is_none() {
return IndexingThread(None);
}
let saved = platform::enter_indexing();
if saved.is_some() {
INDEXING_THREADS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
IndexingThread(saved)
}
impl Drop for IndexingThread {
fn drop(&mut self) {
if let Some(saved) = self.0.take() {
platform::leave_indexing(saved);
INDEXING_THREADS.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
}
}
}
pub fn worker() {
platform::clear_background();
}
#[cfg(target_os = "macos")]
mod platform {
pub const SUPPORTED: bool = true;
const PRIO_DARWIN_PROCESS: libc::c_int = 4;
const PRIO_DARWIN_BG: libc::c_int = 0x1000;
pub fn set(background: bool) -> Result<(), String> {
let value = if background { PRIO_DARWIN_BG } else { 0 };
let rc = unsafe { libc::setpriority(PRIO_DARWIN_PROCESS, 0, value) };
if rc == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error().to_string())
}
}
pub fn clear_background() {
let _ = set(false);
}
pub const INDEXING_CLASS: &str = "utility";
pub type Saved = libc::qos_class_t;
pub fn enter_indexing() -> Option<Saved> {
unsafe {
let mut before = libc::qos_class_t::QOS_CLASS_UNSPECIFIED;
let mut relative = 0;
libc::pthread_get_qos_class_np(libc::pthread_self(), &mut before, &mut relative);
if libc::pthread_set_qos_class_self_np(libc::qos_class_t::QOS_CLASS_UTILITY, 0) != 0 {
return None;
}
let mut now = libc::qos_class_t::QOS_CLASS_UNSPECIFIED;
libc::pthread_get_qos_class_np(libc::pthread_self(), &mut now, &mut relative);
(now as u32 == libc::qos_class_t::QOS_CLASS_UTILITY as u32).then_some(before)
}
}
pub fn leave_indexing(before: Saved) {
let back = match before {
libc::qos_class_t::QOS_CLASS_UNSPECIFIED => libc::qos_class_t::QOS_CLASS_DEFAULT,
other => other,
};
unsafe {
libc::pthread_set_qos_class_self_np(back, 0);
}
}
}
#[cfg(windows)]
mod platform {
use windows_sys::Win32::System::Threading::{
BELOW_NORMAL_PRIORITY_CLASS, GetCurrentProcess, PROCESS_POWER_THROTTLING_CURRENT_VERSION,
PROCESS_POWER_THROTTLING_EXECUTION_SPEED, PROCESS_POWER_THROTTLING_STATE,
ProcessPowerThrottling, SetPriorityClass, SetProcessInformation,
};
pub const SUPPORTED: bool = true;
pub fn set(background: bool) -> Result<(), String> {
let class = BELOW_NORMAL_PRIORITY_CLASS;
let state = PROCESS_POWER_THROTTLING_STATE {
Version: PROCESS_POWER_THROTTLING_CURRENT_VERSION,
ControlMask: PROCESS_POWER_THROTTLING_EXECUTION_SPEED,
StateMask: if background {
PROCESS_POWER_THROTTLING_EXECUTION_SPEED
} else {
0
},
};
let (class_ok, throttle_ok) = unsafe {
let me = GetCurrentProcess();
(
SetPriorityClass(me, class) != 0,
SetProcessInformation(
me,
ProcessPowerThrottling,
&state as *const _ as *const core::ffi::c_void,
std::mem::size_of::<PROCESS_POWER_THROTTLING_STATE>() as u32,
) != 0,
)
};
match (class_ok, throttle_ok) {
(true, true) => Ok(()),
(true, false) => Ok(()),
_ => Err(std::io::Error::last_os_error().to_string()),
}
}
pub fn clear_background() {
let _ = set(false);
}
pub const INDEXING_CLASS: &str = "below-normal, EcoQoS off";
pub type Saved = ();
pub fn enter_indexing() -> Option<Saved> {
Some(())
}
pub fn leave_indexing(_: Saved) {}
}
#[cfg(target_os = "linux")]
mod platform {
pub const SUPPORTED: bool = true;
pub fn set(_background: bool) -> Result<(), String> {
Ok(())
}
pub fn clear_background() {}
pub const INDEXING_CLASS: &str = "SCHED_BATCH";
pub type Saved = (libc::c_int, libc::sched_param);
pub fn enter_indexing() -> Option<Saved> {
unsafe {
let policy = libc::sched_getscheduler(0);
let mut before: libc::sched_param = std::mem::zeroed();
libc::sched_getparam(0, &mut before);
let batch = libc::sched_param { sched_priority: 0 };
if libc::sched_setscheduler(0, libc::SCHED_BATCH, &batch) != 0 {
return None;
}
(libc::sched_getscheduler(0) == libc::SCHED_BATCH).then_some((policy, before))
}
}
pub fn leave_indexing((policy, before): Saved) {
unsafe {
libc::sched_setscheduler(0, policy, &before);
}
}
}
#[cfg(not(any(target_os = "macos", windows, target_os = "linux")))]
mod platform {
pub const SUPPORTED: bool = false;
pub fn set(_background: bool) -> Result<(), String> {
Ok(())
}
pub fn clear_background() {}
pub const INDEXING_CLASS: &str = "unmanaged";
pub type Saved = ();
pub fn enter_indexing() -> Option<Saved> {
None
}
pub fn leave_indexing(_: Saved) {}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_guard_outside_the_daemon_does_nothing() {
let guard = embedding();
assert!(!guard.0 || MANAGER.get().is_some());
drop(guard);
}
}