#![cfg_attr(not(feature = "full"), allow(dead_code))]
use crate::runtime::context;
#[derive(Debug, Copy, Clone)]
pub(crate) struct Budget(Option<u8>);
pub(crate) struct BudgetDecrement {
success: bool,
hit_zero: bool,
}
impl Budget {
const fn initial() -> Budget {
Budget(Some(128))
}
pub(super) const fn unconstrained() -> Budget {
Budget(None)
}
fn has_remaining(self) -> bool {
self.0.map(|budget| budget > 0).unwrap_or(true)
}
}
#[inline(always)]
pub(crate) fn budget<R>(f: impl FnOnce() -> R) -> R {
with_budget(Budget::initial(), f)
}
#[inline(always)]
pub(crate) fn with_unconstrained<R>(f: impl FnOnce() -> R) -> R {
with_budget(Budget::unconstrained(), f)
}
#[inline(always)]
fn with_budget<R>(budget: Budget, f: impl FnOnce() -> R) -> R {
struct ResetGuard {
prev: Budget,
}
impl Drop for ResetGuard {
fn drop(&mut self) {
let _ = context::budget(|cell| {
cell.set(self.prev);
});
}
}
#[allow(unused_variables)]
let maybe_guard = context::budget(|cell| {
let prev = cell.get();
cell.set(budget);
ResetGuard { prev }
});
f()
}
#[inline(always)]
pub(crate) fn has_budget_remaining() -> bool {
context::budget(|cell| cell.get().has_remaining()).unwrap_or(true)
}
cfg_rt_multi_thread! {
pub(crate) fn set(budget: Budget) {
let _ = context::budget(|cell| cell.set(budget));
}
}
cfg_rt! {
pub(crate) fn stop() -> Budget {
context::budget(|cell| {
let prev = cell.get();
cell.set(Budget::unconstrained());
prev
}).unwrap_or(Budget::unconstrained())
}
}
cfg_coop! {
use std::cell::Cell;
use std::task::{Context, Poll};
#[must_use]
pub(crate) struct RestoreOnPending(Cell<Budget>);
impl RestoreOnPending {
pub(crate) fn made_progress(&self) {
self.0.set(Budget::unconstrained());
}
}
impl Drop for RestoreOnPending {
fn drop(&mut self) {
let budget = self.0.get();
if !budget.is_unconstrained() {
let _ = context::budget(|cell| {
cell.set(budget);
});
}
}
}
#[inline]
pub(crate) fn poll_proceed(cx: &mut Context<'_>) -> Poll<RestoreOnPending> {
context::budget(|cell| {
let mut budget = cell.get();
let decrement = budget.decrement();
if decrement.success {
let restore = RestoreOnPending(Cell::new(cell.get()));
cell.set(budget);
if decrement.hit_zero {
inc_budget_forced_yield_count();
}
Poll::Ready(restore)
} else {
cx.waker().wake_by_ref();
Poll::Pending
}
}).unwrap_or(Poll::Ready(RestoreOnPending(Cell::new(Budget::unconstrained()))))
}
cfg_rt! {
cfg_metrics! {
#[inline(always)]
fn inc_budget_forced_yield_count() {
let _ = context::with_current(|handle| {
handle.scheduler_metrics().inc_budget_forced_yield_count();
});
}
}
cfg_not_metrics! {
#[inline(always)]
fn inc_budget_forced_yield_count() {}
}
}
cfg_not_rt! {
#[inline(always)]
fn inc_budget_forced_yield_count() {}
}
impl Budget {
fn decrement(&mut self) -> BudgetDecrement {
if let Some(num) = &mut self.0 {
if *num > 0 {
*num -= 1;
let hit_zero = *num == 0;
BudgetDecrement { success: true, hit_zero }
} else {
BudgetDecrement { success: false, hit_zero: false }
}
} else {
BudgetDecrement { success: true, hit_zero: false }
}
}
fn is_unconstrained(self) -> bool {
self.0.is_none()
}
}
}
#[cfg(all(test, not(loom)))]
mod test {
use super::*;
#[cfg(tokio_wasm_not_wasi)]
use wasm_bindgen_test::wasm_bindgen_test as test;
fn get() -> Budget {
context::budget(|cell| cell.get()).unwrap_or(Budget::unconstrained())
}
#[test]
fn budgeting() {
use futures::future::poll_fn;
use tokio_test::*;
assert!(get().0.is_none());
let coop = assert_ready!(task::spawn(()).enter(|cx, _| poll_proceed(cx)));
assert!(get().0.is_none());
drop(coop);
assert!(get().0.is_none());
budget(|| {
assert_eq!(get().0, Budget::initial().0);
let coop = assert_ready!(task::spawn(()).enter(|cx, _| poll_proceed(cx)));
assert_eq!(get().0.unwrap(), Budget::initial().0.unwrap() - 1);
drop(coop);
assert_eq!(get().0, Budget::initial().0);
let coop = assert_ready!(task::spawn(()).enter(|cx, _| poll_proceed(cx)));
assert_eq!(get().0.unwrap(), Budget::initial().0.unwrap() - 1);
coop.made_progress();
drop(coop);
assert_eq!(get().0.unwrap(), Budget::initial().0.unwrap() - 1);
let coop = assert_ready!(task::spawn(()).enter(|cx, _| poll_proceed(cx)));
assert_eq!(get().0.unwrap(), Budget::initial().0.unwrap() - 2);
coop.made_progress();
drop(coop);
assert_eq!(get().0.unwrap(), Budget::initial().0.unwrap() - 2);
budget(|| {
assert_eq!(get().0, Budget::initial().0);
let coop = assert_ready!(task::spawn(()).enter(|cx, _| poll_proceed(cx)));
assert_eq!(get().0.unwrap(), Budget::initial().0.unwrap() - 1);
coop.made_progress();
drop(coop);
assert_eq!(get().0.unwrap(), Budget::initial().0.unwrap() - 1);
});
assert_eq!(get().0.unwrap(), Budget::initial().0.unwrap() - 2);
});
assert!(get().0.is_none());
budget(|| {
let n = get().0.unwrap();
for _ in 0..n {
let coop = assert_ready!(task::spawn(()).enter(|cx, _| poll_proceed(cx)));
coop.made_progress();
}
let mut task = task::spawn(poll_fn(|cx| {
let coop = ready!(poll_proceed(cx));
coop.made_progress();
Poll::Ready(())
}));
assert_pending!(task.poll());
});
}
}