Skip to content

Commit 52da177

Browse files
authored
metrics: add a new metric for budget exhaustion yields (#5517)
1 parent ee1c940 commit 52da177

File tree

4 files changed

+140
-5
lines changed

4 files changed

+140
-5
lines changed

tokio/src/runtime/coop.rs

+42-5
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ use crate::runtime::context;
3636
#[derive(Debug, Copy, Clone)]
3737
pub(crate) struct Budget(Option<u8>);
3838

39+
pub(crate) struct BudgetDecrement {
40+
success: bool,
41+
hit_zero: bool,
42+
}
43+
3944
impl Budget {
4045
/// Budget assigned to a task on each poll.
4146
///
@@ -172,9 +177,17 @@ cfg_coop! {
172177
context::budget(|cell| {
173178
let mut budget = cell.get();
174179

175-
if budget.decrement() {
180+
let decrement = budget.decrement();
181+
182+
if decrement.success {
176183
let restore = RestoreOnPending(Cell::new(cell.get()));
177184
cell.set(budget);
185+
186+
// avoid double counting
187+
if decrement.hit_zero {
188+
inc_budget_forced_yield_count();
189+
}
190+
178191
Poll::Ready(restore)
179192
} else {
180193
cx.waker().wake_by_ref();
@@ -183,19 +196,43 @@ cfg_coop! {
183196
}).unwrap_or(Poll::Ready(RestoreOnPending(Cell::new(Budget::unconstrained()))))
184197
}
185198

199+
cfg_rt! {
200+
cfg_metrics! {
201+
#[inline(always)]
202+
fn inc_budget_forced_yield_count() {
203+
if let Ok(handle) = context::try_current() {
204+
handle.scheduler_metrics().inc_budget_forced_yield_count();
205+
}
206+
}
207+
}
208+
209+
cfg_not_metrics! {
210+
#[inline(always)]
211+
fn inc_budget_forced_yield_count() {}
212+
}
213+
}
214+
215+
cfg_not_rt! {
216+
#[inline(always)]
217+
fn inc_budget_forced_yield_count() {}
218+
}
219+
186220
impl Budget {
187221
/// Decrements the budget. Returns `true` if successful. Decrementing fails
188222
/// when there is not enough remaining budget.
189-
fn decrement(&mut self) -> bool {
223+
fn decrement(&mut self) -> BudgetDecrement {
190224
if let Some(num) = &mut self.0 {
191225
if *num > 0 {
192226
*num -= 1;
193-
true
227+
228+
let hit_zero = *num == 0;
229+
230+
BudgetDecrement { success: true, hit_zero }
194231
} else {
195-
false
232+
BudgetDecrement { success: false, hit_zero: false }
196233
}
197234
} else {
198-
true
235+
BudgetDecrement { success: true, hit_zero: false }
199236
}
200237
}
201238

tokio/src/runtime/metrics/runtime.rs

+15
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,21 @@ impl RuntimeMetrics {
124124
.load(Relaxed)
125125
}
126126

127+
/// Returns the number of times that tasks have been forced to yield back to the scheduler
128+
/// after exhausting their task budgets.
129+
///
130+
/// This count starts at zero when the runtime is created and increases by one each time a task yields due to exhausting its budget.
131+
///
132+
/// The counter is monotonically increasing. It is never decremented or
133+
/// reset to zero.
134+
pub fn budget_forced_yield_count(&self) -> u64 {
135+
self.handle
136+
.inner
137+
.scheduler_metrics()
138+
.budget_forced_yield_count
139+
.load(Relaxed)
140+
}
141+
127142
/// Returns the total number of times the given worker thread has parked.
128143
///
129144
/// The worker park count starts at zero when the runtime is created and

tokio/src/runtime/metrics/scheduler.rs

+7
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,24 @@ use crate::loom::sync::atomic::{AtomicU64, Ordering::Relaxed};
1111
pub(crate) struct SchedulerMetrics {
1212
/// Number of tasks that are scheduled from outside the runtime.
1313
pub(super) remote_schedule_count: AtomicU64,
14+
pub(super) budget_forced_yield_count: AtomicU64,
1415
}
1516

1617
impl SchedulerMetrics {
1718
pub(crate) fn new() -> SchedulerMetrics {
1819
SchedulerMetrics {
1920
remote_schedule_count: AtomicU64::new(0),
21+
budget_forced_yield_count: AtomicU64::new(0),
2022
}
2123
}
2224

2325
/// Increment the number of tasks scheduled externally
2426
pub(crate) fn inc_remote_schedule_count(&self) {
2527
self.remote_schedule_count.fetch_add(1, Relaxed);
2628
}
29+
30+
/// Increment the number of tasks forced to yield due to budget exhaustion
31+
pub(crate) fn inc_budget_forced_yield_count(&self) {
32+
self.budget_forced_yield_count.fetch_add(1, Relaxed);
33+
}
2734
}

tokio/tests/rt_metrics.rs

+76
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
#![warn(rust_2018_idioms)]
22
#![cfg(all(feature = "full", tokio_unstable, not(tokio_wasi)))]
33

4+
use std::future::Future;
45
use std::sync::{Arc, Mutex};
6+
use std::task::Poll;
7+
use tokio::macros::support::poll_fn;
58

69
use tokio::runtime::Runtime;
10+
use tokio::task::consume_budget;
711
use tokio::time::{self, Duration};
812

913
#[test]
@@ -433,6 +437,78 @@ fn worker_local_queue_depth() {
433437
});
434438
}
435439

440+
#[test]
441+
fn budget_exhaustion_yield() {
442+
let rt = current_thread();
443+
let metrics = rt.metrics();
444+
445+
assert_eq!(0, metrics.budget_forced_yield_count());
446+
447+
let mut did_yield = false;
448+
449+
// block on a task which consumes budget until it yields
450+
rt.block_on(poll_fn(|cx| loop {
451+
if did_yield {
452+
return Poll::Ready(());
453+
}
454+
455+
let fut = consume_budget();
456+
tokio::pin!(fut);
457+
458+
if fut.poll(cx).is_pending() {
459+
did_yield = true;
460+
return Poll::Pending;
461+
}
462+
}));
463+
464+
assert_eq!(1, rt.metrics().budget_forced_yield_count());
465+
}
466+
467+
#[test]
468+
fn budget_exhaustion_yield_with_joins() {
469+
let rt = current_thread();
470+
let metrics = rt.metrics();
471+
472+
assert_eq!(0, metrics.budget_forced_yield_count());
473+
474+
let mut did_yield_1 = false;
475+
let mut did_yield_2 = false;
476+
477+
// block on a task which consumes budget until it yields
478+
rt.block_on(async {
479+
tokio::join!(
480+
poll_fn(|cx| loop {
481+
if did_yield_1 {
482+
return Poll::Ready(());
483+
}
484+
485+
let fut = consume_budget();
486+
tokio::pin!(fut);
487+
488+
if fut.poll(cx).is_pending() {
489+
did_yield_1 = true;
490+
return Poll::Pending;
491+
}
492+
}),
493+
poll_fn(|cx| loop {
494+
if did_yield_2 {
495+
return Poll::Ready(());
496+
}
497+
498+
let fut = consume_budget();
499+
tokio::pin!(fut);
500+
501+
if fut.poll(cx).is_pending() {
502+
did_yield_2 = true;
503+
return Poll::Pending;
504+
}
505+
})
506+
)
507+
});
508+
509+
assert_eq!(1, rt.metrics().budget_forced_yield_count());
510+
}
511+
436512
#[cfg(any(target_os = "linux", target_os = "macos"))]
437513
#[test]
438514
fn io_driver_fd_count() {

0 commit comments

Comments
 (0)