Skip to main content

ntex_server/
metrics.rs

1use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering, fence};
2use std::sync::{Arc, Mutex, OnceLock};
3use std::{cell::RefCell, panic::Location, time::Duration, time::Instant};
4
5use ntex_bytes::{PageCacheMetrics, PageCacheSnapshot};
6use ntex_io::{IoConfigMetrics, IoMetricsGroup, IoMetricsSnapshot};
7use ntex_rt::Arbiter;
8use ntex_rt::metrics::{Histogram, LatencyMetrics};
9
10use crate::WorkerId;
11
12/// Snapshot of server metrics.
13///
14/// Counters and durations are totals since the server started. Rates over an
15/// interval are computed from the deltas of two snapshots.
16#[derive(Clone, Debug, Default, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct ServerMetrics {
19    /// Per-worker metrics, indexed by worker id.
20    pub workers: Vec<WorkerMetrics>,
21    /// Per-listener metrics, empty for servers without listeners.
22    pub listeners: Vec<ListenerMetrics>,
23    /// Number of times the accept loop stopped accepting connections, because
24    /// no worker was available or the server was paused.
25    pub accept_pauses: u64,
26    /// Total time the accept loop did not accept connections, including the
27    /// current pause.
28    pub accept_paused_duration: Duration,
29    /// Number of accepted connections waiting in the accept loop for an
30    /// available worker.
31    pub accept_backlog: usize,
32    /// Number of items sent to the worker on their incoming CPU, see
33    /// [`LoadBalancing::IncomingCpu`](crate::LoadBalancing::IncomingCpu).
34    pub incoming_cpu_hits: u64,
35    /// Number of items with a known incoming CPU that were sent to another
36    /// worker, because no worker is placed on that CPU or its worker was
37    /// busier than other workers.
38    pub incoming_cpu_misses: u64,
39    /// Number of items taken from the queue of a failed, unavailable or
40    /// blocked worker and sent to another worker.
41    pub rescheduled: u64,
42}
43
44/// Metrics of a single worker.
45///
46/// A restarted worker keeps the metrics of the worker it replaces.
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
48#[non_exhaustive]
49pub struct WorkerMetrics {
50    /// Worker id.
51    pub id: WorkerId,
52    /// Number of items, connections for network servers, the worker received.
53    pub items: u64,
54    /// Number of open connections, network servers only.
55    pub active_connections: usize,
56    /// Number of times the worker service stopped being ready, e.g. it reached
57    /// the maximum number of connections.
58    pub not_ready: u64,
59    /// Total time the worker service was not ready, including the current
60    /// period.
61    pub not_ready_duration: Duration,
62    /// Number of times the worker was restarted after a failure.
63    pub restarts: u64,
64    /// I/O statistics of connections created on the worker, one entry per
65    /// listener configuration and per configuration with
66    /// `IoConfig::set_metrics()` enabled.
67    pub io: Vec<IoConfigMetrics>,
68    /// I/O statistics of the worker's other connections, e.g. client
69    /// connections of configurations without their own metrics.
70    pub io_other: IoMetricsSnapshot,
71    /// Statistics of the worker thread's buffer page cache.
72    ///
73    /// The metrics are single-writer. While a failed worker's thread is still
74    /// releasing pages and its replacement already runs, updates can be lost,
75    /// so after a worker restart the statistics may be slightly off.
76    pub page_cache: PageCacheSnapshot,
77    /// Statistics of the worker's runtime.
78    pub runtime: RuntimeMetrics,
79}
80
81/// Runtime statistics of a worker.
82///
83/// Counters, durations and histograms are totals over all runtimes the
84/// worker slot ran on, a restarted worker continues the totals of the worker
85/// it replaces. Values a runtime backend does not provide are `None`.
86#[derive(Clone, Debug, Default, PartialEq, Eq)]
87#[non_exhaustive]
88pub struct RuntimeMetrics {
89    /// Total time the runtime was busy running tasks.
90    ///
91    /// The `busy_duration` delta divided by the elapsed time is the worker's
92    /// utilization. On tokio it is available only for current-thread
93    /// runtimes.
94    pub busy_duration: Option<Duration>,
95    /// Number of runtime turns that ran scheduled tasks. Not available on
96    /// tokio.
97    pub polls: Option<u64>,
98    /// Number of runtime turns that left scheduled tasks for the next turn.
99    /// Not available on tokio.
100    pub polls_again: Option<u64>,
101    /// Number of times a task woke itself while it was polled. Neon only.
102    pub self_wakes: Option<u64>,
103    /// Durations of runtime turns. Not available on tokio.
104    pub poll_histogram: Option<Histogram>,
105    /// How long the current runtime turn has been running, `None` while the
106    /// runtime is idle or the backend does not track turns.
107    pub current_turn: Option<Duration>,
108    /// Spawn location of the task the runtime is polling now. Neon only.
109    pub current_task: Option<&'static Location<'static>>,
110    /// Scheduling latency and stalls, `None` if latency sampling is disabled,
111    /// see `ntex_rt::Builder::latency_interval()`.
112    ///
113    /// `histogram`, `stalls` and `stall_duration` are totals over all
114    /// runtimes of the worker slot, the other fields describe the current
115    /// runtime.
116    pub latency: Option<LatencyMetrics>,
117}
118
119/// Metrics of a single listener.
120#[derive(Clone, Debug, Default, PartialEq, Eq)]
121#[non_exhaustive]
122pub struct ListenerMetrics {
123    /// Service name the listener was registered with.
124    pub name: String,
125    /// Local address of the listener.
126    pub addr: String,
127    /// Number of accepted connections.
128    pub accepted: u64,
129    /// Number of per-connection accept errors, e.g. the connection was reset
130    /// before it was accepted.
131    pub connection_errors: u64,
132    /// Number of other accept errors, e.g. the process ran out of file
133    /// descriptors. The listener stops accepting for a short time after each.
134    pub resource_errors: u64,
135}
136
137fn as_nanos(d: Duration) -> u64 {
138    u64::try_from(d.as_nanos()).unwrap_or(u64::MAX)
139}
140
141/// Accumulated period, e.g. time the worker was not ready.
142///
143/// The total and the open period are packed into one word, so a reader sees a
144/// coherent value with a single load: `state >> 2` is the total in
145/// nanoseconds, minus the start of the open period if `OPEN` is set. Values
146/// are nanoseconds since the epoch, modulo 2^62 (about 146 years).
147#[derive(Debug, Default)]
148struct Period {
149    count: AtomicU64,
150    state: AtomicU64,
151}
152
153impl Period {
154    const OPEN: u64 = 1;
155    // the open period is ending, its end time is not stored yet
156    const ENDING: u64 = 2;
157    const MASK: u64 = u64::MAX >> 2;
158
159    // single writer
160    fn set(&self, epoch: Instant, on: bool) {
161        let state = self.state.load(Ordering::Relaxed);
162        // called on every readiness check, read the clock only on changes
163        if on == (state & Self::OPEN != 0) {
164            return;
165        }
166        if on {
167            self.count.fetch_add(1, Ordering::Relaxed);
168            let acc = (state >> 2).wrapping_sub(as_nanos(epoch.elapsed()));
169            self.state.store((acc << 2) | Self::OPEN, Ordering::Release);
170        } else {
171            // readers wait for the end time instead of reading a later clock
172            self.state.store(state | Self::ENDING, Ordering::Relaxed);
173            fence(Ordering::SeqCst);
174            let total = (state >> 2).wrapping_add(as_nanos(epoch.elapsed()));
175            self.state.store(total << 2, Ordering::Release);
176        }
177    }
178
179    fn get(&self, epoch: Instant) -> (u64, Duration) {
180        let total = loop {
181            // acquire, the clock is read after the writer's
182            let state = self.state.load(Ordering::Acquire);
183            if state & Self::OPEN == 0 {
184                break state >> 2;
185            }
186            if state & Self::ENDING == 0 {
187                let now = as_nanos(epoch.elapsed());
188                fence(Ordering::SeqCst);
189                // the period did not start ending before the clock was read
190                if self.state.load(Ordering::Relaxed) == state {
191                    break (state >> 2).wrapping_add(now) & Self::MASK;
192                }
193            }
194            std::thread::yield_now();
195        };
196        (
197            self.count.load(Ordering::Relaxed),
198            Duration::from_nanos(total),
199        )
200    }
201}
202
203#[derive(Debug)]
204pub(crate) struct Metrics {
205    epoch: Instant,
206    workers: Box<[Arc<WorkerSlot>]>,
207    accept: OnceLock<AcceptSlot>,
208    incoming_cpu_hits: AtomicU64,
209    incoming_cpu_misses: AtomicU64,
210    rescheduled: AtomicU64,
211}
212
213impl Metrics {
214    pub(crate) fn new(workers: usize) -> Self {
215        let epoch = Instant::now();
216        Self {
217            epoch,
218            workers: (0..workers)
219                .map(|_| Arc::new(WorkerSlot::new(epoch)))
220                .collect(),
221            accept: OnceLock::new(),
222            incoming_cpu_hits: AtomicU64::new(0),
223            incoming_cpu_misses: AtomicU64::new(0),
224            rescheduled: AtomicU64::new(0),
225        }
226    }
227
228    /// An item with a known incoming CPU was sent to the worker on that CPU,
229    /// or to another worker.
230    pub(crate) fn incoming_cpu(&self, hit: bool) {
231        let counter = if hit {
232            &self.incoming_cpu_hits
233        } else {
234            &self.incoming_cpu_misses
235        };
236        counter.fetch_add(1, Ordering::Relaxed);
237    }
238
239    /// Items were taken from a worker's queue to be sent to other workers.
240    pub(crate) fn rescheduled(&self, n: u64) {
241        self.rescheduled.fetch_add(n, Ordering::Relaxed);
242    }
243
244    pub(crate) fn worker(&self, id: WorkerId) -> Option<Arc<WorkerSlot>> {
245        self.workers.get(id.0).cloned()
246    }
247
248    /// Registers the listeners, in accept loop order.
249    ///
250    /// Only the first call has an effect.
251    pub(crate) fn listeners<I>(&self, listeners: I) -> &AcceptSlot
252    where
253        I: IntoIterator<Item = (String, String)>,
254    {
255        self.accept.get_or_init(|| AcceptSlot {
256            epoch: self.epoch,
257            listeners: listeners
258                .into_iter()
259                .map(|(name, addr)| ListenerSlot {
260                    name,
261                    addr,
262                    ..Default::default()
263                })
264                .collect(),
265            paused: Period::default(),
266            backlog: AtomicUsize::new(0),
267        })
268    }
269
270    pub(crate) fn snapshot(&self) -> ServerMetrics {
271        let workers = self
272            .workers
273            .iter()
274            .enumerate()
275            .map(|(id, w)| {
276                let (not_ready, not_ready_duration) = w.not_ready.get(self.epoch);
277                WorkerMetrics {
278                    id: WorkerId(id),
279                    items: w.items.load(Ordering::Relaxed),
280                    active_connections: w.active.load(Ordering::Relaxed),
281                    not_ready,
282                    not_ready_duration,
283                    restarts: w.restarts.load(Ordering::Relaxed),
284                    io: w.io.snapshot(),
285                    io_other: w.io.other().snapshot(),
286                    page_cache: w.page_cache.snapshot(),
287                    runtime: w.runtime_metrics(),
288                }
289            })
290            .collect();
291
292        let mut metrics = ServerMetrics {
293            workers,
294            incoming_cpu_hits: self.incoming_cpu_hits.load(Ordering::Relaxed),
295            incoming_cpu_misses: self.incoming_cpu_misses.load(Ordering::Relaxed),
296            rescheduled: self.rescheduled.load(Ordering::Relaxed),
297            ..Default::default()
298        };
299        if let Some(accept) = self.accept.get() {
300            (metrics.accept_pauses, metrics.accept_paused_duration) = accept.paused.get(self.epoch);
301            metrics.accept_backlog = accept.backlog.load(Ordering::Relaxed);
302            metrics.listeners = accept
303                .listeners
304                .iter()
305                .map(|l| ListenerMetrics {
306                    name: l.name.clone(),
307                    addr: l.addr.clone(),
308                    accepted: l.accepted.load(Ordering::Relaxed),
309                    connection_errors: l.connection_errors.load(Ordering::Relaxed),
310                    resource_errors: l.resource_errors.load(Ordering::Relaxed),
311                })
312                .collect();
313        }
314        metrics
315    }
316}
317
318// separate cache lines, every worker updates its own slot
319#[derive(Debug)]
320#[repr(align(64))]
321pub(crate) struct WorkerSlot {
322    epoch: Instant,
323    items: AtomicU64,
324    active: AtomicUsize,
325    not_ready: Period,
326    restarts: AtomicU64,
327    io: Arc<IoMetricsGroup>,
328    page_cache: Arc<PageCacheMetrics>,
329    runtime: Mutex<RuntimeSlot>,
330}
331
332/// Runtime of the worker and the totals of the runtimes it replaced.
333#[derive(Debug, Default)]
334struct RuntimeSlot {
335    arbiter: Option<Arbiter>,
336    base: RuntimeTotals,
337}
338
339#[derive(Debug, Default)]
340struct RuntimeTotals {
341    busy: Duration,
342    polls: u64,
343    polls_again: u64,
344    self_wakes: u64,
345    poll_histogram: Histogram,
346    latency_histogram: Histogram,
347    stalls: u64,
348    stall_duration: Duration,
349}
350
351impl RuntimeTotals {
352    fn add(&mut self, m: &RuntimeMetrics) {
353        self.busy += m.busy_duration.unwrap_or_default();
354        self.polls += m.polls.unwrap_or_default();
355        self.polls_again += m.polls_again.unwrap_or_default();
356        self.self_wakes += m.self_wakes.unwrap_or_default();
357        if let Some(ref h) = m.poll_histogram {
358            self.poll_histogram = self.poll_histogram.merge(h);
359        }
360        if let Some(ref l) = m.latency {
361            self.latency_histogram = self.latency_histogram.merge(&l.histogram);
362            self.stalls += l.stalls;
363            self.stall_duration += l.stall_duration;
364        }
365    }
366}
367
368fn runtime_metrics(arb: &Arbiter) -> RuntimeMetrics {
369    let hnd = arb.handle();
370    RuntimeMetrics {
371        busy_duration: hnd.busy_duration(),
372        polls: hnd.poll_count(),
373        polls_again: hnd.poll_again_count(),
374        self_wakes: hnd.self_wake_count(),
375        poll_histogram: hnd.poll_histogram(),
376        current_turn: hnd.current_turn(),
377        current_task: hnd.current_task(),
378        latency: arb.latency(),
379    }
380}
381
382thread_local! {
383    static CURRENT_WORKER: RefCell<Option<Arc<WorkerSlot>>> = const { RefCell::new(None) };
384}
385
386impl WorkerSlot {
387    fn new(epoch: Instant) -> Self {
388        Self {
389            epoch,
390            items: AtomicU64::new(0),
391            active: AtomicUsize::new(0),
392            not_ready: Period::default(),
393            restarts: AtomicU64::new(0),
394            io: IoMetricsGroup::new(),
395            page_cache: PageCacheMetrics::new(),
396            runtime: Mutex::default(),
397        }
398    }
399
400    /// Sets the arbiter the worker runs on, the totals of the previous
401    /// arbiter are kept.
402    pub(crate) fn set_arbiter(&self, arbiter: Arbiter) {
403        let mut rt = self.runtime.lock().unwrap();
404        if let Some(prev) = rt.arbiter.replace(arbiter) {
405            let m = runtime_metrics(&prev);
406            rt.base.add(&m);
407        }
408    }
409
410    fn runtime_metrics(&self) -> RuntimeMetrics {
411        let rt = self.runtime.lock().unwrap();
412        let Some(ref arb) = rt.arbiter else {
413            return RuntimeMetrics::default();
414        };
415        let base = &rt.base;
416        let mut m = runtime_metrics(arb);
417        m.busy_duration = m.busy_duration.map(|d| d + base.busy);
418        m.polls = m.polls.map(|n| n + base.polls);
419        m.polls_again = m.polls_again.map(|n| n + base.polls_again);
420        m.self_wakes = m.self_wakes.map(|n| n + base.self_wakes);
421        m.poll_histogram = m.poll_histogram.map(|h| h.merge(&base.poll_histogram));
422        if let Some(ref mut l) = m.latency {
423            l.histogram = l.histogram.merge(&base.latency_histogram);
424            l.stalls += base.stalls;
425            l.stall_duration += base.stall_duration;
426        }
427        m
428    }
429
430    /// Metrics slot of the worker running on the current thread.
431    pub(crate) fn current() -> Option<Arc<WorkerSlot>> {
432        CURRENT_WORKER.with(|w| w.borrow().clone())
433    }
434
435    /// Installs the slot for the current thread, connections created on the
436    /// thread afterwards report I/O statistics to the slot, the thread's page
437    /// cache reports to the slot.
438    pub(crate) fn set_current(slot: Option<Arc<WorkerSlot>>) {
439        if let Some(ref slot) = slot {
440            IoMetricsGroup::set_current(slot.io.clone());
441            PageCacheMetrics::set_current(slot.page_cache.clone());
442        }
443        CURRENT_WORKER.with(|w| *w.borrow_mut() = slot);
444    }
445
446    pub(crate) fn item(&self) {
447        self.items.fetch_add(1, Ordering::Relaxed);
448    }
449
450    pub(crate) fn ready(&self, ready: bool) {
451        self.not_ready.set(self.epoch, !ready);
452    }
453
454    pub(crate) fn restart(&self) {
455        self.restarts.fetch_add(1, Ordering::Relaxed);
456    }
457
458    pub(crate) fn active(&self) -> usize {
459        self.active.load(Ordering::Relaxed)
460    }
461
462    pub(crate) fn conn_opened(&self) {
463        self.active.fetch_add(1, Ordering::Relaxed);
464    }
465
466    pub(crate) fn conn_closed(&self) {
467        self.active.fetch_sub(1, Ordering::Relaxed);
468    }
469}
470
471#[derive(Debug)]
472pub(crate) struct AcceptSlot {
473    epoch: Instant,
474    listeners: Box<[ListenerSlot]>,
475    paused: Period,
476    backlog: AtomicUsize,
477}
478
479#[derive(Debug, Default)]
480struct ListenerSlot {
481    name: String,
482    addr: String,
483    accepted: AtomicU64,
484    connection_errors: AtomicU64,
485    resource_errors: AtomicU64,
486}
487
488impl AcceptSlot {
489    pub(crate) fn paused(&self, paused: bool) {
490        self.paused.set(self.epoch, paused);
491    }
492
493    pub(crate) fn backlog(&self, len: usize) {
494        self.backlog.store(len, Ordering::Relaxed);
495    }
496
497    pub(crate) fn accepted(&self, idx: usize) {
498        if let Some(l) = self.listeners.get(idx) {
499            l.accepted.fetch_add(1, Ordering::Relaxed);
500        }
501    }
502
503    pub(crate) fn connection_error(&self, idx: usize) {
504        if let Some(l) = self.listeners.get(idx) {
505            l.connection_errors.fetch_add(1, Ordering::Relaxed);
506        }
507    }
508
509    pub(crate) fn resource_error(&self, idx: usize) {
510        if let Some(l) = self.listeners.get(idx) {
511            l.resource_errors.fetch_add(1, Ordering::Relaxed);
512        }
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use std::sync::atomic::AtomicBool;
519
520    use super::*;
521
522    #[test]
523    fn period() {
524        let epoch = Instant::now();
525        let p = Period::default();
526        assert_eq!(p.get(epoch), (0, Duration::ZERO));
527
528        p.set(epoch, true);
529        p.set(epoch, true);
530        std::thread::sleep(Duration::from_millis(10));
531        let (cnt, cur) = p.get(epoch);
532        assert_eq!(cnt, 1);
533        assert!(cur >= Duration::from_millis(10));
534
535        p.set(epoch, false);
536        p.set(epoch, false);
537        let (cnt, total) = p.get(epoch);
538        assert_eq!(cnt, 1);
539        assert!(total >= cur);
540        std::thread::sleep(Duration::from_millis(5));
541        assert_eq!(p.get(epoch), (1, total));
542
543        p.set(epoch, true);
544        assert_eq!(p.get(epoch).0, 2);
545    }
546
547    #[test]
548    fn period_is_coherent() {
549        let epoch = Instant::now();
550        let p = Arc::new(Period::default());
551        let stop = Arc::new(AtomicBool::new(false));
552        let writer = {
553            let (p, stop) = (p.clone(), stop.clone());
554            std::thread::spawn(move || {
555                while !stop.load(Ordering::Relaxed) {
556                    p.set(epoch, true);
557                    let start = Instant::now();
558                    while start.elapsed() < Duration::from_micros(20) {}
559                    p.set(epoch, false);
560                }
561            })
562        };
563        let deadline = Instant::now() + Duration::from_millis(500);
564        let mut prev = Duration::ZERO;
565        while Instant::now() < deadline {
566            let (_, total) = p.get(epoch);
567            // a completed period is not counted twice
568            assert!(total >= prev, "{total:?} < {prev:?}");
569            prev = total;
570            // periods do not overlap, the writer is almost always in one
571            let uptime = epoch.elapsed();
572            assert!(total <= uptime, "{total:?} > {uptime:?}");
573        }
574        stop.store(true, Ordering::Relaxed);
575        writer.join().unwrap();
576    }
577
578    #[test]
579    fn snapshot() {
580        let m = Metrics::new(2);
581        let w = m.worker(WorkerId(1)).unwrap();
582        assert!(m.worker(WorkerId(2)).is_none());
583        w.item();
584        w.item();
585        w.conn_opened();
586        w.conn_opened();
587        w.conn_closed();
588        w.restart();
589
590        let acc = m.listeners([("srv".to_string(), "127.0.0.1:1".to_string())]);
591        acc.accepted(0);
592        acc.connection_error(0);
593        acc.resource_error(0);
594        acc.resource_error(1);
595        acc.backlog(3);
596        // the first registration wins
597        m.listeners([]);
598
599        let s = m.snapshot();
600        assert_eq!(s.workers.len(), 2);
601        assert_eq!(s.workers[0], WorkerMetrics::default());
602        assert_eq!(s.workers[1].id, WorkerId(1));
603        assert_eq!(s.workers[1].items, 2);
604        assert_eq!(s.workers[1].active_connections, 1);
605        assert_eq!(s.workers[1].restarts, 1);
606        assert_eq!(s.listeners.len(), 1);
607        assert_eq!(s.listeners[0].name, "srv");
608        assert_eq!(s.listeners[0].addr, "127.0.0.1:1");
609        assert_eq!(s.listeners[0].accepted, 1);
610        assert_eq!(s.listeners[0].connection_errors, 1);
611        assert_eq!(s.listeners[0].resource_errors, 1);
612        assert_eq!(s.accept_backlog, 3);
613        assert_eq!(s.accept_pauses, 0);
614    }
615
616    /// Runtime totals are kept when a worker's arbiter is replaced.
617    #[ntex::test]
618    async fn runtime_restart() {
619        let busy = |a: &Arbiter| {
620            a.handle().spawn(async {
621                std::thread::sleep(Duration::from_millis(20));
622            });
623        };
624
625        let m = Metrics::new(1);
626        let w = m.worker(WorkerId(0)).unwrap();
627        assert_eq!(w.runtime_metrics(), RuntimeMetrics::default());
628
629        let a1 = Arbiter::with_name("rt-metrics-1".to_string());
630        w.set_arbiter(a1.clone());
631        busy(&a1);
632        ntex::time::sleep(ntex::time::Millis(250)).await;
633        let m1 = w.runtime_metrics();
634        let b1 = m1.busy_duration.unwrap();
635        let l1 = m1.latency.clone().unwrap().histogram.count();
636        assert!(b1 >= Duration::from_millis(20));
637        assert!(l1 > 0);
638        a1.stop();
639
640        let a2 = Arbiter::with_name("rt-metrics-2".to_string());
641        w.set_arbiter(a2.clone());
642        busy(&a2);
643        ntex::time::sleep(ntex::time::Millis(250)).await;
644        let m2 = w.runtime_metrics();
645        assert!(m2.busy_duration.unwrap() >= b1 + Duration::from_millis(20));
646        assert!(m2.latency.unwrap().histogram.count() > l1);
647        if let (Some(p1), Some(p2)) = (m1.polls, m2.polls) {
648            assert!(p2 > p1);
649        }
650        a2.stop();
651    }
652}