Skip to main content

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}