Skip to main content

detcore/scheduler/
runqueue.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! The main queue for runnable tasks.
10//!
11//! Tasks are selected from the queue based on 2 factors:
12//! 1. Their priority
13//! 2. Their round-robin order.
14//!
15//! The factors are compared in that order. Round-robin orders monotonically increase across
16//! the entire queue; a task is assigned an order at insertion time.
17//!
18//! Round-robin orders can also be negative when the `push_front` method is
19//! used. This "skips the line" within the priority level. This is mostly
20//! relevant in non-chaos modes, where all threads have the same priority. In
21//! this case, time-based events should skip the line and end-up in the front of
22//! the queue.
23//!
24//! In addition, some priority values are reserved, such as a high priority
25//! for IO eager polling.
26//!
27//! # Polling strategy
28//!
29//! We employ a polling strategy for guest threads that *would* blocked, but where we
30//! don't model precisely what conditions they're waiting for.  Anywhere we have a precise
31//! model of inter-thread dependencies (e.g. futexes), we can sleep a thread until we
32//! encounter the matching event that will wake it.  But for Linux features that we don't
33//! model 100% precisely, polling is a way to remain agnostic as to the exact
34//! dependencies, but still support these blocking behaviors deterministically.
35//!
36//! The older DetTrace system used polling, but it would poll every time through the
37//! round robin queue, which can create extremely bad performance with many threads
38//! polling an unbounded number of times.  We can greatly improve the performance by
39//! polling only at less-frequent, but still deterministically-defined intervals, such
40//! as when we think we're out of "productive" work to do.
41//!
42//! Thererefore we have special handling for scheduling polling tasks. When
43//! initially queued, there is exponential backoff in priority with the number
44//! of attempts. After enough queueing operations are performed, however,
45//! polling tasks are upgraded to their original priority to prevent complete
46//! starvation. The frequency of upgrades is controlled by `POLLING_UPGRADE_INTERVAL`,
47//! and the scheduler also runs one when none has run for a poll-upgrade period
48//! of virtual time, at least 200 ms (`Scheduler::upgrade_pollers_on_time`).
49//!
50//! An upgraded poller competes as ordinary runnable work, which under chaos
51//! includes the bound on how often a change point may pass it over
52//! (`Scheduler::bound_chaos_starvation`). A poller whose normal priority is
53//! the last band already sits there, so its upgrade cannot move it; it
54//! records the upgrade instead (`QueueValue::upgraded_in_place`), which
55//! gives it the same protection from starvation while the
56//! deterministic-work-first gates keep counting it as a poller.
57
58use std::cmp::Ordering;
59use std::collections::BTreeMap;
60use std::collections::BTreeSet;
61use std::fmt;
62use std::fmt::Display;
63
64use rand::RngExt as _;
65use rand::SeedableRng;
66use rand::distr::uniform::SampleUniform;
67use rand_pcg::Pcg64Mcg;
68
69use crate::config::SchedHeuristic;
70use crate::detlog;
71use crate::types::DetTid;
72
73/// The user-accessible priority of a thread. Lowest runs first.
74pub type Priority = u64;
75
76const EAGER_IO_REPOLL_PRIORITY: Priority = Priority::MIN;
77
78/// The lowest/highest priority a thread can have.
79pub const FIRST_PRIORITY: Priority = EAGER_IO_REPOLL_PRIORITY + 1;
80
81/// The last/lowest (numerically largest) priority a thread can have.
82pub const LAST_PRIORITY: Priority = 10000;
83
84/// A high priority for threads the replayer DOES want to run.
85pub const REPLAY_FOREGROUND_PRIORITY: Priority = FIRST_PRIORITY;
86
87/// A low priority given to threads the replayer does NOT want to run.
88pub const REPLAY_DEFERRED_PRIORITY: Priority = LAST_PRIORITY - 1;
89
90/// The default priority for a thread. If chaos mode is not enabled, all threads have this
91/// priority.
92pub const DEFAULT_PRIORITY: Priority = 1000;
93
94/// Whether the priority is user-accessible. We use some values for special
95/// purposes; these shouldn't be set by the user.
96pub fn is_ordinary_priority(prio: Priority) -> bool {
97    (FIRST_PRIORITY..=LAST_PRIORITY).contains(&prio)
98}
99
100/// Deterministically transform 64 bits of entropy into a random user-settable
101/// priority.
102pub fn entropy_to_priority(entropy: u64) -> Priority {
103    let range = LAST_PRIORITY - FIRST_PRIORITY + 1;
104    let offset = entropy % range;
105    FIRST_PRIORITY + offset
106}
107
108/// How many chaos `sched_yield` turns that pass another runnable thread over
109/// a thread may take, since the yield epoch last ended, before its next one
110/// joins the epoch ([`RunQueue::push_yielded_into_epoch`]). One plain yield
111/// keeps the schedules in which a yielder runs again before a thread that
112/// just became runnable (a futex waiter woken for a signal, for example);
113/// joining the epoch at the second bounds how long a spinner can hold a later
114/// band off (<https://github.com/rrnewton/hermit/issues/4068>).
115pub const PLAIN_YIELDS_BEFORE_EPOCH: u32 = 1;
116
117/// The round robin turn of threads within a given priority level. Lowest runs
118/// first. Both negative and positive values are used to allow insertion of a
119/// thread at both the "front" and "back" of a priority level.
120type RoundRobinTurn = i64;
121
122/// The key into the priority queue that uniquely determines what to run next.
123/// Priorities that compare lower run first.
124#[derive(Debug, Copy, Clone)]
125pub struct PrioritizedOrder {
126    priority: Priority,
127    turn: RoundRobinTurn,
128}
129
130// These match the derived definitions, but clearly display our intention:
131impl Ord for PrioritizedOrder {
132    fn cmp(&self, other: &Self) -> Ordering {
133        self.priority
134            .cmp(&other.priority)
135            .then(self.turn.cmp(&other.turn))
136    }
137}
138impl PartialOrd for PrioritizedOrder {
139    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
140        Some(self.cmp(other))
141    }
142}
143impl PartialEq for PrioritizedOrder {
144    fn eq(&self, other: &Self) -> bool {
145        self.cmp(other).is_eq()
146    }
147}
148impl Eq for PrioritizedOrder {}
149
150impl fmt::Display for PrioritizedOrder {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        write!(f, "(p: {}, t: {})", self.priority, self.turn)
153    }
154}
155
156/// The outcome of [`RunQueue::restore_poller_priority`].
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum PollerRestore {
159    /// The thread was queued as a backed-off poller and now has its normal priority.
160    Restored,
161    /// The thread is queued, but not backed off: a poll upgrade or a priority
162    /// change has already restored it.
163    NotBackedOff,
164    /// The thread is not in the run queue.
165    NotQueued,
166}
167
168/// The first queue turn of an entry ordered by run time
169/// ([`RunQueue::order_by_run_time`]), far above any turn `push_back` hands out.
170const RUN_TIME_TURNS: RoundRobinTurn = 1 << 61;
171
172/// After queueing this many new tasks, perform a "poll upgrade," in which
173/// we upgrade all outstanding polling tasks their original priority levels,
174/// temporarily negating backoff behavior.
175pub(crate) const POLLING_UPGRADE_INTERVAL: u64 = 200;
176
177#[derive(Debug, Copy, Clone)]
178struct QueueValue {
179    tid: DetTid,
180    /// Upgrade to this priority during polling upgrades
181    poll_upgrade: Option<Priority>,
182    /// Set when `restore_poller_priority` restored this poller, and the
183    /// poller was at `LAST_PRIORITY` while backed off. It has two callers:
184    /// the poller's timeout deadline passed, or, under
185    /// `--pipe-wake-latency`, another thread's action readied its channel
186    /// (`Scheduler::ready_channel`,
187    /// <https://github.com/rrnewton/hermit/issues/4380>). Until it runs, the deterministic-work-first gates
188    /// (`only_pollers`, `has_runnable_besides`) keep counting it as a poller,
189    /// exactly as they did while it was backed off. A poller backed off below
190    /// `LAST_PRIORITY` (after 1 to 3 retries) was already ordinary work for
191    /// the gates, and stays so
192    /// (<https://github.com/rrnewton/hermit/issues/3952>).
193    deadline_restored: bool,
194    /// Set by a poll upgrade that reached this entry but could not move it:
195    /// a backed-off poller, or a deadline-restored one, whose normal priority
196    /// is the last band, where it already sits. Every other poller a poll
197    /// upgrade reaches becomes ordinary work, which a chaos change point can
198    /// pass over only a bounded number of times
199    /// (`Scheduler::bound_chaos_starvation`). Without this mark such a poller
200    /// was never upgraded and never counted, so two threads that sleep in
201    /// turns at better priorities kept it from its next retry forever, even
202    /// after another thread wrote what it polled for. It stays a poller for
203    /// the deterministic-work-first gates (`counts_as_poller`) and the yield
204    /// epoch, as the upgrade leaves it in the last band; it only joins the
205    /// change-point bound (`RunQueue::can_be_passed_over`). A requeue makes a
206    /// fresh entry without it, so it lasts until the poller's next retry;
207    /// `suspend`/`restore` keep the entry, and the mark with it.
208    upgraded_in_place: bool,
209}
210
211impl QueueValue {
212    /// Whether this entry is a thread that will run its turn, not a polling
213    /// thread retrying a call that is not ready: neither a backed-off poller
214    /// (`push_poller`) nor one a deadline restored and that has not run since.
215    fn is_non_polling(&self) -> bool {
216        self.poll_upgrade.is_none() && !self.deadline_restored
217    }
218
219    /// Whether this entry counts as a poller for the deterministic-work-first
220    /// gates, given its queue priority: a polling thread whose backoff has
221    /// reached the last band, or one `restore_poller_priority` restored from
222    /// it. The band alone does not make a poller: under chaos, or a
223    /// scheduling heuristic, an ordinary runnable thread can sit at
224    /// `LAST_PRIORITY`, and counting it as a poller let run mode admit a
225    /// host-timed completion between its turns
226    /// (<https://github.com/rrnewton/hermit/issues/4234>).
227    fn counts_as_poller(&self, priority: Priority) -> bool {
228        self.is_fully_backed_off_poller(priority) || self.deadline_restored
229    }
230
231    /// Whether this entry is a polling thread whose backoff has reached the
232    /// last band ([`RunQueue::push_poller`]): only then does it stop holding a
233    /// yield epoch open.
234    fn is_fully_backed_off_poller(&self, priority: Priority) -> bool {
235        self.poll_upgrade.is_some() && priority >= LAST_PRIORITY
236    }
237}
238
239/// One suspended queue entry. Global yield/random-selection state stays live.
240#[derive(Debug, Clone)]
241pub(super) struct SuspendedRunQueueEntry {
242    key: PrioritizedOrder,
243    value: QueueValue,
244    persistent_priority: Priority,
245}
246
247#[derive(Debug, Clone)]
248pub struct RunQueue {
249    /// We use a "flattened" queue (rather than a Priority -> Vec<DetTid> map)
250    /// to simplify peek/pop logic: there's no need to ignore clear empty
251    /// from unused priority levels vectors. This could also reduce allocator
252    /// pressure. Also, each thread having a clear global key makes it easier to
253    /// change their priorities after they are in the queue.
254    ///
255    /// Additionally, we use a TreeMap rather than a Heap to ease removing /
256    /// inserting random values for poll upgrades. std::BinaryHeap would require
257    /// destroying/re-allocating the entire structure to do this.
258    queue: BTreeMap<PrioritizedOrder, QueueValue>,
259
260    // We use global turn counters across all priority levels. This foregoes
261    // the need for an extra data structure to track them, while also ensuring
262    // unique keys for every insertion. Because of this, we never need to alter
263    // a turn value when altering the priority level of a thread.
264    last_back_turn: RoundRobinTurn,
265    last_front_turn: RoundRobinTurn,
266
267    /// How many poll upgrades have run, from the queueing-operation interval
268    /// or [`Self::upgrade_pollers`], so the scheduler can tell how long ago
269    /// the last one was.
270    poll_upgrades: u64,
271
272    /// Used to lock the queue from other changes while we are tentatively popping from it, and also
273    /// cache the result.
274    tentative_selection: Option<DetTid>,
275    tentative_selection_is_exact: bool,
276
277    /// A thread that explicitly yielded must not be selected again until some
278    /// other runnable thread receives a turn.
279    yielded_skip: Option<DetTid>,
280
281    /// The threads whose chaos `sched_yield` streak passed
282    /// [`PLAIN_YIELDS_BEFORE_EPOCH`] since the last turn of a thread that did
283    /// not yield ([`Self::push_yielded_into_epoch`]). While any queued thread
284    /// outside the set is not a fully backed-off poller, selection skips its
285    /// members. A turn
286    /// in which a thread that did not yield ran empties it
287    /// ([`Self::end_yield_epoch`]): a completed guest turn (a SaBRe loopback
288    /// poll's yield included), a background grant and a priority change point.
289    /// A poller's non-committing backoff does not. Covering every queued
290    /// thread empties it too, as does leaving only fully backed-off pollers
291    /// outside it, and a
292    /// thread leaves it when it leaves the queue. Bands and queue keys are
293    /// untouched.
294    yield_epoch: Option<BTreeSet<DetTid>>,
295    /// Per thread, the chaos `sched_yield` turns since the yield epoch last
296    /// ended that were taken while another thread was queued, so that the
297    /// yield passed it over. The first
298    /// [`PLAIN_YIELDS_BEFORE_EPOCH`] are plain requeues; the next joins the
299    /// epoch.
300    yield_streaks: BTreeMap<DetTid, u32>,
301    /// Every queued thread that is queued because it yielded (`push_yielded`,
302    /// or `push_yielded_into_epoch` under chaos) and has not run since. Unlike
303    /// `yielded_skip`, which only the latest yielder holds, this keeps every
304    /// one of several threads spinning on `sched_yield`
305    /// (`only_pollers_or_yielders`).
306    queued_by_yield: BTreeSet<DetTid>,
307
308    // TODO: The following fields need to be properly abstracted into separate types of run queues.
309    /// Which scheduling strategy shall we use.
310    sched_strategy: SchedHeuristic,
311    prng: Pcg64Mcg,
312
313    sticky_random_param: f64,
314    sticky_random_selection: Option<DetTid>,
315}
316
317/// A multi-line print of the runqueue.
318impl fmt::Display for RunQueue {
319    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320        writeln!(
321            f,
322            "Run queue, size={}, last_back_turn={}, last_front_turn={}:",
323            self.queue.len(),
324            self.last_back_turn,
325            self.last_front_turn,
326        )?;
327        for x in self.queue.iter() {
328            writeln!(f, "    {:.500?}", x)?;
329        }
330        Ok(())
331    }
332}
333
334impl RunQueue {
335    /// Create a new RunQueue.
336    pub fn new(ss: SchedHeuristic, seed: u64, srp: f64) -> Self {
337        detlog!("SCHEDRAND: seeding scheduler runqueue with seed {}", seed);
338        Self {
339            queue: BTreeMap::new(),
340            // For clarity, 0 is unused so that positive/negative == back/front:
341            last_back_turn: 0,
342            last_front_turn: 0,
343            poll_upgrades: 0,
344            sched_strategy: ss,
345            tentative_selection: None,
346            tentative_selection_is_exact: false,
347            yielded_skip: None,
348            yield_epoch: None,
349            yield_streaks: BTreeMap::new(),
350            queued_by_yield: BTreeSet::new(),
351            prng: Pcg64Mcg::seed_from_u64(seed),
352            sticky_random_param: srp,
353            sticky_random_selection: None,
354        }
355    }
356
357    fn push_safety_check(&self, tid: DetTid) {
358        if cfg!(debug_assertions) {
359            // Expensive.
360            for qv in self.queue.values() {
361                if qv.tid == tid {
362                    panic!(
363                        "Invariant violation! Tried to add {} to runqueue, but it's already present:\n {:?}",
364                        tid, self
365                    );
366                }
367            }
368        }
369    }
370
371    /// [`Self::has_runnable_besides`], with the queued threads for which
372    /// `passed_over` holds not counted as work either. Read-only.
373    pub fn has_runnable_besides_unless(
374        &self,
375        exclude: DetTid,
376        passed_over: impl Fn(DetTid) -> bool,
377    ) -> bool {
378        self.queue.iter().any(|(k, v)| {
379            v.tid != exclude && !v.counts_as_poller(k.priority) && !passed_over(v.tid)
380        })
381    }
382
383    /// True if any thread other than `exclude` is runnable at ordinary
384    /// (non-poller) priority, where a thread `restore_poller_priority` restored
385    /// still counts as a poller until it runs. This is the "deterministic work still runnable"
386    /// test used to decide whether an asynchronous signal delivery must defer to
387    /// guest work that was already scheduled. Read-only, so it is safe to call
388    /// while a tentative_pop selection is in progress.
389    pub fn has_runnable_besides(&self, exclude: DetTid) -> bool {
390        self.queue
391            .iter()
392            .any(|(k, v)| v.tid != exclude && !v.counts_as_poller(k.priority))
393    }
394
395    /// True when every queued thread is a poller (or the queue is empty): the
396    /// "no deterministic work is runnable" test of the deterministic-work-first
397    /// gates. A poller is a polling entry (`push_poller`) whose backoff has
398    /// reached `LAST_PRIORITY`, or one that `restore_poller_priority` restored
399    /// and that has not run since; an ordinary thread at `LAST_PRIORITY` is
400    /// work (<https://github.com/rrnewton/hermit/issues/4234>). Read-only.
401    pub fn only_pollers(&self) -> bool {
402        self.queue
403            .iter()
404            .all(|(k, v)| v.counts_as_poller(k.priority))
405    }
406
407    /// `only_pollers`, also counting every thread queued because it yielded
408    /// and not run since (`queued_by_yield`), by either requeue. A thread that calls
409    /// `sched_yield` asks for another thread to run first, so, like a poller,
410    /// it is not work that a finished background call must wait behind.
411    /// Counting it as such work starved the background call forever whenever
412    /// a sibling spun on `sched_yield`
413    /// (<https://github.com/rrnewton/hermit/issues/4092>), and counting all but
414    /// the latest yielder did the same for two spinners, which keep clearing
415    /// each other's `yielded_skip` (Claude review of
416    /// <https://github.com/rrnewton/hermit/pull/4120>). Read-only.
417    pub fn only_pollers_or_yielders(&self) -> bool {
418        self.queue
419            .iter()
420            .all(|(k, v)| v.counts_as_poller(k.priority) || self.queued_by_yield.contains(&v.tid))
421    }
422
423    /// True while a `tentative_pop`/commit transaction is underway, i.e. the
424    /// daemon has peeked a selection and may have released the scheduler lock
425    /// across an await. Fatal backend publication uses this read-only query to
426    /// close the transaction once before waking consuming cleanup. Ordinary
427    /// run-queue admissions and removals are always buffered and
428    /// applied at the deterministic `step2` drain (window guaranteed closed), so
429    /// a handler never needs to ask whether a window is live — see
430    /// `Scheduler::admit_to_run_queue` / `deschedule_or_defer`. Read-only.
431    pub fn tentative_pop_in_progress(&self) -> bool {
432        self.tentative_selection.is_some()
433    }
434
435    /// Push a thread to the back of the specified priority. Return the
436    /// resulting overall position in the queue.
437    ///
438    /// Mutating operation: this will error if a tentative_pop/commit transaction is underway.
439    pub fn push_back(&mut self, tid: DetTid, priority: Priority) -> PrioritizedOrder {
440        assert!(self.tentative_selection.is_none());
441        self.push_safety_check(tid);
442        if !is_ordinary_priority(priority) {
443            panic!("This is not an acceptable priority value: {}", priority);
444        }
445        self.push_back_inner(tid, priority, None)
446    }
447
448    /// Requeue an explicitly yielding thread at its persistent priority while
449    /// excluding it from the next selection. The exclusion, rather than the
450    /// queue key, makes this a one-turn operation under every heuristic.
451    pub fn push_yielded(&mut self, tid: DetTid, priority: Priority) -> PrioritizedOrder {
452        assert!(self.yielded_skip.is_none());
453        self.yielded_skip = Some(tid);
454        self.queued_by_yield.insert(tid);
455        self.push_back(tid, priority)
456    }
457
458    /// Requeue a thread that called `sched_yield` under chaos at its own
459    /// priority. If another thread is queued, the yield passes it over and
460    /// counts toward the thread's streak; once the streak
461    /// exceeds [`PLAIN_YIELDS_BEFORE_EPOCH`], the thread joins the epoch,
462    /// beginning one if none is open. A yield made with no one else to run
463    /// does not count, so a thread that spins alone has no streak when another
464    /// becomes runnable. Its turn must have been committed with
465    /// [`Self::commit_tentative_pop_yield`]. Unlike [`Self::push_yielded`], the
466    /// epoch lasts until a thread that did not yield has a turn, however many
467    /// yielders take turns in between, and it changes neither the thread's
468    /// band nor its queue key.
469    pub fn push_yielded_into_epoch(&mut self, tid: DetTid, priority: Priority) -> PrioritizedOrder {
470        // Any other queued thread counts, an epoch member included: a thread
471        // left outside an epoch of yielders must be able to join it, or it
472        // would spin ahead of them forever.
473        let skipped_someone = self.queue.values().any(|value| value.tid != tid);
474        if skipped_someone {
475            let streak = self.yield_streaks.entry(tid).or_insert(0);
476            *streak += 1;
477            if *streak > PLAIN_YIELDS_BEFORE_EPOCH {
478                self.yield_epoch
479                    .get_or_insert_with(BTreeSet::new)
480                    .insert(tid);
481            }
482        }
483        self.queued_by_yield.insert(tid);
484        self.push_back(tid, priority)
485    }
486
487    /// Push a polling thread. The priority level is an exponential backoff from
488    /// the given `normal_priority` value. The pushed thread will also
489    /// participatein "poll upgrades" in which periodically polling threads are
490    /// re-boosted to their original `normal_priority` values.
491    ///
492    /// Mutating operation: this will error if a tentative_pop/commit transaction is underway.
493    pub fn push_poller(
494        &mut self,
495        tid: DetTid,
496        normal_priority: Priority,
497        poll_attempt: u32,
498    ) -> PrioritizedOrder {
499        assert!(self.tentative_selection.is_none());
500        self.push_safety_check(tid);
501        // Exponential backoff in priority, up to LAST_PRIORITY:
502        let priority = 1u64
503            .checked_shl(poll_attempt)
504            .and_then(|f| f.checked_mul(normal_priority))
505            .unwrap_or(Priority::MAX)
506            .min(LAST_PRIORITY);
507        // Upgrade back to original priority:
508        self.push_back_inner(tid, priority, Some(normal_priority))
509    }
510
511    fn push_back_inner(
512        &mut self,
513        tid: DetTid,
514        priority: Priority,
515        poll_upgrade: Option<Priority>,
516    ) -> PrioritizedOrder {
517        self.last_back_turn += 1;
518        let turn = self.last_back_turn;
519        let prio = PrioritizedOrder { priority, turn };
520        self.push_inner(tid, prio, poll_upgrade)
521    }
522
523    /// Push a thread to the front of the specified priority. `push_back` should
524    /// be used unless special circumstances call for `push_front`. Return the
525    /// resulting overall position in the queue.
526    ///
527    /// Mutating operation: this will error if a tentative_pop/commit transaction is underway.
528    pub fn push_front(&mut self, tid: DetTid, priority: Priority) -> PrioritizedOrder {
529        assert!(self.tentative_selection.is_none());
530        self.push_safety_check(tid);
531        assert!(is_ordinary_priority(priority));
532        self.push_front_inner(tid, priority, None)
533    }
534
535    /// Workaround for eager io repolling: this will send the thread to the
536    /// absolute front of the queue. Return the resulting overall position in
537    /// the queue.
538    ///
539    /// Mutating operation: this will error if a tentative_pop/commit transaction is underway.
540    pub fn push_eager_io_repoll(&mut self, tid: DetTid) -> PrioritizedOrder {
541        assert!(self.tentative_selection.is_none());
542        self.push_safety_check(tid);
543        let priority = EAGER_IO_REPOLL_PRIORITY;
544        self.push_front_inner(tid, priority, None)
545    }
546
547    fn push_front_inner(
548        &mut self,
549        tid: DetTid,
550        priority: Priority,
551        poll_upgrade: Option<Priority>,
552    ) -> PrioritizedOrder {
553        self.last_front_turn -= 1;
554        let turn = self.last_front_turn;
555        let prio = PrioritizedOrder { priority, turn };
556        self.push_inner(tid, prio, poll_upgrade)
557    }
558
559    fn push_inner(
560        &mut self,
561        tid: DetTid,
562        prio: PrioritizedOrder,
563        poll_upgrade: Option<Priority>,
564    ) -> PrioritizedOrder {
565        let qval = QueueValue {
566            tid,
567            poll_upgrade,
568            deadline_restored: false,
569            upgraded_in_place: false,
570        };
571        let old = self.queue.insert(prio, qval);
572        assert!(old.is_none()); // last_*_turn should be monotonic
573        self.check_poll_upgrade();
574        prio
575    }
576
577    /// Read-only: this is ok while locked by tentative_pop.
578    pub fn is_empty(&self) -> bool {
579        self.queue.is_empty()
580    }
581
582    /// Read-only: this is ok while locked by tentative_pop.
583    pub fn len(&self) -> usize {
584        self.queue.len()
585    }
586
587    /// Read-only: this is ok while locked by tentative_pop.
588    pub fn tids(&self) -> impl Iterator<Item = &DetTid> {
589        self.queue.values().map(|v| &v.tid)
590    }
591
592    /// Read-only: this is ok while locked by tentative_pop.
593    pub fn contains_tid(&self, tid: DetTid) -> bool {
594        self.tids().any(|t| t == &tid)
595    }
596
597    /// Whether `tid` is queued as a thread that will run its turn, not as a
598    /// polling thread retrying a call that is not ready: one pushed by
599    /// [`RunQueue::push_poller`] (which records a polling upgrade) or restored
600    /// as a poller and not run since. Its priority band does not decide it: a
601    /// runnable thread can sit in the last band, as under
602    /// `--chaos-target-races`. Read-only.
603    pub fn contains_non_polling_tid(&self, tid: DetTid) -> bool {
604        self.queue
605            .iter()
606            .any(|(_, v)| v.tid == tid && v.is_non_polling())
607    }
608
609    /// Whether `tid` is queued as work that a chaos change point can pass
610    /// over (`Scheduler::bound_chaos_starvation`): a thread that will run its
611    /// turn ([`Self::contains_non_polling_tid`]), which includes every poller
612    /// a poll upgrade has moved back to its normal priority, or a poller the
613    /// upgrade reached in the last band, where it could not move
614    /// (`QueueValue::upgraded_in_place`). A poller still backed off is not:
615    /// its backoff, not the bound, decides when it retries. Read-only.
616    pub fn can_be_passed_over(&self, tid: DetTid) -> bool {
617        self.queue
618            .values()
619            .any(|v| v.tid == tid && (v.is_non_polling() || v.upgraded_in_place))
620    }
621
622    pub(super) fn suspend(
623        &mut self,
624        tid: DetTid,
625        persistent_priority: Priority,
626    ) -> Option<SuspendedRunQueueEntry> {
627        assert!(self.tentative_selection.is_none());
628        let key = *self.queue.iter().find(|(_, v)| v.tid == tid)?.0;
629        let value = self.queue.remove(&key).expect("located queue entry");
630        Some(SuspendedRunQueueEntry {
631            key,
632            value,
633            persistent_priority,
634        })
635    }
636
637    pub(super) fn restore(
638        &mut self,
639        mut entry: SuspendedRunQueueEntry,
640        current_priority: Priority,
641    ) {
642        assert!(self.tentative_selection.is_none());
643        assert!(!self.contains_tid(entry.value.tid));
644        if entry.persistent_priority != current_priority {
645            entry.key.priority = current_priority;
646            if entry.value.poll_upgrade.is_some() {
647                entry.value.poll_upgrade = Some(current_priority);
648            }
649        }
650        // An entry ordered by run time may find its old turn taken: the order
651        // hands those turns out again at every selection. It takes the next
652        // free one, and the next selection orders it again.
653        if entry.key.turn >= RUN_TIME_TURNS {
654            while self.queue.contains_key(&entry.key) {
655                entry.key.turn += 1;
656            }
657        }
658        assert!(self.queue.insert(entry.key, entry.value).is_none());
659    }
660
661    /// Whether an entry is in round-robin order at an ordinary band: neither
662    /// pushed to the front of its band (a time event, an eager readmission)
663    /// nor a poller, backed off or restored from its backoff. These are the
664    /// entries [`Self::order_by_run_time`] orders.
665    fn in_round_robin_order(key: &PrioritizedOrder, value: &QueueValue) -> bool {
666        key.turn >= 0
667            && key.priority != EAGER_IO_REPOLL_PRIORITY
668            && value.poll_upgrade.is_none()
669            && !value.deadline_restored
670    }
671
672    /// The threads queued in round-robin order at an ordinary band, in
673    /// thread order ([`Self::order_by_run_time`]). Read-only.
674    pub(super) fn round_robin_tids(&self) -> Vec<DetTid> {
675        let mut tids: Vec<DetTid> = self
676            .queue
677            .iter()
678            .filter(|(key, value)| Self::in_round_robin_order(key, value))
679            .map(|(_, value)| value.tid)
680            .collect();
681        tids.sort();
682        tids
683    }
684
685    /// Order the round-robin entries of `order`'s threads within their bands
686    /// by their sort keys, least first, and equal keys by thread order. Their
687    /// queue turns move to a range far above any turn `push_back` hands out,
688    /// so they never meet one; the entries this leaves alone (pushed to the
689    /// front, pollers, pollers restored from their backoff) keep their turns,
690    /// which are ahead of every ordered entry of their band.
691    pub(super) fn order_by_run_time(&mut self, order: &[(DetTid, (u64, u64))]) {
692        assert!(self.tentative_selection.is_none());
693        let wanted: BTreeMap<DetTid, (u64, u64)> = order.iter().copied().collect();
694        let keys: Vec<PrioritizedOrder> = self
695            .queue
696            .iter()
697            .filter(|(key, value)| {
698                wanted.contains_key(&value.tid) && Self::in_round_robin_order(key, value)
699            })
700            .map(|(key, _)| *key)
701            .collect();
702        let mut moved: Vec<(Priority, (u64, u64), QueueValue)> = keys
703            .into_iter()
704            .map(|key| {
705                let value = self.queue.remove(&key).expect("located queue entry");
706                (key.priority, wanted[&value.tid], value)
707            })
708            .collect();
709        moved.sort_by_key(|(priority, sort_key, value)| (*priority, *sort_key, value.tid));
710        // Every ordered entry of these bands was taken out, and they go back
711        // in order, so each takes its run time's turn or the turn after the
712        // previous one in its band, whichever is later.
713        let mut last: Option<PrioritizedOrder> = None;
714        for (priority, (run_time, _), value) in moved {
715            let offset = RoundRobinTurn::try_from(run_time).unwrap_or(RoundRobinTurn::MAX);
716            let mut turn = RUN_TIME_TURNS.saturating_add(offset.min(RUN_TIME_TURNS));
717            if let Some(previous) = last
718                && previous.priority == priority
719            {
720                turn = turn.max(previous.turn + 1);
721            }
722            let mut key = PrioritizedOrder { priority, turn };
723            while self.queue.contains_key(&key) {
724                key.turn += 1;
725            }
726            assert!(self.queue.insert(key, value).is_none());
727            last = Some(key);
728        }
729    }
730
731    /// Remove `tid` from the queue, returning true if removal ocurred.
732    /// Mutating operation: this will error if a tentative_pop/commit transaction is underway.
733    pub fn remove_tid(&mut self, tid: DetTid) -> bool {
734        assert!(self.tentative_selection.is_none());
735
736        // This is O(N), but could be faster if we also stored a thread -> priority mapping.
737        let mut kept_all = true;
738        self.queue.retain(|_k, v| {
739            let ret = v.tid != tid;
740            kept_all = kept_all && ret;
741            ret
742        });
743        if self.yielded_skip == Some(tid) {
744            self.yielded_skip = None;
745        }
746        if let Some(epoch) = &mut self.yield_epoch {
747            epoch.remove(&tid);
748        }
749        self.yield_streaks.remove(&tid);
750        self.queued_by_yield.remove(&tid);
751        // A successful nonleader exec can remove one thread incarnation and
752        // admit its replacement under the same raw TID in a single scheduler
753        // drain. Do not let the replacement inherit the destroyed leader's
754        // sticky-random selection.
755        if self.sticky_random_selection == Some(tid) {
756            self.sticky_random_selection = None;
757        }
758        !kept_all
759    }
760
761    // Helper function for logging purposes.
762    fn random_range<T>(&mut self, start: T, end: T) -> T
763    where
764        T: SampleUniform + Display + PartialOrd + Copy,
765    {
766        let r = self.prng.random_range(start..end);
767        detlog!("SCHEDRAND: [{},{}) => {}", start, end, r);
768        r
769    }
770
771    /// Begin, but do not complete, a pop_transaction.  This can be committed or undone later.  But
772    /// one of those must happen before other modification operations can occur on the RunQueue.
773    ///
774    /// Postcondition: if return a `Some` value, the RunQueue enters a *locked* state where
775    /// commit or undo must happen before any other mutations to the structure.
776    pub fn tentative_pop_next(&mut self) -> Option<DetTid> {
777        assert!(!self.tentative_selection_is_exact);
778        let skip = self
779            .yielded_skip
780            .filter(|tid| self.queue.len() > 1 && self.contains_tid(*tid));
781        // A yield epoch is over when it covers every queued thread, or when
782        // every queued thread outside it is a poller backed off all the way:
783        // a poller's backoff turns do not commit, so do not end the epoch, and
784        // one waiting on a yielder must not hold the yielders off forever. A
785        // thread merely in the last band (as `--chaos-target-races` places
786        // half of all new threads) is not a poller.
787        if self.yield_epoch.as_ref().is_some_and(|epoch| {
788            self.queue.iter().all(|(key, value)| {
789                epoch.contains(&value.tid) || value.is_fully_backed_off_poller(key.priority)
790            })
791        }) {
792            self.end_yield_epoch();
793        }
794        let mut excluded: BTreeSet<DetTid> = skip.into_iter().collect();
795        if let Some(epoch) = &self.yield_epoch {
796            let with_epoch: BTreeSet<DetTid> = excluded.union(epoch).copied().collect();
797            // The epoch never covers every queued thread here, but a one-turn
798            // skip outside it could make up the rest: then only the skip holds.
799            if self
800                .queue
801                .values()
802                .any(|value| !with_epoch.contains(&value.tid))
803            {
804                excluded = with_epoch;
805            }
806        }
807        let excluded_count = self
808            .queue
809            .values()
810            .filter(|value| excluded.contains(&value.tid))
811            .count();
812        self.tentative_selection = match self.sched_strategy {
813            SchedHeuristic::None | SchedHeuristic::ConnectBind => self
814                .queue
815                .values()
816                .find(|value| !excluded.contains(&value.tid))
817                .map(|value| value.tid),
818            SchedHeuristic::Random => {
819                if self.queue.is_empty() {
820                    return None;
821                }
822
823                // If there is not Tid picked from a previous operation, let's pick one now.
824                if self.tentative_selection.is_none() {
825                    let eligible = self.queue.len() - excluded_count;
826                    let random_idx = self.random_range(0, eligible);
827                    self.tentative_selection = self
828                        .queue
829                        .values()
830                        .filter(|value| !excluded.contains(&value.tid))
831                        .nth(random_idx)
832                        .map(|value| value.tid);
833                };
834
835                self.tentative_selection
836            }
837            SchedHeuristic::StickyRandom => {
838                if self.queue.is_empty() {
839                    return None;
840                }
841
842                if self
843                    .sticky_random_selection
844                    .is_some_and(|tid| excluded.contains(&tid))
845                {
846                    self.sticky_random_selection = None;
847                }
848                if self.sticky_random_selection.is_none()
849                    || !self.contains_tid(self.sticky_random_selection.unwrap())
850                {
851                    let eligible = self.queue.len() - excluded_count;
852                    let random_idx = self.random_range(0, eligible);
853                    self.sticky_random_selection = self
854                        .queue
855                        .values()
856                        .filter(|value| !excluded.contains(&value.tid))
857                        .nth(random_idx)
858                        .map(|value| value.tid);
859                }
860
861                self.sticky_random_selection
862            }
863        };
864
865        self.tentative_selection
866    }
867
868    // TODO-HUMAN-REVIEW(PR-868): Review exact run-queue selection for vfork barriers.
869    /// Begin a pop transaction for one specific queued thread, bypassing the
870    /// configured scheduling heuristic without changing the thread's priority.
871    pub fn tentative_pop_tid(&mut self, tid: DetTid) -> Option<DetTid> {
872        assert!(self.tentative_selection.is_none());
873        if self.contains_tid(tid) {
874            self.tentative_selection = Some(tid);
875            self.tentative_selection_is_exact = true;
876        }
877        self.tentative_selection
878    }
879
880    /// Complete the tentative pop operation, readying the RunQueue for future operations.  This
881    /// operation is only permissible when the queue is locked, i.e. the tentative_pop has
882    /// previously returned `Some`.
883    /// Commit the queue transaction alone. It neither consumes the one-turn
884    /// exclusion nor ends the yield epoch: a poller's non-committing backoff
885    /// uses it, and a priority change point ends the epoch itself.
886    pub fn commit_tentative_pop(&mut self) -> DetTid {
887        self.commit_selection()
888    }
889
890    /// End the yield epoch: a thread that did not yield has had a turn.
891    pub fn end_yield_epoch(&mut self) {
892        self.yield_epoch = None;
893        self.yield_streaks.clear();
894    }
895
896    /// Commit the turn of a thread that called `sched_yield` under chaos,
897    /// keeping the yield epoch it is about to join
898    /// ([`Self::push_yielded_into_epoch`]).
899    pub fn commit_tentative_pop_yield(&mut self) -> DetTid {
900        let tid = self.commit_selection();
901        self.consume_yield_exclusion();
902        tid
903    }
904
905    fn commit_selection(&mut self) -> DetTid {
906        // Check that queue is locked and unlock it.
907        let tentative_selection = self
908            .tentative_selection
909            .take()
910            .expect("tentative_pop to already returned a `Some`");
911        let exact = std::mem::take(&mut self.tentative_selection_is_exact);
912
913        let ret = if exact {
914            let key = *self
915                .queue
916                .iter()
917                .find(|(_key, value)| value.tid == tentative_selection)
918                .map(|(key, _value)| key)
919                .unwrap();
920            self.queue.remove(&key).map(|value| value.tid)
921        } else {
922            match self.sched_strategy {
923                SchedHeuristic::None | SchedHeuristic::ConnectBind | SchedHeuristic::Random => {
924                    let key = *self
925                        .queue
926                        .iter()
927                        .find(|(_k, v)| v.tid == tentative_selection)
928                        .map(|(k, _v)| k)
929                        .unwrap();
930                    self.queue.remove(&key).map(|v| v.tid)
931                }
932                SchedHeuristic::StickyRandom => {
933                    let tid = self.sticky_random_selection.unwrap();
934                    // Probability of staying to our current thread on the next round.
935                    // If the generated random number is smaller than what we set, we switch threads.
936                    if self.random_range(0f64, 1f64) <= 1.0 - self.sticky_random_param {
937                        self.sticky_random_selection = None;
938                    }
939
940                    let key = *self
941                        .queue
942                        .iter()
943                        .find(|(_k, v)| v.tid == tid)
944                        .map(|(k, _v)| k)
945                        .unwrap();
946
947                    self.queue.remove(&key).map(|v| v.tid)
948                }
949            }
950        }
951        .expect("to always return a DetTid");
952        // The above should always return a DetTid as we peeked right before.
953        // If this invariant is violated, then it's a bug, or the queue is modified
954        // between the peek and tentative_pop and commit_tentative_pop.
955        debug_assert!(ret == tentative_selection);
956        self.queued_by_yield.remove(&ret);
957        ret
958    }
959
960    /// Commit a tentative pop for a guest turn that will actually run. A
961    /// yielded thread's one-turn exclusion is consumed only here, not by
962    /// scheduler bookkeeping turns that never unblock a guest.
963    pub fn commit_tentative_pop_completed_turn(&mut self) -> DetTid {
964        let tid = self.commit_tentative_pop();
965        self.consume_yield_exclusion();
966        // A completed guest turn that was not a chaos `sched_yield` (those use
967        // `commit_tentative_pop_yield`) ends the yield epoch. That includes a
968        // SaBRe loopback poll's yield: its count is host-timed, but a loopback
969        // poller may be waiting on a yielder, so it must not be held behind
970        // the epoch.
971        self.end_yield_epoch();
972        tid
973    }
974
975    /// Mark that a different guest received execution after an explicit yield.
976    pub fn consume_yield_exclusion(&mut self) {
977        self.yielded_skip = None;
978    }
979
980    /// Forget the tentative pop as though it never happened.
981    pub fn undo_tentative_pop(&mut self) {
982        assert!(self.tentative_selection.is_some());
983        self.tentative_selection = None;
984        self.tentative_selection_is_exact = false;
985    }
986
987    /// Return how many things have been queued.
988    fn turn_counter(&self) -> u64 {
989        debug_assert!(self.last_back_turn >= 0);
990        debug_assert!(self.last_front_turn <= 0);
991        self.last_back_turn as u64 + self.last_front_turn.unsigned_abs()
992    }
993
994    fn check_poll_upgrade(&mut self) {
995        if self.turn_counter().is_multiple_of(POLLING_UPGRADE_INTERVAL) {
996            self.do_poll_upgrade();
997        }
998    }
999
1000    /// Whether selection follows priority order
1001    /// ([`SchedHeuristic::is_priority_ordered`]).
1002    pub fn is_priority_ordered(&self) -> bool {
1003        self.sched_strategy.is_priority_ordered()
1004    }
1005
1006    /// Run a poll upgrade now, as the `POLLING_UPGRADE_INTERVAL`th push would.
1007    #[cfg(test)]
1008    pub fn poll_upgrade_now(&mut self) {
1009        self.do_poll_upgrade();
1010    }
1011
1012    /// The priority `tid` is queued at, if it is queued.
1013    #[cfg(test)]
1014    pub fn queued_priority(&self, tid: DetTid) -> Option<Priority> {
1015        self.queue
1016            .iter()
1017            .find(|(_, value)| value.tid == tid)
1018            .map(|(key, _)| key.priority)
1019    }
1020
1021    /// Restore one backed-off poller to the normal priority recorded when it
1022    /// was queued, and move it to the back of that priority level, as a push
1023    /// does (<https://github.com/rrnewton/hermit/issues/3952>). At the back,
1024    /// it runs after every thread that was already runnable when its deadline
1025    /// passed, as it would have while backed off, and before any thread pushed
1026    /// later, such as a spinner requeued at its next preemption. Keeping its
1027    /// older round-robin turn instead let it overtake a thread that became
1028    /// runnable before its deadline. The move is a queueing operation: it
1029    /// takes a new back turn and counts toward the poll-upgrade interval.
1030    /// Under `--replay-continue-fairly`, past the end of a replayed schedule,
1031    /// the threads ordered by run time sit behind every turn this hands out,
1032    /// so a restored poller goes ahead of them
1033    /// ([`Self::order_by_run_time`]).
1034    ///
1035    /// Mutating operation: this will error if a tentative_pop/commit transaction is underway.
1036    pub fn restore_poller_priority(&mut self, tid: DetTid) -> PollerRestore {
1037        assert!(self.tentative_selection.is_none());
1038        let Some((&key, value)) = self.queue.iter().find(|(_, value)| value.tid == tid) else {
1039            return PollerRestore::NotQueued;
1040        };
1041        let Some(normal_priority) = value.poll_upgrade else {
1042            return PollerRestore::NotBackedOff;
1043        };
1044        let mut value = self.queue.remove(&key).expect("located queue entry");
1045        value.poll_upgrade = None;
1046        // The gates count a backed-off poller as a poller only at
1047        // LAST_PRIORITY; keep exactly that classification until it runs.
1048        value.deadline_restored = key.priority >= LAST_PRIORITY;
1049        self.last_back_turn += 1;
1050        let restored = PrioritizedOrder {
1051            priority: normal_priority,
1052            turn: self.last_back_turn,
1053        };
1054        assert!(self.queue.insert(restored, value).is_none());
1055        self.check_poll_upgrade();
1056        PollerRestore::Restored
1057    }
1058
1059    /// How many poll upgrades have run so far.
1060    pub fn poll_upgrade_count(&self) -> u64 {
1061        self.poll_upgrades
1062    }
1063
1064    /// Run a poll upgrade now, outside the queueing-operation interval, and
1065    /// answer which backed-off pollers it restored to their normal priority.
1066    /// The scheduler calls this when virtual time, not queueing operations,
1067    /// says an upgrade is due.
1068    ///
1069    /// Mutating operation: this will error if a tentative_pop/commit transaction is underway.
1070    pub fn upgrade_pollers(&mut self) -> Vec<DetTid> {
1071        assert!(self.tentative_selection.is_none());
1072        self.do_poll_upgrade()
1073    }
1074
1075    /// Upgrade polled tasks to their specified normal priority, and answer
1076    /// which it restored.
1077    #[cold]
1078    fn do_poll_upgrade(&mut self) -> Vec<DetTid> {
1079        self.poll_upgrades += 1;
1080        let upgrades = self
1081            .queue
1082            .iter()
1083            .filter_map(|(k, v)| v.poll_upgrade.map(|upgd| (*k, upgd)))
1084            .collect::<Vec<(PrioritizedOrder, Priority)>>();
1085        // TODO(T100400409): if all polling threads are below a certain priority, this
1086        // can use a range query rather than iterating over all threads in the
1087        // run queue:
1088        let mut restored = Vec::new();
1089        for (key, upgrade_prio) in upgrades {
1090            // A poller whose normal priority is the last band has nowhere to
1091            // be upgraded to. Clearing its marker would only make it ordinary
1092            // work for the gates, and with `POLLING_UPGRADE_INTERVAL` pollers
1093            // like it queued, every one could lose the marker again before
1094            // the next push restored it, so that no gate ever opened (Codex
1095            // review of https://github.com/rrnewton/hermit/pull/4314). It
1096            // keeps the marker and records the upgrade instead, which lets
1097            // the chaos change-point bound count it as it counts every other
1098            // upgraded poller (`QueueValue::upgraded_in_place`).
1099            if upgrade_prio >= LAST_PRIORITY {
1100                self.queue
1101                    .get_mut(&key)
1102                    .expect("located queue entry")
1103                    .upgraded_in_place = true;
1104                continue;
1105            }
1106            let mut new_key = key;
1107            new_key.priority = upgrade_prio;
1108            let mut qval = self.queue.remove(&key).unwrap();
1109            qval.poll_upgrade = None; // there's no need to upgrade to the same priority twice
1110            restored.push(qval.tid);
1111            let old = self.queue.insert(new_key, qval);
1112            assert!(old.is_none()); // round robin turns should ensure uniqueness
1113        }
1114        // An upgrade makes every backed-off poller ordinary work for the
1115        // deterministic-work-first gates. A poller its deadline restored
1116        // earlier would have been upgraded here too, so it stops counting as
1117        // a poller now as well (https://github.com/rrnewton/hermit/issues/3952),
1118        // unless it is already in the last band, where an upgrade cannot move
1119        // it: there it keeps counting as a poller, and records the upgrade.
1120        for (key, value) in self.queue.iter_mut() {
1121            if key.priority < LAST_PRIORITY {
1122                value.deadline_restored = false;
1123            } else if value.deadline_restored {
1124                value.upgraded_in_place = true;
1125            }
1126        }
1127        restored
1128    }
1129}
1130
1131impl Default for RunQueue {
1132    fn default() -> Self {
1133        Self::new(SchedHeuristic::None, 0, 0.0)
1134    }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139    use super::*;
1140
1141    /// An entry ordered by run time and suspended finds its turn taken by
1142    /// another entry ordered to the same run time meanwhile; restoring it
1143    /// takes the next free turn, behind that entry.
1144    #[test]
1145    fn a_restored_entry_ordered_by_run_time_takes_the_next_free_turn() {
1146        let mut queue = RunQueue::new(SchedHeuristic::None, 0, 0.0);
1147        let (first, second) = (DetTid::from_raw(3), DetTid::from_raw(5));
1148        queue.push_back(first, DEFAULT_PRIORITY);
1149        queue.push_back(second, DEFAULT_PRIORITY);
1150        queue.order_by_run_time(&[(first, (5, 0)), (second, (9, 0))]);
1151        let suspended = queue.suspend(first, DEFAULT_PRIORITY).expect("queued");
1152        queue.order_by_run_time(&[(second, (5, 0))]);
1153        queue.restore(suspended, DEFAULT_PRIORITY);
1154        assert_eq!(queue.tentative_pop_next(), Some(second));
1155        queue.commit_tentative_pop();
1156        assert_eq!(queue.tentative_pop_next(), Some(first));
1157    }
1158
1159    /// `restore_poller_priority` restores one backed-off poller to its recorded
1160    /// priority at the back of that level: behind a thread queued before the
1161    /// restore, ahead of one pushed after it, such as a spinner requeued at its
1162    /// next preemption. Other pollers stay backed off, and the restore counts
1163    /// as one push (https://github.com/rrnewton/hermit/issues/3952).
1164    #[test]
1165    fn restore_poller_priority_moves_one_poller_to_the_back_of_its_level() {
1166        let poller = DetTid::from_raw(1);
1167        let other = DetTid::from_raw(2);
1168        let spinner = DetTid::from_raw(3);
1169        let mut queue = RunQueue::new(SchedHeuristic::None, 0, 0.0);
1170        queue.push_poller(poller, DEFAULT_PRIORITY, 3);
1171        queue.push_poller(other, DEFAULT_PRIORITY, 3);
1172        queue.push_back(spinner, DEFAULT_PRIORITY);
1173        let pushes = queue.turn_counter();
1174        // Backed off, the poller loses to the spinner.
1175        assert_eq!(queue.tentative_pop_next(), Some(spinner));
1176        queue.undo_tentative_pop();
1177
1178        assert_eq!(
1179            queue.restore_poller_priority(poller),
1180            PollerRestore::Restored
1181        );
1182        assert_eq!(queue.turn_counter(), pushes + 1, "a restore is one push");
1183        assert_eq!(queue.queued_priority(poller), Some(DEFAULT_PRIORITY));
1184        assert_eq!(queue.tentative_pop_next(), Some(spinner), "queued first");
1185        assert_eq!(queue.commit_tentative_pop(), spinner);
1186        queue.push_back(spinner, DEFAULT_PRIORITY);
1187        assert_eq!(
1188            queue.tentative_pop_next(),
1189            Some(poller),
1190            "ahead of the requeue"
1191        );
1192        queue.undo_tentative_pop();
1193        let (other_key, other_value) = queue
1194            .queue
1195            .iter()
1196            .find(|(_, value)| value.tid == other)
1197            .unwrap();
1198        assert_eq!(other_key.priority, 8 * DEFAULT_PRIORITY);
1199        assert_eq!(other_value.poll_upgrade, Some(DEFAULT_PRIORITY));
1200
1201        assert_eq!(
1202            queue.restore_poller_priority(poller),
1203            PollerRestore::NotBackedOff
1204        );
1205        assert_eq!(
1206            queue.restore_poller_priority(spinner),
1207            PollerRestore::NotBackedOff
1208        );
1209        assert_eq!(
1210            queue.restore_poller_priority(DetTid::from_raw(9)),
1211            PollerRestore::NotQueued
1212        );
1213    }
1214
1215    /// An ordinary thread queued at `LAST_PRIORITY` is work for the
1216    /// deterministic-work-first gates, as at any other priority; only a
1217    /// polling thread whose backoff reached the last band is a poller
1218    /// (<https://github.com/rrnewton/hermit/issues/4234>).
1219    #[test]
1220    fn an_ordinary_thread_in_the_last_band_is_not_a_poller() {
1221        let worker = DetTid::from_raw(1);
1222        let poller = DetTid::from_raw(2);
1223        let other = DetTid::from_raw(3);
1224
1225        let mut queue = RunQueue::new(SchedHeuristic::None, 0, 0.0);
1226        queue.push_back(worker, LAST_PRIORITY);
1227        assert!(!queue.only_pollers());
1228        assert!(!queue.only_pollers_or_yielders());
1229        assert!(queue.has_runnable_besides(other));
1230
1231        let mut queue = RunQueue::new(SchedHeuristic::None, 0, 0.0);
1232        queue.push_poller(poller, DEFAULT_PRIORITY, u32::MAX);
1233        assert!(
1234            queue.only_pollers(),
1235            "a fully backed-off poller is a poller"
1236        );
1237        assert!(!queue.has_runnable_besides(other));
1238        queue.push_back(worker, LAST_PRIORITY);
1239        assert!(
1240            !queue.only_pollers(),
1241            "the ordinary thread beside it is work"
1242        );
1243        assert!(queue.has_runnable_besides(poller));
1244    }
1245
1246    /// A poller backed off to `LAST_PRIORITY` and restored by its deadline
1247    /// still counts as a poller for the deterministic-work-first gates until
1248    /// it runs; requeued after its turn, it is ordinary work. One backed off
1249    /// below `LAST_PRIORITY` was ordinary work for the gates already, and
1250    /// stays so after the restore, as on main.
1251    #[test]
1252    fn a_deadline_restored_poller_counts_as_a_poller_until_it_runs() {
1253        let poller = DetTid::from_raw(1);
1254        let other = DetTid::from_raw(2);
1255        let mut queue = RunQueue::new(SchedHeuristic::None, 0, 0.0);
1256        queue.push_poller(poller, DEFAULT_PRIORITY, 30);
1257        assert!(queue.only_pollers());
1258        assert_eq!(
1259            queue.restore_poller_priority(poller),
1260            PollerRestore::Restored
1261        );
1262        assert!(queue.only_pollers(), "restored, but it has not run");
1263        assert!(!queue.has_runnable_besides(other));
1264
1265        assert_eq!(queue.tentative_pop_next(), Some(poller));
1266        assert_eq!(queue.commit_tentative_pop(), poller);
1267        queue.push_back(poller, DEFAULT_PRIORITY);
1268        assert!(!queue.only_pollers(), "after its turn it is ordinary work");
1269        assert!(queue.has_runnable_besides(other));
1270
1271        let mut queue = RunQueue::new(SchedHeuristic::None, 0, 0.0);
1272        queue.push_poller(poller, DEFAULT_PRIORITY, 1);
1273        assert!(
1274            !queue.only_pollers(),
1275            "at 2000 it is ordinary work, as on main"
1276        );
1277        assert_eq!(
1278            queue.restore_poller_priority(poller),
1279            PollerRestore::Restored
1280        );
1281        assert!(!queue.only_pollers(), "and stays so after the restore");
1282        assert!(queue.has_runnable_besides(other));
1283    }
1284
1285    /// A poll upgrade turns every backed-off poller into ordinary work for the
1286    /// gates; a deadline-restored poller stops counting as a poller at the
1287    /// same point.
1288    #[test]
1289    fn a_poll_upgrade_ends_a_restored_pollers_poller_status() {
1290        let poller = DetTid::from_raw(1);
1291        let mut queue = RunQueue::new(SchedHeuristic::None, 0, 0.0);
1292        queue.push_poller(poller, DEFAULT_PRIORITY, 30);
1293        assert_eq!(
1294            queue.restore_poller_priority(poller),
1295            PollerRestore::Restored
1296        );
1297        assert!(queue.only_pollers());
1298        queue.do_poll_upgrade();
1299        assert!(
1300            !queue.only_pollers(),
1301            "upgraded like every backed-off poller"
1302        );
1303    }
1304
1305    /// A poller whose normal priority is the last band is still a poller
1306    /// after a poll upgrade, which has nowhere to move it, and so is one its
1307    /// deadline restored there. With `POLLING_UPGRADE_INTERVAL` such pollers
1308    /// queued, clearing their markers at every upgrade kept at least one of
1309    /// them reading as ordinary work at every gate query, so no gate ever
1310    /// opened (Codex review of https://github.com/rrnewton/hermit/pull/4314).
1311    #[test]
1312    fn a_last_band_pollers_upgrade_keeps_it_a_poller() {
1313        let poller = DetTid::from_raw(1);
1314        let mut queue = RunQueue::default();
1315        queue.push_poller(poller, LAST_PRIORITY, 0);
1316        assert!(queue.only_pollers());
1317        queue.do_poll_upgrade();
1318        assert!(
1319            queue.only_pollers(),
1320            "an upgrade to the same band is a no-op"
1321        );
1322        assert_eq!(queue.queued_priority(poller), Some(LAST_PRIORITY));
1323
1324        let restored = DetTid::from_raw(2);
1325        let mut queue = RunQueue::default();
1326        queue.push_poller(restored, LAST_PRIORITY, 3);
1327        assert_eq!(
1328            queue.restore_poller_priority(restored),
1329            PollerRestore::Restored
1330        );
1331        queue.do_poll_upgrade();
1332        assert!(
1333            queue.only_pollers(),
1334            "so is one its deadline restored there"
1335        );
1336
1337        let count = POLLING_UPGRADE_INTERVAL as i32;
1338        let mut queue = RunQueue::default();
1339        for raw in 1..=count {
1340            queue.push_poller(DetTid::from_raw(raw), LAST_PRIORITY, 0);
1341        }
1342        for cycle in 0..20 * count {
1343            assert!(queue.only_pollers(), "cycle {cycle}");
1344            let tid = queue.tentative_pop_next().expect("a queued poller");
1345            assert_eq!(queue.commit_tentative_pop(), tid);
1346            queue.push_poller(tid, LAST_PRIORITY, 1);
1347        }
1348        assert!(queue.only_pollers());
1349    }
1350
1351    /// A poll upgrade reaches a poller in the last band without moving it,
1352    /// and records that it did: from then on the chaos change-point bound can
1353    /// count it (`can_be_passed_over`), as it counts every poller an upgrade
1354    /// moved back to its normal priority, while the gates and the queue key
1355    /// still see a backed-off poller. The same holds for one its deadline
1356    /// restored there. A poller still backed off is not counted, and the mark
1357    /// goes with the entry when the thread is requeued after its retry.
1358    #[test]
1359    fn a_poll_upgrade_lets_the_change_point_bound_count_a_last_band_poller() {
1360        let poller = DetTid::from_raw(1);
1361        let other = DetTid::from_raw(2);
1362        let mut queue = RunQueue::default();
1363        queue.push_poller(poller, LAST_PRIORITY, 5);
1364        queue.push_poller(other, DEFAULT_PRIORITY, 30);
1365        assert!(!queue.can_be_passed_over(poller), "backed off: not yet");
1366        assert!(!queue.can_be_passed_over(other));
1367        queue.do_poll_upgrade();
1368        assert!(queue.can_be_passed_over(poller), "the upgrade reached it");
1369        assert!(queue.can_be_passed_over(other), "moved, as on main");
1370        assert!(!queue.contains_non_polling_tid(poller));
1371        assert_eq!(queue.queued_priority(poller), Some(LAST_PRIORITY));
1372        assert!(queue.remove_tid(other));
1373        assert!(queue.only_pollers(), "still a poller for the gates");
1374        assert!(!queue.has_runnable_besides(other));
1375
1376        assert_eq!(queue.tentative_pop_next(), Some(poller));
1377        assert_eq!(queue.commit_tentative_pop(), poller);
1378        queue.push_poller(poller, LAST_PRIORITY, 6);
1379        assert!(!queue.can_be_passed_over(poller), "requeued: backed off");
1380
1381        let restored = DetTid::from_raw(3);
1382        let mut queue = RunQueue::default();
1383        queue.push_poller(restored, LAST_PRIORITY, 3);
1384        assert_eq!(
1385            queue.restore_poller_priority(restored),
1386            PollerRestore::Restored
1387        );
1388        assert!(!queue.can_be_passed_over(restored));
1389        queue.do_poll_upgrade();
1390        assert!(queue.can_be_passed_over(restored));
1391        assert!(queue.only_pollers());
1392        assert!(!queue.contains_tid(DetTid::from_raw(9)));
1393        assert!(!queue.can_be_passed_over(DetTid::from_raw(9)));
1394    }
1395
1396    /// A chaos `sched_yield` requeue marks its thread as a yielder, as the
1397    /// plain one does, until it runs again.
1398    #[test]
1399    fn a_chaos_yield_marks_its_thread_as_a_yielder() {
1400        let yielder = DetTid::from_raw(1);
1401        let other = DetTid::from_raw(2);
1402        let mut queue = RunQueue::default();
1403        queue.push_back(yielder, LAST_PRIORITY);
1404        yield_turn(&mut queue, yielder, LAST_PRIORITY);
1405        assert!(!queue.only_pollers(), "a yielder is not a poller");
1406        assert!(queue.only_pollers_or_yielders());
1407        queue.push_back(other, DEFAULT_PRIORITY);
1408        assert!(!queue.only_pollers_or_yielders());
1409        assert!(queue.remove_tid(other));
1410        assert_eq!(take_turn(&mut queue), yielder);
1411        queue.push_back(yielder, LAST_PRIORITY);
1412        assert!(!queue.only_pollers_or_yielders(), "it ran without yielding");
1413    }
1414
1415    #[test]
1416    fn only_first_entry_heuristics_are_priority_ordered() {
1417        for (strategy, ordered) in [
1418            (SchedHeuristic::None, true),
1419            (SchedHeuristic::ConnectBind, true),
1420            (SchedHeuristic::Random, false),
1421            (SchedHeuristic::StickyRandom, false),
1422        ] {
1423            assert_eq!(
1424                RunQueue::new(strategy, 0, 0.5).is_priority_ordered(),
1425                ordered,
1426                "{strategy:?}"
1427            );
1428        }
1429    }
1430
1431    #[test]
1432    fn transport_suspend_restore_preserves_all_queue_state() {
1433        for strategy in [
1434            SchedHeuristic::None,
1435            SchedHeuristic::Random,
1436            SchedHeuristic::StickyRandom,
1437        ] {
1438            let tid = DetTid::from_raw(1);
1439            let peer = DetTid::from_raw(2);
1440            let mut queue = RunQueue::new(strategy, 391, 0.5);
1441            queue.push_yielded(tid, DEFAULT_PRIORITY);
1442            queue.push_back(peer, DEFAULT_PRIORITY);
1443            let key = *queue
1444                .queue
1445                .iter()
1446                .find(|(_, value)| value.tid == tid)
1447                .unwrap()
1448                .0;
1449            queue.queue.get_mut(&key).unwrap().poll_upgrade = Some(DEFAULT_PRIORITY);
1450            queue.sticky_random_selection = Some(tid);
1451            let before = format!("{queue:?}");
1452            let saved = queue.suspend(tid, DEFAULT_PRIORITY).unwrap();
1453            assert_eq!(queue.yielded_skip, Some(tid));
1454            assert_eq!(queue.sticky_random_selection, Some(tid));
1455            queue.restore(saved, DEFAULT_PRIORITY);
1456            assert_eq!(format!("{queue:?}"), before, "{strategy:?}");
1457        }
1458    }
1459
1460    #[test]
1461    fn observation_restore_keeps_real_selection_progress_and_new_priority() {
1462        for strategy in [
1463            SchedHeuristic::None,
1464            SchedHeuristic::Random,
1465            SchedHeuristic::StickyRandom,
1466        ] {
1467            let tid = DetTid::from_raw(1);
1468            let peer = DetTid::from_raw(2);
1469            let mut queue = RunQueue::new(strategy, 391, 0.5);
1470            queue.push_yielded(tid, DEFAULT_PRIORITY);
1471            queue.push_back(peer, DEFAULT_PRIORITY);
1472            let key = *queue
1473                .queue
1474                .iter()
1475                .find(|(_, value)| value.tid == tid)
1476                .unwrap()
1477                .0;
1478            queue.queue.get_mut(&key).unwrap().poll_upgrade = Some(DEFAULT_PRIORITY);
1479            let saved = queue.suspend(tid, DEFAULT_PRIORITY).unwrap();
1480            assert_eq!(queue.tentative_pop_next(), Some(peer));
1481            assert_eq!(queue.commit_tentative_pop_completed_turn(), peer);
1482            assert_eq!(queue.yielded_skip, None);
1483            queue.push_back(peer, DEFAULT_PRIORITY);
1484            let random_after_turn = format!("{:?}", queue.prng);
1485            let sticky_after_turn = queue.sticky_random_selection;
1486            let turns_after_turn = (queue.last_back_turn, queue.last_front_turn);
1487            queue.restore(saved, DEFAULT_PRIORITY + 3);
1488            let (restored, value) = queue
1489                .queue
1490                .iter()
1491                .find(|(_, value)| value.tid == tid)
1492                .unwrap();
1493            assert_eq!(restored.turn, key.turn);
1494            assert_eq!(restored.priority, DEFAULT_PRIORITY + 3);
1495            assert_eq!(value.poll_upgrade, Some(DEFAULT_PRIORITY + 3));
1496            assert_eq!(queue.yielded_skip, None);
1497            assert_eq!(queue.sticky_random_selection, sticky_after_turn);
1498            assert_eq!(
1499                (queue.last_back_turn, queue.last_front_turn),
1500                turns_after_turn
1501            );
1502            assert_eq!(format!("{:?}", queue.prng), random_after_turn);
1503        }
1504    }
1505
1506    #[test]
1507    fn yielded_thread_cedes_exactly_one_turn_under_every_heuristic() {
1508        for strategy in [
1509            SchedHeuristic::None,
1510            SchedHeuristic::ConnectBind,
1511            SchedHeuristic::Random,
1512            SchedHeuristic::StickyRandom,
1513        ] {
1514            let yielding = DetTid::from_raw(1);
1515            let peer = DetTid::from_raw(2);
1516            let mut queue = RunQueue::new(strategy, 0, 1.0);
1517
1518            queue.push_back(yielding, DEFAULT_PRIORITY - 1);
1519            assert_eq!(queue.tentative_pop_next(), Some(yielding));
1520            assert_eq!(queue.commit_tentative_pop(), yielding);
1521
1522            queue.push_yielded(yielding, DEFAULT_PRIORITY - 1);
1523            queue.push_back(peer, LAST_PRIORITY);
1524            assert_eq!(queue.tentative_pop_next(), Some(peer), "{strategy:?}");
1525            assert_eq!(
1526                queue.commit_tentative_pop_completed_turn(),
1527                peer,
1528                "{strategy:?}"
1529            );
1530
1531            assert_eq!(queue.yielded_skip, None, "{strategy:?}");
1532            let restored_priority = queue
1533                .queue
1534                .iter()
1535                .find(|(_key, value)| value.tid == yielding)
1536                .map(|(key, _value)| key.priority);
1537            assert_eq!(
1538                restored_priority,
1539                Some(DEFAULT_PRIORITY - 1),
1540                "{strategy:?}"
1541            );
1542
1543            if matches!(strategy, SchedHeuristic::None | SchedHeuristic::ConnectBind) {
1544                queue.push_back(peer, LAST_PRIORITY);
1545                assert_eq!(queue.tentative_pop_next(), Some(yielding), "{strategy:?}");
1546            }
1547        }
1548    }
1549
1550    #[test]
1551    fn exact_selection_bypasses_priority_and_heuristic() {
1552        for strategy in [
1553            SchedHeuristic::None,
1554            SchedHeuristic::ConnectBind,
1555            SchedHeuristic::Random,
1556            SchedHeuristic::StickyRandom,
1557        ] {
1558            let higher_priority = DetTid::from_raw(1);
1559            let selected = DetTid::from_raw(2);
1560            let mut queue = RunQueue::new(strategy, 0, 1.0);
1561            queue.push_back(higher_priority, FIRST_PRIORITY);
1562            queue.push_back(selected, LAST_PRIORITY);
1563
1564            assert_eq!(queue.tentative_pop_tid(selected), Some(selected));
1565            assert_eq!(queue.commit_tentative_pop(), selected);
1566            assert!(queue.contains_tid(higher_priority));
1567            assert!(!queue.contains_tid(selected));
1568        }
1569    }
1570
1571    #[test]
1572    fn scheduler_only_commit_does_not_consume_yield_exclusion() {
1573        let yielding = DetTid::from_raw(1);
1574        let peer = DetTid::from_raw(2);
1575        let mut queue = RunQueue::default();
1576
1577        queue.push_back(yielding, DEFAULT_PRIORITY);
1578        assert_eq!(queue.tentative_pop_next(), Some(yielding));
1579        assert_eq!(queue.commit_tentative_pop(), yielding);
1580
1581        queue.push_yielded(yielding, DEFAULT_PRIORITY);
1582        queue.push_back(peer, DEFAULT_PRIORITY);
1583        assert_eq!(queue.tentative_pop_next(), Some(peer));
1584        assert_eq!(queue.commit_tentative_pop(), peer);
1585
1586        queue.push_back(peer, DEFAULT_PRIORITY);
1587        assert_eq!(queue.yielded_skip, Some(yielding));
1588        assert_eq!(queue.tentative_pop_next(), Some(peer));
1589        assert_eq!(queue.commit_tentative_pop_completed_turn(), peer);
1590        assert_eq!(queue.yielded_skip, None);
1591    }
1592
1593    /// Pop the next selection and commit it as a completed turn.
1594    fn take_turn(queue: &mut RunQueue) -> DetTid {
1595        queue.tentative_pop_next().expect("a runnable thread");
1596        queue.commit_tentative_pop_completed_turn()
1597    }
1598
1599    /// Give `tid` a chaos `sched_yield` turn: commit it and requeue it into
1600    /// the yield epoch at `priority`.
1601    fn yield_turn(queue: &mut RunQueue, tid: DetTid, priority: Priority) {
1602        assert_eq!(queue.tentative_pop_tid(tid), Some(tid));
1603        assert_eq!(queue.commit_tentative_pop_yield(), tid);
1604        queue.push_yielded_into_epoch(tid, priority);
1605    }
1606
1607    /// Yield `tid` until it joins the yield epoch: its plain yields, then
1608    /// the one that joins. Another thread must be queued.
1609    fn yield_into_epoch(queue: &mut RunQueue, tid: DetTid, priority: Priority) {
1610        for _ in 0..=PLAIN_YIELDS_BEFORE_EPOCH {
1611            yield_turn(queue, tid, priority);
1612        }
1613        assert!(
1614            queue
1615                .yield_epoch
1616                .as_ref()
1617                .is_some_and(|epoch| epoch.contains(&tid))
1618        );
1619    }
1620
1621    /// The first chaos yield that passes another runnable thread over is a
1622    /// plain requeue, so the yielder in the earlier band runs again; the next
1623    /// one joins the epoch, and the other thread runs.
1624    #[test]
1625    fn a_yielder_is_reselected_once_before_it_joins_the_epoch() {
1626        let yielder = DetTid::from_raw(1);
1627        let awaited = DetTid::from_raw(2);
1628        let mut queue = RunQueue::default();
1629        queue.push_back(yielder, FIRST_PRIORITY);
1630        queue.push_back(awaited, LAST_PRIORITY);
1631
1632        yield_turn(&mut queue, yielder, FIRST_PRIORITY);
1633        assert_eq!(queue.yield_epoch, None);
1634        assert_eq!(queue.tentative_pop_next(), Some(yielder));
1635        queue.undo_tentative_pop();
1636        yield_turn(&mut queue, yielder, FIRST_PRIORITY);
1637        assert_eq!(take_turn(&mut queue), awaited);
1638        assert!(queue.yield_streaks.is_empty());
1639    }
1640
1641    /// Consecutive yields made with no other thread queued build no streak,
1642    /// so when a thread becomes runnable (a futex waiter woken for a signal,
1643    /// say) the spinner's next yield is still a plain one and it may run
1644    /// first. The yields follow one another with no other turn between them,
1645    /// so nothing else can have emptied the streak (review round 3 of
1646    /// https://github.com/rrnewton/hermit/pull/4080).
1647    #[test]
1648    fn consecutive_lone_yields_build_no_streak() {
1649        let spinner = DetTid::from_raw(1);
1650        let woken = DetTid::from_raw(2);
1651        let mut queue = RunQueue::default();
1652        queue.push_back(spinner, FIRST_PRIORITY);
1653        for _ in 0..5 {
1654            yield_turn(&mut queue, spinner, FIRST_PRIORITY);
1655        }
1656        assert!(queue.yield_streaks.is_empty());
1657        assert_eq!(queue.yield_epoch, None);
1658        queue.push_back(woken, LAST_PRIORITY);
1659        yield_turn(&mut queue, spinner, FIRST_PRIORITY);
1660        assert_eq!(queue.yield_epoch, None);
1661        assert_eq!(queue.tentative_pop_next(), Some(spinner));
1662    }
1663
1664    /// A thread left as the only one outside an epoch of yielders joins it
1665    /// by yielding, so the epoch covers the queue and ends, instead of that
1666    /// thread spinning ahead of the others forever.
1667    #[test]
1668    fn the_last_thread_outside_an_epoch_joins_it_by_yielding() {
1669        let first = DetTid::from_raw(1);
1670        let last = DetTid::from_raw(2);
1671        let mut queue = RunQueue::default();
1672        queue.push_back(first, FIRST_PRIORITY);
1673        queue.push_back(last, LAST_PRIORITY);
1674
1675        yield_into_epoch(&mut queue, first, FIRST_PRIORITY);
1676        assert_eq!(queue.tentative_pop_next(), Some(last));
1677        queue.undo_tentative_pop();
1678        yield_into_epoch(&mut queue, last, LAST_PRIORITY);
1679        assert_eq!(queue.tentative_pop_next(), Some(first));
1680        assert_eq!(queue.yield_epoch, None);
1681    }
1682
1683    /// Two threads that keep yielding cannot take turns ahead of a third in a
1684    /// later band, as a one-turn exclusion lets them
1685    /// (https://github.com/rrnewton/hermit/issues/4068).
1686    #[test]
1687    fn two_yielders_cannot_shut_out_a_thread_in_a_later_band() {
1688        let first = DetTid::from_raw(1);
1689        let second = DetTid::from_raw(2);
1690        let awaited = DetTid::from_raw(3);
1691        let mut queue = RunQueue::default();
1692        queue.push_back(first, FIRST_PRIORITY);
1693        queue.push_back(second, FIRST_PRIORITY + 1);
1694        queue.push_back(awaited, LAST_PRIORITY);
1695
1696        yield_into_epoch(&mut queue, first, FIRST_PRIORITY);
1697        assert_eq!(queue.tentative_pop_next(), Some(second));
1698        queue.undo_tentative_pop();
1699        yield_into_epoch(&mut queue, second, FIRST_PRIORITY + 1);
1700        assert_eq!(take_turn(&mut queue), awaited);
1701    }
1702
1703    /// The exclusion lasts only until a thread that did not yield has had a
1704    /// completed turn; then the bands decide again, so a yielder is never held
1705    /// behind later turns of a thread in an earlier band.
1706    #[test]
1707    fn a_turn_by_a_thread_that_did_not_yield_ends_the_yield_epoch() {
1708        let yielder = DetTid::from_raw(1);
1709        let early = DetTid::from_raw(2);
1710        let late = DetTid::from_raw(3);
1711        let mut queue = RunQueue::default();
1712        queue.push_back(yielder, FIRST_PRIORITY + 1);
1713        queue.push_back(early, FIRST_PRIORITY);
1714        queue.push_back(late, LAST_PRIORITY);
1715
1716        yield_into_epoch(&mut queue, yielder, FIRST_PRIORITY + 1);
1717        assert_eq!(take_turn(&mut queue), early);
1718        assert_eq!(queue.yield_epoch, None);
1719        queue.push_back(early, FIRST_PRIORITY);
1720        assert_eq!(take_turn(&mut queue), early);
1721        queue.push_back(early, FIRST_PRIORITY);
1722        // A bare queue commit, as a poller's non-committing backoff makes,
1723        // keeps the epoch; the scheduler ends it explicitly where a thread that
1724        // did not yield ran (a priority change point, a background grant).
1725        yield_into_epoch(&mut queue, yielder, FIRST_PRIORITY + 1);
1726        queue.tentative_pop_next();
1727        assert_eq!(queue.commit_tentative_pop(), early);
1728        queue.push_back(early, FIRST_PRIORITY);
1729        assert!(
1730            queue
1731                .yield_epoch
1732                .as_ref()
1733                .is_some_and(|epoch| epoch.contains(&yielder))
1734        );
1735        queue.end_yield_epoch();
1736        assert_eq!(queue.yield_epoch, None);
1737        assert!(queue.yield_streaks.is_empty());
1738        assert_eq!(
1739            queue.tids().copied().collect::<Vec<_>>(),
1740            [early, yielder, late]
1741        );
1742    }
1743
1744    /// An epoch that covers every queued thread is over, so yielders that
1745    /// wait on each other alternate instead of the first one running forever.
1746    #[test]
1747    fn a_yield_epoch_covering_every_queued_thread_ends() {
1748        let first = DetTid::from_raw(1);
1749        let second = DetTid::from_raw(2);
1750        let mut queue = RunQueue::default();
1751        queue.push_back(first, FIRST_PRIORITY);
1752        queue.push_back(second, LAST_PRIORITY);
1753
1754        yield_into_epoch(&mut queue, first, FIRST_PRIORITY);
1755        assert_eq!(queue.tentative_pop_next(), Some(second));
1756        queue.undo_tentative_pop();
1757        yield_into_epoch(&mut queue, second, LAST_PRIORITY);
1758        assert_eq!(queue.tentative_pop_next(), Some(first));
1759        assert_eq!(queue.yield_epoch, None);
1760        queue.undo_tentative_pop();
1761        yield_into_epoch(&mut queue, first, FIRST_PRIORITY);
1762        assert_eq!(queue.tentative_pop_next(), Some(second));
1763    }
1764
1765    /// A yielder keeps its queue key: it does not join the last band, so it
1766    /// is not counted as a poller by the deterministic-work-first gates.
1767    #[test]
1768    fn a_yield_epoch_keeps_the_yielders_band_and_poller_status() {
1769        let yielder = DetTid::from_raw(1);
1770        let awaited = DetTid::from_raw(2);
1771        let mut queue = RunQueue::default();
1772        queue.push_back(yielder, FIRST_PRIORITY);
1773        queue.push_back(awaited, LAST_PRIORITY);
1774
1775        yield_into_epoch(&mut queue, yielder, FIRST_PRIORITY);
1776        assert!(!queue.only_pollers());
1777        assert!(queue.has_runnable_besides(awaited));
1778        assert_eq!(
1779            queue.tids().copied().collect::<Vec<_>>(),
1780            [yielder, awaited]
1781        );
1782        assert_eq!(take_turn(&mut queue), awaited);
1783    }
1784
1785    /// A thread that leaves the queue leaves the yield epoch, so a yielder
1786    /// readmitted at once, as a signal's forced unblock does, is not skipped.
1787    #[test]
1788    fn leaving_the_queue_leaves_the_yield_epoch() {
1789        let yielder = DetTid::from_raw(1);
1790        let other = DetTid::from_raw(2);
1791        let mut queue = RunQueue::default();
1792        queue.push_back(yielder, FIRST_PRIORITY);
1793        queue.push_back(other, LAST_PRIORITY);
1794
1795        yield_into_epoch(&mut queue, yielder, FIRST_PRIORITY);
1796        assert!(queue.remove_tid(yielder));
1797        queue.push_front(yielder, FIRST_PRIORITY);
1798        assert_eq!(take_turn(&mut queue), yielder);
1799    }
1800
1801    /// A poller backed off all the way to the last band does not hold a yield
1802    /// epoch open: its retries do not commit, so a yielder it waits on would
1803    /// otherwise never run. A thread that is merely in the last band, as
1804    /// `--chaos-target-races` places half of all new threads, does hold it.
1805    #[test]
1806    fn only_a_fully_backed_off_poller_releases_a_yield_epoch() {
1807        let yielder = DetTid::from_raw(1);
1808        let other = DetTid::from_raw(2);
1809        for poller in [false, true] {
1810            let mut queue = RunQueue::default();
1811            queue.push_back(yielder, FIRST_PRIORITY);
1812            if poller {
1813                queue.push_poller(other, DEFAULT_PRIORITY, 20);
1814            } else {
1815                queue.push_back(other, LAST_PRIORITY);
1816            }
1817            yield_into_epoch(&mut queue, yielder, FIRST_PRIORITY);
1818            let expected = if poller { yielder } else { other };
1819            assert_eq!(
1820                queue.tentative_pop_next(),
1821                Some(expected),
1822                "poller={poller}"
1823            );
1824        }
1825    }
1826
1827    /// A thread that leaves the queue loses its streak too, so after it is
1828    /// readmitted its next yield past another thread is a plain one again.
1829    #[test]
1830    fn leaving_the_queue_resets_the_yield_streak() {
1831        let yielder = DetTid::from_raw(1);
1832        let other = DetTid::from_raw(2);
1833        let mut queue = RunQueue::default();
1834        queue.push_back(yielder, FIRST_PRIORITY);
1835        queue.push_back(other, LAST_PRIORITY);
1836
1837        yield_turn(&mut queue, yielder, FIRST_PRIORITY);
1838        assert!(queue.remove_tid(yielder));
1839        queue.push_front(yielder, FIRST_PRIORITY);
1840        yield_turn(&mut queue, yielder, FIRST_PRIORITY);
1841        assert_eq!(queue.yield_epoch, None);
1842        assert_eq!(queue.tentative_pop_next(), Some(yielder));
1843    }
1844
1845    /// A one-turn skip (a loopback poll's yield) and the yield epoch together
1846    /// never exclude every queued thread: the skip alone then holds.
1847    #[test]
1848    fn a_one_turn_skip_and_a_yield_epoch_never_exclude_everyone() {
1849        let yielder = DetTid::from_raw(1);
1850        let poller = DetTid::from_raw(2);
1851        let mut queue = RunQueue::default();
1852        queue.push_back(yielder, FIRST_PRIORITY);
1853        queue.push_back(poller, DEFAULT_PRIORITY);
1854
1855        yield_into_epoch(&mut queue, yielder, FIRST_PRIORITY);
1856        assert_eq!(queue.tentative_pop_tid(poller), Some(poller));
1857        assert_eq!(queue.commit_tentative_pop_yield(), poller);
1858        queue.push_yielded(poller, DEFAULT_PRIORITY);
1859        // The epoch holds the yielder; the skip holds the poller; together they
1860        // would cover the queue, so only the skip applies.
1861        assert_eq!(queue.tentative_pop_next(), Some(yielder));
1862    }
1863
1864    /// A yielder alone in the queue runs again, and the epoch ends.
1865    #[test]
1866    fn a_lone_yielder_runs_again() {
1867        let yielder = DetTid::from_raw(1);
1868        let mut queue = RunQueue::default();
1869        queue.push_back(yielder, DEFAULT_PRIORITY);
1870
1871        yield_turn(&mut queue, yielder, DEFAULT_PRIORITY);
1872        assert_eq!(take_turn(&mut queue), yielder);
1873        assert_eq!(queue.yield_epoch, None);
1874        assert!(queue.yield_streaks.is_empty());
1875    }
1876
1877    /// The epoch holds under the random heuristics too: after two yielders,
1878    /// the third thread runs.
1879    #[test]
1880    fn a_yield_epoch_holds_under_the_random_heuristics() {
1881        for strategy in [SchedHeuristic::Random, SchedHeuristic::StickyRandom] {
1882            for seed in 0..16 {
1883                let yielders = [DetTid::from_raw(1), DetTid::from_raw(2)];
1884                let awaited = DetTid::from_raw(3);
1885                let mut queue = RunQueue::new(strategy, seed, 1.0);
1886                for tid in [yielders[0], yielders[1], awaited] {
1887                    queue.push_back(tid, DEFAULT_PRIORITY);
1888                }
1889                for tid in yielders {
1890                    yield_into_epoch(&mut queue, tid, DEFAULT_PRIORITY);
1891                }
1892                assert_eq!(take_turn(&mut queue), awaited, "{strategy:?} seed {seed}");
1893            }
1894        }
1895    }
1896
1897    #[test]
1898    fn threads_queued_by_a_yield_are_not_work_a_background_call_waits_behind() {
1899        let first = DetTid::from_raw(1);
1900        let second = DetTid::from_raw(2);
1901        let worker = DetTid::from_raw(3);
1902        let mut queue = RunQueue::default();
1903
1904        queue.push_back(first, DEFAULT_PRIORITY);
1905        queue.push_back(second, DEFAULT_PRIORITY);
1906        assert!(!queue.only_pollers_or_yielders());
1907
1908        // Two spinners take turns: each yield replaces the other's
1909        // `yielded_skip`, and both still count as yielders.
1910        assert_eq!(queue.tentative_pop_next(), Some(first));
1911        assert_eq!(queue.commit_tentative_pop_completed_turn(), first);
1912        queue.push_yielded(first, DEFAULT_PRIORITY);
1913        assert!(
1914            !queue.only_pollers_or_yielders(),
1915            "second has not yielded yet"
1916        );
1917        assert_eq!(queue.tentative_pop_next(), Some(second));
1918        assert_eq!(queue.commit_tentative_pop_completed_turn(), second);
1919        queue.push_yielded(second, DEFAULT_PRIORITY);
1920        assert_eq!(queue.yielded_skip, Some(second));
1921        assert!(!queue.only_pollers());
1922        assert!(queue.only_pollers_or_yielders());
1923
1924        // Ordinary work besides them still counts.
1925        queue.push_back(worker, DEFAULT_PRIORITY);
1926        assert!(!queue.only_pollers_or_yielders());
1927        assert!(queue.remove_tid(worker));
1928        assert!(queue.only_pollers_or_yielders());
1929
1930        // A yielder that runs again and is queued without yielding is work.
1931        assert_eq!(queue.tentative_pop_next(), Some(first));
1932        assert_eq!(queue.commit_tentative_pop_completed_turn(), first);
1933        queue.push_back(first, DEFAULT_PRIORITY);
1934        assert!(!queue.only_pollers_or_yielders());
1935    }
1936
1937    #[test]
1938    fn tentative_pop_in_progress_tracks_the_transaction() {
1939        let a = DetTid::from_raw(1);
1940        let b = DetTid::from_raw(2);
1941        let mut queue = RunQueue::default();
1942
1943        // No selection: safe to push.
1944        assert!(!queue.tentative_pop_in_progress());
1945        queue.push_back(a, DEFAULT_PRIORITY);
1946        queue.push_back(b, DEFAULT_PRIORITY);
1947        assert!(!queue.tentative_pop_in_progress());
1948
1949        // Peeking a tentative selection opens the transaction; this is exactly
1950        // the window in which a concurrent handler must defer its admission
1951        // rather than push (a push here trips the tentative-selection guard).
1952        assert_eq!(queue.tentative_pop_next(), Some(a));
1953        assert!(queue.tentative_pop_in_progress());
1954
1955        // Committing closes it again.
1956        assert_eq!(queue.commit_tentative_pop(), a);
1957        assert!(!queue.tentative_pop_in_progress());
1958
1959        // The exact-selection form and undo path behave the same way.
1960        assert_eq!(queue.tentative_pop_tid(b), Some(b));
1961        assert!(queue.tentative_pop_in_progress());
1962        queue.undo_tentative_pop();
1963        assert!(!queue.tentative_pop_in_progress());
1964    }
1965
1966    #[test]
1967    fn removal_clears_per_incarnation_selection_state_before_tid_reuse() {
1968        let tid = DetTid::from_raw(7);
1969        let mut queue = RunQueue::new(SchedHeuristic::StickyRandom, 0x5107, 1.0);
1970        queue.push_back(tid, DEFAULT_PRIORITY);
1971
1972        assert_eq!(queue.tentative_pop_next(), Some(tid));
1973        queue.undo_tentative_pop();
1974        assert_eq!(queue.sticky_random_selection, Some(tid));
1975        // Model an earlier explicit yield by the old incarnation. Both caches
1976        // are keyed only by raw TID and therefore must be cleared together.
1977        queue.yielded_skip = Some(tid);
1978
1979        assert!(queue.remove_tid(tid));
1980        assert_eq!(queue.sticky_random_selection, None);
1981        assert_eq!(queue.yielded_skip, None);
1982
1983        queue.push_back(tid, DEFAULT_PRIORITY);
1984        assert_eq!(queue.sticky_random_selection, None);
1985        assert_eq!(queue.yielded_skip, None);
1986    }
1987}