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#[derive(Clone, Debug, Default, PartialEq, Eq)]
16#[non_exhaustive]
17pub struct ServerMetrics {
18 pub workers: Vec<WorkerMetrics>,
20 pub listeners: Vec<ListenerMetrics>,
22 pub accept_pauses: u64,
25 pub accept_paused_duration: Duration,
28 pub accept_backlog: usize,
31 pub incoming_cpu_hits: u64,
34 pub incoming_cpu_misses: u64,
38 pub rescheduled: u64,
41}
42
43#[derive(Clone, Debug, Default, PartialEq, Eq)]
47#[non_exhaustive]
48pub struct WorkerMetrics {
49 pub id: WorkerId,
51 pub items: u64,
53 pub active_connections: usize,
55 pub not_ready: u64,
58 pub not_ready_duration: Duration,
61 pub restarts: u64,
63 pub io: Vec<IoConfigMetrics>,
67 pub io_other: IoMetricsSnapshot,
70 pub page_cache: PageCacheSnapshot,
76 pub runtime: RuntimeMetrics,
78}
79
80#[derive(Clone, Debug, Default, PartialEq, Eq)]
86#[non_exhaustive]
87pub struct RuntimeMetrics {
88 pub busy_duration: Option<Duration>,
94 pub polls: Option<u64>,
97 pub polls_again: Option<u64>,
100 pub self_wakes: Option<u64>,
102 pub poll_histogram: Option<Histogram>,
104 pub current_turn: Option<Duration>,
107 pub current_task: Option<&'static Location<'static>>,
109 pub latency: Option<LatencyMetrics>,
116}
117
118#[derive(Clone, Debug, Default, PartialEq, Eq)]
120#[non_exhaustive]
121pub struct ListenerMetrics {
122 pub name: String,
124 pub addr: String,
126 pub accepted: u64,
128 pub connection_errors: u64,
131 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#[derive(Debug, Default)]
147struct Period {
148 count: AtomicU64,
149 state: AtomicU64,
150}
151
152impl Period {
153 const OPEN: u64 = 1;
154 const ENDING: u64 = 2;
156 const MASK: u64 = u64::MAX >> 2;
157
158 fn set(&self, epoch: Instant, on: bool) {
160 let state = self.state.load(Ordering::Relaxed);
161 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 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 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 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 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 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 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#[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#[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 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 pub(crate) fn current() -> Option<Arc<WorkerSlot>> {
431 CURRENT_WORKER.with(|w| w.borrow().clone())
432 }
433
434 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 assert!(total >= prev, "{total:?} < {prev:?}");
568 prev = total;
569 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 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 #[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}