Skip to main content

ntex_server/
metrics.rs

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