embassy_supervisor/budget.rs
1//! A divisible resource: one budget of units, split among the nodes that
2//! declare `resources: [NAME: divisible]`, with every holder's share released
3//! by the supervisor when that holder stops.
4
5use core::cell::Cell;
6use core::task::{Poll, Waker};
7
8use embassy_sync::blocking_mutex::Mutex;
9use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
10use embassy_sync::signal::Signal;
11use embassy_sync::waitqueue::AtomicWaker;
12use embassy_time::{Duration, Instant};
13use portable_atomic::{AtomicU32, Ordering};
14
15use crate::ResourceGate;
16
17/// A budget of `u32` units divided among `N` claimant slots.
18///
19/// Declared (as a `pub static`) by [`supervisor_graph!`](crate::supervisor_graph)
20/// for each `divisible` resource name, sized to the nodes and pool members that
21/// declare it: one slot each, numbered in declaration order. The protocol:
22///
23/// 1. Something [`provide`](Self::provide)s the capacity — `main` for a fixed
24/// budget, or an allocator node that also names the slot in `provides:` so
25/// the budget empties when it stops. A holder whose budget is still
26/// unprovided at its gate deadline faults with
27/// [`FaultKind::ResourceMissing`](crate::FaultKind::ResourceMissing), like
28/// any other slot.
29/// 2. Each holder's shell receives a [`Claimant`] bound to its slot and states
30/// what it [`want`](Claimant::want)s. The allocator divides the capacity
31/// over the wants with a [`BudgetPolicy`] ([`rebalance`](Self::rebalance)),
32/// and each holder reads its [`grant`](Claimant::grant), or parks on
33/// [`wait_grant_change`](Claimant::wait_grant_change) until it moves.
34/// 3. When a holder stops — cleanly, or by missing its shutdown ack — the
35/// supervisor [`release`](Self::release)s its slot, so a dead session never
36/// strands its share. A parked (`Pause`) holder keeps its claim.
37///
38/// The budget never chooses a division itself: what "fair" means (equal,
39/// proportional, ramped) is the policy's, and *when* to re-divide is the
40/// allocator's — usually on [`wait_change`](Self::wait_change), which fires on
41/// every want, release and capacity change. Costs `4 + 8N` bytes of atomics,
42/// two `Signal`s and `N` single-waker slots per budget: `28 + 16N` bytes on a
43/// 32-bit target.
44pub struct Budget<const N: usize> {
45 /// `0` is "not provided": the gate reads empty.
46 capacity: AtomicU32,
47 wants: [AtomicU32; N],
48 grants: [AtomicU32; N],
49 /// The [`ResourceGate`] wake for the supervisor's pre-spawn wait.
50 filled: Signal<CriticalSectionRawMutex, ()>,
51 /// Holders parked in `wait_grant_change`, at most one per slot.
52 claimants: [AtomicWaker; N],
53 /// The allocator's wake, single waiter: anything that should trigger a
54 /// re-division.
55 watch: Signal<CriticalSectionRawMutex, ()>,
56}
57
58impl<const N: usize> Default for Budget<N> {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl<const N: usize> Budget<N> {
65 /// An unprovided budget (`const` — it lives in a `static` the macro emits).
66 pub const fn new() -> Self {
67 const { assert!(N > 0, "a Budget needs at least one claimant slot") };
68 Self {
69 capacity: AtomicU32::new(0),
70 wants: [const { AtomicU32::new(0) }; N],
71 grants: [const { AtomicU32::new(0) }; N],
72 filled: Signal::new(),
73 claimants: [const { AtomicWaker::new() }; N],
74 watch: Signal::new(),
75 }
76 }
77
78 /// Set the capacity and wake both the supervisor's gate wait and the
79 /// allocator.
80 pub fn provide(&self, capacity: u32) {
81 self.capacity.store(capacity, Ordering::Release);
82 if capacity > 0 {
83 self.filled.signal(());
84 crate::__sv_gate_event();
85 }
86 self.watch.signal(());
87 }
88
89 /// The number of claimant slots: what the graph sized the budget to.
90 pub const fn slots(&self) -> usize {
91 N
92 }
93
94 /// The provided capacity, `0` when unprovided.
95 pub fn capacity(&self) -> u32 {
96 self.capacity.load(Ordering::Acquire)
97 }
98
99 /// State slot `slot`'s demand and wake the allocator.
100 pub fn want(&self, slot: u8, units: u32) {
101 self.wants[usize::from(slot)].store(units, Ordering::Release);
102 self.watch.signal(());
103 }
104
105 /// Drop slot `slot`'s demand and grant, and wake the allocator: what the
106 /// supervisor does when the slot's holder stops.
107 pub fn release(&self, slot: u8) {
108 let slot = usize::from(slot);
109 self.wants[slot].store(0, Ordering::Release);
110 self.grants[slot].store(0, Ordering::Release);
111 self.watch.signal(());
112 }
113
114 /// Slot `slot`'s current grant.
115 pub fn grant(&self, slot: u8) -> u32 {
116 self.grants[usize::from(slot)].load(Ordering::Acquire)
117 }
118
119 /// Slot `slot`'s stated demand.
120 pub fn want_of(&self, slot: u8) -> u32 {
121 self.wants[usize::from(slot)].load(Ordering::Acquire)
122 }
123
124 /// The sum of every slot's grant.
125 pub fn total_granted(&self) -> u32 {
126 self.grants
127 .iter()
128 .fold(0u32, |acc, g| acc.saturating_add(g.load(Ordering::Acquire)))
129 }
130
131 /// Re-divide the capacity over the current wants with `policy`, publish
132 /// the new grants, and wake every holder whose grant moved.
133 pub fn rebalance(&self, policy: &impl BudgetPolicy, now: Instant) -> Option<Instant> {
134 let capacity = self.capacity();
135 let wants: [u32; N] = core::array::from_fn(|i| self.wants[i].load(Ordering::Acquire));
136 let mut grants: [u32; N] = core::array::from_fn(|i| self.grants[i].load(Ordering::Acquire));
137 let next = policy.divide(capacity, &wants, &mut grants, now);
138 for (i, g) in grants.iter().enumerate() {
139 if self.grants[i].swap(*g, Ordering::AcqRel) != *g {
140 self.claimants[i].wake();
141 }
142 }
143 let stale = self.capacity() != capacity
144 || wants
145 .iter()
146 .zip(&self.wants)
147 .any(|(seen, cur)| cur.load(Ordering::Acquire) != *seen);
148 if stale {
149 self.watch.signal(());
150 }
151 next
152 }
153
154 /// The allocator's wait: resolves after any want, release or capacity
155 /// change since the last wait (latching, single waiter).
156 pub async fn wait_change(&self) {
157 self.watch.wait().await;
158 }
159
160 /// The handle a holder of `slot` claims through. Emitted by the macro
161 /// into the holder's task shell; hand-built nodes may call it directly.
162 pub fn claimant(&'static self, slot: u8) -> Claimant {
163 Claimant { budget: self, slot }
164 }
165
166 fn wake_claimants(&self) {
167 for w in &self.claimants {
168 w.wake();
169 }
170 }
171}
172
173impl<const N: usize> ResourceGate for Budget<N> {
174 fn is_filled(&self) -> bool {
175 self.capacity() > 0
176 }
177
178 fn filled_signal(&self) -> &Signal<CriticalSectionRawMutex, ()> {
179 &self.filled
180 }
181
182 /// Empty the budget: no capacity, no grants. Holders are woken so a loop
183 /// parked on its grant sees the zero.
184 fn clear(&self) {
185 self.capacity.store(0, Ordering::Release);
186 for g in &self.grants {
187 g.store(0, Ordering::Release);
188 }
189 self.filled.reset();
190 self.wake_claimants();
191 self.watch.signal(());
192 }
193}
194
195/// The object-safe view of a [`Budget`] a [`Claimant`] and a node's claims
196/// table go through, so neither names `N`.
197pub trait Divisible: Sync {
198 /// State `slot`'s demand.
199 fn want(&self, slot: u8, units: u32);
200 /// Drop `slot`'s demand and grant.
201 fn release(&self, slot: u8);
202 /// `slot`'s current grant.
203 fn grant(&self, slot: u8) -> u32;
204 /// Park `waker` until the next rebalance that moves `slot`'s grant.
205 fn register(&self, slot: u8, waker: &Waker);
206}
207
208impl<const N: usize> Divisible for Budget<N> {
209 fn want(&self, slot: u8, units: u32) {
210 Budget::want(self, slot, units);
211 }
212
213 fn release(&self, slot: u8) {
214 Budget::release(self, slot);
215 }
216
217 fn grant(&self, slot: u8) -> u32 {
218 Budget::grant(self, slot)
219 }
220
221 fn register(&self, slot: u8, waker: &Waker) {
222 self.claimants[usize::from(slot)].register(waker);
223 }
224}
225
226/// A holder's handle on one slot of a [`Budget`]: what a `divisible` entry
227/// hands the worker. `Copy`, so a worker may keep one per loop it runs.
228#[derive(Clone, Copy)]
229pub struct Claimant {
230 budget: &'static dyn Divisible,
231 slot: u8,
232}
233
234impl Claimant {
235 /// State this slot's demand.
236 pub fn want(&self, units: u32) {
237 self.budget.want(self.slot, units);
238 }
239
240 /// Give the share back early. The supervisor does this when the holder
241 /// stops; a holder that is done with the budget mid-run does it itself.
242 pub fn release(&self) {
243 self.budget.release(self.slot);
244 }
245
246 /// This slot's current grant.
247 pub fn grant(&self) -> u32 {
248 self.budget.grant(self.slot)
249 }
250
251 /// Wait until the grant differs from `seen`, then return it.
252 /// Uses a check/register/recheck to avoid missing a rebalance between the
253 /// load and the park. Only one waiter may park per slot; use your own
254 /// `Claimant`.
255 pub async fn wait_grant_change(&self, seen: u32) -> u32 {
256 core::future::poll_fn(|cx| {
257 let g = self.grant();
258 if g != seen {
259 return Poll::Ready(g);
260 }
261 self.budget.register(self.slot, cx.waker());
262 let g = self.grant();
263 if g != seen {
264 Poll::Ready(g)
265 } else {
266 Poll::Pending
267 }
268 })
269 .await
270 }
271
272 /// The slot this handle is bound to.
273 pub const fn slot(&self) -> u8 {
274 self.slot
275 }
276}
277
278/// How a [`Budget`]'s capacity is divided over its holders' wants.
279///
280/// `&self`, like [`ScalingPolicy`](crate::ScalingPolicy): a policy that keeps
281/// state (a ramp deadline) holds it in interior mutability so the impl can
282/// live in a `static`.
283pub trait BudgetPolicy {
284 /// Write the new grants into `grants`, which arrives holding the previous
285 /// division. Return when to run again while the division is converging,
286 /// `None` once it is settled.
287 fn divide(
288 &self,
289 capacity: u32,
290 wants: &[u32],
291 grants: &mut [u32],
292 now: Instant,
293 ) -> Option<Instant>;
294}
295
296/// Every holder gets its want when the capacity covers the sum; otherwise
297/// the capacity is split in proportion to the wants, with the integer
298/// remainder handed one unit at a time to the lowest slots that can use it.
299/// Changes apply immediately in both directions.
300pub struct FairShare;
301
302impl FairShare {
303 fn targets(capacity: u32, wants: &[u32], mut visit: impl FnMut(usize, u32)) {
304 let total: u64 = wants.iter().map(|w| u64::from(*w)).sum();
305 if total <= u64::from(capacity) {
306 for (i, w) in wants.iter().enumerate() {
307 visit(i, *w);
308 }
309 return;
310 }
311 let capacity = u64::from(capacity);
312 let floor = |w: &u32| (capacity * u64::from(*w) / total) as u32;
313 let used: u64 = wants.iter().map(|w| u64::from(floor(w))).sum();
314 let mut left = capacity - used;
315 for (i, w) in wants.iter().enumerate() {
316 let mut t = floor(w);
317 if left > 0 && t < *w {
318 t += 1;
319 left -= 1;
320 }
321 visit(i, t);
322 }
323 }
324}
325
326impl BudgetPolicy for FairShare {
327 fn divide(
328 &self,
329 capacity: u32,
330 wants: &[u32],
331 grants: &mut [u32],
332 _now: Instant,
333 ) -> Option<Instant> {
334 Self::targets(capacity, wants, |i, t| grants[i] = t);
335 None
336 }
337}
338
339/// [`FairShare`]'s division, applied asymmetrically: a cut lands at once, an
340/// increase is ramped at most `step` units per `interval`. The safety shape
341/// of a shared power or bandwidth budget — a holder must never be granted
342/// more than the budget can carry, so reductions cannot wait, while a holder
343/// drawing more can wait for the others to have backed off.
344pub struct ShrinkFastGrowSlow {
345 step: u32,
346 interval: Duration,
347 /// The earliest instant the next increase may land.
348 next: Mutex<CriticalSectionRawMutex, Cell<Option<Instant>>>,
349}
350
351impl ShrinkFastGrowSlow {
352 /// A policy raising any grant by at most `step` units per `interval`.
353 pub const fn new(step: u32, interval: Duration) -> Self {
354 Self {
355 step,
356 interval,
357 next: Mutex::new(Cell::new(None)),
358 }
359 }
360}
361
362impl BudgetPolicy for ShrinkFastGrowSlow {
363 fn divide(
364 &self,
365 capacity: u32,
366 wants: &[u32],
367 grants: &mut [u32],
368 now: Instant,
369 ) -> Option<Instant> {
370 let due = self.next.lock(|c| c.get()).is_none_or(|t| now >= t);
371 let mut converging = false;
372 FairShare::targets(capacity, wants, |i, t| {
373 let g = &mut grants[i];
374 if t < *g {
375 *g = t;
376 } else if t > *g {
377 if due {
378 *g = (*g).saturating_add(self.step).min(t);
379 }
380 converging |= *g < t;
381 }
382 });
383 let next = if converging {
384 let at = if due {
385 now + self.interval
386 } else {
387 self.next.lock(|c| c.get()).unwrap_or(now)
388 };
389 Some(at)
390 } else {
391 None
392 };
393 self.next.lock(|c| c.set(next));
394 next
395 }
396}