rt(threaded): basic self-tuning of injection queue (#5720)

Each multi-threaded runtime worker prioritizes pulling tasks off of its
local queue. Every so often, it checks the injection (global) queue for
work submitted there. Previously, "every so often," was a constant
"number of tasks polled" value. Tokio sets a default of 61, but allows
users to configure this value.

If workers are under load with tasks that are slow to poll, the
injection queue can be starved. To prevent starvation in this case, this
commit implements some basic self-tuning. The multi-threaded scheduler
tracks the mean task poll time using an exponentially-weighted moving
average. It then uses this value to pick an interval at which to check
the injection queue.

This commit is a first pass at adding self-tuning to the scheduler.
There are other values in the scheduler that could benefit from
self-tuning (e.g. the maintenance interval). Additionally, the
current-thread scheduler could also benfit from self-tuning. However, we
have reached the point where we should start investigating ways to unify
logic in both schedulers. Adding self-tuning to the current-thread
scheduler will be punted until after this unification.
This commit is contained in:
Carl Lerche
2023-06-01 08:13:24 -07:00
committed by GitHub
parent 7c12e41d07
commit 79a7e78c0d
15 changed files with 513 additions and 106 deletions
+3 -3
View File
@@ -366,14 +366,14 @@ jobs:
# First run with all features (including parking_lot)
- run: cross test -p tokio --all-features --target ${{ matrix.target }} --tests
env:
RUSTFLAGS: --cfg tokio_unstable -Dwarnings --cfg tokio_no_ipv6 ${{ matrix.rustflags }}
RUSTFLAGS: --cfg tokio_unstable -Dwarnings --cfg tokio_no_ipv6 --cfg tokio_cross ${{ matrix.rustflags }}
# Now run without parking_lot
- name: Remove `parking_lot` from `full` feature
run: sed -i '0,/parking_lot/{/parking_lot/d;}' tokio/Cargo.toml
# The `tokio_no_parking_lot` cfg is here to ensure the `sed` above does not silently break.
- run: cross test -p tokio --features full,test-util --target ${{ matrix.target }} --tests
env:
RUSTFLAGS: --cfg tokio_unstable -Dwarnings --cfg tokio_no_ipv6 --cfg tokio_no_parking_lot ${{ matrix.rustflags }}
RUSTFLAGS: --cfg tokio_unstable -Dwarnings --cfg tokio_no_ipv6 --cfg tokio_no_parking_lot --cfg tokio_cross ${{ matrix.rustflags }}
# See https://github.com/tokio-rs/tokio/issues/5187
no-atomic-u64:
@@ -393,7 +393,7 @@ jobs:
- uses: taiki-e/setup-cross-toolchain-action@v1
with:
target: i686-unknown-linux-gnu
- run: cargo test -Zbuild-std --target target-specs/i686-unknown-linux-gnu.json -p tokio --all-features
- run: cargo test -Zbuild-std --target target-specs/i686-unknown-linux-gnu.json -p tokio --all-features -- --test-threads 1 --nocapture
env:
RUSTFLAGS: --cfg tokio_unstable --cfg tokio_taskdump -Dwarnings --cfg tokio_no_atomic_u64
# https://github.com/tokio-rs/tokio/pull/5356
+63 -6
View File
@@ -6,12 +6,14 @@ use tokio::runtime::{self, Runtime};
use tokio::sync::oneshot;
use bencher::{benchmark_group, benchmark_main, Bencher};
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering::Relaxed;
use std::sync::atomic::{AtomicBool, AtomicUsize};
use std::sync::{mpsc, Arc};
use std::time::{Duration, Instant};
const NUM_WORKERS: usize = 4;
const NUM_SPAWN: usize = 10_000;
const STALL_DUR: Duration = Duration::from_micros(10);
fn spawn_many_local(b: &mut Bencher) {
let rt = rt();
@@ -57,17 +59,21 @@ fn spawn_many_remote_idle(b: &mut Bencher) {
});
}
fn spawn_many_remote_busy(b: &mut Bencher) {
// The runtime is busy with tasks that consume CPU time and yield. Yielding is a
// lower notification priority than spawning / regular notification.
fn spawn_many_remote_busy1(b: &mut Bencher) {
let rt = rt();
let rt_handle = rt.handle();
let mut handles = Vec::with_capacity(NUM_SPAWN);
let flag = Arc::new(AtomicBool::new(true));
// Spawn some tasks to keep the runtimes busy
for _ in 0..(2 * NUM_WORKERS) {
rt.spawn(async {
loop {
let flag = flag.clone();
rt.spawn(async move {
while flag.load(Relaxed) {
tokio::task::yield_now().await;
std::thread::sleep(std::time::Duration::from_micros(10));
stall();
}
});
}
@@ -83,6 +89,49 @@ fn spawn_many_remote_busy(b: &mut Bencher) {
}
});
});
flag.store(false, Relaxed);
}
// The runtime is busy with tasks that consume CPU time and spawn new high-CPU
// tasks. Spawning goes via a higher notification priority than yielding.
fn spawn_many_remote_busy2(b: &mut Bencher) {
const NUM_SPAWN: usize = 1_000;
let rt = rt();
let rt_handle = rt.handle();
let mut handles = Vec::with_capacity(NUM_SPAWN);
let flag = Arc::new(AtomicBool::new(true));
// Spawn some tasks to keep the runtimes busy
for _ in 0..(NUM_WORKERS) {
let flag = flag.clone();
fn iter(flag: Arc<AtomicBool>) {
tokio::spawn(async {
if flag.load(Relaxed) {
stall();
iter(flag);
}
});
}
rt.spawn(async {
iter(flag);
});
}
b.iter(|| {
for _ in 0..NUM_SPAWN {
handles.push(rt_handle.spawn(async {}));
}
rt.block_on(async {
for handle in handles.drain(..) {
handle.await.unwrap();
}
});
});
flag.store(false, Relaxed);
}
fn yield_many(b: &mut Bencher) {
@@ -193,11 +242,19 @@ fn rt() -> Runtime {
.unwrap()
}
fn stall() {
let now = Instant::now();
while now.elapsed() < STALL_DUR {
std::thread::yield_now();
}
}
benchmark_group!(
scheduler,
spawn_many_local,
spawn_many_remote_idle,
spawn_many_remote_busy,
spawn_many_remote_busy1,
spawn_many_remote_busy2,
ping_pong,
yield_many,
chained_spawn,
+12 -6
View File
@@ -82,7 +82,13 @@ pub struct Builder {
pub(super) keep_alive: Option<Duration>,
/// How many ticks before pulling a task from the global/remote queue?
pub(super) global_queue_interval: u32,
///
/// When `None`, the value is unspecified and behavior details are left to
/// the scheduler. Each scheduler flavor could choose to either pick its own
/// default value or use some other strategy to decide when to poll from the
/// global queue. For example, the multi-threaded scheduler uses a
/// self-tuning strategy based on mean task poll times.
pub(super) global_queue_interval: Option<u32>,
/// How many ticks before yielding to the driver for timer and I/O events?
pub(super) event_interval: u32,
@@ -211,7 +217,7 @@ impl Builder {
#[cfg(not(loom))]
const EVENT_INTERVAL: u32 = 61;
Builder::new(Kind::CurrentThread, 31, EVENT_INTERVAL)
Builder::new(Kind::CurrentThread, EVENT_INTERVAL)
}
cfg_not_wasi! {
@@ -222,7 +228,7 @@ impl Builder {
#[cfg_attr(docsrs, doc(cfg(feature = "rt-multi-thread")))]
pub fn new_multi_thread() -> Builder {
// The number `61` is fairly arbitrary. I believe this value was copied from golang.
Builder::new(Kind::MultiThread, 61, 61)
Builder::new(Kind::MultiThread, 61)
}
}
@@ -230,7 +236,7 @@ impl Builder {
/// values.
///
/// Configuration methods can be chained on the return value.
pub(crate) fn new(kind: Kind, global_queue_interval: u32, event_interval: u32) -> Builder {
pub(crate) fn new(kind: Kind, event_interval: u32) -> Builder {
Builder {
kind,
@@ -266,7 +272,7 @@ impl Builder {
// Defaults for these values depend on the scheduler kind, so we get them
// as parameters.
global_queue_interval,
global_queue_interval: None,
event_interval,
seed_generator: RngSeedGenerator::new(RngSeed::new()),
@@ -716,7 +722,7 @@ impl Builder {
/// # }
/// ```
pub fn global_queue_interval(&mut self, val: u32) -> &mut Self {
self.global_queue_interval = val;
self.global_queue_interval = Some(val);
self
}
+1 -1
View File
@@ -4,7 +4,7 @@ use crate::util::RngSeedGenerator;
pub(crate) struct Config {
/// How many ticks before pulling a task from the global/remote queue?
pub(crate) global_queue_interval: u32,
pub(crate) global_queue_interval: Option<u32>,
/// How many ticks before yielding to the driver for timer and I/O events?
pub(crate) event_interval: u32,
+13 -7
View File
@@ -34,7 +34,7 @@ pub(crate) struct MetricsBatch {
busy_duration_total: u64,
/// Instant at which work last resumed (continued after park).
last_resume_time: Instant,
processing_scheduled_tasks_started_at: Instant,
/// If `Some`, tracks poll times in nanoseconds
poll_timer: Option<PollTimer>,
@@ -62,7 +62,7 @@ impl MetricsBatch {
local_schedule_count: 0,
overflow_count: 0,
busy_duration_total: 0,
last_resume_time: now,
processing_scheduled_tasks_started_at: now,
poll_timer: worker_metrics
.poll_count_histogram
.as_ref()
@@ -106,11 +106,20 @@ impl MetricsBatch {
} else {
self.poll_count_on_last_park = self.poll_count;
}
}
let busy_duration = self.last_resume_time.elapsed();
/// Start processing a batch of tasks
pub(crate) fn start_processing_scheduled_tasks(&mut self) {
self.processing_scheduled_tasks_started_at = Instant::now();
}
/// Stop processing a batch of tasks
pub(crate) fn end_processing_scheduled_tasks(&mut self) {
let busy_duration = self.processing_scheduled_tasks_started_at.elapsed();
self.busy_duration_total += duration_as_u64(busy_duration);
}
/// Start polling an individual task
pub(crate) fn start_poll(&mut self) {
self.poll_count += 1;
@@ -119,6 +128,7 @@ impl MetricsBatch {
}
}
/// Stop polling an individual task
pub(crate) fn end_poll(&mut self) {
if let Some(poll_timer) = &mut self.poll_timer {
let elapsed = duration_as_u64(poll_timer.poll_started_at.elapsed());
@@ -126,10 +136,6 @@ impl MetricsBatch {
}
}
pub(crate) fn returned_from_park(&mut self) {
self.last_resume_time = Instant::now();
}
pub(crate) fn inc_local_schedule_count(&mut self) {
self.local_schedule_count += 1;
}
+2 -1
View File
@@ -39,8 +39,9 @@ impl MetricsBatch {
pub(crate) fn submit(&mut self, _to: &WorkerMetrics) {}
pub(crate) fn about_to_park(&mut self) {}
pub(crate) fn returned_from_park(&mut self) {}
pub(crate) fn inc_local_schedule_count(&mut self) {}
pub(crate) fn start_processing_scheduled_tasks(&mut self) {}
pub(crate) fn end_processing_scheduled_tasks(&mut self) {}
pub(crate) fn start_poll(&mut self) {}
pub(crate) fn end_poll(&mut self) {}
}
+25 -2
View File
@@ -59,6 +59,9 @@ struct Core {
/// Metrics batch
metrics: MetricsBatch,
/// How often to check the global queue
global_queue_interval: u32,
/// True if a task panicked without being handled and the runtime is
/// configured to shutdown on unhandled panic.
unhandled_panic: bool,
@@ -102,6 +105,11 @@ type Notified = task::Notified<Arc<Handle>>;
/// Initial queue capacity.
const INITIAL_CAPACITY: usize = 64;
/// Used if none is specified. This is a temporary constant and will be removed
/// as we unify tuning logic between the multi-thread and current-thread
/// schedulers.
const DEFAULT_GLOBAL_QUEUE_INTERVAL: u32 = 31;
impl CurrentThread {
pub(crate) fn new(
driver: Driver,
@@ -112,6 +120,11 @@ impl CurrentThread {
) -> (CurrentThread, Arc<Handle>) {
let worker_metrics = WorkerMetrics::from_config(&config);
// Get the configured global queue interval, or use the default.
let global_queue_interval = config
.global_queue_interval
.unwrap_or(DEFAULT_GLOBAL_QUEUE_INTERVAL);
let handle = Arc::new(Handle {
shared: Shared {
inject: Inject::new(),
@@ -131,6 +144,7 @@ impl CurrentThread {
tick: 0,
driver: Some(driver),
metrics: MetricsBatch::new(&handle.shared.worker_metrics),
global_queue_interval,
unhandled_panic: false,
})));
@@ -273,7 +287,7 @@ impl Core {
}
fn next_task(&mut self, handle: &Handle) -> Option<Notified> {
if self.tick % handle.shared.config.global_queue_interval == 0 {
if self.tick % self.global_queue_interval == 0 {
handle
.next_remote_task()
.or_else(|| self.next_local_task(handle))
@@ -362,7 +376,6 @@ impl Context {
});
core = c;
core.metrics.returned_from_park();
}
if let Some(f) = &handle.shared.config.after_unpark {
@@ -626,6 +639,8 @@ impl CoreGuard<'_> {
pin!(future);
core.metrics.start_processing_scheduled_tasks();
'outer: loop {
let handle = &context.handle;
@@ -654,12 +669,16 @@ impl CoreGuard<'_> {
let task = match entry {
Some(entry) => entry,
None => {
core.metrics.end_processing_scheduled_tasks();
core = if did_defer_tasks() {
context.park_yield(core, handle)
} else {
context.park(core, handle)
};
core.metrics.start_processing_scheduled_tasks();
// Try polling the `block_on` future next
continue 'outer;
}
@@ -674,9 +693,13 @@ impl CoreGuard<'_> {
core = c;
}
core.metrics.end_processing_scheduled_tasks();
// Yield to the driver, this drives the timer and pulls any
// pending I/O events.
core = context.park_yield(core, handle);
core.metrics.start_processing_scheduled_tasks();
}
});
@@ -9,6 +9,9 @@ pub(crate) use handle::Handle;
mod idle;
use self::idle::Idle;
mod stats;
pub(crate) use stats::Stats;
mod park;
pub(crate) use park::{Parker, Unparker};
@@ -2,8 +2,8 @@
use crate::loom::cell::UnsafeCell;
use crate::loom::sync::Arc;
use crate::runtime::scheduler::multi_thread::Stats;
use crate::runtime::task::{self, Inject};
use crate::runtime::MetricsBatch;
use std::mem::{self, MaybeUninit};
use std::ptr;
@@ -186,7 +186,7 @@ impl<T> Local<T> {
&mut self,
mut task: task::Notified<T>,
inject: &Inject<T>,
metrics: &mut MetricsBatch,
stats: &mut Stats,
) {
let tail = loop {
let head = self.inner.head.load(Acquire);
@@ -206,7 +206,7 @@ impl<T> Local<T> {
} else {
// Push the current task and half of the queue into the
// inject queue.
match self.push_overflow(task, real, tail, inject, metrics) {
match self.push_overflow(task, real, tail, inject, stats) {
Ok(_) => return,
// Lost the race, try again
Err(v) => {
@@ -253,7 +253,7 @@ impl<T> Local<T> {
head: UnsignedShort,
tail: UnsignedShort,
inject: &Inject<T>,
metrics: &mut MetricsBatch,
stats: &mut Stats,
) -> Result<(), task::Notified<T>> {
/// How many elements are we taking from the local queue.
///
@@ -338,7 +338,7 @@ impl<T> Local<T> {
inject.push_batch(batch_iter.chain(std::iter::once(task)));
// Add 1 to factor in the task currently being scheduled.
metrics.incr_overflow_count();
stats.incr_overflow_count();
Ok(())
}
@@ -394,7 +394,7 @@ impl<T> Steal<T> {
pub(crate) fn steal_into(
&self,
dst: &mut Local<T>,
dst_metrics: &mut MetricsBatch,
dst_stats: &mut Stats,
) -> Option<task::Notified<T>> {
// Safety: the caller is the only thread that mutates `dst.tail` and
// holds a mutable reference.
@@ -420,8 +420,8 @@ impl<T> Steal<T> {
return None;
}
dst_metrics.incr_steal_count(n as u16);
dst_metrics.incr_steal_operations();
dst_stats.incr_steal_count(n as u16);
dst_stats.incr_steal_operations();
// We are returning a task here
n -= 1;
@@ -0,0 +1,140 @@
use crate::runtime::{Config, MetricsBatch, WorkerMetrics};
use std::cmp;
use std::time::{Duration, Instant};
/// Per-worker statistics. This is used for both tuning the scheduler and
/// reporting runtime-level metrics/stats.
pub(crate) struct Stats {
/// The metrics batch used to report runtime-level metrics/stats to the
/// user.
batch: MetricsBatch,
/// Instant at which work last resumed (continued after park).
///
/// This duplicates the value stored in `MetricsBatch`. We will unify
/// `Stats` and `MetricsBatch` when we stabilize metrics.
processing_scheduled_tasks_started_at: Instant,
/// Number of tasks polled in the batch of scheduled tasks
tasks_polled_in_batch: usize,
/// Exponentially-weighted moving average of time spent polling scheduled a
/// task.
///
/// Tracked in nanoseconds, stored as a f64 since that is what we use with
/// the EWMA calculations
task_poll_time_ewma: f64,
}
/// How to weigh each individual poll time, value is plucked from thin air.
const TASK_POLL_TIME_EWMA_ALPHA: f64 = 0.1;
/// Ideally, we wouldn't go above this, value is plucked from thin air.
const TARGET_GLOBAL_QUEUE_INTERVAL: f64 = Duration::from_micros(200).as_nanos() as f64;
/// Max value for the global queue interval. This is 2x the previous default
const MAX_TASKS_POLLED_PER_GLOBAL_QUEUE_INTERVAL: u32 = 127;
/// This is the previous default
const TARGET_TASKS_POLLED_PER_GLOBAL_QUEUE_INTERVAL: u32 = 61;
impl Stats {
pub(crate) fn new(worker_metrics: &WorkerMetrics) -> Stats {
// Seed the value with what we hope to see.
let task_poll_time_ewma =
TARGET_GLOBAL_QUEUE_INTERVAL / TARGET_TASKS_POLLED_PER_GLOBAL_QUEUE_INTERVAL as f64;
Stats {
batch: MetricsBatch::new(worker_metrics),
processing_scheduled_tasks_started_at: Instant::now(),
tasks_polled_in_batch: 0,
task_poll_time_ewma,
}
}
pub(crate) fn tuned_global_queue_interval(&self, config: &Config) -> u32 {
// If an interval is explicitly set, don't tune.
if let Some(configured) = config.global_queue_interval {
return configured;
}
// As of Rust 1.45, casts from f64 -> u32 are saturating, which is fine here.
let tasks_per_interval = (TARGET_GLOBAL_QUEUE_INTERVAL / self.task_poll_time_ewma) as u32;
cmp::max(
// We don't want to return less than 2 as that would result in the
// global queue always getting checked first.
2,
cmp::min(
MAX_TASKS_POLLED_PER_GLOBAL_QUEUE_INTERVAL,
tasks_per_interval,
),
)
}
pub(crate) fn submit(&mut self, to: &WorkerMetrics) {
self.batch.submit(to);
}
pub(crate) fn about_to_park(&mut self) {
self.batch.about_to_park();
}
pub(crate) fn inc_local_schedule_count(&mut self) {
self.batch.inc_local_schedule_count();
}
pub(crate) fn start_processing_scheduled_tasks(&mut self) {
self.batch.start_processing_scheduled_tasks();
self.processing_scheduled_tasks_started_at = Instant::now();
self.tasks_polled_in_batch = 0;
}
pub(crate) fn end_processing_scheduled_tasks(&mut self) {
self.batch.end_processing_scheduled_tasks();
// Update the EWMA task poll time
if self.tasks_polled_in_batch > 0 {
let now = Instant::now();
// If we "overflow" this conversion, we have bigger problems than
// slightly off stats.
let elapsed = (now - self.processing_scheduled_tasks_started_at).as_nanos() as f64;
let num_polls = self.tasks_polled_in_batch as f64;
// Calculate the mean poll duration for a single task in the batch
let mean_poll_duration = elapsed / num_polls;
// Compute the alpha weighted by the number of tasks polled this batch.
let weighted_alpha = 1.0 - (1.0 - TASK_POLL_TIME_EWMA_ALPHA).powf(num_polls);
// Now compute the new weighted average task poll time.
self.task_poll_time_ewma = weighted_alpha * mean_poll_duration
+ (1.0 - weighted_alpha) * self.task_poll_time_ewma;
}
}
pub(crate) fn start_poll(&mut self) {
self.batch.start_poll();
self.tasks_polled_in_batch += 1;
}
pub(crate) fn end_poll(&mut self) {
self.batch.end_poll();
}
pub(crate) fn incr_steal_count(&mut self, by: u16) {
self.batch.incr_steal_count(by);
}
pub(crate) fn incr_steal_operations(&mut self) {
self.batch.incr_steal_operations();
}
pub(crate) fn incr_overflow_count(&mut self) {
self.batch.incr_overflow_count();
}
}
@@ -59,10 +59,12 @@
use crate::loom::sync::{Arc, Mutex};
use crate::runtime;
use crate::runtime::context;
use crate::runtime::scheduler::multi_thread::{queue, Counters, Handle, Idle, Parker, Unparker};
use crate::runtime::scheduler::multi_thread::{
queue, Counters, Handle, Idle, Parker, Stats, Unparker,
};
use crate::runtime::task::{Inject, OwnedTasks};
use crate::runtime::{
blocking, coop, driver, scheduler, task, Config, MetricsBatch, SchedulerMetrics, WorkerMetrics,
blocking, coop, driver, scheduler, task, Config, SchedulerMetrics, WorkerMetrics,
};
use crate::util::atomic_cell::AtomicCell;
use crate::util::rand::{FastRand, RngSeedGenerator};
@@ -114,8 +116,11 @@ struct Core {
/// borrow checker happy.
park: Option<Parker>,
/// Batching metrics so they can be submitted to RuntimeMetrics.
metrics: MetricsBatch,
/// Per-worker runtime stats
stats: Stats,
/// How often to check the global queue
global_queue_interval: u32,
/// Fast random number generator.
rand: FastRand,
@@ -217,6 +222,7 @@ pub(super) fn create(
let park = park.clone();
let unpark = park.unpark();
let metrics = WorkerMetrics::from_config(&config);
let stats = Stats::new(&metrics);
cores.push(Box::new(Core {
tick: 0,
@@ -226,7 +232,8 @@ pub(super) fn create(
is_searching: false,
is_shutdown: false,
park: Some(park),
metrics: MetricsBatch::new(&metrics),
global_queue_interval: stats.tuned_global_queue_interval(&config),
stats,
rand: FastRand::new(config.seed_generator.next_seed()),
}));
@@ -436,6 +443,10 @@ impl Context {
// a task that had the LIFO slot disabled.
self.reset_lifo_enabled(&mut core);
// Start as "processing" tasks as polling tasks from the local queue
// will be one of the first things we do.
core.stats.start_processing_scheduled_tasks();
while !core.is_shutdown {
self.assert_lifo_enabled_is_correct(&core);
@@ -451,9 +462,14 @@ impl Context {
continue;
}
// We consumed all work in the queues and will start searching for work.
core.stats.end_processing_scheduled_tasks();
// There is no more **local** work to process, try to steal work
// from other workers.
if let Some(task) = core.steal_work(&self.worker) {
// Found work, switch back to processing
core.stats.start_processing_scheduled_tasks();
core = self.run_task(task, core)?;
} else {
// Wait for work
@@ -481,8 +497,13 @@ impl Context {
self.assert_lifo_enabled_is_correct(&core);
// Measure the poll start time. Note that we may end up polling other
// tasks under this measurement. In this case, the tasks came from the
// LIFO slot and are considered part of the current task for scheduling
// purposes. These tasks inherent the "parent"'s limits.
core.stats.start_poll();
// Make the core available to the runtime context
core.metrics.start_poll();
*self.core.borrow_mut() = Some(core);
// Run the task
@@ -505,29 +526,25 @@ impl Context {
}
};
// If task poll times is enabled, measure the poll time. Note
// that, if the `core` is stolen, this means `block_in_place`
// was called, turning the poll into a "blocking op". In this
// case, we don't want to measure the poll time as it doesn't
// really count as an async poll anymore.
core.metrics.end_poll();
// Check for a task in the LIFO slot
let task = match core.lifo_slot.take() {
Some(task) => task,
None => {
self.reset_lifo_enabled(&mut core);
core.stats.end_poll();
return Ok(core);
}
};
if !coop::has_budget_remaining() {
core.stats.end_poll();
// Not enough budget left to run the LIFO task, push it to
// the back of the queue and return.
core.run_queue.push_back_or_overflow(
task,
self.worker.inject(),
&mut core.metrics,
&mut core.stats,
);
// If we hit this point, the LIFO slot should be enabled.
// There is no need to reset it.
@@ -552,7 +569,6 @@ impl Context {
}
// Run the LIFO task, then loop
core.metrics.start_poll();
*self.core.borrow_mut() = Some(core);
let task = self.worker.handle.shared.owned.assert_owner(task);
task.run();
@@ -575,12 +591,16 @@ impl Context {
if core.tick % self.worker.handle.shared.config.event_interval == 0 {
super::counters::inc_num_maintenance();
core.stats.end_processing_scheduled_tasks();
// Call `park` with a 0 timeout. This enables the I/O driver, timer, ...
// to run without actually putting the thread to sleep.
core = self.park_timeout(core, Some(Duration::from_millis(0)));
// Run regularly scheduled maintenance
core.maintenance(&self.worker);
core.stats.start_processing_scheduled_tasks();
}
core
@@ -604,9 +624,8 @@ impl Context {
if core.transition_to_parked(&self.worker) {
while !core.is_shutdown {
core.metrics.about_to_park();
core.stats.about_to_park();
core = self.park_timeout(core, None);
core.metrics.returned_from_park();
// Run regularly scheduled maintenance
core.maintenance(&self.worker);
@@ -665,7 +684,10 @@ impl Core {
/// Return the next notified task available to this worker.
fn next_task(&mut self, worker: &Worker) -> Option<Notified> {
if self.tick % worker.handle.shared.config.global_queue_interval == 0 {
if self.tick % self.global_queue_interval == 0 {
// Update the global queue interval, if needed
self.tune_global_queue_interval(worker);
worker.inject().pop().or_else(|| self.next_local_task())
} else {
let maybe_task = self.next_local_task();
@@ -732,7 +754,7 @@ impl Core {
let target = &worker.handle.shared.remotes[i];
if let Some(task) = target
.steal
.steal_into(&mut self.run_queue, &mut self.metrics)
.steal_into(&mut self.run_queue, &mut self.stats)
{
return Some(task);
}
@@ -812,7 +834,7 @@ impl Core {
/// Runs maintenance work such as checking the pool's state.
fn maintenance(&mut self, worker: &Worker) {
self.metrics
self.stats
.submit(&worker.handle.shared.worker_metrics[worker.index]);
if !self.is_shutdown {
@@ -827,7 +849,7 @@ impl Core {
// Signal to all tasks to shut down.
worker.handle.shared.owned.close_and_shutdown_all();
self.metrics
self.stats
.submit(&worker.handle.shared.worker_metrics[worker.index]);
}
@@ -841,6 +863,19 @@ impl Core {
park.shutdown(&handle.driver);
}
fn tune_global_queue_interval(&mut self, worker: &Worker) {
let next = self
.stats
.tuned_global_queue_interval(&worker.handle.shared.config);
debug_assert!(next > 1);
// Smooth out jitter
if abs_diff(self.global_queue_interval, next) > 2 {
self.global_queue_interval = next;
}
}
}
impl Worker {
@@ -887,7 +922,7 @@ impl Handle {
}
fn schedule_local(&self, core: &mut Core, task: Notified, is_yield: bool) {
core.metrics.inc_local_schedule_count();
core.stats.inc_local_schedule_count();
// Spawning from the worker thread. If scheduling a "yield" then the
// task must always be pushed to the back of the queue, enabling other
@@ -895,7 +930,7 @@ impl Handle {
// flexibility and the task may go to the front of the queue.
let should_notify = if is_yield || !core.lifo_enabled {
core.run_queue
.push_back_or_overflow(task, &self.shared.inject, &mut core.metrics);
.push_back_or_overflow(task, &self.shared.inject, &mut core.stats);
true
} else {
// Push to the LIFO slot
@@ -904,7 +939,7 @@ impl Handle {
if let Some(prev) = prev {
core.run_queue
.push_back_or_overflow(prev, &self.shared.inject, &mut core.metrics);
.push_back_or_overflow(prev, &self.shared.inject, &mut core.stats);
}
core.lifo_slot = Some(task);
@@ -1028,3 +1063,12 @@ cfg_metrics! {
}
}
}
// `u32::abs_diff` is not available on Tokio's MSRV.
fn abs_diff(a: u32, b: u32) -> u32 {
if a > b {
a - b
} else {
b - a
}
}
+1
View File
@@ -347,6 +347,7 @@ mod group_d {
fn mk_pool(num_threads: usize) -> Runtime {
runtime::Builder::new_multi_thread()
.worker_threads(num_threads)
// Set the intervals to avoid tuning logic
.event_interval(2)
.build()
.unwrap()
+23 -24
View File
@@ -1,12 +1,11 @@
use crate::runtime::scheduler::multi_thread::queue;
use crate::runtime::scheduler::multi_thread::{queue, Stats};
use crate::runtime::task::Inject;
use crate::runtime::tests::NoopSchedule;
use crate::runtime::MetricsBatch;
use loom::thread;
fn metrics_batch() -> MetricsBatch {
MetricsBatch::new(&crate::runtime::WorkerMetrics::new())
fn new_stats() -> Stats {
Stats::new(&crate::runtime::WorkerMetrics::new())
}
#[test]
@@ -14,15 +13,15 @@ fn basic() {
loom::model(|| {
let (steal, mut local) = queue::local();
let inject = Inject::new();
let mut metrics = metrics_batch();
let mut stats = new_stats();
let th = thread::spawn(move || {
let mut metrics = metrics_batch();
let mut stats = new_stats();
let (_, mut local) = queue::local();
let mut n = 0;
for _ in 0..3 {
if steal.steal_into(&mut local, &mut metrics).is_some() {
if steal.steal_into(&mut local, &mut stats).is_some() {
n += 1;
}
@@ -39,7 +38,7 @@ fn basic() {
for _ in 0..2 {
for _ in 0..2 {
let (task, _) = super::unowned(async {});
local.push_back_or_overflow(task, &inject, &mut metrics);
local.push_back_or_overflow(task, &inject, &mut stats);
}
if local.pop().is_some() {
@@ -48,7 +47,7 @@ fn basic() {
// Push another task
let (task, _) = super::unowned(async {});
local.push_back_or_overflow(task, &inject, &mut metrics);
local.push_back_or_overflow(task, &inject, &mut stats);
while local.pop().is_some() {
n += 1;
@@ -70,14 +69,14 @@ fn steal_overflow() {
loom::model(|| {
let (steal, mut local) = queue::local();
let inject = Inject::new();
let mut metrics = metrics_batch();
let mut stats = new_stats();
let th = thread::spawn(move || {
let mut metrics = metrics_batch();
let mut stats = new_stats();
let (_, mut local) = queue::local();
let mut n = 0;
if steal.steal_into(&mut local, &mut metrics).is_some() {
if steal.steal_into(&mut local, &mut stats).is_some() {
n += 1;
}
@@ -92,7 +91,7 @@ fn steal_overflow() {
// push a task, pop a task
let (task, _) = super::unowned(async {});
local.push_back_or_overflow(task, &inject, &mut metrics);
local.push_back_or_overflow(task, &inject, &mut stats);
if local.pop().is_some() {
n += 1;
@@ -100,7 +99,7 @@ fn steal_overflow() {
for _ in 0..6 {
let (task, _) = super::unowned(async {});
local.push_back_or_overflow(task, &inject, &mut metrics);
local.push_back_or_overflow(task, &inject, &mut stats);
}
n += th.join().unwrap();
@@ -122,10 +121,10 @@ fn multi_stealer() {
const NUM_TASKS: usize = 5;
fn steal_tasks(steal: queue::Steal<NoopSchedule>) -> usize {
let mut metrics = metrics_batch();
let mut stats = new_stats();
let (_, mut local) = queue::local();
if steal.steal_into(&mut local, &mut metrics).is_none() {
if steal.steal_into(&mut local, &mut stats).is_none() {
return 0;
}
@@ -141,12 +140,12 @@ fn multi_stealer() {
loom::model(|| {
let (steal, mut local) = queue::local();
let inject = Inject::new();
let mut metrics = metrics_batch();
let mut stats = new_stats();
// Push work
for _ in 0..NUM_TASKS {
let (task, _) = super::unowned(async {});
local.push_back_or_overflow(task, &inject, &mut metrics);
local.push_back_or_overflow(task, &inject, &mut stats);
}
let th1 = {
@@ -176,7 +175,7 @@ fn multi_stealer() {
#[test]
fn chained_steal() {
loom::model(|| {
let mut metrics = metrics_batch();
let mut stats = new_stats();
let (s1, mut l1) = queue::local();
let (s2, mut l2) = queue::local();
let inject = Inject::new();
@@ -184,17 +183,17 @@ fn chained_steal() {
// Load up some tasks
for _ in 0..4 {
let (task, _) = super::unowned(async {});
l1.push_back_or_overflow(task, &inject, &mut metrics);
l1.push_back_or_overflow(task, &inject, &mut stats);
let (task, _) = super::unowned(async {});
l2.push_back_or_overflow(task, &inject, &mut metrics);
l2.push_back_or_overflow(task, &inject, &mut stats);
}
// Spawn a task to steal from **our** queue
let th = thread::spawn(move || {
let mut metrics = metrics_batch();
let mut stats = new_stats();
let (_, mut local) = queue::local();
s1.steal_into(&mut local, &mut metrics);
s1.steal_into(&mut local, &mut stats);
while local.pop().is_some() {}
});
@@ -202,7 +201,7 @@ fn chained_steal() {
// Drain our tasks, then attempt to steal
while l1.pop().is_some() {}
s2.steal_into(&mut l1, &mut metrics);
s2.steal_into(&mut l1, &mut stats);
th.join().unwrap();
+23 -24
View File
@@ -1,18 +1,17 @@
use crate::runtime::scheduler::multi_thread::queue;
use crate::runtime::scheduler::multi_thread::{queue, Stats};
use crate::runtime::task::{self, Inject, Schedule, Task};
use crate::runtime::MetricsBatch;
use std::thread;
use std::time::Duration;
#[allow(unused)]
macro_rules! assert_metrics {
($metrics:ident, $field:ident == $v:expr) => {{
($stats:ident, $field:ident == $v:expr) => {{
use crate::runtime::WorkerMetrics;
use std::sync::atomic::Ordering::Relaxed;
let worker = WorkerMetrics::new();
$metrics.submit(&worker);
$stats.submit(&worker);
let expect = $v;
let actual = worker.$field.load(Relaxed);
@@ -21,24 +20,24 @@ macro_rules! assert_metrics {
}};
}
fn metrics_batch() -> MetricsBatch {
fn new_stats() -> Stats {
use crate::runtime::WorkerMetrics;
MetricsBatch::new(&WorkerMetrics::new())
Stats::new(&WorkerMetrics::new())
}
#[test]
fn fits_256_one_at_a_time() {
let (_, mut local) = queue::local();
let inject = Inject::new();
let mut metrics = metrics_batch();
let mut stats = new_stats();
for _ in 0..256 {
let (task, _) = super::unowned(async {});
local.push_back_or_overflow(task, &inject, &mut metrics);
local.push_back_or_overflow(task, &inject, &mut stats);
}
cfg_metrics! {
assert_metrics!(metrics, overflow_count == 0);
assert_metrics!(stats, overflow_count == 0);
}
assert!(inject.pop().is_none());
@@ -88,15 +87,15 @@ fn fits_256_all_in_chunks() {
fn overflow() {
let (_, mut local) = queue::local();
let inject = Inject::new();
let mut metrics = metrics_batch();
let mut stats = new_stats();
for _ in 0..257 {
let (task, _) = super::unowned(async {});
local.push_back_or_overflow(task, &inject, &mut metrics);
local.push_back_or_overflow(task, &inject, &mut stats);
}
cfg_metrics! {
assert_metrics!(metrics, overflow_count == 1);
assert_metrics!(stats, overflow_count == 1);
}
let mut n = 0;
@@ -114,7 +113,7 @@ fn overflow() {
#[test]
fn steal_batch() {
let mut metrics = metrics_batch();
let mut stats = new_stats();
let (steal1, mut local1) = queue::local();
let (_, mut local2) = queue::local();
@@ -122,13 +121,13 @@ fn steal_batch() {
for _ in 0..4 {
let (task, _) = super::unowned(async {});
local1.push_back_or_overflow(task, &inject, &mut metrics);
local1.push_back_or_overflow(task, &inject, &mut stats);
}
assert!(steal1.steal_into(&mut local2, &mut metrics).is_some());
assert!(steal1.steal_into(&mut local2, &mut stats).is_some());
cfg_metrics! {
assert_metrics!(metrics, steal_count == 2);
assert_metrics!(stats, steal_count == 2);
}
for _ in 0..1 {
@@ -160,19 +159,19 @@ fn stress1() {
const NUM_PUSH: usize = normal_or_miri(500, 10);
const NUM_POP: usize = normal_or_miri(250, 10);
let mut metrics = metrics_batch();
let mut stats = new_stats();
for _ in 0..NUM_ITER {
let (steal, mut local) = queue::local();
let inject = Inject::new();
let th = thread::spawn(move || {
let mut metrics = metrics_batch();
let mut stats = new_stats();
let (_, mut local) = queue::local();
let mut n = 0;
for _ in 0..NUM_STEAL {
if steal.steal_into(&mut local, &mut metrics).is_some() {
if steal.steal_into(&mut local, &mut stats).is_some() {
n += 1;
}
@@ -184,7 +183,7 @@ fn stress1() {
}
cfg_metrics! {
assert_metrics!(metrics, steal_count == n as _);
assert_metrics!(stats, steal_count == n as _);
}
n
@@ -195,7 +194,7 @@ fn stress1() {
for _ in 0..NUM_LOCAL {
for _ in 0..NUM_PUSH {
let (task, _) = super::unowned(async {});
local.push_back_or_overflow(task, &inject, &mut metrics);
local.push_back_or_overflow(task, &inject, &mut stats);
}
for _ in 0..NUM_POP {
@@ -223,14 +222,14 @@ fn stress2() {
const NUM_TASKS: usize = normal_or_miri(1_000_000, 50);
const NUM_STEAL: usize = normal_or_miri(1_000, 10);
let mut metrics = metrics_batch();
let mut stats = new_stats();
for _ in 0..NUM_ITER {
let (steal, mut local) = queue::local();
let inject = Inject::new();
let th = thread::spawn(move || {
let mut stats = metrics_batch();
let mut stats = new_stats();
let (_, mut local) = queue::local();
let mut n = 0;
@@ -253,7 +252,7 @@ fn stress2() {
for i in 0..NUM_TASKS {
let (task, _) = super::unowned(async {});
local.push_back_or_overflow(task, &inject, &mut metrics);
local.push_back_or_overflow(task, &inject, &mut stats);
if i % 128 == 0 && local.pop().is_some() {
num_pop += 1;
+128
View File
@@ -35,6 +35,7 @@ fn single_thread() {
}
#[test]
#[cfg(not(tokio_cross))]
fn many_oneshot_futures() {
// used for notifying the main thread
const NUM: usize = 1_000;
@@ -588,6 +589,133 @@ async fn test_block_in_place4() {
tokio::task::block_in_place(|| {});
}
// Testing the tuning logic is tricky as it is inherently timing based, and more
// of a heuristic than an exact behavior. This test checks that the interval
// changes over time based on load factors. There are no assertions, completion
// is sufficient. If there is a regression, this test will hang. In theory, we
// could add limits, but that would be likely to fail on CI.
#[test]
#[cfg(not(tokio_cross))]
fn test_tuning() {
use std::sync::atomic::AtomicBool;
use std::time::Duration;
let rt = runtime::Builder::new_multi_thread()
.worker_threads(1)
.build()
.unwrap();
fn iter(flag: Arc<AtomicBool>, counter: Arc<AtomicUsize>, stall: bool) {
if flag.load(Relaxed) {
if stall {
std::thread::sleep(Duration::from_micros(5));
}
counter.fetch_add(1, Relaxed);
tokio::spawn(async move { iter(flag, counter, stall) });
}
}
let flag = Arc::new(AtomicBool::new(true));
let counter = Arc::new(AtomicUsize::new(61));
let interval = Arc::new(AtomicUsize::new(61));
{
let flag = flag.clone();
let counter = counter.clone();
rt.spawn(async move { iter(flag, counter, true) });
}
// Now, hammer the injection queue until the interval drops.
let mut i = 0;
let mut n = 0;
loop {
let curr = interval.load(Relaxed);
if curr <= 8 {
n += 1;
} else {
n = 0;
}
// Make sure we get a few good rounds. Jitter in the tuning could result
// in one "good" value without being representative of reaching a good
// state.
if n == 3 {
break;
}
let counter = counter.clone();
let interval = interval.clone();
if i <= 5_000 {
i += 1;
rt.spawn(async move {
let prev = counter.swap(0, Relaxed);
interval.store(prev, Relaxed);
});
std::thread::yield_now();
} else {
std::thread::sleep(Duration::from_micros(500));
}
}
flag.store(false, Relaxed);
let w = Arc::downgrade(&interval);
drop(interval);
while w.strong_count() > 0 {
std::thread::sleep(Duration::from_micros(500));
}
// Now, run it again with a faster task
let flag = Arc::new(AtomicBool::new(true));
// Set it high, we know it shouldn't ever really be this high
let counter = Arc::new(AtomicUsize::new(10_000));
let interval = Arc::new(AtomicUsize::new(10_000));
{
let flag = flag.clone();
let counter = counter.clone();
rt.spawn(async move { iter(flag, counter, false) });
}
// Now, hammer the injection queue until the interval reaches the expected range.
let mut i = 0;
let mut n = 0;
loop {
let curr = interval.load(Relaxed);
if curr <= 1_000 && curr > 32 {
n += 1;
} else {
n = 0;
}
if n == 3 {
break;
}
let counter = counter.clone();
let interval = interval.clone();
if i <= 5_000 {
i += 1;
rt.spawn(async move {
let prev = counter.swap(0, Relaxed);
interval.store(prev, Relaxed);
});
std::thread::yield_now();
} else {
std::thread::sleep(Duration::from_micros(500));
}
}
flag.store(false, Relaxed);
}
fn rt() -> runtime::Runtime {
runtime::Runtime::new().unwrap()
}