metrics: add task schedule latency metric (#7986)

This commit is contained in:
Robert Holt
2026-06-23 10:23:34 +02:00
committed by GitHub
parent 5f52f113d4
commit aee321206b
18 changed files with 689 additions and 29 deletions
+6 -5
View File
@@ -701,8 +701,9 @@ jobs:
# https://github.com/tokio-rs/tokio/pull/5356
# https://github.com/tokio-rs/tokio/issues/5373
- name: Check
# We use `--skip io-uring` since io-uring crate doesn't provide a binding for the i686 target.
run: cargo hack check -Zbuild-std --target target-specs/i686-unknown-linux-gnu.json -p tokio --feature-powerset --skip io-uring --depth 2 --keep-going
# We use `--skip io-uring,schedule-latency` since io-uring crate doesn't provide a binding for the i686 target
# and schedule latency tracking is only supported on 64-bit targets.
run: cargo hack check -Zbuild-std --target target-specs/i686-unknown-linux-gnu.json -p tokio --feature-powerset --skip io-uring,schedule-latency --depth 2 --keep-going
env:
RUSTFLAGS: --cfg tokio_unstable -Dwarnings
@@ -715,11 +716,11 @@ jobs:
include:
- name: ""
rustflags: ""
exclude_features: "io-uring,taskdump"
exclude_features: "io-uring,taskdump,schedule-latency"
- name: "--unstable"
rustflags: "--cfg tokio_unstable -Dwarnings"
exclude_features: "io-uring,taskdump"
- name: "--unstable io-uring,taskdump"
exclude_features: "io-uring,taskdump,schedule-latency"
- name: "--unstable io-uring,taskdump,schedule-latency"
rustflags: "--cfg tokio_unstable -Dwarnings"
exclude_features: ""
steps:
+2
View File
@@ -88,6 +88,8 @@ time = []
io-uring = ["dep:io-uring", "libc", "mio/os-poll", "mio/os-ext", "dep:slab"]
# Unstable feature. Requires `--cfg tokio_unstable` to enable.
taskdump = ["dep:backtrace"]
# Unstable feature. Requires `--cfg tokio_unstable` to enable.
schedule-latency = []
[dependencies]
tokio-macros = { version = "~2.7.0", optional = true }
+10
View File
@@ -354,6 +354,7 @@
//! Some feature flags are only available when specifying the `tokio_unstable` flag:
//!
//! - `tracing`: Enables tracing events.
//! - `schedule-latency`: Allows measurement of task scheduling latencies.
//! - `io-uring`: Enables `io-uring` (Linux only).
//! - `taskdump`: Enables `taskdump` (Linux only).
//!
@@ -501,6 +502,15 @@ compile_error!(
linux, on `aarch64`, `x86`, `x86_64` and `s390x`."
);
#[cfg(all(not(tokio_unstable), feature = "schedule-latency"))]
compile_error!("The `schedule-latency` feature requires `--cfg tokio_unstable`.");
#[cfg(all(
feature = "schedule-latency",
not(all(target_pointer_width = "64", target_has_atomic = "64"))
))]
compile_error!("The `schedule-latency` feature is only currently supported on 64-bit targets.");
// Includes re-exports used by macros.
//
// This module is not intended to be part of the public API. In general, any
+19
View File
@@ -749,3 +749,22 @@ macro_rules! cfg_io_uring {
)*
};
}
macro_rules! cfg_schedule_latency {
($($item:item)*) => {
$(
#[cfg(feature = "schedule-latency")]
#[cfg_attr(docsrs, doc(cfg(feature = "schedule-latency")))]
$item
)*
};
}
macro_rules! cfg_not_schedule_latency {
($($item:item)*) => {
$(
#[cfg(not(feature = "schedule-latency"))]
$item
)*
}
}
+150
View File
@@ -138,6 +138,12 @@ pub struct Builder {
/// Configures the task poll count histogram
pub(super) metrics_poll_count_histogram: HistogramBuilder,
/// When true, enables task schedule latency instrumentation.
pub(super) metrics_schedule_latency_histogram_enabled: bool,
/// Configures the task schedule latency histogram.
pub(super) metrics_schedule_latency_histogram: HistogramBuilder,
#[cfg(tokio_unstable)]
pub(super) unhandled_panic: UnhandledPanic,
@@ -335,6 +341,10 @@ impl Builder {
metrics_poll_count_histogram: HistogramBuilder::default(),
metrics_schedule_latency_histogram_enabled: false,
metrics_schedule_latency_histogram: HistogramBuilder::default(),
disable_lifo_slot: false,
timer_flavor: TimerFlavor::Traditional,
@@ -1709,6 +1719,8 @@ impl Builder {
enable_eager_driver_handoff: false,
seed_generator: seed_generator_1,
metrics_poll_count_histogram: self.metrics_poll_count_histogram_builder(),
metrics_schedule_latency_histogram: self
.metrics_schedule_latency_histogram_builder(),
},
local_tid,
self.name.clone(),
@@ -1728,6 +1740,14 @@ impl Builder {
None
}
}
fn metrics_schedule_latency_histogram_builder(&self) -> Option<HistogramBuilder> {
if self.metrics_schedule_latency_histogram_enabled {
Some(self.metrics_schedule_latency_histogram.clone())
} else {
None
}
}
}
cfg_io_driver! {
@@ -1849,6 +1869,135 @@ cfg_test_util! {
}
}
cfg_schedule_latency! {
impl Builder {
/// Enables tracking the distribution of task schedule latencies. Task
/// schedule latency is the time between when a task is scheduled for
/// execution and when it is polled.
///
/// **This feature is only supported on 64-bit targets.**
///
/// Task schedule latencies are not instrumented by default as doing
/// so requires calling [`Instant::now()`] when a task is scheduled
/// and when it is polled, which could add measurable overhead. Use
/// the [`Handle::metrics()`] to access the metrics data.
///
/// By default, a linear histogram with 10 buckets each 100 microseconds wide will be used.
/// This has an extremely low memory footprint, but may not provide enough granularity. For
/// better granularity with low memory usage, use [`metrics_schedule_latency_histogram_configuration()`]
/// to select [`LogHistogram`] instead.
///
/// # Examples
///
/// ```
/// # #[cfg(not(target_family = "wasm"))]
/// # {
/// use tokio::runtime;
///
/// let rt = runtime::Builder::new_multi_thread()
/// .enable_metrics_schedule_latency_histogram()
/// .build()
/// .unwrap();
/// # // Test default values here
/// # fn us(n: u64) -> std::time::Duration { std::time::Duration::from_micros(n) }
/// # let m = rt.handle().metrics();
/// # assert_eq!(m.schedule_latency_histogram_num_buckets(), 10);
/// # assert_eq!(m.schedule_latency_histogram_bucket_range(0), us(0)..us(100));
/// # assert_eq!(m.schedule_latency_histogram_bucket_range(1), us(100)..us(200));
/// # }
/// ```
///
/// [`Handle::metrics()`]: crate::runtime::Handle::metrics
/// [`Instant::now()`]: std::time::Instant::now
/// [`LogHistogram`]: crate::runtime::LogHistogram
/// [`metrics_schedule_latency_histogram_configuration()`]: Builder::metrics_schedule_latency_histogram_configuration
pub fn enable_metrics_schedule_latency_histogram(&mut self) -> &mut Self {
self.metrics_schedule_latency_histogram_enabled = true;
self
}
/// Configure the histogram for tracking task schedule latencies.
///
/// Tracking of task schedule latencies must be enabled with
/// [`enable_metrics_schedule_latency_histogram()`] for this function
/// to have any effect.
///
/// By default, a linear histogram with 10 buckets each 100 microseconds wide will be used.
/// This has an extremely low memory footprint, but may not provide enough granularity. For
/// better granularity with low memory usage, use [`LogHistogram`] instead.
///
/// # Examples
/// Configure a [`LogHistogram`] with [default configuration]:
/// ```
/// # #[cfg(not(target_family = "wasm"))]
/// # {
/// use tokio::runtime;
/// use tokio::runtime::{HistogramConfiguration, LogHistogram};
///
/// let rt = runtime::Builder::new_multi_thread()
/// .enable_metrics_schedule_latency_histogram()
/// .metrics_schedule_latency_histogram_configuration(
/// HistogramConfiguration::log(LogHistogram::default())
/// )
/// .build()
/// .unwrap();
/// # }
/// ```
///
/// Configure a linear histogram with 100 buckets, each 10μs wide
/// ```
/// # #[cfg(not(target_family = "wasm"))]
/// # {
/// use tokio::runtime;
/// use std::time::Duration;
/// use tokio::runtime::HistogramConfiguration;
///
/// let rt = runtime::Builder::new_multi_thread()
/// .enable_metrics_schedule_latency_histogram()
/// .metrics_schedule_latency_histogram_configuration(
/// HistogramConfiguration::linear(Duration::from_micros(10), 100)
/// )
/// .build()
/// .unwrap();
/// # }
/// ```
///
/// Configure a [`LogHistogram`] with the following settings:
/// - Measure times from 100ns to 120s
/// - Max error of 0.1
/// - No more than 1024 buckets
/// ```
/// # #[cfg(not(target_family = "wasm"))]
/// # {
/// use std::time::Duration;
/// use tokio::runtime;
/// use tokio::runtime::{HistogramConfiguration, LogHistogram};
///
/// let rt = runtime::Builder::new_multi_thread()
/// .enable_metrics_schedule_latency_histogram()
/// .metrics_schedule_latency_histogram_configuration(
/// HistogramConfiguration::log(LogHistogram::builder()
/// .max_value(Duration::from_secs(120))
/// .min_value(Duration::from_nanos(100))
/// .max_error(0.1)
/// .max_buckets(1024)
/// .expect("configuration uses 488 buckets")
/// )
/// )
/// .build()
/// .unwrap();
/// # }
/// ```
///
/// [`LogHistogram`]: crate::runtime::LogHistogram
/// [`enable_metrics_schedule_latency_histogram()`]: Builder::enable_metrics_schedule_latency_histogram
pub fn metrics_schedule_latency_histogram_configuration(&mut self, configuration: HistogramConfiguration) -> &mut Self {
self.metrics_schedule_latency_histogram.histogram_type = configuration.inner;
self
}
}
}
cfg_rt_multi_thread! {
impl Builder {
fn build_threaded_runtime(&mut self) -> io::Result<Runtime> {
@@ -1892,6 +2041,7 @@ cfg_rt_multi_thread! {
enable_eager_driver_handoff: self.enable_eager_driver_handoff,
seed_generator: seed_generator_1,
metrics_poll_count_histogram: self.metrics_poll_count_histogram_builder(),
metrics_schedule_latency_histogram: self.metrics_schedule_latency_histogram_builder(),
},
self.timer_flavor,
self.name.clone(),
+3
View File
@@ -48,6 +48,9 @@ pub(crate) struct Config {
/// How to build poll time histograms
pub(crate) metrics_poll_count_histogram: Option<crate::runtime::HistogramBuilder>,
/// How to build schedule latency histograms
pub(crate) metrics_schedule_latency_histogram: Option<crate::runtime::HistogramBuilder>,
#[cfg(tokio_unstable)]
/// How to respond to unhandled task panics.
pub(crate) unhandled_panic: crate::runtime::UnhandledPanic,
+37 -2
View File
@@ -4,6 +4,7 @@ cfg_unstable_metrics! {
use crate::runtime::metrics::HistogramBatch;
}
use crate::runtime::metrics::ScheduleLatencyContext;
use std::sync::atomic::Ordering::Relaxed;
use std::time::{Duration, Instant};
@@ -53,6 +54,9 @@ pub(crate) struct MetricsBatch {
#[cfg(tokio_unstable)]
/// If `Some`, tracks poll times in nanoseconds
poll_timer: Option<PollTimer>,
#[cfg(feature = "schedule-latency")]
schedule_latencies: Option<HistogramBatch>,
}
cfg_unstable_metrics! {
@@ -95,6 +99,14 @@ impl MetricsBatch {
poll_started_at: now,
})
});
// Schedule latencies cannot be tracked if `Instant::now()` is unavailable
#[cfg(feature = "schedule-latency")]
let schedule_latencies = maybe_now.and_then(|_| {
worker_metrics
.schedule_latency_histogram
.as_ref()
.map(HistogramBatch::from_histogram)
});
MetricsBatch {
park_count: 0,
park_unpark_count: 0,
@@ -108,6 +120,8 @@ impl MetricsBatch {
busy_duration_total: 0,
processing_scheduled_tasks_started_at: maybe_now,
poll_timer,
#[cfg(feature = "schedule-latency")]
schedule_latencies,
}
}
}
@@ -155,6 +169,12 @@ impl MetricsBatch {
let dst = worker.poll_count_histogram.as_ref().unwrap();
poll_timer.poll_counts.submit(dst);
}
#[cfg(feature = "schedule-latency")]
if let Some(schedule_latencies) = &self.schedule_latencies {
let dst = worker.schedule_latency_histogram.as_ref().unwrap();
schedule_latencies.submit(dst);
}
}
}
}
@@ -206,15 +226,30 @@ impl MetricsBatch {
cfg_metrics_variant! {
stable: {
/// Start polling an individual task
pub(crate) fn start_poll(&mut self) {}
pub(crate) fn start_poll(&mut self, _task_scheduled_at: Option<ScheduleLatencyContext>) {}
},
unstable: {
/// Start polling an individual task
pub(crate) fn start_poll(&mut self) {
///
/// # Arguments
///
/// `task_scheduled_at` is used to calculate task schedule latency.
/// A `ScheduleLatencyContext` can be obtained by calling `prepare` on a task's
/// `ScheduleLatencyInstant`.
pub(crate) fn start_poll(&mut self, _task_scheduled_at: Option<ScheduleLatencyContext>) {
self.poll_count += 1;
if let Some(poll_timer) = &mut self.poll_timer {
poll_timer.poll_started_at = Instant::now();
}
#[cfg(feature = "schedule-latency")]
if let Some(task_scheduled_at) = _task_scheduled_at {
if let Some(schedule_latencies) = &mut self.schedule_latencies {
if let Some(now) = self.poll_timer.as_ref().map(|p| p.poll_started_at).or_else(now) {
let elapsed = task_scheduled_at.elapsed_nanos(now);
schedule_latencies.measure(elapsed, 1);
}
}
}
}
}
}
+10
View File
@@ -38,3 +38,13 @@ cfg_not_unstable_metrics! {
mod mock;
pub(crate) use mock::{SchedulerMetrics, HistogramBuilder};
}
cfg_schedule_latency! {
mod schedule_latency;
pub(crate) use schedule_latency::{ScheduleLatencyInstant, ScheduleLatencyContext};
}
cfg_not_schedule_latency! {
mod schedule_latency_mock;
pub(crate) use schedule_latency_mock::{ScheduleLatencyInstant, ScheduleLatencyContext};
}
+188
View File
@@ -1029,6 +1029,194 @@ impl RuntimeMetrics {
}
}
feature! {
#![feature = "schedule-latency"]
/// Returns `true` if the runtime is tracking the distribution of task
/// schedule latencies.
///
/// Task schedule latencies are not instrumented by default as doing so
/// requires calling [`Instant::now()`] when a task is scheduled and when
/// it is polled. The feature is enabled by calling
/// [`enable_metrics_schedule_latency_histogram()`] when building the runtime.
///
/// # Examples
///
/// ```
/// use tokio::runtime::{self, Handle};
///
/// fn main() {
/// runtime::Builder::new_current_thread()
/// .enable_metrics_schedule_latency_histogram()
/// .build()
/// .unwrap()
/// .block_on(async {
/// let metrics = Handle::current().metrics();
/// let enabled = metrics.schedule_latency_histogram_enabled();
///
/// println!("Tracking task schedule latency distribution: {:?}", enabled);
/// });
/// }
/// ```
///
/// [`enable_metrics_schedule_latency_histogram()`]: crate::runtime::Builder::enable_metrics_schedule_latency_histogram
/// [`Instant::now()`]: std::time::Instant::now
pub fn schedule_latency_histogram_enabled(&self) -> bool {
self.handle.inner.worker_metrics(0).schedule_latency_histogram.is_some()
}
/// Returns the number of histogram buckets tracking the distribution of
/// task schedule latencies.
///
/// This value is configured by calling
/// [`metrics_schedule_latency_histogram_configuration()`] when building the runtime.
///
/// # Examples
///
/// ```
/// use tokio::runtime::{self, Handle};
///
/// fn main() {
/// runtime::Builder::new_current_thread()
/// .enable_metrics_schedule_latency_histogram()
/// .build()
/// .unwrap()
/// .block_on(async {
/// let metrics = Handle::current().metrics();
/// let buckets = metrics.schedule_latency_histogram_num_buckets();
///
/// println!("Histogram buckets: {:?}", buckets);
/// });
/// }
/// ```
///
/// [`metrics_schedule_latency_histogram_configuration()`]: crate::runtime::Builder::metrics_schedule_latency_histogram_configuration
pub fn schedule_latency_histogram_num_buckets(&self) -> usize {
self.handle
.inner
.worker_metrics(0)
.schedule_latency_histogram
.as_ref()
.map(|histogram| histogram.num_buckets())
.unwrap_or_default()
}
/// Returns the range of task schedule latencies tracked by the given bucket.
///
/// This value is configured by calling
/// [`metrics_schedule_latency_histogram_configuration()`] when building the runtime.
///
/// # Panics
///
/// The method panics if `bucket` represents an invalid bucket index, i.e.
/// is greater than or equal to `schedule_latency_histogram_num_buckets()`.
///
/// # Examples
///
/// ```
/// use tokio::runtime::{self, Handle};
///
/// fn main() {
/// runtime::Builder::new_current_thread()
/// .enable_metrics_schedule_latency_histogram()
/// .build()
/// .unwrap()
/// .block_on(async {
/// let metrics = Handle::current().metrics();
/// let buckets = metrics.schedule_latency_histogram_num_buckets();
///
/// for i in 0..buckets {
/// let range = metrics.schedule_latency_histogram_bucket_range(i);
/// println!("Histogram bucket {} range: {:?}", i, range);
/// }
/// });
/// }
/// ```
///
/// [`metrics_schedule_latency_histogram_configuration()`]: crate::runtime::Builder::metrics_schedule_latency_histogram_configuration
#[track_caller]
pub fn schedule_latency_histogram_bucket_range(&self, bucket: usize) -> Range<Duration> {
self.handle
.inner
.worker_metrics(0)
.schedule_latency_histogram
.as_ref()
.map(|histogram| {
let range = histogram.bucket_range(bucket);
std::ops::Range {
start: Duration::from_nanos(range.start),
end: Duration::from_nanos(range.end),
}
})
.unwrap_or_default()
}
/// Returns the number of times the given worker polled tasks with a schedule
/// latency within the given bucket's range.
///
/// Each worker maintains its own histogram and the counts for each bucket
/// starts at zero when the runtime is created. Each time the worker polls a
/// task, it tracks the time elapsed between when the task was scheduled and
/// when it was polled and increments the associated bucket by 1.
///
/// Each bucket is a monotonically increasing counter. It is never
/// decremented or reset to zero.
///
/// # Arguments
///
/// `worker` is the index of the worker being queried. The given value must
/// be between 0 and `num_workers()`. The index uniquely identifies a single
/// worker and will continue to identify the worker throughout the lifetime
/// of the runtime instance.
///
/// `bucket` is the index of the bucket being queried. The bucket is scoped
/// to the worker. The range represented by the bucket can be queried by
/// calling [`schedule_latency_histogram_bucket_range()`]. Each worker maintains
/// identical bucket ranges.
///
/// # Panics
///
/// The method panics when `worker` represents an invalid worker, i.e. is
/// greater than or equal to `num_workers()` or if `bucket` represents an
/// invalid bucket.
///
/// # Examples
///
/// ```
/// use tokio::runtime::{self, Handle};
///
/// fn main() {
/// runtime::Builder::new_current_thread()
/// .enable_metrics_schedule_latency_histogram()
/// .build()
/// .unwrap()
/// .block_on(async {
/// let metrics = Handle::current().metrics();
/// let buckets = metrics.schedule_latency_histogram_num_buckets();
///
/// for worker in 0..metrics.num_workers() {
/// for i in 0..buckets {
/// let range = metrics.schedule_latency_histogram_bucket_range(i);
/// let count = metrics.schedule_latency_histogram_bucket_count(worker, i);
/// println!("{} tasks encountered a scheduling latency between {}us and {}us", count, range.start.as_micros(), range.end.as_micros());
/// }
/// }
/// });
/// }
/// ```
///
/// [`schedule_latency_histogram_bucket_range()`]: crate::runtime::RuntimeMetrics::schedule_latency_histogram_bucket_range
#[track_caller]
pub fn schedule_latency_histogram_bucket_count(&self, worker: usize, bucket: usize) -> u64 {
self.handle
.inner
.worker_metrics(worker)
.schedule_latency_histogram
.as_ref()
.map(|histogram| histogram.get(bucket))
.unwrap_or_default()
}
}
feature! {
#![all(
tokio_unstable,
@@ -0,0 +1,61 @@
use std::num::NonZeroU64;
use std::time::Instant;
/// `ScheduleLatencyInstant` tracks the time a task was scheduled.
///
/// The time a task was scheduled is stored as the number of nanoseconds
/// since startup of the task's scheduler.
///
/// **Note**: This is an [unstable API][unstable]. The public API of this type
/// may break in 1.x releases. See [the documentation on unstable
/// features][unstable] for details.
///
/// [unstable]: crate#unstable-features
#[derive(Copy, Clone)]
pub(crate) struct ScheduleLatencyInstant(Option<NonZeroU64>);
impl ScheduleLatencyInstant {
/// Create a new `ScheduleLatencyInstant` using the provided scheduler startup Instant.
pub(crate) fn new(scheduler_start: Option<Instant>) -> Self {
Self(scheduler_start.map(|scheduler_start| {
NonZeroU64::new(scheduler_start.elapsed().as_nanos() as u64).unwrap_or(NonZeroU64::MIN)
}))
}
/// Prepare a context that can calculate the number of nanoseconds elapsed
/// since this task was scheduled.
pub(crate) fn prepare(
self,
scheduler_start: Option<Instant>,
) -> Option<ScheduleLatencyContext> {
match (scheduler_start, self.0) {
(Some(scheduler_start), Some(scheduled_at_delta)) => Some(ScheduleLatencyContext {
scheduler_start,
scheduled_at_delta,
}),
_ => None,
}
}
}
/// `ScheduleLatencyContext` contains all the data required to calculate the time elapsed
/// since a task was scheduled.
///
/// `ScheduleLatencyInstant` on its own in insufficient because it only contains a delta.
/// The scheduler startup time is required to convert the delta back into an actual time
/// but is omitted from `ScheduleLatencyInstant` to keep its memory size minimal.
pub(crate) struct ScheduleLatencyContext {
scheduler_start: Instant,
scheduled_at_delta: NonZeroU64,
}
impl ScheduleLatencyContext {
/// Calculate how many nanoseconds have elapsed between `now` and when this task
/// was last scheduled.
pub(crate) fn elapsed_nanos(&self, now: Instant) -> u64 {
let nanos_since_start = now
.saturating_duration_since(self.scheduler_start)
.as_nanos() as u64;
nanos_since_start.saturating_sub(self.scheduled_at_delta.get())
}
}
@@ -0,0 +1,31 @@
//! A mock implementation of the types in `schedule_latency`. These types
//! are zero-sized so that the size of types using them are not increased
//! unless schedule latency tracking is explicitly enabled.
use std::time::Instant;
#[derive(Copy, Clone)]
pub(crate) struct ScheduleLatencyInstant();
impl ScheduleLatencyInstant {
pub(crate) fn new(_runtime_start: Option<Instant>) -> Self {
Self()
}
pub(crate) fn prepare(self, _runtime_start: Option<Instant>) -> Option<ScheduleLatencyContext> {
None
}
}
pub(crate) struct ScheduleLatencyContext {
_private: (),
}
impl ScheduleLatencyContext {
// This method is referenced (but never called) when the `schedule-latency`
// feature is disabled and `tokio_unstable` is enabled.
#[allow(dead_code)]
pub(crate) fn elapsed_nanos(&self, _now: Instant) -> u64 {
unimplemented!("This should never be called because prepare() always returns None")
}
}
+12
View File
@@ -65,6 +65,10 @@ pub(crate) struct WorkerMetrics {
#[cfg(tokio_unstable)]
/// If `Some`, tracks the number of polls by duration range.
pub(super) poll_count_histogram: Option<Histogram>,
#[cfg(feature = "schedule-latency")]
/// If `Some`, tracks the number of times tasks were scheduled by duration range.
pub(super) schedule_latency_histogram: Option<Histogram>,
}
impl WorkerMetrics {
@@ -93,6 +97,14 @@ impl WorkerMetrics {
.metrics_poll_count_histogram
.as_ref()
.map(|histogram_builder| histogram_builder.build());
#[cfg(feature = "schedule-latency")]
{
worker_metrics.schedule_latency_histogram = config
.metrics_schedule_latency_histogram
.as_ref()
.map(|histogram_builder| histogram_builder.build());
}
worker_metrics
}
}
@@ -3,7 +3,8 @@ use crate::loom::sync::Arc;
use crate::runtime::driver::{self, Driver};
use crate::runtime::scheduler::{self, Defer, Inject};
use crate::runtime::task::{
self, JoinHandle, OwnedTasks, Schedule, SpawnLocation, Task, TaskHarnessScheduleHooks,
self, JoinHandle, LocalNotified, OwnedTasks, Schedule, SpawnLocation, Task,
TaskHarnessScheduleHooks,
};
use crate::runtime::{
blocking, context, Config, MetricsBatch, SchedulerMetrics, TaskHooks, TaskMeta, WorkerMetrics,
@@ -20,6 +21,7 @@ use std::task::Poll::{Pending, Ready};
use std::task::Waker;
use std::thread::ThreadId;
use std::time::Duration;
use std::time::Instant;
use std::{fmt, thread};
/// Executes tasks on the current thread
@@ -100,6 +102,11 @@ struct Shared {
/// This scheduler only has one worker.
worker_metrics: WorkerMetrics,
/// Startup time of this scheduler.
///
/// This instant is used as the basis of task `scheduled_at` measurements.
started_at: Option<Instant>,
}
/// Thread-local context.
@@ -145,6 +152,11 @@ impl CurrentThread {
.global_queue_interval
.unwrap_or(DEFAULT_GLOBAL_QUEUE_INTERVAL);
let started_at = config
.metrics_schedule_latency_histogram
.as_ref()
.map(|_| Instant::now());
let handle = Arc::new(Handle {
name,
task_hooks: TaskHooks {
@@ -162,6 +174,7 @@ impl CurrentThread {
config,
scheduler_metrics: SchedulerMetrics::new(),
worker_metrics,
started_at,
},
driver: driver_handle,
blocking_spawner,
@@ -368,11 +381,28 @@ fn wake_deferred_tasks_and_free(context: &Context) {
impl Context {
/// Execute the closure with the given scheduler core stored in the
/// thread-local context.
fn run_task<R>(&self, mut core: Box<Core>, f: impl FnOnce() -> R) -> (Box<Core>, R) {
core.metrics.start_poll();
let mut ret = self.enter(core, || crate::task::coop::budget(f));
ret.0.metrics.end_poll();
ret
fn run_task(&self, task: LocalNotified<Arc<Handle>>, mut core: Box<Core>) -> Box<Core> {
#[cfg(tokio_unstable)]
let task_meta = task.task_meta();
core.metrics.start_poll(
task.get_scheduled_at()
.prepare(self.handle.shared.started_at),
);
let (mut c, ()) = self.enter(core, || {
crate::task::coop::budget(|| {
#[cfg(tokio_unstable)]
self.handle.task_hooks.poll_start_callback(&task_meta);
task.run();
#[cfg(tokio_unstable)]
self.handle.task_hooks.poll_stop_callback(&task_meta);
})
});
c.metrics.end_poll();
c
}
/// Blocks the current thread until an event is received by the driver,
@@ -643,6 +673,7 @@ cfg_unstable_metrics! {
}
}
use crate::runtime::metrics::ScheduleLatencyInstant;
use std::num::NonZeroU64;
impl Handle {
@@ -671,6 +702,15 @@ impl Schedule for Arc<Handle> {
fn schedule(&self, task: task::Notified<Self>) {
use scheduler::Context::CurrentThread;
if self
.shared
.config
.metrics_schedule_latency_histogram
.is_some()
{
task.set_scheduled_at(ScheduleLatencyInstant::new(self.shared.started_at));
}
context::with_scheduler(|maybe_cx| match maybe_cx {
Some(CurrentThread(cx)) if Arc::ptr_eq(self, &cx.handle) => {
let mut core = cx.core.borrow_mut();
@@ -820,18 +860,7 @@ impl CoreGuard<'_> {
let task = context.handle.shared.owned.assert_owner(task);
#[cfg(tokio_unstable)]
let task_meta = task.task_meta();
let (c, ()) = context.run_task(core, || {
#[cfg(tokio_unstable)]
context.handle.task_hooks.poll_start_callback(&task_meta);
task.run();
#[cfg(tokio_unstable)]
context.handle.task_hooks.poll_stop_callback(&task_meta);
});
let c = context.run_task(task, core);
core = c;
}
@@ -1,3 +1,4 @@
use crate::runtime::metrics::ScheduleLatencyContext;
use crate::runtime::{Config, MetricsBatch, WorkerMetrics};
use std::time::{Duration, Instant};
@@ -113,8 +114,8 @@ impl Stats {
}
}
pub(crate) fn start_poll(&mut self) {
self.batch.start_poll();
pub(crate) fn start_poll(&mut self, task_scheduled_at: Option<ScheduleLatencyContext>) {
self.batch.start_poll(task_scheduled_at);
self.tasks_polled_in_batch += 1;
}
@@ -74,7 +74,7 @@ use crate::util::rand::{FastRand, RngSeedGenerator};
use std::cell::RefCell;
use std::task::Waker;
use std::thread;
use std::time::Duration;
use std::time::{Duration, Instant};
mod metrics;
@@ -92,6 +92,7 @@ use crate::loom::sync::atomic::AtomicBool;
#[cfg(all(tokio_unstable, feature = "time"))]
use crate::runtime::time_alt;
use crate::runtime::metrics::ScheduleLatencyInstant;
#[cfg(all(tokio_unstable, feature = "time"))]
use crate::runtime::scheduler::util;
@@ -202,6 +203,11 @@ pub(crate) struct Shared {
pub(super) worker_metrics: Box<[WorkerMetrics]>,
/// Startup time of this scheduler.
///
/// This instant is used as the basis of task `scheduled_at` measurements.
started_at: Option<Instant>,
/// Only held to trigger some code on drop. This is used to get internal
/// runtime metrics that can be useful when doing performance
/// investigations. This does nothing (empty struct, no drop impl) unless
@@ -311,6 +317,10 @@ pub(super) fn create(
let (idle, idle_synced) = Idle::new(size);
let (inject, inject_synced) = inject::Shared::new();
let started_at = config
.metrics_schedule_latency_histogram
.as_ref()
.map(|_| Instant::now());
let remotes_len = remotes.len();
let handle = Arc::new(Handle {
@@ -332,6 +342,7 @@ pub(super) fn create(
config,
scheduler_metrics: SchedulerMetrics::new(),
worker_metrics: worker_metrics.into_boxed_slice(),
started_at,
_counters: Counters,
},
driver: driver_handle,
@@ -666,7 +677,10 @@ impl Context {
// 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();
core.stats.start_poll(
task.get_scheduled_at()
.prepare(self.worker.handle.shared.started_at),
);
// Make the core available to the runtime context
*self.core.borrow_mut() = Some(core);
@@ -1325,6 +1339,15 @@ impl Worker {
impl Handle {
pub(super) fn schedule_task(&self, task: Notified, is_yield: bool) {
if self
.shared
.config
.metrics_schedule_latency_histogram
.is_some()
{
task.set_scheduled_at(ScheduleLatencyInstant::new(self.shared.started_at));
}
with_current(|maybe_cx| {
if let Some(cx) = maybe_cx {
// Make sure the task is part of the **current** scheduler.
+22
View File
@@ -21,6 +21,7 @@
use crate::future::Future;
use crate::loom::cell::UnsafeCell;
use crate::runtime::context;
use crate::runtime::metrics::ScheduleLatencyInstant;
use crate::runtime::task::raw::{self, Vtable};
use crate::runtime::task::state::State;
use crate::runtime::task::{Id, Schedule, TaskHarnessScheduleHooks};
@@ -191,6 +192,9 @@ pub(crate) struct Header {
/// The tracing ID for this instrumented task.
#[cfg(all(tokio_unstable, feature = "tracing"))]
pub(super) tracing_id: Option<tracing::Id>,
/// The last time this task was scheduled. Used to measure schedule latency.
pub(super) scheduled_at: UnsafeCell<ScheduleLatencyInstant>,
}
unsafe impl Send for Header {}
@@ -247,6 +251,7 @@ impl<T: Future, S: Schedule> Cell<T, S> {
owner_id: UnsafeCell::new(None),
#[cfg(all(tokio_unstable, feature = "tracing"))]
tracing_id,
scheduled_at: UnsafeCell::new(ScheduleLatencyInstant::new(None)),
}
}
@@ -534,6 +539,23 @@ impl Header {
pub(super) unsafe fn get_tracing_id(me: &NonNull<Header>) -> Option<&tracing::Id> {
me.as_ref().tracing_id.as_ref()
}
/// Updates the last time this task was scheduled. Used to calculate
/// the time elapsed between task scheduling and polling.
///
/// # Safety
///
/// The caller must guarantee exclusive access to this field.
pub(super) unsafe fn set_scheduled_at(&self, scheduled_at: ScheduleLatencyInstant) {
self.scheduled_at.with_mut(|ptr| *ptr = scheduled_at);
}
/// Gets the last time this task was scheduled.
pub(super) fn get_scheduled_at(&self) -> ScheduleLatencyInstant {
// Safety: If there are concurrent writes, then that write has violated
// the safety requirements on `set_scheduled_at`.
unsafe { self.scheduled_at.with(|ptr| *ptr) }
}
}
impl Trailer {
+14
View File
@@ -221,6 +221,7 @@ use crate::future::Future;
use crate::util::linked_list;
use crate::util::sharded_list;
use crate::runtime::metrics::ScheduleLatencyInstant;
use crate::runtime::TaskCallback;
use std::marker::PhantomData;
use std::panic::Location;
@@ -247,6 +248,15 @@ impl<S> Notified<S> {
pub(crate) fn task_meta<'meta>(&self) -> crate::runtime::TaskMeta<'meta> {
self.0.task_meta()
}
pub(crate) fn set_scheduled_at(&self, scheduled_at: ScheduleLatencyInstant) {
// SAFETY: There are no concurrent writes because there is only ever one `Notified`
// reference per task. There are no concurrent reads because this field is only read
// when polling the task, which can only happen after it's scheduled.
unsafe {
self.0.header().set_scheduled_at(scheduled_at);
}
}
}
// safety: This type cannot be used to touch the task without first verifying
@@ -268,6 +278,10 @@ impl<S> LocalNotified<S> {
pub(crate) fn task_meta<'meta>(&self) -> crate::runtime::TaskMeta<'meta> {
self.task.task_meta()
}
pub(crate) fn get_scheduled_at(&self) -> ScheduleLatencyInstant {
self.task.header().get_scheduled_at()
}
}
/// A task that is not owned by any `OwnedTasks`. Used for blocking tasks.
+49
View File
@@ -800,6 +800,55 @@ fn io_driver_ready_count() {
assert_eq!(metrics.io_driver_ready_count(), 1);
}
#[cfg(feature = "schedule-latency")]
#[test]
fn schedule_latency_counts() {
const N: u64 = 50;
let rts = [
tokio::runtime::Builder::new_current_thread()
.enable_all()
.enable_metrics_schedule_latency_histogram()
.metrics_schedule_latency_histogram_configuration(HistogramConfiguration::linear(
Duration::from_millis(50),
3,
))
.build()
.unwrap(),
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.enable_metrics_schedule_latency_histogram()
.metrics_schedule_latency_histogram_configuration(HistogramConfiguration::linear(
Duration::from_millis(50),
3,
))
.build()
.unwrap(),
];
for rt in rts {
let metrics = rt.metrics();
rt.block_on(async {
for _ in 0..N {
tokio::spawn(async {}).await.unwrap();
}
});
drop(rt);
let num_workers = metrics.num_workers();
let num_buckets = metrics.schedule_latency_histogram_num_buckets();
assert!(metrics.schedule_latency_histogram_enabled());
assert_eq!(num_buckets, 3);
let n = (0..num_workers)
.flat_map(|i| (0..num_buckets).map(move |j| (i, j)))
.map(|(worker, bucket)| metrics.schedule_latency_histogram_bucket_count(worker, bucket))
.sum();
assert_eq!(N, n);
}
}
async fn try_spawn_stealable_task() -> Result<(), mpsc::RecvTimeoutError> {
// We use a blocking channel to synchronize the tasks.
let (tx, rx) = mpsc::channel();