nuclei/stats.rs
1//! What the runtime is doing: a snapshot of every shard's counters, taken
2//! by [`stats()`](crate::stats()), or of one runtime's by
3//! [`Runtime::stats`](crate::Runtime::stats). A runtime built with
4//! [`Builder::record_latencies`](crate::Builder::record_latencies) adds a
5//! [`Histogram`] of its poll times, and with the `io-stats` feature each
6//! worker's IO counts and latencies.
7//!
8//! The snapshot covers every shard alive in the process: each runtime's
9//! workers, and the shard of each thread that runs `block_on` (made on its
10//! first need, gone with the thread). A shard's counts are read without
11//! stopping it, one at a time, so they need not agree with each other to
12//! the operation. Most are counted in the shard's own memory and published
13//! for snapshots: by a shard about to wait for work, so a quiet shard's are
14//! current, and by a busy one at least once every few hundred task runs.
15//! Errors are counted as they happen. Taking a snapshot costs a read of each shard's
16//! counters; a running shard pays nothing for it.
17
18use std::io;
19
20use crate::IoBackend;
21use crate::rt::counters::{KINDS, kind_index};
22
23/// A snapshot of the runtime's counters: [`stats()`](crate::stats()).
24#[derive(Clone, Debug)]
25#[non_exhaustive]
26pub struct Stats {
27 /// Every shard alive: the workers first, by their place in their
28 /// runtime, then the shards of threads that run `block_on`.
29 pub shards: Vec<ShardStats>,
30 /// Threads of the blocking pools alive now, at work or idle.
31 pub blocking_threads: usize,
32 /// Jobs waiting in the blocking pools' queues for a thread to take them.
33 pub blocking_queue_depth: usize,
34 /// Errors of IO run on threads that run no shard: a handle polled
35 /// outside nuclei's `block_on` and its tasks, or a completion delivered
36 /// while its shard was not running.
37 pub off_shard_errors: ErrorCounts,
38 /// Shards that started without the io_uring receive buffers their
39 /// configuration asked for, because the system had no memory for them
40 /// (most often the locked-memory limit); their reads use the pool's
41 /// buffers instead.
42 pub receive_buffers_missing: u64,
43}
44
45/// One shard's counters. Those marked "published" are as the shard last
46/// published them (see [the module](self)).
47#[derive(Clone, Debug)]
48#[non_exhaustive]
49pub struct ShardStats {
50 /// The shard's place among its runtime's workers; `None` for the shard
51 /// of a thread that runs `block_on`.
52 pub worker: Option<usize>,
53 /// The runtime the shard is a worker of; `None` for the shard of a
54 /// thread that runs `block_on`, which serves every runtime the thread
55 /// enters.
56 pub runtime: Option<crate::runtime::RuntimeId>,
57 /// The kernel interface the shard's IO runs on.
58 pub backend: IoBackend,
59 /// Tasks placed on the shard and not done yet.
60 pub tasks: usize,
61 /// Task polls the shard has run (published).
62 pub polls: u64,
63 /// Times the shard slept with nothing to run (published).
64 pub parks: u64,
65 /// Time the shard spent asleep, in all (published).
66 pub parked: std::time::Duration,
67 /// Time since the shard was made that it did not spend asleep: running
68 /// tasks, driving its IO and timers, spinning before a park (from the
69 /// published time asleep).
70 pub busy: std::time::Duration,
71 /// Runnables waiting in the shard's own queue, its LIFO slot included
72 /// (published).
73 pub queue_depth: usize,
74 /// Runnables of the shard's tasks it took from other threads: a task
75 /// woken there, or spawned onto it from there (published).
76 pub remote_wakes: u64,
77 /// IO operations the shard handed to the kernel that have not ended:
78 /// on io_uring the operations submitted, on IOCP the ones issued. A
79 /// readiness poller (epoll, kqueue) hands the kernel none, as its
80 /// driver makes each call once the descriptor is ready: 0 there
81 /// (published).
82 pub ops_in_flight: usize,
83 /// Times the shard found completions the kernel was holding for want of
84 /// room in the ring's completion queue (none is lost: the kernel keeps
85 /// them until there is room); 0 on other backends (published).
86 pub cq_overflows: u64,
87 /// Bytes the shard's IO buffers hold: its pool's, in use and free, and on
88 /// io_uring its receive buffers' (published).
89 pub buffer_bytes: usize,
90 /// The most bytes the shard's IO buffers may hold, its receive buffers
91 /// included ([`Builder::io_buffer_cap`](crate::Builder::io_buffer_cap)).
92 pub buffer_cap: usize,
93 /// Times a buffer was wanted while every byte under the pool's cap was
94 /// in use; the IO waits for one to come back (published).
95 pub buffer_exhausted: u64,
96 /// Timers armed on the shard (published).
97 pub timers: usize,
98 /// Times the shard went to another thread because the thread running it
99 /// stayed in one poll past [`Builder::handoff_after`](crate::Builder::handoff_after).
100 pub handoffs: u64,
101 /// The longest poll the runtime saw on the shard, at the resolution of
102 /// its watch (a quarter of the handoff threshold); zero when no poll
103 /// lasted that long, or with the handoff off.
104 pub longest_poll: std::time::Duration,
105 /// Times a task's poll on the shard spent its budget of operations and
106 /// yielded to the shard's other tasks (see
107 /// [`task::coop`](crate::task::coop)).
108 pub budget_yields: u64,
109 /// How long each task poll on the shard ran, while its runtime records
110 /// it ([`Builder::record_latencies`](crate::Builder::record_latencies));
111 /// `None` when it does not, which is not a shard with no polls.
112 pub poll_times: Option<Histogram>,
113 /// How long the shard's IO operations took from their submission to the
114 /// kernel to their completion, by kind, while its runtime records
115 /// latencies ([`Builder::record_latencies`](crate::Builder::record_latencies));
116 /// `None` when it does not, and `None` on a readiness backend (epoll,
117 /// kqueue), whose calls are made once the kernel reports the descriptor
118 /// ready, so there is no wait in the kernel per operation to time. With
119 /// the `io-stats` feature only.
120 #[cfg(feature = "io-stats")]
121 pub io_latencies: Option<IoLatencies>,
122 /// The IO operations the shard's thread completed, while its runtime
123 /// counts them ([`Builder::count_io`](crate::Builder::count_io));
124 /// `None` when it does not, which is not a shard that did no IO
125 /// (published). With the `io-stats` feature only.
126 #[cfg(feature = "io-stats")]
127 pub io: Option<IoCounts>,
128 /// Errors of IO run on the shard.
129 pub errors: ErrorCounts,
130}
131
132/// The IO operations a shard's thread completed: see [`ShardStats::io`].
133/// With the `io-stats` feature only.
134///
135/// An operation is counted when the kernel completes it: a read or receive
136/// that returned bytes or the end of the stream, a write or send that took
137/// bytes, a connection accepted. A read served from the read-ahead is
138/// counted when the read-ahead's own read completed, and a write taken into
139/// the write-behind when the write-behind's own write did. Peeks and
140/// readiness probes are not counted. IO completed on a thread that runs no
141/// shard (the blocking pool's own calls, a thread outside the runtime) is
142/// counted by no shard.
143#[cfg(feature = "io-stats")]
144#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
145#[non_exhaustive]
146pub struct IoCounts {
147 /// Reads and receives.
148 pub reads: u64,
149 /// Writes and sends.
150 pub writes: u64,
151 /// Bytes the counted reads returned.
152 pub bytes_read: u64,
153 /// Bytes the counted writes took.
154 pub bytes_written: u64,
155 /// Connections accepted.
156 pub accepts: u64,
157}
158
159/// How long each kind of a shard's IO operations took, from their
160/// submission to the kernel to their completion: see
161/// [`ShardStats::io_latencies`].
162///
163/// A multishot receive or accept is one operation for many arrivals and is
164/// not timed; nor is a cancelled operation or a readiness probe. File calls
165/// are timed on the ring, not the ones the blocking pool makes.
166#[cfg(feature = "io-stats")]
167#[derive(Clone, Debug)]
168#[non_exhaustive]
169pub struct IoLatencies {
170 /// Reads and receives.
171 pub reads: Histogram,
172 /// Writes and sends.
173 pub writes: Histogram,
174 /// Accepts, one connection each.
175 pub accepts: Histogram,
176 /// Connects.
177 pub connects: Histogram,
178 /// Reads and writes of files.
179 pub files: Histogram,
180}
181
182/// Buckets of a [`Histogram`].
183pub(crate) const BUCKETS: usize = 64;
184
185/// The bucket of a sample of `nanos` nanoseconds.
186pub(crate) fn bucket_of(nanos: u64) -> usize {
187 (u64::BITS - 1 - (nanos | 1).leading_zeros()) as usize
188}
189
190/// A log2 histogram of durations: bucket `i` counts the samples from `2^i`
191/// nanoseconds up to (and not including) `2^(i+1)`; the first bucket also
192/// counts samples under a nanosecond.
193#[derive(Clone, PartialEq, Eq)]
194pub struct Histogram {
195 pub(crate) buckets: [u64; BUCKETS],
196}
197
198impl Histogram {
199 /// The samples counted.
200 pub fn count(&self) -> u64 {
201 self.buckets.iter().sum()
202 }
203
204 /// Each bucket that holds samples: its lowest duration, the duration it
205 /// ends before, and its count, from the shortest up.
206 pub fn buckets(
207 &self,
208 ) -> impl Iterator<Item = (std::time::Duration, std::time::Duration, u64)> + '_ {
209 self.buckets
210 .iter()
211 .enumerate()
212 .filter(|&(_, &n)| n > 0)
213 .map(|(i, &n)| {
214 let low = if i == 0 { 0 } else { 1u64 << i };
215 let high = 1u64.checked_shl(i as u32 + 1).unwrap_or(u64::MAX);
216 (
217 std::time::Duration::from_nanos(low),
218 std::time::Duration::from_nanos(high),
219 n,
220 )
221 })
222 }
223}
224
225impl core::fmt::Debug for Histogram {
226 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
227 f.debug_list().entries(self.buckets()).finish()
228 }
229}
230
231/// IO errors counted by [`io::ErrorKind`], and two kinds of failure
232/// counted apart.
233///
234/// An error counted is one an operation on a socket, pipe or file ended
235/// with, as the system gave it: not a "try again" the driver answers by
236/// waiting, nor the cancel of an operation whose task went away.
237#[derive(Clone, PartialEq, Eq)]
238#[non_exhaustive]
239pub struct ErrorCounts {
240 pub(crate) by_kind: [u64; KINDS.len()],
241 /// Handles the shard could not register: the kernel refused to watch
242 /// the descriptor, or to let the shard's driver take it.
243 pub refused_registrations: u64,
244 /// Accepts that failed because the process or the system had no file
245 /// descriptor left (counted among the errors by kind too).
246 pub accepts_out_of_descriptors: u64,
247}
248
249impl ErrorCounts {
250 /// Errors of `kind`. The kinds are counted apart from a fixed table
251 /// holding every stable kind the system's IO errors map to; one outside
252 /// it is counted, and read, as [`io::ErrorKind::Other`].
253 pub fn get(&self, kind: io::ErrorKind) -> u64 {
254 self.by_kind[kind_index(kind)]
255 }
256
257 /// Each kind with errors, and how many, in the table's fixed order.
258 pub fn iter(&self) -> impl Iterator<Item = (io::ErrorKind, u64)> + '_ {
259 KINDS
260 .iter()
261 .copied()
262 .zip(self.by_kind.iter().copied())
263 .filter(|&(_, n)| n > 0)
264 }
265}
266
267impl Default for ErrorCounts {
268 fn default() -> Self {
269 Self {
270 by_kind: [0; KINDS.len()],
271 refused_registrations: 0,
272 accepts_out_of_descriptors: 0,
273 }
274 }
275}
276
277impl core::fmt::Debug for ErrorCounts {
278 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
279 f.debug_struct("ErrorCounts")
280 .field("by_kind", &self.iter().collect::<Vec<_>>())
281 .field("refused_registrations", &self.refused_registrations)
282 .field(
283 "accepts_out_of_descriptors",
284 &self.accepts_out_of_descriptors,
285 )
286 .finish()
287 }
288}
289
290/// A snapshot of the counters of every shard alive in the process and of
291/// the blocking pools. See [`Stats`].
292///
293/// ```
294/// let stats = nuclei::stats();
295/// for shard in &stats.shards {
296/// println!("{:?}: {} tasks, {} polls", shard.worker, shard.tasks, shard.polls);
297/// }
298/// ```
299pub fn stats() -> Stats {
300 crate::rt::counters::snapshot()
301}