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