Instrumented 与 instrument_poll
主线位置:任务主线的起点和每一次 poll。
instrument是包装的那一刻,instrument_poll是之后每次 poll 都要经过的那一层。
flowchart TB
s["poll_start = now()"] --> first{第一次 poll?}
first -->|是| fp["记首次 poll 延迟:poll_start 减 instrumented_at"]
first -->|否| wake
fp --> wake["取出并清零 woke_at"]
wake --> idle["算空闲、排队(下一页)"]
idle --> reg["存下调用方的 waker,换成 State"]
reg --> ps["inner_poll_start = now()"]
ps --> inner["poll 内层 future"]
inner --> pe["inner_poll_end = now()"]
pe --> idl["idled_at = 这一刻"]
idl --> bucket{"耗时 >= slow_poll_threshold?"}
bucket -->|是| slow[慢 poll 桶]
bucket -->|否| fast[快 poll 桶]
instrument:包装的那一刻
TaskMonitor::instrument 只是把自己 clone 一份(一个 Arc),转给 TaskMonitorCore::instrument_with:
pub fn instrument_with<F, M: AsRef<TaskMonitorCore> + Send + Sync + 'static>(
task: F,
monitor: M,
) -> Instrumented<F, M> {
monitor
.as_ref()
.metrics
.instrumented_count
.fetch_add(1, SeqCst);
let log = monitor
.as_ref()
.record_scheduling_log
.then(|| Arc::new(SchedulingLog::default()));
let state: State<M> = State {
monitor,
instrumented_at: Instant::now(),
woke_at: AtomicU64::new(0),
waker: AtomicWaker::new(),
log,
};
let instrumented: Instrumented<F, M> = Instrumented {
task,
did_poll_once: false,
idled_at: 0,
state: Arc::new(state),
};
instrumented
}
三件事:instrumented_count 加 1;记下 instrumented_at;建一个 Arc<State>,它将来既是指标的去处,也是交给内层 future 的 waker。log 只在 monitor 打开了 publish_scheduling_delay 时才分配,给 FutureMonitor 用,平时是 None。
所以每包一个 future 有一次堆分配(Arc<State>),TaskMonitor 自己的 clone 只是引用计数加 1。
Instrumented 本身用 pin_project! 声明,四个字段:
pin_project! {
#[derive(Debug)]
pub struct Instrumented<T, M: AsRef<TaskMonitorCore> = TaskMonitor> {
// The task being instrumented
#[pin]
task: T,
// True when the task is polled for the first time
did_poll_once: bool,
// The instant, tracked as nanoseconds since `instrumented_at`, at which the future finished
// its last poll.
idled_at: u64,
// State shared between the task and its instrumented waker.
state: Arc<State<M>>,
}
impl<T, M: AsRef<TaskMonitorCore>> PinnedDrop for Instrumented<T, M> {
fn drop(this: Pin<&mut Self>) {
this.state.monitor.as_ref().metrics.dropped_count.fetch_add(1, SeqCst);
}
}
}
| 字段 | 谁读写 | 作用 |
|---|---|---|
task | poll 时 | 被包住的 future,结构性 pin |
did_poll_once | 只有 poll 路径 | 第一次 poll 时记首次延迟,之后不再记 |
idled_at | 只有 poll 路径 | 上次 poll 结束的时刻,存成「距 instrumented_at 的纳秒数」 |
state | poll 路径和 waker 两边 | monitor、instrumented_at、woke_at、调用方的 waker |
idled_at 不需要原子:只有 poll 它的那个线程会读写。woke_at 在 State 里,是 AtomicU64,因为 wake() 可能在任何线程上被调用。两者都存成相对 instrumented_at 的纳秒数,而不是 Instant:Instant 不能原子地存取,换成 u64 纳秒就可以,而且两边用同一种编码,可以直接比大小。u64 纳秒能表示 584 年,on_wake 在转换失败时直接放弃记录,注释说这得是任务跑了 500 多年,还向 500 年后读到这段代码的人道了歉。
第二个泛型参数 M 是「指标记到哪里」:默认是 TaskMonitor,也可以是 &'static TaskMonitorCore 或任何 AsRef<TaskMonitorCore>,见 TaskMonitorCore 与 FutureMonitor。
PinnedDrop 在 future 被 drop 时把 dropped_count 加 1,不管它有没有完成、有没有被 poll 过。一个被 spawn 的任务完成后,tokio 会立刻 drop 它的 future,所以 instrumented_count - dropped_count 就是还活着的任务数,见 TaskMetrics 与派生指标。
instrument_poll:逐段走一遍
Future for Instrumented 和 Stream for Instrumented 都只有一行,把 Future::poll 或 Stream::poll_next 当作参数传给同一个函数 instrument_poll:
fn instrument_poll<T, M: AsRef<TaskMonitorCore> + Send + Sync + 'static, Out>(
cx: &mut Context<'_>,
instrumented: Pin<&mut Instrumented<T, M>>,
poll_fn: impl FnOnce(Pin<&mut T>, &mut Context<'_>) -> Poll<Out>,
) -> Poll<Out> {
连签名一共 146 行,分五段。
第一段:首次 poll 延迟
let poll_start = Instant::now();
let this = instrumented.project();
let idled_at = this.idled_at;
let state = this.state;
let instrumented_at = state.instrumented_at;
let metrics = &state.monitor.as_ref().metrics;
// ...
if !*this.did_poll_once {
// ...
*this.did_poll_once = true;
// ...
let elapsed = poll_start
.saturating_duration_since(instrumented_at)
.as_nanos()
.try_into()
.unwrap_or(u64::MAX);
// add this duration to `time_to_first_poll_ns_total`
metrics.total_first_poll_delay_ns.fetch_add(elapsed, SeqCst);
// ...
metrics.first_poll_count.fetch_add(1, SeqCst);
}
第 2905 行取第一次时间。首次 poll 延迟 = 这一刻减去 instrumented_at,也就是从 instrument() 被调用算起,不是从 tokio::spawn 算起。平时两者写在同一行,差别是几十微秒;如果先 instrument 好一批 future、过一会儿再 spawn,中间的时间全算进去。
use std::time::Duration;
use futures::StreamExt;
use tokio_metrics::TaskMonitor;
#[tokio::main]
async fn main() {
// 1. instrument() 和 spawn 之间隔 200ms
let early = TaskMonitor::new();
let fut = early.instrument(async {});
tokio::time::sleep(Duration::from_millis(200)).await;
tokio::spawn(fut).await.unwrap();
// 2. 在 spawn 的同一行 instrument
let late = TaskMonitor::new();
tokio::time::sleep(Duration::from_millis(200)).await;
tokio::spawn(late.instrument(async {})).await.unwrap();
println!("instrument early: first_poll_delay={:?}", early.cumulative().total_first_poll_delay);
println!("instrument late: first_poll_delay={:?}", late.cumulative().total_first_poll_delay);
// 3. 包一个 Stream:每次 poll_next 都算一次 poll
let streams = TaskMonitor::new();
let items: Vec<u32> = streams
.instrument(futures::stream::iter([1, 2, 3]).then(|x| async move {
tokio::task::yield_now().await;
x
}))
.collect()
.await;
let s = streams.cumulative();
println!(
"stream: items={items:?} instrumented={} first_polls={} polls={} dropped={}",
s.instrumented_count, s.first_poll_count, s.total_poll_count, s.dropped_count
);
}
依赖是 tokio-metrics = "=0.5.2"、tokio = { version = "=1.54.1", features = ["full"] }、futures = "0.3"。M2 Max 笔记本上连跑 3 次:
instrument early: first_poll_delay=202.161792ms
instrument late: first_poll_delay=12.292µs
stream: items=[1, 2, 3] instrumented=1 first_polls=1 polls=7 dropped=1
后两次的第一行是 202.05ms、202.22ms,第二行是 30µs、45µs,第三行完全相同。200ms 之外多出的 2ms 是 sleep 本身的误差(定时器精度 1 毫秒,加上驱动处理到期的时机)。Stream 那一行在下面讲。
第二段:空闲与排队
let woke_at = state.woke_at.swap(0, SeqCst);
// ...
if *idled_at < woke_at {
// ... 记一次空闲:woke_at - idled_at
}
// ...
if woke_at > 0 {
// ... 记一次排队:poll_start - woke_at,按 long_delay_threshold 分长短
}
第 2934 行把上次 poll 之后记下的唤醒时刻取出来并清零。三个时刻(idled_at、woke_at、poll_start)切出两段:上次 poll 结束到被唤醒是空闲,被唤醒到这次 poll 开始是排队。这一段是任务主线上唯一的算法,边界情况很多,放在下一页包装 waker。
第三段:换 waker
// Register the waker
state.waker.register(cx.waker());
// Get the instrumented waker
let waker_ref = futures_util::task::waker_ref(state);
let mut cx = Context::from_waker(&waker_ref);
第 3006 行把调用方传进来的 waker(通常是 tokio 任务的 waker)存进 State 里的 AtomicWaker,然后用 State 自己造一个新的 Context。waker_ref 不增加 Arc 的引用计数,只是借用;内层 future 如果要把 waker 存起来(几乎所有叶子 future 都会),clone 时才会给 Arc<State> 加计数。
每次 poll 都要重新 register:Future 的约定是唤醒最近一次 poll 传进来的 waker,而调用方不一定每次传同一个(比如外面包着 FuturesUnordered 这类组合子,或者 future 被挪到了另一个执行器上)。tokio 直接 poll 任务时,同一个任务每次传的是同一个 waker。register 内部会用 will_wake 和上一次存的比较,是同一个就不 clone、不替换。
第四段:poll 内层
let inner_poll_start = Instant::now();
// ...
let ret = if let Some(log) = &state.log {
let _sched_guard = SchedulingLogGuard::enter(log.clone());
poll_fn(this.task, &mut cx)
} else {
poll_fn(this.task, &mut cx)
};
let inner_poll_end = Instant::now();
第二次和第三次取时间夹住内层的 poll。log 那个分支只在 publish_scheduling_delay 打开时走,在 poll 期间把本任务的排队记录放进线程局部变量,给里面的 FutureMonitor 读;源码注释说,不打开时这条路径「byte-for-byte」和原来一样。
注意 poll 时长只算内层:inner_poll_end - inner_poll_start,不含前面记账和换 waker 的时间。包装本身的开销不会让 poll 显得更慢,但它确实花在了这次 poll 里,设计取舍的「实验:每次 poll 多花多少」量了它。
第五段:空闲开始、按阈值分桶
/* idle time starts now */
*idled_at = inner_poll_end
.saturating_duration_since(instrumented_at)
.as_nanos()
.try_into()
.unwrap_or(u64::MAX);
/* accounting for poll time */
let inner_poll_duration = inner_poll_end.saturating_duration_since(inner_poll_start);
// ...
let (count_bucket, duration_bucket) = // was this a slow or fast poll?
if inner_poll_duration >= metrics.slow_poll_threshold {
(&metrics.total_slow_poll_count, &metrics.total_slow_poll_duration)
} else {
(&metrics.total_fast_poll_count, &metrics.total_fast_poll_duration_ns)
};
// update the appropriate bucket
count_bucket.fetch_add(1, SeqCst);
duration_bucket.fetch_add(inner_poll_ns, SeqCst);
ret
第 3024 行把这次 poll 结束的时刻记进 idled_at。不管内层返回 Ready 还是 Pending 都记;返回 Ready 的话这个值不会再被用到。
poll 时长用 >= 和阈值比较,落进快、慢两个桶之一。没有单独的「总 poll 次数」计数器,总数是读的时候两桶相加得到的。
一次 poll 的成本
数一下 instrument_poll 在一次「被唤醒过的普通 poll」上做的事:
| 项 | 次数 |
|---|---|
Instant::now() | 3:poll_start、inner_poll_start、inner_poll_end |
SeqCst 原子操作 | 11:woke_at.swap 1,空闲 4(次数、区间最大、全局最大、总时长),排队 4(次数、长短桶两个、总时长),快慢桶 2 |
AtomicWaker::register | 1,内部还有自己的原子操作 |
第一次 poll 另加 2 次原子操作;打开 publish_scheduling_delay 时再加 2–3 次,外加一次 Arc clone 和线程局部变量的读写。另外,每次 wake() 还有一次 Instant::now()(instrumented_at.elapsed())和一次 compare_exchange。
这些原子变量都在同一个 monitor 的 RawMetrics 里,所有被它包住的任务、所有 worker 线程都在往同一组缓存行上写。monitor 分得越粗,争用越多。
Stream 也能包,但只限默认 monitor
instrument 不要求 F: Future,所以它也能包 Stream。上面实验的第三行:一个三个元素的流,每个元素前 yield_now 一次,collect 的过程中 poll_next 被调了 7 次:3 个元素各 2 次(一次 Pending、一次 Ready(Some)),最后一次 Ready(None)。instrumented_count 和 first_poll_count 都是 1:整个流算一个「任务」。
但 Stream 的实现写的是 impl<T: Stream> Stream for Instrumented<T>,没有第二个泛型参数,等于只给 Instrumented<T, TaskMonitor> 实现了。用静态的 TaskMonitorCore 包出来的是 Instrumented<T, &'static TaskMonitorCore>,只能当 Future 用:
use tokio_metrics::TaskMonitorCore;
static CORE: TaskMonitorCore = TaskMonitorCore::new();
fn main() {
let s = CORE.instrument(futures::stream::iter([1, 2]));
use futures::StreamExt;
let _ = s.collect::<Vec<_>>();
}
error[E0599]: the method `collect` exists for struct `Instrumented<futures::stream::Iter<std::array::IntoIter<{integer}, 2>>, &TaskMonitorCore>`, but its trait bounds were not satisfied
--> src/main.rs:7:13
|
7 | let _ = s.collect::<Vec<_>>();
| ^^^^^^^ method cannot be called due to unsatisfied trait bounds
|
::: pin-project-lite-0.2.17/src/lib.rs:745:9
|
745 | / $vis struct $ident $($def_generics)*
...
| |_________- doesn't satisfy `_: Iterator`, `_: StreamExt` or `_: Stream`
|
= note: the following trait bounds were not satisfied:
`Instrumented<futures::stream::Iter<std::array::IntoIter<{integer}, 2>>, &TaskMonitorCore>: Stream`
which is required by `Instrumented<futures::stream::Iter<std::array::IntoIter<{integer}, 2>>, &TaskMonitorCore>: StreamExt`
...
(rustc 1.99.0。以三个冒号开头的那一行指向 pin_project! 宏展开出来的结构体定义,原文是注册表里的绝对路径,这里只留 crate 名;... 处删掉了几行宏源码和三条类似的 note。)
Future 的实现是泛型的,Stream 的不是;instrument_poll 本身对 M 是泛型的,给 Stream 的 impl 加上同样的 M 约束在技术上没有障碍,源码和 CHANGELOG 都没有说明为什么没加。0.5.2 里要包 Stream 就用 TaskMonitor。这条报错和两种修法收在 05 常见坑的 E0599 那一页。
同一个 future 包两次
instrument 不检查 future 是不是已经被包过。TaskMonitor::instrument 的文档里有两个例子:
- 两个不同的 monitor 包同一个 future(
monitor_a.instrument(monitor_b.instrument(fut))),两边各记一次,各自完整。这是合理用法:一个按路由聚合,一个按服务聚合。 - 同一个 monitor 包两次,
first_poll_count变成 2,文档说「probably undesirable」。两层都会算 poll 时长,同一次 poll 被记两遍。
外层和内层量到的排队时间不完全相同,内层多算了外层在 poll 到它之前花的时间,见下一页最后一个实验。
小结
instrument做三件事:instrumented_count加 1、记下instrumented_at、建一个Arc<State>;每包一个 future 一次堆分配。Instrumented四个字段:内层 future、did_poll_once、idled_at(相对包装时刻的纳秒数),Arc<State>;drop 时dropped_count加 1,不管完成与否。instrument_poll五段:首次 poll 延迟、空闲与排队、换 waker、poll 内层、按阈值分快慢桶。首次延迟从instrument()算起,实测先包后等 200ms 记为 202ms。- 一次被唤醒过的 poll:3 次
Instant::now()、11 次SeqCst原子操作、1 次AtomicWaker::register,全部写在同一个 monitor 共享的计数器上。 Stream只给Instrumented<T, TaskMonitor>实现;用静态TaskMonitorCore包的流不能当Stream用。