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#[derive(Clone, Debug, Default, PartialEq, Eq)]
18#[non_exhaustive]
19pub struct ServerMetrics {
20 pub workers: Vec<WorkerMetrics>,
22 pub listeners: Vec<ListenerMetrics>,
24 pub accept_pauses: u64,
27 pub accept_paused_duration: Duration,
30 pub accept_backlog: usize,
33 pub incoming_cpu_hits: u64,
36 pub incoming_cpu_misses: u64,
40 pub rescheduled: u64,
43}
44
45#[derive(Clone, Debug, Default, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct WorkerMetrics {
51 pub id: WorkerId,
53 pub items: u64,
55 pub active_connections: usize,
57 pub not_ready: u64,
60 pub not_ready_duration: Duration,
63 pub restarts: u64,
65 pub io: Vec<IoConfigMetrics>,
69 pub io_other: IoMetricsSnapshot,
72 pub page_cache: PageCacheSnapshot,
78 pub runtime: RuntimeMetrics,
80}
81
82#[derive(Clone, Debug, Default, PartialEq, Eq)]
88#[non_exhaustive]
89pub struct RuntimeMetrics {
90 pub busy_duration: Option<Duration>,
96 pub polls: Option<u64>,
99 pub polls_again: Option<u64>,
102 pub self_wakes: Option<u64>,
104 pub poll_histogram: Option<Histogram>,
106 pub current_turn: Option<Duration>,
109 pub current_task: Option<&'static Location<'static>>,
111 pub latency: Option<LatencyMetrics>,
118}
119
120#[derive(Clone, Debug, Default, PartialEq, Eq)]
122#[non_exhaustive]
123pub struct ListenerMetrics {
124 pub name: String,
126 pub addr: String,
128 pub accepted: u64,
130 pub connection_errors: u64,
133 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#[derive(Debug, Default)]
149struct Period {
150 count: AtomicU64,
151 state: AtomicU64,
152}
153
154impl Period {
155 const OPEN: u64 = 1;
156 const ENDING: u64 = 2;
158 const MASK: u64 = u64::MAX >> 2;
159
160 fn set(&self, epoch: Instant, on: bool) {
162 let state = self.state.load(Ordering::Relaxed);
163 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 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 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 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 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 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 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#[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#[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 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 pub(crate) fn current() -> Option<Arc<WorkerSlot>> {
434 CURRENT_WORKER.with(|w| w.borrow().clone())
435 }
436
437 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 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 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 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 assert!(total >= prev, "{total:?} < {prev:?}");
591 prev = total;
592 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 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 #[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}