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#[derive(Clone, Debug, Default, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct ServerMetrics {
19 pub workers: Vec<WorkerMetrics>,
21 pub listeners: Vec<ListenerMetrics>,
23 pub accept_pauses: u64,
26 pub accept_paused_duration: Duration,
29 pub accept_backlog: usize,
32 pub incoming_cpu_hits: u64,
35 pub incoming_cpu_misses: u64,
39 pub rescheduled: u64,
42}
43
44#[derive(Clone, Debug, Default, PartialEq, Eq)]
48#[non_exhaustive]
49pub struct WorkerMetrics {
50 pub id: WorkerId,
52 pub items: u64,
54 pub active_connections: usize,
56 pub not_ready: u64,
59 pub not_ready_duration: Duration,
62 pub restarts: u64,
64 pub io: Vec<IoConfigMetrics>,
68 pub io_other: IoMetricsSnapshot,
71 pub page_cache: PageCacheSnapshot,
77 pub runtime: RuntimeMetrics,
79}
80
81#[derive(Clone, Debug, Default, PartialEq, Eq)]
87#[non_exhaustive]
88pub struct RuntimeMetrics {
89 pub busy_duration: Option<Duration>,
95 pub polls: Option<u64>,
98 pub polls_again: Option<u64>,
101 pub self_wakes: Option<u64>,
103 pub poll_histogram: Option<Histogram>,
105 pub current_turn: Option<Duration>,
108 pub current_task: Option<&'static Location<'static>>,
110 pub latency: Option<LatencyMetrics>,
117}
118
119#[derive(Clone, Debug, Default, PartialEq, Eq)]
121#[non_exhaustive]
122pub struct ListenerMetrics {
123 pub name: String,
125 pub addr: String,
127 pub accepted: u64,
129 pub connection_errors: u64,
132 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#[derive(Debug, Default)]
148struct Period {
149 count: AtomicU64,
150 state: AtomicU64,
151}
152
153impl Period {
154 const OPEN: u64 = 1;
155 const ENDING: u64 = 2;
157 const MASK: u64 = u64::MAX >> 2;
158
159 fn set(&self, epoch: Instant, on: bool) {
161 let state = self.state.load(Ordering::Relaxed);
162 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 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 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 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 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 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 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#[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#[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 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 pub(crate) fn current() -> Option<Arc<WorkerSlot>> {
432 CURRENT_WORKER.with(|w| w.borrow().clone())
433 }
434
435 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 assert!(total >= prev, "{total:?} < {prev:?}");
569 prev = total;
570 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 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 #[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}