nuclei/coop.rs
1//! Cooperative scheduling: every poll of a task (and of `block_on`'s own
2//! future) gets a budget of [`BUDGET`] operations. IO that completes spends
3//! one, and so does a receive, acquire or lock of `nuclei::sync` that
4//! completes without queueing (a wait that queued and was granted spends
5//! nothing); a task whose budget is spent is answered `Pending` and woken at
6//! once, so it goes behind the other tasks of its shard instead of starving
7//! them on IO or a channel that is always ready. An IO operation that does
8//! not complete is refunded: only progress spends budget.
9//!
10//! The budget is a per-thread counter, saved and restored around each poll,
11//! so a `block_on` nested in a task gives its own polls a budget of their own
12//! and leaves the task's as it was. It lives in the thread's one context
13//! (`rt::context`) next to the id of the task being polled, so a task's poll
14//! saves and restores both through one access. Outside a budgeted poll, and
15//! in an [`unconstrained`] one, the counter holds `UNBUDGETED`, which no
16//! thread could spend in its life: an operation there spends without a branch
17//! of its own, and never yields.
18
19use core::cell::Cell;
20use core::future::Future;
21use core::pin::Pin;
22use core::task::{Context, Poll, Waker};
23
24use crate::rt::task::Id;
25
26/// Operations one poll of a task may complete before it yields.
27pub const BUDGET: u8 = 128;
28
29/// The budget outside a budgeted poll: `2^64 - 1` operations, more than a
30/// thread completes at one per nanosecond in 500 years, so it never runs out.
31pub(crate) const UNBUDGETED: u64 = u64::MAX;
32
33/// The budget left in this thread's poll.
34#[inline(always)]
35fn left() -> &'static Cell<u64> {
36 &crate::rt::context::local().budget
37}
38
39/// What [`enter_task`] replaced, for [`leave_task`] to put back.
40#[derive(Clone, Copy)]
41pub(crate) struct Entered {
42 left: u64,
43 task: Option<Id>,
44}
45
46/// Starts the poll of task `id`: a fresh budget, and `id` as the current
47/// task. Answers what it replaced (a `block_on` inside a task polls other
48/// tasks in between).
49#[inline(always)]
50pub(crate) fn enter_task(id: Id) -> Entered {
51 let here = crate::rt::context::local();
52 Entered {
53 left: here.budget.replace(u64::from(BUDGET)),
54 task: here.task.replace(Some(id)),
55 }
56}
57
58/// Puts back what [`enter_task`] replaced.
59#[inline(always)]
60pub(crate) fn leave_task(entered: Entered) {
61 let here = crate::rt::context::local();
62 here.budget.set(entered.left);
63 here.task.set(entered.task);
64}
65
66/// The task being polled on this thread, if any.
67#[inline]
68pub(crate) fn current_task() -> Option<Id> {
69 crate::rt::context::local().task.get()
70}
71
72/// Starts a poll's budget, and answers the budget before it.
73#[inline]
74pub(crate) fn enter() -> u64 {
75 left().replace(u64::from(BUDGET))
76}
77
78/// Puts back the budget `enter` replaced.
79#[inline]
80pub(crate) fn leave(previous: u64) {
81 left().set(previous);
82}
83
84/// Runs `f` outside the budget, as a call that does not wait (a `try_`
85/// read or write) is: it neither spends nor yields.
86#[inline]
87pub(crate) fn outside<T>(f: impl FnOnce() -> T) -> T {
88 let left = left();
89 let saved = left.replace(UNBUDGETED);
90 let result = f();
91 left.set(saved);
92 result
93}
94
95/// Spends one operation of the budget for an operation about to complete,
96/// and answers whether it may. False when the budget is spent: nothing is
97/// spent, and the operation completes nothing and yields.
98#[inline(always)]
99pub(crate) fn try_spend() -> bool {
100 let left = left();
101 // Stored before the branch, so the step is one decrement of the counter
102 // in memory; a spent budget has its zero put back.
103 let (rest, spent) = left.get().overflowing_sub(1);
104 left.set(rest);
105 if spent {
106 was_spent();
107 }
108 !spent
109}
110
111#[cold]
112#[inline(never)]
113fn was_spent() {
114 left().set(0);
115}
116
117/// [`try_spend`] for an operation polled with `waker`: when the budget is
118/// spent the task is woken, to run again after the shard's other tasks, and
119/// the caller answers `Pending`.
120#[inline(always)]
121pub(crate) fn spend(waker: &Waker) -> bool {
122 try_spend() || refused(waker)
123}
124
125#[cold]
126#[inline(never)]
127fn refused(waker: &Waker) -> bool {
128 crate::rt::counters::budget_yield();
129 waker.wake_by_ref();
130 false
131}
132
133/// Yields for a spent budget, as an operation [`spend`] refuses does: the task
134/// runs again after the shard's other tasks.
135pub(crate) async fn yield_spent() {
136 crate::rt::counters::budget_yield();
137 crate::task::yield_now().await;
138}
139
140/// Whether the budget is spent, for an operation that cannot undo what it
141/// completes: it looks before it starts, and spends with [`try_spend`] once it
142/// completed. When the budget is spent the task is woken and the caller
143/// answers `Pending` without starting.
144#[inline]
145pub(crate) fn exhausted(waker: &Waker) -> bool {
146 left().get() == 0 && !refused(waker)
147}
148
149/// Spends one operation of a budget [`exhausted`] just found not spent, for
150/// the operation that then completed.
151#[inline(always)]
152pub(crate) fn spent_one() {
153 let left = left();
154 let rest = left.get();
155 debug_assert_ne!(rest, 0, "spent without a look at the budget");
156 left.set(rest.wrapping_sub(1));
157}
158
159/// Gives back the operation [`try_spend`] took, for one that did not
160/// complete after all; read again, as a nested poll may have run its own
161/// budget meanwhile.
162#[inline]
163fn refund() {
164 let left = left();
165 left.set(left.get().saturating_add(1));
166}
167
168/// Spends one operation of the budget for `poll`, or yields when it is spent:
169/// `Pending`, with the task woken to run again after the others. A `poll`
170/// that answers `Pending` gets its operation back. Always inlined: it wraps
171/// every IO operation's poll, and its spent branch is a call out of line.
172#[inline(always)]
173pub(crate) fn poll_budget<T>(
174 cx: &mut Context<'_>,
175 poll: impl FnOnce(&mut Context<'_>) -> Poll<T>,
176) -> Poll<T> {
177 if !spend(cx.waker()) {
178 return Poll::Pending;
179 }
180 let result = poll(cx);
181 if result.is_pending() {
182 refund();
183 }
184 result
185}
186
187/// Whether the current poll has budget left (always `true` outside a poll
188/// and in an [`unconstrained`] one).
189pub fn has_budget_remaining() -> bool {
190 left().get() != 0
191}
192
193/// Spends one operation of the budget, yielding first when it is spent: a
194/// loop of work that awaits nothing else can call it to stay fair.
195pub async fn consume_budget() {
196 core::future::poll_fn(|cx| poll_budget(cx, |_| Poll::Ready(()))).await
197}
198
199/// Runs `future` with no budget: none of its operations spends any, and it
200/// never yields for the budget's sake.
201pub fn unconstrained<F: Future>(future: F) -> Unconstrained<F> {
202 Unconstrained { future }
203}
204
205/// The future of [`unconstrained`].
206#[derive(Debug)]
207#[must_use = "futures do nothing unless polled"]
208pub struct Unconstrained<F> {
209 future: F,
210}
211
212impl<F: Future> Future for Unconstrained<F> {
213 type Output = F::Output;
214
215 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
216 // SAFETY: `future` is structurally pinned and never moved.
217 let future = unsafe { self.map_unchecked_mut(|this| &mut this.future) };
218 outside(|| future.poll(cx))
219 }
220}
221
222/// Runs `future` under the budget: each of its polls spends one operation,
223/// and it yields instead of being polled when the budget is spent.
224pub fn cooperative<F: Future>(future: F) -> Cooperative<F> {
225 Cooperative { future }
226}
227
228/// The future of [`cooperative`].
229#[derive(Debug)]
230#[must_use = "futures do nothing unless polled"]
231pub struct Cooperative<F> {
232 future: F,
233}
234
235impl<F: Future> Future for Cooperative<F> {
236 type Output = F::Output;
237
238 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
239 // SAFETY: `future` is structurally pinned and never moved.
240 let future = unsafe { self.map_unchecked_mut(|this| &mut this.future) };
241 poll_budget(cx, |cx| future.poll(cx))
242 }
243}