Skip to content

Commit c84d0a1

Browse files
authored
rt: instrument task poll times with a histogram (#5685)
Adds support for instrumenting the poll times of all spawned tasks. Data is tracked in a histogram. The user must specify the histogram scale and bucket ranges. Implementation-wise, the same strategy is used in the runtime where we are just using atomic counters. Because instrumenting each poll duration will result in frequent calls to `Instant::now()`, I think it should be an opt-in metric.
1 parent a883fd4 commit c84d0a1

File tree

14 files changed

+1105
-40
lines changed

14 files changed

+1105
-40
lines changed

tokio/src/runtime/builder.rs

+148-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::runtime::handle::Handle;
2-
use crate::runtime::{blocking, driver, Callback, Runtime};
2+
use crate::runtime::{blocking, driver, Callback, HistogramBuilder, Runtime};
33
use crate::util::rand::{RngSeed, RngSeedGenerator};
44

55
use std::fmt;
@@ -95,6 +95,12 @@ pub struct Builder {
9595
/// Specify a random number generator seed to provide deterministic results
9696
pub(super) seed_generator: RngSeedGenerator,
9797

98+
/// When true, enables task poll count histogram instrumentation.
99+
pub(super) metrics_poll_count_histogram_enable: bool,
100+
101+
/// Configures the task poll count histogram
102+
pub(super) metrics_poll_count_histogram: HistogramBuilder,
103+
98104
#[cfg(tokio_unstable)]
99105
pub(super) unhandled_panic: UnhandledPanic,
100106
}
@@ -268,6 +274,10 @@ impl Builder {
268274
#[cfg(tokio_unstable)]
269275
unhandled_panic: UnhandledPanic::Ignore,
270276

277+
metrics_poll_count_histogram_enable: false,
278+
279+
metrics_poll_count_histogram: Default::default(),
280+
271281
disable_lifo_slot: false,
272282
}
273283
}
@@ -877,6 +887,133 @@ impl Builder {
877887
}
878888
}
879889

890+
cfg_metrics! {
891+
/// Enables tracking the distribution of task poll times.
892+
///
893+
/// Task poll times are not instrumented by default as doing so requires
894+
/// calling [`Instant::now()`] twice per task poll, which could add
895+
/// measurable overhead. Use the [`Handle::metrics()`] to access the
896+
/// metrics data.
897+
///
898+
/// The histogram uses fixed bucket sizes. In other words, the histogram
899+
/// buckets are not dynamic based on input values. Use the
900+
/// `metrics_poll_count_histogram_` builder methods to configure the
901+
/// histogram details.
902+
///
903+
/// # Examples
904+
///
905+
/// ```
906+
/// use tokio::runtime;
907+
///
908+
/// let rt = runtime::Builder::new_multi_thread()
909+
/// .enable_metrics_poll_count_histogram()
910+
/// .build()
911+
/// .unwrap();
912+
/// # // Test default values here
913+
/// # fn us(n: u64) -> std::time::Duration { std::time::Duration::from_micros(n) }
914+
/// # let m = rt.handle().metrics();
915+
/// # assert_eq!(m.poll_count_histogram_num_buckets(), 10);
916+
/// # assert_eq!(m.poll_count_histogram_bucket_range(0), us(0)..us(100));
917+
/// # assert_eq!(m.poll_count_histogram_bucket_range(1), us(100)..us(200));
918+
/// ```
919+
///
920+
/// [`Handle::metrics()`]: crate::runtime::Handle::metrics
921+
/// [`Instant::now()`]: std::time::Instant::now
922+
pub fn enable_metrics_poll_count_histogram(&mut self) -> &mut Self {
923+
self.metrics_poll_count_histogram_enable = true;
924+
self
925+
}
926+
927+
/// Sets the histogram scale for tracking the distribution of task poll
928+
/// times.
929+
///
930+
/// Tracking the distribution of task poll times can be done using a
931+
/// linear or log scale. When using linear scale, each histogram bucket
932+
/// will represent the same range of poll times. When using log scale,
933+
/// each histogram bucket will cover a range twice as big as the
934+
/// previous bucket.
935+
///
936+
/// **Default:** linear scale.
937+
///
938+
/// # Examples
939+
///
940+
/// ```
941+
/// use tokio::runtime::{self, HistogramScale};
942+
///
943+
/// let rt = runtime::Builder::new_multi_thread()
944+
/// .enable_metrics_poll_count_histogram()
945+
/// .metrics_poll_count_histogram_scale(HistogramScale::Log)
946+
/// .build()
947+
/// .unwrap();
948+
/// ```
949+
pub fn metrics_poll_count_histogram_scale(&mut self, histogram_scale: crate::runtime::HistogramScale) -> &mut Self {
950+
self.metrics_poll_count_histogram.scale = histogram_scale;
951+
self
952+
}
953+
954+
/// Sets the histogram resolution for tracking the distribution of task
955+
/// poll times.
956+
///
957+
/// The resolution is the histogram's first bucket's range. When using a
958+
/// linear histogram scale, each bucket will cover the same range. When
959+
/// using a log scale, each bucket will cover a range twice as big as
960+
/// the previous bucket. In the log case, the resolution represents the
961+
/// smallest bucket range.
962+
///
963+
/// Note that, when using log scale, the resolution is rounded up to the
964+
/// nearest power of 2 in nanoseconds.
965+
///
966+
/// **Default:** 100 microseconds.
967+
///
968+
/// # Examples
969+
///
970+
/// ```
971+
/// use tokio::runtime;
972+
/// use std::time::Duration;
973+
///
974+
/// let rt = runtime::Builder::new_multi_thread()
975+
/// .enable_metrics_poll_count_histogram()
976+
/// .metrics_poll_count_histogram_resolution(Duration::from_micros(100))
977+
/// .build()
978+
/// .unwrap();
979+
/// ```
980+
pub fn metrics_poll_count_histogram_resolution(&mut self, resolution: Duration) -> &mut Self {
981+
assert!(resolution > Duration::from_secs(0));
982+
// Sanity check the argument and also make the cast below safe.
983+
assert!(resolution <= Duration::from_secs(1));
984+
985+
let resolution = resolution.as_nanos() as u64;
986+
self.metrics_poll_count_histogram.resolution = resolution;
987+
self
988+
}
989+
990+
/// Sets the number of buckets for the histogram tracking the
991+
/// distribution of task poll times.
992+
///
993+
/// The last bucket tracks all greater values that fall out of other
994+
/// ranges. So, configuring the histogram using a linear scale,
995+
/// resolution of 50ms, and 10 buckets, the 10th bucket will track task
996+
/// polls that take more than 450ms to complete.
997+
///
998+
/// **Default:** 10
999+
///
1000+
/// # Examples
1001+
///
1002+
/// ```
1003+
/// use tokio::runtime;
1004+
///
1005+
/// let rt = runtime::Builder::new_multi_thread()
1006+
/// .enable_metrics_poll_count_histogram()
1007+
/// .metrics_poll_count_histogram_buckets(15)
1008+
/// .build()
1009+
/// .unwrap();
1010+
/// ```
1011+
pub fn metrics_poll_count_histogram_buckets(&mut self, buckets: usize) -> &mut Self {
1012+
self.metrics_poll_count_histogram.num_buckets = buckets;
1013+
self
1014+
}
1015+
}
1016+
8801017
fn build_current_thread_runtime(&mut self) -> io::Result<Runtime> {
8811018
use crate::runtime::scheduler::{self, CurrentThread};
8821019
use crate::runtime::{runtime::Scheduler, Config};
@@ -909,6 +1046,7 @@ impl Builder {
9091046
unhandled_panic: self.unhandled_panic.clone(),
9101047
disable_lifo_slot: self.disable_lifo_slot,
9111048
seed_generator: seed_generator_1,
1049+
metrics_poll_count_histogram: self.metrics_poll_count_histogram_builder(),
9121050
},
9131051
);
9141052

@@ -922,6 +1060,14 @@ impl Builder {
9221060
blocking_pool,
9231061
))
9241062
}
1063+
1064+
fn metrics_poll_count_histogram_builder(&self) -> Option<HistogramBuilder> {
1065+
if self.metrics_poll_count_histogram_enable {
1066+
Some(self.metrics_poll_count_histogram.clone())
1067+
} else {
1068+
None
1069+
}
1070+
}
9251071
}
9261072

9271073
cfg_io_driver! {
@@ -1050,6 +1196,7 @@ cfg_rt_multi_thread! {
10501196
unhandled_panic: self.unhandled_panic.clone(),
10511197
disable_lifo_slot: self.disable_lifo_slot,
10521198
seed_generator: seed_generator_1,
1199+
metrics_poll_count_histogram: self.metrics_poll_count_histogram_builder(),
10531200
},
10541201
);
10551202

tokio/src/runtime/config.rs

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ pub(crate) struct Config {
2828
/// deterministic way.
2929
pub(crate) seed_generator: RngSeedGenerator,
3030

31+
/// How to build poll time histograms
32+
pub(crate) metrics_poll_count_histogram: Option<crate::runtime::HistogramBuilder>,
33+
3134
#[cfg(tokio_unstable)]
3235
/// How to respond to unhandled task panics.
3336
pub(crate) unhandled_panic: crate::runtime::UnhandledPanic,

tokio/src/runtime/metrics/batch.rs

+51-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use crate::runtime::WorkerMetrics;
1+
use crate::runtime::metrics::{HistogramBatch, WorkerMetrics};
22

33
use std::sync::atomic::Ordering::Relaxed;
4-
use std::time::Instant;
4+
use std::time::{Duration, Instant};
55

66
pub(crate) struct MetricsBatch {
77
/// Number of times the worker parked.
@@ -32,11 +32,26 @@ pub(crate) struct MetricsBatch {
3232

3333
/// The total busy duration in nanoseconds.
3434
busy_duration_total: u64,
35+
36+
/// Instant at which work last resumed (continued after park).
3537
last_resume_time: Instant,
38+
39+
/// If `Some`, tracks poll times in nanoseconds
40+
poll_timer: Option<PollTimer>,
41+
}
42+
43+
struct PollTimer {
44+
/// Histogram of poll counts within each band.
45+
poll_counts: HistogramBatch,
46+
47+
/// Instant when the most recent task started polling.
48+
poll_started_at: Instant,
3649
}
3750

3851
impl MetricsBatch {
39-
pub(crate) fn new() -> MetricsBatch {
52+
pub(crate) fn new(worker_metrics: &WorkerMetrics) -> MetricsBatch {
53+
let now = Instant::now();
54+
4055
MetricsBatch {
4156
park_count: 0,
4257
noop_count: 0,
@@ -47,7 +62,14 @@ impl MetricsBatch {
4762
local_schedule_count: 0,
4863
overflow_count: 0,
4964
busy_duration_total: 0,
50-
last_resume_time: Instant::now(),
65+
last_resume_time: now,
66+
poll_timer: worker_metrics
67+
.poll_count_histogram
68+
.as_ref()
69+
.map(|worker_poll_counts| PollTimer {
70+
poll_counts: HistogramBatch::from_histogram(worker_poll_counts),
71+
poll_started_at: now,
72+
}),
5173
}
5274
}
5375

@@ -68,6 +90,11 @@ impl MetricsBatch {
6890
.local_schedule_count
6991
.store(self.local_schedule_count, Relaxed);
7092
worker.overflow_count.store(self.overflow_count, Relaxed);
93+
94+
if let Some(poll_timer) = &self.poll_timer {
95+
let dst = worker.poll_count_histogram.as_ref().unwrap();
96+
poll_timer.poll_counts.submit(dst);
97+
}
7198
}
7299

73100
/// The worker is about to park.
@@ -81,8 +108,22 @@ impl MetricsBatch {
81108
}
82109

83110
let busy_duration = self.last_resume_time.elapsed();
84-
let busy_duration = u64::try_from(busy_duration.as_nanos()).unwrap_or(u64::MAX);
85-
self.busy_duration_total += busy_duration;
111+
self.busy_duration_total += duration_as_u64(busy_duration);
112+
}
113+
114+
pub(crate) fn start_poll(&mut self) {
115+
self.poll_count += 1;
116+
117+
if let Some(poll_timer) = &mut self.poll_timer {
118+
poll_timer.poll_started_at = Instant::now();
119+
}
120+
}
121+
122+
pub(crate) fn end_poll(&mut self) {
123+
if let Some(poll_timer) = &mut self.poll_timer {
124+
let elapsed = duration_as_u64(poll_timer.poll_started_at.elapsed());
125+
poll_timer.poll_counts.measure(elapsed, 1);
126+
}
86127
}
87128

88129
pub(crate) fn returned_from_park(&mut self) {
@@ -92,10 +133,6 @@ impl MetricsBatch {
92133
pub(crate) fn inc_local_schedule_count(&mut self) {
93134
self.local_schedule_count += 1;
94135
}
95-
96-
pub(crate) fn incr_poll_count(&mut self) {
97-
self.poll_count += 1;
98-
}
99136
}
100137

101138
cfg_rt_multi_thread! {
@@ -113,3 +150,7 @@ cfg_rt_multi_thread! {
113150
}
114151
}
115152
}
153+
154+
fn duration_as_u64(dur: Duration) -> u64 {
155+
u64::try_from(dur.as_nanos()).unwrap_or(u64::MAX)
156+
}

0 commit comments

Comments
 (0)