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}