Skip to main content

gdt_cpus/
priority.rs

1//! Defines thread priority levels used for scheduling.
2//!
3//! This module contains the [`ThreadPriority`] enum, which specifies various
4//! priority levels that can be assigned to a thread. These levels help the
5//! operating system determine how to schedule threads, which is crucial for
6//! performance-sensitive applications like games.
7//!
8//! Each priority level is described with an example workload and notes on its
9//! typical behavior, especially on Linux systems. Higher priority levels generally
10//! mean that a thread is more likely to be run and less likely to be preempted,
11//! but the exact behavior is OS-dependent. Using very high (real-time) priorities
12//! often requires special permissions.
13
14/// Represents different priority levels that can be assigned to a thread.
15///
16/// These priority levels are hints to the operating system's scheduler.
17/// The actual behavior can vary based on the OS, system load, and other factors.
18///
19/// # What each level actually maps to
20///
21/// | Level | Linux | Windows | macOS (Apple Silicon) |
22/// |---|---|---|---|
23/// | `Background` | nice 19 | `THREAD_PRIORITY_IDLE` | QoS `BACKGROUND` (E-cores) |
24/// | `Lowest` | nice 10 | `THREAD_PRIORITY_LOWEST` | QoS `UTILITY` (E-core-leaning) |
25/// | `BelowNormal` | nice 5 | `THREAD_PRIORITY_BELOW_NORMAL` | QoS `DEFAULT` |
26/// | `Normal` | nice 0 | `THREAD_PRIORITY_NORMAL` | QoS `USER_INITIATED` |
27/// | `AboveNormal` | nice -5 ¹ | `THREAD_PRIORITY_ABOVE_NORMAL` | QoS `USER_INTERACTIVE` (rel -4) |
28/// | `Highest` | nice -10 ¹ | `THREAD_PRIORITY_HIGHEST` | QoS `USER_INTERACTIVE` |
29/// | `TimeCritical` | nice -20 ¹ | `THREAD_PRIORITY_TIME_CRITICAL` | `SCHED_RR` 47 ² |
30///
31/// ¹ Negative nice needs `CAP_SYS_NICE` or a raised `RLIMIT_NICE`. Without it
32/// the cascade asks **rtkit** for the value (feature `rtkit`, on by default -
33/// rtkit's default floor is nice -15, so `TimeCritical` lands on -15 there)
34/// and finally keeps the level the thread already has - reported as data, never
35/// an error. Probe the outcome up front with
36/// [`crate::priority_capabilities`]. The Linux ladder is deliberately pure
37/// timeshare - no level requests `SCHED_RR`; true real-time is the explicit
38/// opt-in [`crate::promote_thread_to_realtime`] (a spinning RT thread owns
39/// its core, and rtkit-brokered RT comes with a process-wide SIGKILL leash -
40/// that trade-off belongs to the application, not a priority table).
41///
42/// ² macOS `TimeCritical` is a one-way door: `pthread_setschedparam` gives the
43/// thread FIXED priority 47 (no timeshare decay - the audio-feeder use case) but
44/// per Apple's `qos.h` it **permanently opts the thread out of the QoS system**.
45/// Setting a QoS-backed level on that thread afterwards is handled by falling back
46/// to legacy `SCHED_OTHER` with a scaled priority, but the thread never rejoins
47/// QoS (and loses its P/E-core routing hints). Dedicate such threads.
48///
49/// # The same name is NOT the same strength everywhere
50///
51/// On Linux `TimeCritical` is the strongest *timeshare* slot (nice -20 ≈ a
52/// ×9 CFS weight edge over `Highest`): it wins virtually every wake-up race
53/// but cannot starve the machine - for preempt-everything semantics use
54/// [`crate::promote_thread_to_realtime`]. On Windows it is the top of the
55/// *dynamic* priority band (priority 15), not the `REALTIME_PRIORITY_CLASS`.
56/// On macOS it is the top of the *user* band (47 = `MAXPRI_USER`) with fixed
57/// (no-decay) semantics, not the Mach time-constraint band that CoreAudio
58/// render threads occupy. Write code that treats these as strong hints, not
59/// guarantees.
60///
61/// The enum derives common traits like `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`,
62/// `PartialOrd`, `Ord`, `Hash`, and `Default` (where `Normal` is the default).
63/// It also implements `Display` for easy printing of priority level names.
64// repr(u8): the discriminant is used as an ordinal -- `priority as usize` indexes
65// PriorityCaps::effective_rank -- so it is pinned to a compact, stable 0..6 byte.
66// The C ABI uses a separate raw `i32` enum and reconstructs this type at the boundary.
67#[repr(u8)]
68#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub enum ThreadPriority {
71    /// Background priority: For tasks that should only run when CPU is idle.
72    ///
73    /// Ideal for non-critical background operations that should have minimal impact
74    /// on foreground tasks.
75    ///
76    /// # Example Workloads
77    /// *   Steam API synchronization, achievement updates, cloud saves.
78    /// *   Other absolute background noise processes.
79    ///
80    /// # Platform Notes
81    /// *   **Linux:** Typically uses `SCHED_OTHER` policy with a high `nice` value (e.g., 19).
82    ///     Under heavy system load, p99 latency can spike significantly, potentially
83    ///     into hundreds of milliseconds or even seconds.
84    Background = 0,
85
86    /// Lowest priority: throughput work that should yield under contention
87    /// but still make real progress - roughly a ninth of a `Normal` thread's
88    /// CPU share on Linux when both compete.
89    ///
90    /// # Example Workloads
91    /// *   Shader/PSO compilation, navmesh and lighting bakes.
92    /// *   Batch asset processing, analytics, telemetry.
93    ///
94    /// # Platform Notes
95    /// *   **Linux:** `SCHED_OTHER` with nice 10.
96    ///     Tail-latencies can be long under load - by design for this level.
97    Lowest = 1,
98
99    /// Below normal priority: For tasks that are less critical than normal operations.
100    ///
101    /// Use for asynchronous workers, secondary game systems, AI planning, or other
102    /// non-urgent gameplay systems that can be preempted by more critical tasks.
103    ///
104    /// # Example Workloads
105    /// *   Asynchronous worker threads, secondary game systems.
106    /// *   AI pathfinding or planning.
107    /// *   Non-urgent gameplay logic.
108    ///
109    /// # Platform Notes
110    /// *   **Linux:** `SCHED_OTHER` with nice 5 - about a third of a `Normal`
111    ///     thread's share under contention, gentle enough that streaming keeps
112    ///     flowing while the frame is busy.
113    BelowNormal = 2,
114
115    /// Normal priority: The default priority for most threads.
116    ///
117    /// Suitable for general tasks like asset loading, streaming, or prefetching,
118    /// where I/O is often the bottleneck but latency still matters.
119    ///
120    /// # Example Workloads
121    /// *   Asset loading and streaming.
122    /// *   Prefetching game data.
123    /// *   Standard application threads.
124    ///
125    /// # Platform Notes
126    /// *   **Linux:** Typically uses `SCHED_OTHER` with a `nice` value of 0.
127    ///     Offers no real-time guarantees and can experience latency spikes under heavy load.
128    #[default]
129    Normal = 3,
130
131    /// Above normal priority: For tasks that are more important than normal but not critical.
132    ///
133    /// Use for main game logic, input processing, or UI threads that need to be responsive
134    /// but don't require hard real-time guarantees.
135    ///
136    /// # Example Workloads
137    /// *   Main game loop, primary game logic.
138    /// *   User input processing.
139    /// *   UI rendering and interaction thread.
140    ///
141    /// # Platform Notes
142    /// *   **Linux:** `SCHED_OTHER` with nice -5 (≈3× a `Normal` thread's
143    ///     share). Needs privilege or rtkit; see the cascade note above.
144    AboveNormal = 4,
145
146    /// Highest priority: For critical tasks that are deadline-sensitive.
147    ///
148    /// Recommended for render threads or audio processing threads where meeting deadlines
149    /// is crucial for smooth user experience.
150    ///
151    /// # Example Workloads
152    /// *   Main render thread.
153    /// *   Audio processing and mixing thread.
154    ///
155    /// # Platform Notes
156    /// *   **Linux:** `SCHED_OTHER` with nice -10 (≈9× a `Normal` thread's
157    ///     share - a pinned render thread owns its core without legally
158    ///     starving anything). Needs privilege or rtkit; see the cascade
159    ///     note above.
160    Highest = 5,
161
162    /// Time-critical priority: For extremely sensitive tasks requiring minimum latency.
163    ///
164    /// **Use with extreme caution.** This level gives threads the highest possible precedence
165    /// and can potentially starve other system processes if not managed carefully.
166    /// Ideal for short, critical bursts of work on performance cores.
167    ///
168    /// # Example Workloads
169    /// *   Highly critical worker threads pinned to Performance-cores (P-cores).
170    /// *   Tasks demanding absolute minimum latency.
171    ///
172    /// # Platform Notes
173    /// *   **General:** the strongest level `set_thread_priority` hands out;
174    ///     true real-time is the separate opt-in
175    ///     [`crate::promote_thread_to_realtime`].
176    /// *   **Linux:** `SCHED_OTHER` with nice -20 - ≈9× the share of a
177    ///     `Highest` thread, so the audio feeder wins its wake-up races even
178    ///     against your own best threads, without RT's ability to wedge a
179    ///     core. rtkit-brokered grants clamp to the daemon's floor (default
180    ///     -15).
181    /// *   **macOS:** `SCHED_RR` 47 - fixed priority (no timeshare decay), no privileges needed,
182    ///     but PERMANENTLY opts the thread out of the QoS system (see the table above).
183    TimeCritical = 6,
184}
185
186impl std::fmt::Display for ThreadPriority {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        match self {
189            ThreadPriority::Background => write!(f, "Background"),
190            ThreadPriority::Lowest => write!(f, "Lowest"),
191            ThreadPriority::BelowNormal => write!(f, "BelowNormal"),
192            ThreadPriority::Normal => write!(f, "Normal"),
193            ThreadPriority::AboveNormal => write!(f, "AboveNormal"),
194            ThreadPriority::Highest => write!(f, "Highest"),
195            ThreadPriority::TimeCritical => write!(f, "TimeCritical"),
196        }
197    }
198}
199
200/// How the OS satisfied a thread-priority request.
201///
202/// The `path`/`tier` split is deliberate: a request can be brokered *and*
203/// real-time (Linux rtkit granting `SCHED_RR`), so "how it was applied" and
204/// "what tier it landed in" are orthogonal questions.
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
207pub enum Grant {
208    /// Applied directly by the OS scheduler (`setpriority`, `SetThreadPriority`,
209    /// `pthread_set_qos_class_self_np`).
210    Direct,
211    /// Negotiated through a privilege broker - Linux rtkit / the xdg realtime
212    /// portal - because the direct syscall was denied.
213    Brokered,
214    /// A real-time policy was engaged (macOS `TimeCritical` `SCHED_RR`, or the
215    /// consent API [`crate::promote_thread_to_realtime`]).
216    Realtime,
217}
218
219impl std::fmt::Display for Grant {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        match self {
222            Grant::Direct => write!(f, "Direct"),
223            Grant::Brokered => write!(f, "Brokered"),
224            Grant::Realtime => write!(f, "Realtime"),
225        }
226    }
227}
228
229/// Why a thread-priority request didn't get a clean, direct grant of exactly
230/// what was asked.
231///
232/// Carried by [`AppliedPriority::reason`]. `None` means you got the requested
233/// level directly (or brokered at full strength) - nothing to worry about.
234/// `Some(_)` is the answer to *"my engine feels wonky on this box - what did my
235/// priority actually do?"* - returned as data so the caller decides (retry,
236/// warn, telemeter), never as a hidden log side-effect.
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
239pub enum FallbackReason {
240    /// Direct syscall denied and no broker could satisfy it - feature `rtkit`
241    /// off, no system bus, or no rtkit daemon. The effective level reports
242    /// what the thread kept.
243    NoBroker,
244    /// Broker reached but it didn't answer in time - a busy bus, or a daemon
245    /// starved by the very load you're prioritizing against. Transient; a retry
246    /// when the system is calmer may succeed. The effective level reports what
247    /// the thread kept.
248    BrokerTimedOut,
249    /// Broker reached and it explicitly refused (policy / rate limit). The
250    /// effective level reports what the thread kept.
251    BrokerRefused,
252    /// Broker granted, but weaker than asked - it hit its ceiling (rtkit caps
253    /// negative nice at `MinNiceLevel`, default -15). You kept the *level* but
254    /// lost strength; reach for [`crate::promote_thread_to_realtime`] if you
255    /// need the real thing.
256    Clamped,
257}
258
259impl std::fmt::Display for FallbackReason {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        match self {
262            FallbackReason::NoBroker => write!(f, "NoBroker"),
263            FallbackReason::BrokerTimedOut => write!(f, "BrokerTimedOut"),
264            FallbackReason::BrokerRefused => write!(f, "BrokerRefused"),
265            FallbackReason::Clamped => write!(f, "Clamped"),
266        }
267    }
268}
269
270/// The specific reason a privilege broker REFUSED a grant - the typed form of
271/// the D-Bus error name it answered with, carried by [`AppliedPriority::broker_error`]
272/// when [`reason`](AppliedPriority::reason) is [`FallbackReason::BrokerRefused`].
273///
274/// This is the *actionable* classification behind a refusal: branch on it to
275/// decide whether to retry. We deliberately keep only the well-known names as
276/// variants and collapse everything else to [`Other`](BrokerError::Other) - the
277/// name is the signal, the daemon's free-text message is not worth a heap
278/// allocation for the rare unmapped case (read the rtkit journal for that).
279///
280/// `#[non_exhaustive]`: brokers can grow error names; matching must carry a `_`.
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
283#[non_exhaustive]
284pub enum BrokerError {
285    /// Policy denied the grant - polkit, rlimits, or no active/seated login
286    /// session (common over SSH). Treat as persistent for the current process
287    /// and configuration; retry only after a privilege or session change.
288    AccessDenied,
289    /// The broker's rate limit was hit (rtkit caps actions per interval, e.g.
290    /// 25 / 20 s / UID). Transient - back off and retry later, or elevate fewer
291    /// threads.
292    LimitsExceeded,
293    /// The broker rejected the arguments (e.g. a priority out of its range). A
294    /// bug on our side or a daemon-version skew, not a transient condition.
295    InvalidArgs,
296    /// A generic daemon-side failure with no more specific name.
297    Failed,
298    /// An error name this version doesn't map. The grant was refused; the
299    /// specific cause is in the rtkit journal.
300    Other,
301}
302
303impl BrokerError {
304    /// Maps a D-Bus error name to its [`BrokerError`]. Unmapped names (including
305    /// rtkit-private `org.freedesktop.RealtimeKit1.Error.*` ones) become
306    /// [`Other`](BrokerError::Other).
307    #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
308    pub(crate) fn from_dbus_name(name: &str) -> BrokerError {
309        match name {
310            "org.freedesktop.DBus.Error.AccessDenied" => BrokerError::AccessDenied,
311            "org.freedesktop.DBus.Error.LimitsExceeded" => BrokerError::LimitsExceeded,
312            "org.freedesktop.DBus.Error.InvalidArgs" => BrokerError::InvalidArgs,
313            "org.freedesktop.DBus.Error.Failed" => BrokerError::Failed,
314            _ => BrokerError::Other,
315        }
316    }
317}
318
319impl std::fmt::Display for BrokerError {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        match self {
322            BrokerError::AccessDenied => write!(f, "AccessDenied"),
323            BrokerError::LimitsExceeded => write!(f, "LimitsExceeded"),
324            BrokerError::InvalidArgs => write!(f, "InvalidArgs"),
325            BrokerError::Failed => write!(f, "Failed"),
326            BrokerError::Other => write!(f, "Other"),
327        }
328    }
329}
330
331/// Which OS scheduler API set a thread's priority -- the discriminant that says
332/// how to read [`Mechanism::value`].
333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
335pub enum MechanismPolicy {
336    /// Linux `SCHED_OTHER` via `setpriority` -- `value` is the nice (-20..=19).
337    Nice,
338    /// `SCHED_RR` real-time -- `value` is the RR priority (Linux RT, macOS `TimeCritical`).
339    SchedRr,
340    /// POSIX `SCHED_OTHER` `sched_priority` band -- `value` is the band (macOS QoS opt-out fallback).
341    SchedOther,
342    /// macOS Quality-of-Service -- `value` is a [`QosClass`] ordinal.
343    Qos,
344    /// Windows `SetThreadPriority` -- `value` is the `THREAD_PRIORITY_*` constant (-15..=15).
345    WinPriority,
346}
347
348impl std::fmt::Display for MechanismPolicy {
349    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350        match self {
351            MechanismPolicy::Nice => write!(f, "nice"),
352            MechanismPolicy::SchedRr => write!(f, "SCHED_RR"),
353            MechanismPolicy::SchedOther => write!(f, "SCHED_OTHER"),
354            MechanismPolicy::Qos => write!(f, "QoS"),
355            MechanismPolicy::WinPriority => write!(f, "THREAD_PRIORITY"),
356        }
357    }
358}
359
360/// macOS Quality-of-Service class, stored as [`Mechanism::value`] when the policy
361/// is [`MechanismPolicy::Qos`]. A stable ordinal (NOT the raw darwin `qos_class_t`
362/// hex) so the C ABI and serialized conformance stay trivial.
363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
364#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
365#[repr(i8)]
366pub enum QosClass {
367    /// macOS `QOS_CLASS_BACKGROUND`.
368    Background = 0,
369    /// macOS `QOS_CLASS_UTILITY`.
370    Utility = 1,
371    /// macOS `QOS_CLASS_DEFAULT`.
372    Default = 2,
373    /// macOS `QOS_CLASS_USER_INITIATED`.
374    UserInitiated = 3,
375    /// macOS `QOS_CLASS_USER_INTERACTIVE`.
376    UserInteractive = 4,
377}
378
379impl QosClass {
380    /// The [`QosClass`] for a stored ordinal, or `None` if out of range.
381    #[must_use]
382    pub fn from_value(value: i8) -> Option<QosClass> {
383        match value {
384            0 => Some(QosClass::Background),
385            1 => Some(QosClass::Utility),
386            2 => Some(QosClass::Default),
387            3 => Some(QosClass::UserInitiated),
388            4 => Some(QosClass::UserInteractive),
389            _ => None,
390        }
391    }
392}
393
394impl std::fmt::Display for QosClass {
395    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
396        match self {
397            QosClass::Background => write!(f, "Background"),
398            QosClass::Utility => write!(f, "Utility"),
399            QosClass::Default => write!(f, "Default"),
400            QosClass::UserInitiated => write!(f, "UserInitiated"),
401            QosClass::UserInteractive => write!(f, "UserInteractive"),
402        }
403    }
404}
405
406/// The concrete OS scheduling mechanism a priority request landed on -- the typed
407/// replacement for the old human `detail` string. [`value`](Self::value) is
408/// interpreted per [`policy`](Self::policy) (see [`MechanismPolicy`]). Two bytes,
409/// no allocation; the [`Display`](std::fmt::Display) impl renders the human form
410/// (e.g. `nice -15`, `QoS UserInteractive`, `SCHED_RR 47`).
411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
413pub struct Mechanism {
414    /// Which OS scheduler API set the priority.
415    pub policy: MechanismPolicy,
416    /// The applied parameter, read per `policy` (nice / RR priority / QoS ordinal /
417    /// band / `THREAD_PRIORITY_*` constant). Fits a signed byte on every platform.
418    pub value: i8,
419}
420
421impl std::fmt::Display for Mechanism {
422    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423        match self.policy {
424            MechanismPolicy::Qos => match QosClass::from_value(self.value) {
425                Some(class) => write!(f, "QoS {class}"),
426                None => write!(f, "QoS {}", self.value),
427            },
428            other => write!(f, "{} {}", other, self.value),
429        }
430    }
431}
432
433/// What a thread-priority request actually produced.
434///
435/// Returned by [`set_thread_priority`](crate::set_thread_priority) and
436/// [`promote_thread_to_realtime`](crate::promote_thread_to_realtime) in place
437/// of `()`. The unit return was a lie by omission: on Linux a "successful"
438/// `set_thread_priority(Highest)` can mean you got `Highest`, or that every
439/// privileged path was denied and you silently kept the level you already had
440/// (`Normal` for a fresh thread). This says which - uniformly across platforms,
441/// so callers never `#[cfg]`.
442///
443/// Branch on [`Grant`] / [`AppliedPriority::degraded`] / [`AppliedPriority::reason`]
444/// / [`AppliedPriority::mechanism`] for logic; use the `Display` impl for the
445/// human-readable form.
446#[must_use = "a priority request can silently fall back (no privilege or no broker); \
447              inspect the result -- degraded()/effective/reason -- instead of discarding it, \
448              or the downgrade goes unnoticed"]
449#[derive(Debug, Clone, PartialEq, Eq)]
450#[cfg_attr(feature = "serde", derive(serde::Serialize))]
451pub struct AppliedPriority {
452    /// The level the caller requested. For the consent API this is
453    /// [`ThreadPriority::TimeCritical`] - the strongest named level - since
454    /// real-time has no level above it.
455    requested: ThreadPriority,
456    /// The level actually in effect. Strictly weaker than `requested` means the
457    /// request fell back to a different, lower level.
458    effective: ThreadPriority,
459    /// How the request was satisfied.
460    grant: Grant,
461    /// Why the request fell short, if it did - see [`FallbackReason`]. `None`
462    /// means you got exactly what you asked for.
463    reason: Option<FallbackReason>,
464    /// The concrete OS scheduling mechanism the request landed on, as typed data
465    /// (the former human `detail` string). `value` is interpreted per `policy`.
466    mechanism: Mechanism,
467    /// The typed reason a broker REFUSED the grant - `Some` only when
468    /// [`reason`](Self::reason) is [`FallbackReason::BrokerRefused`], `None`
469    /// otherwise. Branch on it (`AccessDenied` vs `LimitsExceeded`) to decide
470    /// retry vs give-up.
471    broker_error: Option<BrokerError>,
472}
473
474impl AppliedPriority {
475    /// Rebuilds an outcome from structured data.
476    ///
477    /// Returns `None` when the parts contradict each other, such as a broker
478    /// error without a broker-refused reason.
479    #[must_use]
480    pub fn from_parts(
481        requested: ThreadPriority,
482        effective: ThreadPriority,
483        grant: Grant,
484        reason: Option<FallbackReason>,
485        mechanism: Mechanism,
486        broker_error: Option<BrokerError>,
487    ) -> Option<Self> {
488        if broker_error.is_some() && reason != Some(FallbackReason::BrokerRefused) {
489            return None;
490        }
491
492        Some(Self {
493            requested,
494            effective,
495            grant,
496            reason,
497            mechanism,
498            broker_error,
499        })
500    }
501
502    pub(crate) fn new(
503        requested: ThreadPriority,
504        effective: ThreadPriority,
505        grant: Grant,
506        mechanism: Mechanism,
507    ) -> Self {
508        Self::from_parts(requested, effective, grant, None, mechanism, None)
509            .expect("clean priority outcome is valid")
510    }
511
512    /// The level the caller requested.
513    #[must_use]
514    pub fn requested(&self) -> ThreadPriority {
515        self.requested
516    }
517
518    /// The level actually in effect.
519    #[must_use]
520    pub fn effective(&self) -> ThreadPriority {
521        self.effective
522    }
523
524    /// How the request was satisfied.
525    #[must_use]
526    pub fn grant(&self) -> Grant {
527        self.grant
528    }
529
530    /// Why the request fell short, if it did.
531    #[must_use]
532    pub fn reason(&self) -> Option<FallbackReason> {
533        self.reason
534    }
535
536    /// The concrete OS scheduling mechanism the request landed on.
537    #[must_use]
538    pub fn mechanism(&self) -> Mechanism {
539        self.mechanism
540    }
541
542    /// The typed reason a broker refused the grant.
543    #[must_use]
544    pub fn broker_error(&self) -> Option<BrokerError> {
545        self.broker_error
546    }
547
548    /// Records why the request fell short. Builder-style so the clean-grant
549    /// call sites (the overwhelming majority) don't mention it at all. Only the
550    /// Linux cascade clamps or falls back; Windows/macOS never call it.
551    #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
552    pub(crate) fn with_reason(mut self, reason: FallbackReason) -> Self {
553        self.reason = Some(reason);
554
555        self
556    }
557
558    /// Records the typed broker-refusal reason. Builder-style; only the Linux
559    /// cascade's broker-refused path calls it.
560    #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
561    pub(crate) fn with_broker_error(mut self, broker_error: BrokerError) -> Self {
562        self.broker_error = Some(broker_error);
563
564        self
565    }
566
567    /// `true` when the request did NOT get a clean grant of exactly what was
568    /// asked - a fall back to a weaker level (`Highest` -> `Normal`) *or* a
569    /// broker clamp within the level (`TimeCritical` -> nice -15). Equivalent to
570    /// `reason.is_some()`; the [`FallbackReason`] says which.
571    #[must_use]
572    pub fn degraded(&self) -> bool {
573        self.reason.is_some()
574    }
575}
576
577#[cfg(feature = "serde")]
578impl<'de> serde::Deserialize<'de> for AppliedPriority {
579    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
580    where
581        D: serde::Deserializer<'de>,
582    {
583        #[derive(serde::Deserialize)]
584        struct Parts {
585            requested: ThreadPriority,
586            effective: ThreadPriority,
587            grant: Grant,
588            reason: Option<FallbackReason>,
589            mechanism: Mechanism,
590            broker_error: Option<BrokerError>,
591        }
592
593        let parts = Parts::deserialize(deserializer)?;
594
595        AppliedPriority::from_parts(
596            parts.requested,
597            parts.effective,
598            parts.grant,
599            parts.reason,
600            parts.mechanism,
601            parts.broker_error,
602        )
603        .ok_or_else(|| serde::de::Error::custom("broker_error requires reason BrokerRefused"))
604    }
605}
606
607impl std::fmt::Display for AppliedPriority {
608    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
609        if self.requested != self.effective {
610            write!(f, "{} -> {}", self.requested, self.effective)?;
611
612            if self.grant != Grant::Direct || self.reason.is_some() {
613                write!(f, " [")?;
614
615                let mut sep = "";
616
617                if self.grant != Grant::Direct {
618                    write!(f, "{}", self.grant)?;
619                    sep = ", ";
620                }
621
622                if let Some(reason) = self.reason {
623                    write!(f, "{sep}{reason:?}")?;
624                }
625
626                write!(f, "]")?;
627            }
628        } else {
629            write!(f, "{}", self.effective)?;
630
631            if self.grant != Grant::Direct || self.reason.is_some() {
632                write!(f, " [")?;
633
634                let mut sep = "";
635
636                if self.grant != Grant::Direct {
637                    write!(f, "{}", self.grant)?;
638                    sep = ", ";
639                }
640
641                if let Some(reason) = self.reason {
642                    write!(f, "{sep}{reason:?}")?;
643                }
644
645                write!(f, "]")?;
646            }
647        }
648
649        if let Some(broker_error) = self.broker_error {
650            write!(f, " ({broker_error})")?;
651        }
652
653        write!(f, " {}", self.mechanism)
654    }
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660
661    #[test]
662    fn thread_priority_ordinals_are_stable() {
663        assert_eq!(ThreadPriority::Background as u8, 0);
664        assert_eq!(ThreadPriority::Lowest as u8, 1);
665        assert_eq!(ThreadPriority::BelowNormal as u8, 2);
666        assert_eq!(ThreadPriority::Normal as u8, 3);
667        assert_eq!(ThreadPriority::AboveNormal as u8, 4);
668        assert_eq!(ThreadPriority::Highest as u8, 5);
669        assert_eq!(ThreadPriority::TimeCritical as u8, 6);
670    }
671
672    #[test]
673    fn broker_error_maps_known_dbus_names() {
674        assert_eq!(
675            BrokerError::from_dbus_name("org.freedesktop.DBus.Error.AccessDenied"),
676            BrokerError::AccessDenied
677        );
678        assert_eq!(
679            BrokerError::from_dbus_name("org.freedesktop.DBus.Error.LimitsExceeded"),
680            BrokerError::LimitsExceeded
681        );
682        assert_eq!(
683            BrokerError::from_dbus_name("org.freedesktop.DBus.Error.InvalidArgs"),
684            BrokerError::InvalidArgs
685        );
686        assert_eq!(
687            BrokerError::from_dbus_name("org.freedesktop.DBus.Error.Failed"),
688            BrokerError::Failed
689        );
690    }
691
692    #[test]
693    fn broker_error_unmapped_name_is_other() {
694        assert_eq!(
695            BrokerError::from_dbus_name("org.freedesktop.RealtimeKit1.Error.Whatever"),
696            BrokerError::Other
697        );
698        assert_eq!(BrokerError::from_dbus_name(""), BrokerError::Other);
699    }
700
701    #[test]
702    fn mechanism_display_renders_per_policy() {
703        assert_eq!(
704            Mechanism {
705                policy: MechanismPolicy::Nice,
706                value: -15
707            }
708            .to_string(),
709            "nice -15"
710        );
711        assert_eq!(
712            Mechanism {
713                policy: MechanismPolicy::SchedRr,
714                value: 47
715            }
716            .to_string(),
717            "SCHED_RR 47"
718        );
719        assert_eq!(
720            Mechanism {
721                policy: MechanismPolicy::Qos,
722                value: QosClass::UserInteractive as i8
723            }
724            .to_string(),
725            "QoS UserInteractive"
726        );
727        assert_eq!(
728            Mechanism {
729                policy: MechanismPolicy::WinPriority,
730                value: 2
731            }
732            .to_string(),
733            "THREAD_PRIORITY 2"
734        );
735    }
736
737    #[test]
738    fn applied_priority_display_appends_mechanism() {
739        // Clean direct grant: the effective level, then the mechanism.
740        let clean = AppliedPriority::new(
741            ThreadPriority::Normal,
742            ThreadPriority::Normal,
743            Grant::Direct,
744            Mechanism {
745                policy: MechanismPolicy::Nice,
746                value: 0,
747            },
748        );
749        assert_eq!(clean.to_string(), "Normal nice 0");
750
751        let brokered = AppliedPriority::new(
752            ThreadPriority::Highest,
753            ThreadPriority::Highest,
754            Grant::Brokered,
755            Mechanism {
756                policy: MechanismPolicy::Nice,
757                value: -10,
758            },
759        );
760        assert_eq!(brokered.to_string(), "Highest [Brokered] nice -10");
761
762        // A clamp keeps the level, spells the loss out, then the kept mechanism.
763        let clamped = AppliedPriority::new(
764            ThreadPriority::TimeCritical,
765            ThreadPriority::TimeCritical,
766            Grant::Brokered,
767            Mechanism {
768                policy: MechanismPolicy::Nice,
769                value: -15,
770            },
771        )
772        .with_reason(FallbackReason::Clamped);
773        assert_eq!(
774            clamped.to_string(),
775            "TimeCritical [Brokered, Clamped] nice -15"
776        );
777    }
778}