1use crate::config::Server;
4use crate::metrics::{MetricsCollector, MetricsSnapshot};
5use crate::router::BackendSelector;
6use crate::tui::dashboard::{
7 BackendDisplay, BackendView, BufferPoolStats, DashboardMetrics, DashboardState,
8 DashboardUserStats, RemoteBackendView, RemoteDashboardState,
9};
10use crate::tui::log_capture::LogBuffer;
11use crate::tui::rate_estimator::{CumulativeCount, RateEstimate, RateEstimator, RatePerSecond};
12use crate::types::tui::{CommandsPerSecond, HistorySize, Throughput, Timestamp};
13use std::collections::{HashMap, HashSet, VecDeque};
14use std::sync::Arc;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
18pub enum ViewMode {
19 #[default]
21 Normal,
22 LogFullscreen,
24}
25
26#[allow(clippy::struct_field_names)]
28#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
29pub struct ThroughputPoint {
30 sent_per_sec: Throughput,
32 received_per_sec: Throughput,
34 commands_per_sec: Option<CommandsPerSecond>,
36 raw_sent_per_sec: Option<Throughput>,
38 raw_received_per_sec: Option<Throughput>,
40 raw_commands_per_sec: Option<CommandsPerSecond>,
42}
43
44impl ThroughputPoint {
45 #[must_use]
47 pub const fn new_backend(
48 _timestamp: Timestamp,
49 sent_per_sec: Throughput,
50 received_per_sec: Throughput,
51 commands_per_sec: CommandsPerSecond,
52 ) -> Self {
53 Self {
54 sent_per_sec,
55 received_per_sec,
56 commands_per_sec: Some(commands_per_sec),
57 raw_sent_per_sec: Some(sent_per_sec),
58 raw_received_per_sec: Some(received_per_sec),
59 raw_commands_per_sec: Some(commands_per_sec),
60 }
61 }
62
63 #[must_use]
65 #[allow(clippy::too_many_arguments)]
66 pub const fn new_backend_estimate(
67 _timestamp: Timestamp,
68 raw_sent_per_sec: Throughput,
69 sent_per_sec: Throughput,
70 raw_received_per_sec: Throughput,
71 received_per_sec: Throughput,
72 raw_commands_per_sec: CommandsPerSecond,
73 commands_per_sec: CommandsPerSecond,
74 ) -> Self {
75 Self {
76 sent_per_sec,
77 received_per_sec,
78 commands_per_sec: Some(commands_per_sec),
79 raw_sent_per_sec: Some(raw_sent_per_sec),
80 raw_received_per_sec: Some(raw_received_per_sec),
81 raw_commands_per_sec: Some(raw_commands_per_sec),
82 }
83 }
84
85 #[must_use]
87 pub const fn new_client(
88 _timestamp: Timestamp,
89 sent_per_sec: Throughput,
90 received_per_sec: Throughput,
91 ) -> Self {
92 Self {
93 sent_per_sec,
94 received_per_sec,
95 commands_per_sec: None,
96 raw_sent_per_sec: Some(sent_per_sec),
97 raw_received_per_sec: Some(received_per_sec),
98 raw_commands_per_sec: None,
99 }
100 }
101
102 #[must_use]
104 pub const fn new_client_estimate(
105 _timestamp: Timestamp,
106 raw_sent_per_sec: Throughput,
107 sent_per_sec: Throughput,
108 raw_received_per_sec: Throughput,
109 received_per_sec: Throughput,
110 ) -> Self {
111 Self {
112 sent_per_sec,
113 received_per_sec,
114 commands_per_sec: None,
115 raw_sent_per_sec: Some(raw_sent_per_sec),
116 raw_received_per_sec: Some(raw_received_per_sec),
117 raw_commands_per_sec: None,
118 }
119 }
120
121 #[must_use]
123 #[inline]
124 pub const fn sent_per_sec(&self) -> Throughput {
125 self.sent_per_sec
126 }
127
128 #[must_use]
130 #[inline]
131 pub const fn received_per_sec(&self) -> Throughput {
132 self.received_per_sec
133 }
134
135 #[must_use]
137 #[inline]
138 pub const fn commands_per_sec(&self) -> Option<CommandsPerSecond> {
139 self.commands_per_sec
140 }
141
142 #[must_use]
144 #[inline]
145 pub fn raw_sent_per_sec(&self) -> Throughput {
146 self.raw_sent_per_sec.unwrap_or(self.sent_per_sec)
147 }
148
149 #[must_use]
151 #[inline]
152 pub fn raw_received_per_sec(&self) -> Throughput {
153 self.raw_received_per_sec.unwrap_or(self.received_per_sec)
154 }
155
156 #[must_use]
158 #[inline]
159 pub fn raw_commands_per_sec(&self) -> Option<CommandsPerSecond> {
160 self.raw_commands_per_sec.or(self.commands_per_sec)
161 }
162}
163
164#[derive(Debug, Clone)]
166struct ThroughputHistory {
167 points: VecDeque<ThroughputPoint>,
168 capacity: HistorySize,
169}
170
171impl ThroughputHistory {
172 #[must_use]
174 fn new(capacity: HistorySize) -> Self {
175 Self {
176 points: VecDeque::with_capacity(capacity.get()),
177 capacity,
178 }
179 }
180
181 fn push(&mut self, point: ThroughputPoint) {
183 if self.points.len() >= self.capacity.get() {
184 self.points.pop_front();
185 }
186 self.points.push_back(point);
187 }
188
189 #[must_use]
191 const fn points(&self) -> &VecDeque<ThroughputPoint> {
192 &self.points
193 }
194
195 #[must_use]
197 fn latest(&self) -> Option<&ThroughputPoint> {
198 self.points.back()
199 }
200}
201
202#[derive(Debug, Clone, Default)]
203struct BackendRateEstimators {
204 sent: RateEstimator,
205 received: RateEstimator,
206 commands: RateEstimator,
207}
208
209#[derive(Debug, Clone, Default)]
210struct UserRateEstimators {
211 sent: RateEstimator,
212 received: RateEstimator,
213}
214
215pub struct TuiAppBuilder {
240 metrics: MetricsCollector,
241 router: Arc<BackendSelector>,
242 servers: Arc<[Server]>,
243 cache: Option<Arc<crate::cache::UnifiedCache>>,
244 buffer_pool: Option<crate::pool::BufferPool>,
245 log_buffer: Option<LogBuffer>,
246 history_size: HistorySize,
247}
248
249impl TuiAppBuilder {
250 #[must_use]
252 pub const fn new(
253 metrics: MetricsCollector,
254 router: Arc<BackendSelector>,
255 servers: Arc<[Server]>,
256 ) -> Self {
257 Self {
258 metrics,
259 router,
260 servers,
261 cache: None,
262 buffer_pool: None,
263 log_buffer: None,
264 history_size: HistorySize::DEFAULT,
265 }
266 }
267
268 #[must_use]
270 pub fn with_cache(mut self, cache: Arc<crate::cache::UnifiedCache>) -> Self {
271 self.cache = Some(cache);
272 self
273 }
274
275 #[must_use]
277 pub fn with_buffer_pool(mut self, buffer_pool: crate::pool::BufferPool) -> Self {
278 self.buffer_pool = Some(buffer_pool);
279 self
280 }
281
282 #[must_use]
284 pub fn with_log_buffer(mut self, log_buffer: LogBuffer) -> Self {
285 self.log_buffer = Some(log_buffer);
286 self
287 }
288
289 #[must_use]
291 pub const fn with_history_size(mut self, history_size: HistorySize) -> Self {
292 self.history_size = history_size;
293 self
294 }
295
296 #[must_use]
298 pub fn build(self) -> TuiApp {
299 use crate::tui::SystemMonitor;
300
301 let snapshot = Arc::new(self.metrics.snapshot(self.cache.as_deref()));
302 let backend_count = self.servers.len();
303
304 let backend_history = (0..backend_count)
306 .map(|_| ThroughputHistory::new(self.history_size))
307 .collect();
308 let backend_rate_estimators = (0..backend_count)
309 .map(|_| BackendRateEstimators::default())
310 .collect();
311
312 TuiApp {
313 metrics: self.metrics,
314 router: self.router,
315 servers: self.servers,
316 cache: self.cache,
317 buffer_pool: self.buffer_pool,
318 snapshot,
319 backend_history,
320 backend_rate_estimators,
321 client_history: ThroughputHistory::new(self.history_size),
322 client_sent_estimator: RateEstimator::default(),
323 client_received_estimator: RateEstimator::default(),
324 user_rate_estimators: HashMap::new(),
325 previous_snapshot: None,
326 last_update: Timestamp::now(),
327 log_buffer: Arc::new(self.log_buffer.unwrap_or_default()),
328 view_mode: ViewMode::default(),
329 show_details: false,
330 system_monitor: SystemMonitor::new(),
331 system_stats: crate::tui::SystemStats::default(),
332 }
333 }
334}
335
336pub struct TuiApp {
338 metrics: MetricsCollector,
340 router: Arc<BackendSelector>,
342 servers: Arc<[Server]>,
344 snapshot: Arc<MetricsSnapshot>,
346 backend_history: Vec<ThroughputHistory>,
348 backend_rate_estimators: Vec<BackendRateEstimators>,
350 client_history: ThroughputHistory,
352 client_sent_estimator: RateEstimator,
354 client_received_estimator: RateEstimator,
356 user_rate_estimators: HashMap<String, UserRateEstimators>,
358 previous_snapshot: Option<Arc<MetricsSnapshot>>,
360 last_update: Timestamp,
362 log_buffer: Arc<LogBuffer>,
364 view_mode: ViewMode,
366 show_details: bool,
368 system_monitor: crate::tui::SystemMonitor,
370 system_stats: crate::tui::SystemStats,
372 cache: Option<Arc<crate::cache::UnifiedCache>>,
374 buffer_pool: Option<crate::pool::BufferPool>,
376}
377
378impl TuiApp {
379 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] const fn rate_as_u64(rate: RatePerSecond) -> u64 {
381 rate.get() as u64
382 }
383
384 #[must_use]
389 pub fn new(
390 metrics: MetricsCollector,
391 router: Arc<BackendSelector>,
392 servers: Arc<[Server]>,
393 ) -> Self {
394 TuiAppBuilder::new(metrics, router, servers).build()
395 }
396
397 fn estimate_or_decay(
398 estimator: &mut RateEstimator,
399 total: u64,
400 now: Timestamp,
401 ) -> RateEstimate {
402 estimator
403 .record(CumulativeCount::new(total), now)
404 .unwrap_or_else(|| RateEstimate::new(RatePerSecond::zero(), estimator.rate_at(now)))
405 }
406
407 fn throughput_estimate(
408 estimator: &mut RateEstimator,
409 total: u64,
410 now: Timestamp,
411 ) -> (Throughput, Throughput) {
412 let estimate = Self::estimate_or_decay(estimator, total, now);
413 (
414 Throughput::new(estimate.raw().get()),
415 Throughput::new(estimate.smoothed().get()),
416 )
417 }
418
419 fn command_estimate(
420 estimator: &mut RateEstimator,
421 total: u64,
422 now: Timestamp,
423 ) -> (CommandsPerSecond, CommandsPerSecond) {
424 let estimate = Self::estimate_or_decay(estimator, total, now);
425 (
426 CommandsPerSecond::new(estimate.raw().get()),
427 CommandsPerSecond::new(estimate.smoothed().get()),
428 )
429 }
430
431 fn build_backend_estimated_throughput_point(
432 now: Timestamp,
433 estimators: &mut BackendRateEstimators,
434 stats: &crate::metrics::BackendStats,
435 ) -> ThroughputPoint {
436 let (raw_sent, sent) =
437 Self::throughput_estimate(&mut estimators.sent, stats.bytes_sent.as_u64(), now);
438 let (raw_received, received) =
439 Self::throughput_estimate(&mut estimators.received, stats.bytes_received.as_u64(), now);
440 let (raw_commands, commands) =
441 Self::command_estimate(&mut estimators.commands, stats.total_commands.get(), now);
442
443 ThroughputPoint::new_backend_estimate(
444 now,
445 raw_sent,
446 sent,
447 raw_received,
448 received,
449 raw_commands,
450 commands,
451 )
452 }
453
454 fn estimate_user_rates(
455 now: Timestamp,
456 estimators: &mut UserRateEstimators,
457 current: &crate::metrics::UserStats,
458 ) -> crate::metrics::UserStats {
459 use crate::types::BytesPerSecondRate;
460
461 let sent = Self::estimate_or_decay(&mut estimators.sent, current.bytes_sent.as_u64(), now);
462 let received = Self::estimate_or_decay(
463 &mut estimators.received,
464 current.bytes_received.as_u64(),
465 now,
466 );
467
468 crate::metrics::UserStats {
469 username: current.username.clone(),
470 active_connections: current.active_connections,
471 total_connections: current.total_connections,
472 bytes_sent: current.bytes_sent,
473 bytes_received: current.bytes_received,
474 total_commands: current.total_commands,
475 errors: current.errors,
476 bytes_sent_per_sec: BytesPerSecondRate::new(Self::rate_as_u64(sent.smoothed())),
477 bytes_received_per_sec: BytesPerSecondRate::new(Self::rate_as_u64(received.smoothed())),
478 }
479 }
480
481 fn empty_user_estimators_at(now: Timestamp) -> UserRateEstimators {
482 let mut estimators = UserRateEstimators::default();
483 estimators.sent.record(CumulativeCount::new(0), now);
484 estimators.received.record(CumulativeCount::new(0), now);
485 estimators
486 }
487
488 fn prune_user_estimators(&mut self, user_stats: &[crate::metrics::UserStats]) {
489 let active_users = user_stats
490 .iter()
491 .map(|stats| stats.username.as_str())
492 .collect::<HashSet<_>>();
493 self.user_rate_estimators
494 .retain(|username, _| active_users.contains(username.as_str()));
495 }
496
497 fn seed_rate_estimators(&mut self, snapshot: &crate::metrics::MetricsSnapshot, now: Timestamp) {
498 self.client_sent_estimator.record(
499 CumulativeCount::new(snapshot.client_to_backend_bytes.as_u64()),
500 now,
501 );
502 self.client_received_estimator.record(
503 CumulativeCount::new(snapshot.backend_to_client_bytes.as_u64()),
504 now,
505 );
506
507 self.backend_rate_estimators
508 .iter_mut()
509 .zip(snapshot.backend_stats.iter())
510 .for_each(|(estimators, stats)| {
511 estimators
512 .sent
513 .record(CumulativeCount::new(stats.bytes_sent.as_u64()), now);
514 estimators
515 .received
516 .record(CumulativeCount::new(stats.bytes_received.as_u64()), now);
517 estimators
518 .commands
519 .record(CumulativeCount::new(stats.total_commands.get()), now);
520 });
521
522 self.user_rate_estimators.clear();
523 snapshot.user_stats.iter().for_each(|stats| {
524 let mut estimators = UserRateEstimators::default();
525 estimators
526 .sent
527 .record(CumulativeCount::new(stats.bytes_sent.as_u64()), now);
528 estimators
529 .received
530 .record(CumulativeCount::new(stats.bytes_received.as_u64()), now);
531 self.user_rate_estimators
532 .insert(stats.username.clone(), estimators);
533 });
534 }
535
536 pub fn update(&mut self) {
538 self.system_stats = self.system_monitor.update();
540
541 let new_snapshot = Arc::new(
542 self.metrics
543 .snapshot(self.cache.as_deref())
544 .with_pool_status(&self.router),
545 );
546 let now = Timestamp::now();
547
548 if self.previous_snapshot.is_some() {
549 let (raw_client_sent_rate, client_sent_rate) = Self::throughput_estimate(
550 &mut self.client_sent_estimator,
551 new_snapshot.client_to_backend_bytes.as_u64(),
552 now,
553 );
554 let (raw_client_recv_rate, client_recv_rate) = Self::throughput_estimate(
555 &mut self.client_received_estimator,
556 new_snapshot.backend_to_client_bytes.as_u64(),
557 now,
558 );
559
560 let client_point = ThroughputPoint::new_client_estimate(
561 now,
562 raw_client_sent_rate,
563 client_sent_rate,
564 raw_client_recv_rate,
565 client_recv_rate,
566 );
567 self.client_history.push(client_point);
568
569 self.backend_history
570 .iter_mut()
571 .zip(self.backend_rate_estimators.iter_mut())
572 .zip(new_snapshot.backend_stats.iter())
573 .for_each(|((history, estimators), stats)| {
574 history.push(Self::build_backend_estimated_throughput_point(
575 now, estimators, stats,
576 ));
577 });
578
579 self.prune_user_estimators(&new_snapshot.user_stats);
580 let user_stats = new_snapshot
581 .user_stats
582 .iter()
583 .map(|stats| {
584 let last_update = self.last_update;
585 let estimators = self
586 .user_rate_estimators
587 .entry(stats.username.clone())
588 .or_insert_with(|| Self::empty_user_estimators_at(last_update));
589 Self::estimate_user_rates(now, estimators, stats)
590 })
591 .collect();
592
593 self.snapshot = Arc::new(crate::metrics::MetricsSnapshot {
595 user_stats,
596 backend_stats: Arc::clone(&new_snapshot.backend_stats),
597 ..*new_snapshot
598 });
599 self.previous_snapshot = Some(new_snapshot);
600 } else {
601 self.seed_rate_estimators(&new_snapshot, now);
602 self.previous_snapshot = Some(Arc::clone(&new_snapshot));
604 self.snapshot = new_snapshot;
605 }
606
607 self.last_update = now;
608 }
609
610 #[must_use]
612 pub fn snapshot(&self) -> &MetricsSnapshot {
613 &self.snapshot
614 }
615
616 #[must_use]
618 pub fn servers(&self) -> &[Server] {
619 &self.servers
620 }
621
622 #[must_use]
624 pub const fn client_throughput_history(&self) -> &VecDeque<ThroughputPoint> {
625 self.client_history.points()
626 }
627
628 #[must_use]
630 pub fn latest_client_throughput(&self) -> Option<&ThroughputPoint> {
631 self.client_history.latest()
632 }
633
634 #[must_use]
636 pub fn backend_pending_count(&self, backend_idx: usize) -> usize {
637 use crate::types::BackendId;
638 self.router
639 .backend_load(BackendId::from_index(backend_idx))
640 .map_or(0, |pending| pending.get())
641 }
642
643 #[must_use]
645 pub fn backend_load_ratio(&self, backend_idx: usize) -> Option<f64> {
646 use crate::types::BackendId;
647 self.router
648 .backend_load_ratio(BackendId::from_index(backend_idx))
649 .map(|ratio| ratio.get())
650 }
651
652 #[must_use]
654 pub fn backend_stateful_count(&self, backend_idx: usize) -> usize {
655 use crate::types::BackendId;
656 self.router
657 .stateful_count(BackendId::from_index(backend_idx))
658 .map_or(0, |count| count.get())
659 }
660
661 #[must_use]
663 pub fn backend_traffic_share(&self, backend_idx: usize) -> Option<f64> {
664 use crate::types::BackendId;
665 self.router
666 .backend_traffic_share(BackendId::from_index(backend_idx))
667 .map(|share| share.get())
668 }
669
670 #[must_use]
672 pub fn throughput_history(&self, backend_idx: usize) -> &VecDeque<ThroughputPoint> {
676 self.backend_history(backend_idx)
677 .expect("backend history index should be valid")
678 .points()
679 }
680
681 #[must_use]
683 pub fn latest_backend_throughput(&self, backend_idx: usize) -> Option<&ThroughputPoint> {
684 self.backend_history(backend_idx)
685 .and_then(ThroughputHistory::latest)
686 }
687
688 #[must_use]
690 pub const fn log_buffer(&self) -> &Arc<LogBuffer> {
691 &self.log_buffer
692 }
693
694 #[must_use]
696 pub const fn view_mode(&self) -> ViewMode {
697 self.view_mode
698 }
699
700 #[must_use]
701 fn backend_history(&self, backend_idx: usize) -> Option<&ThroughputHistory> {
702 self.backend_history.get(backend_idx)
703 }
704
705 pub const fn toggle_log_fullscreen(&mut self) {
707 self.view_mode = match self.view_mode {
708 ViewMode::Normal => ViewMode::LogFullscreen,
709 ViewMode::LogFullscreen => ViewMode::Normal,
710 };
711 }
712
713 pub const fn toggle_details(&mut self) {
715 self.show_details = !self.show_details;
716 }
717
718 #[must_use]
720 pub const fn show_details(&self) -> bool {
721 self.show_details
722 }
723
724 #[must_use]
726 pub const fn system_stats(&self) -> &crate::tui::SystemStats {
727 &self.system_stats
728 }
729
730 #[must_use]
732 pub const fn buffer_pool(&self) -> Option<&crate::pool::BufferPool> {
733 self.buffer_pool.as_ref()
734 }
735
736 #[must_use]
738 pub fn snapshot_state(&self) -> DashboardState {
739 self.snapshot_state_with_limits(None, None)
740 }
741
742 #[must_use]
744 pub fn snapshot_state_with_log_limit(&self, log_limit: Option<usize>) -> DashboardState {
745 self.snapshot_state_with_limits(log_limit, None)
746 }
747
748 #[must_use]
750 pub fn snapshot_state_with_limits(
751 &self,
752 log_limit: Option<usize>,
753 history_limit: Option<usize>,
754 ) -> DashboardState {
755 let buffer_pool = self.buffer_pool.as_ref().map(Self::snapshot_buffer_pool);
756 let mut metrics = DashboardMetrics::from_snapshot(self.snapshot.as_ref());
757 metrics.in_flight_requests = self
758 .servers
759 .iter()
760 .enumerate()
761 .map(|(idx, _)| self.backend_pending_count(idx))
762 .sum();
763 DashboardState {
764 metrics,
765 backend_views: self.snapshot_backend_views(history_limit),
766 top_users: self.snapshot_top_users(),
767 client_history: Self::snapshot_history_points(
768 self.client_history.points(),
769 history_limit,
770 ),
771 system_stats: self.system_stats.clone(),
772 view_mode: self.view_mode,
773 show_details: self.show_details,
774 log_lines: self.snapshot_log_lines(log_limit),
775 buffer_pool,
776 }
777 }
778
779 #[must_use]
781 pub fn snapshot_remote_state_with_limits(
782 &self,
783 log_limit: Option<usize>,
784 history_limit: Option<usize>,
785 top_user_limit: Option<usize>,
786 ) -> RemoteDashboardState {
787 let mut metrics = DashboardMetrics::from_snapshot(self.snapshot.as_ref());
788 metrics.in_flight_requests = self
789 .servers
790 .iter()
791 .enumerate()
792 .map(|(idx, _)| self.backend_pending_count(idx))
793 .sum();
794 RemoteDashboardState {
795 metrics,
796 backend_views: self.snapshot_remote_backend_views(history_limit),
797 top_users: self.snapshot_top_users_with_limit(top_user_limit),
798 latest_client_throughput: self.client_history.latest().cloned(),
799 system_stats: self.system_stats.clone(),
800 log_lines: self.snapshot_log_lines(log_limit),
801 }
802 }
803
804 fn snapshot_log_lines(&self, log_limit: Option<usize>) -> Vec<String> {
805 match log_limit {
806 Some(limit) => self.log_buffer.recent_lines(limit),
807 None => self.log_buffer.all_lines(),
808 }
809 }
810
811 fn snapshot_backend_views(&self, history_limit: Option<usize>) -> Vec<BackendView> {
812 self.snapshot
813 .backend_stats
814 .iter()
815 .zip(self.servers.iter())
816 .enumerate()
817 .map(|(i, (stats, server))| self.snapshot_backend_view(i, server, stats, history_limit))
818 .collect()
819 }
820
821 fn snapshot_remote_backend_views(
822 &self,
823 history_limit: Option<usize>,
824 ) -> Vec<RemoteBackendView> {
825 self.snapshot
826 .backend_stats
827 .iter()
828 .zip(self.servers.iter())
829 .enumerate()
830 .map(|(i, (stats, server))| {
831 self.snapshot_remote_backend_view(i, server, stats, history_limit)
832 })
833 .collect()
834 }
835
836 fn snapshot_backend_view(
837 &self,
838 backend_idx: usize,
839 server: &Server,
840 stats: &crate::metrics::BackendStats,
841 history_limit: Option<usize>,
842 ) -> BackendView {
843 BackendView {
844 server: BackendDisplay {
845 host: server.host.clone(),
846 port: server.port,
847 name: server.name.clone(),
848 max_connections: server.max_connections,
849 },
850 stats: stats.clone(),
851 active_connections: stats.active_connections.get(),
852 health_status: stats.health_status,
853 pending_count: self.backend_pending_count(backend_idx),
854 load_ratio: self.backend_load_ratio(backend_idx),
855 stateful_count: self.backend_stateful_count(backend_idx),
856 traffic_share: self.backend_traffic_share(backend_idx),
857 history: Self::snapshot_throughput_history(
858 self.backend_history(backend_idx),
859 history_limit,
860 ),
861 }
862 }
863
864 fn snapshot_remote_backend_view(
865 &self,
866 backend_idx: usize,
867 server: &Server,
868 stats: &crate::metrics::BackendStats,
869 history_limit: Option<usize>,
870 ) -> RemoteBackendView {
871 RemoteBackendView {
872 server: BackendDisplay {
873 host: server.host.clone(),
874 port: server.port,
875 name: server.name.clone(),
876 max_connections: server.max_connections,
877 },
878 stats: stats.clone(),
879 active_connections: stats.active_connections.get(),
880 health_status: stats.health_status,
881 pending_count: self.backend_pending_count(backend_idx),
882 stateful_count: self.backend_stateful_count(backend_idx),
883 traffic_share: self.backend_traffic_share(backend_idx),
884 history: Self::snapshot_throughput_history(
885 self.backend_history(backend_idx),
886 history_limit,
887 ),
888 }
889 }
890
891 fn snapshot_throughput_history(
892 history: Option<&ThroughputHistory>,
893 history_limit: Option<usize>,
894 ) -> Vec<ThroughputPoint> {
895 history
896 .map(|history| Self::snapshot_history_points(history.points(), history_limit))
897 .unwrap_or_default()
898 }
899
900 fn snapshot_history_points(
901 points: &VecDeque<ThroughputPoint>,
902 history_limit: Option<usize>,
903 ) -> Vec<ThroughputPoint> {
904 let start = history_limit.map_or(0, |limit| points.len().saturating_sub(limit));
905 points.iter().skip(start).cloned().collect()
906 }
907
908 fn snapshot_buffer_pool(pool: &crate::pool::BufferPool) -> BufferPoolStats {
909 let (available, in_use, total) = pool.stats();
910 BufferPoolStats {
911 available,
912 in_use,
913 total,
914 }
915 }
916
917 fn snapshot_top_users(&self) -> Vec<DashboardUserStats> {
918 self.snapshot_top_users_with_limit(Some(10))
919 }
920
921 fn snapshot_top_users_with_limit(
922 &self,
923 top_user_limit: Option<usize>,
924 ) -> Vec<DashboardUserStats> {
925 let mut top_users = self
926 .snapshot
927 .user_stats
928 .iter()
929 .map(DashboardUserStats::from_user_stats)
930 .collect::<Vec<_>>();
931 top_users.sort_by_key(|user| std::cmp::Reverse(user.total_bytes()));
932 if let Some(limit) = top_user_limit {
933 top_users.truncate(limit);
934 }
935 top_users
936 }
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942 use crate::config::Server;
943 use crate::metrics::MetricsCollector;
944 use crate::router::BackendSelector;
945 use crate::types::Port;
946 use std::sync::Arc;
947 use std::time::Duration;
948
949 fn create_test_servers(count: usize) -> Arc<[Server]> {
951 (0..count)
952 .map(|i| {
953 Server::builder(
954 format!("backend{i}.example.com"),
955 Port::try_new(119).unwrap(),
956 )
957 .name(format!("Backend {i}"))
958 .build()
959 .unwrap()
960 })
961 .collect::<Vec<_>>()
962 .into()
963 }
964
965 fn create_test_app(backend_count: usize) -> TuiApp {
967 let metrics = MetricsCollector::new(backend_count);
968 let router = Arc::new(BackendSelector::new());
969 let servers = create_test_servers(backend_count);
970 TuiApp::new(metrics, router, servers)
971 }
972
973 fn assert_f64_eq(actual: f64, expected: f64) {
974 assert_eq!(actual.to_bits(), expected.to_bits());
975 }
976
977 #[test]
984 fn test_previous_snapshot_uses_new_snapshot_not_old() {
985 let metrics = MetricsCollector::new(1);
986 let router = Arc::new(BackendSelector::new());
987 let servers: Arc<[Server]> = vec![
988 Server::builder("test.example.com", Port::try_new(119).unwrap())
989 .name("Test Server".to_string())
990 .build()
991 .unwrap(),
992 ]
993 .into();
994
995 let mut app = TuiApp::new(metrics.clone(), router, servers);
996
997 metrics.record_backend_to_client_bytes_for(crate::types::BackendId::from_index(0), 1000);
999 app.update();
1000 assert_eq!(app.snapshot().backend_to_client_bytes.as_u64(), 1000);
1001 assert_eq!(
1003 app.previous_snapshot
1004 .as_ref()
1005 .unwrap()
1006 .backend_to_client_bytes
1007 .as_u64(),
1008 1000
1009 );
1010
1011 metrics.record_backend_to_client_bytes_for(crate::types::BackendId::from_index(0), 1000);
1013 app.update();
1014 assert_eq!(app.snapshot().backend_to_client_bytes.as_u64(), 2000);
1015 assert_eq!(
1018 app.previous_snapshot
1019 .as_ref()
1020 .unwrap()
1021 .backend_to_client_bytes
1022 .as_u64(),
1023 2000,
1024 "previous_snapshot should be updated to the new_snapshot (2000), not left as old self.snapshot (1000)"
1025 );
1026
1027 metrics.record_backend_to_client_bytes_for(crate::types::BackendId::from_index(0), 1000);
1029 app.update();
1030 assert_eq!(app.snapshot().backend_to_client_bytes.as_u64(), 3000);
1031 assert_eq!(
1034 app.previous_snapshot
1035 .as_ref()
1036 .unwrap()
1037 .backend_to_client_bytes
1038 .as_u64(),
1039 3000,
1040 "previous_snapshot should be 3000 (from update 3), not 2000 (from update 2) - bug would cause 2x deltas"
1041 );
1042 }
1043
1044 #[test]
1045 fn test_initial_state() {
1046 let app = create_test_app(2);
1047
1048 assert_eq!(app.snapshot().active_connections, 0);
1050 assert_eq!(app.snapshot().total_connections, 0);
1051 assert_eq!(app.snapshot().backend_stats.len(), 2);
1052 assert!(app.previous_snapshot.is_none());
1053 assert!(app.latest_client_throughput().is_none());
1054 }
1055
1056 #[test]
1057 fn test_throughput_history_initialization() {
1058 let app = create_test_app(3);
1059
1060 assert_eq!(app.backend_history.len(), 3);
1062 for i in 0..3 {
1063 assert_eq!(app.throughput_history(i).len(), 0);
1064 assert!(app.latest_backend_throughput(i).is_none());
1065 }
1066
1067 assert_eq!(app.client_throughput_history().len(), 0);
1069 }
1070
1071 #[test]
1072 fn test_first_update_establishes_baseline() {
1073 let metrics = MetricsCollector::new(1);
1074 let router = Arc::new(BackendSelector::new());
1075 let servers = create_test_servers(1);
1076 let mut app = TuiApp::new(metrics.clone(), router, servers);
1077
1078 metrics.record_backend_to_client_bytes_for(crate::types::BackendId::from_index(0), 1000);
1080
1081 app.update();
1083
1084 assert!(app.previous_snapshot.is_some());
1086
1087 assert!(app.latest_client_throughput().is_none());
1089 assert!(app.latest_backend_throughput(0).is_none());
1090 }
1091
1092 #[test]
1093 fn test_throughput_calculation_with_time_delta() {
1094 let metrics = MetricsCollector::new(1);
1095 let router = Arc::new(BackendSelector::new());
1096 let servers = create_test_servers(1);
1097 let mut app = TuiApp::new(metrics.clone(), router, servers);
1098
1099 app.update();
1101
1102 std::thread::sleep(Duration::from_millis(100));
1104 metrics.record_backend_to_client_bytes_for(crate::types::BackendId::from_index(0), 100_000);
1105
1106 app.update();
1108
1109 assert!(app.latest_client_throughput().is_some());
1111 assert!(app.latest_backend_throughput(0).is_some());
1112
1113 let client_throughput = app.latest_client_throughput().unwrap();
1114 assert!(client_throughput.received_per_sec().get() > 0.0);
1115 }
1116
1117 #[test]
1118 fn test_history_buffer_circular() {
1119 let metrics = MetricsCollector::new(1);
1120 let router = Arc::new(BackendSelector::new());
1121 let servers = create_test_servers(1);
1122 let mut app = TuiAppBuilder::new(metrics.clone(), router, servers)
1123 .with_history_size(HistorySize::new(5)) .build();
1125
1126 app.update();
1128
1129 for i in 0..10 {
1131 std::thread::sleep(Duration::from_millis(10));
1132 metrics
1133 .record_backend_to_client_bytes_for(crate::types::BackendId::from_index(0), 1000);
1134 app.update();
1135
1136 let len = app.client_throughput_history().len();
1138 assert!(
1139 len <= 5,
1140 "History at iteration {i} should be <= 5, got {len}"
1141 );
1142 }
1143
1144 assert_eq!(app.client_throughput_history().len(), 5);
1146 }
1147
1148 #[test]
1149 fn test_per_backend_throughput_independence() {
1150 let metrics = MetricsCollector::new(3);
1151 let router = Arc::new(BackendSelector::new());
1152 let servers = create_test_servers(3);
1153 let mut app = TuiApp::new(metrics.clone(), router, servers);
1154
1155 app.update();
1157
1158 std::thread::sleep(Duration::from_millis(100));
1159
1160 metrics
1162 .record_backend_to_client_bytes_for(crate::types::BackendId::from_index(0), 1_000_000);
1163 metrics
1164 .record_backend_to_client_bytes_for(crate::types::BackendId::from_index(1), 2_000_000);
1165 metrics
1166 .record_backend_to_client_bytes_for(crate::types::BackendId::from_index(2), 3_000_000);
1167
1168 app.update();
1169
1170 let t0 = app
1172 .latest_backend_throughput(0)
1173 .unwrap()
1174 .received_per_sec()
1175 .get();
1176 let t1 = app
1177 .latest_backend_throughput(1)
1178 .unwrap()
1179 .received_per_sec()
1180 .get();
1181 let t2 = app
1182 .latest_backend_throughput(2)
1183 .unwrap()
1184 .received_per_sec()
1185 .get();
1186
1187 assert!(t0 < t1, "Backend 0 should have less than backend 1");
1188 assert!(t1 < t2, "Backend 1 should have less than backend 2");
1189 }
1190
1191 #[test]
1192 fn test_with_log_buffer() {
1193 use crate::tui::log_capture::LogBuffer;
1194
1195 let log_buffer = LogBuffer::new();
1196 log_buffer.push("Test log 1".to_string());
1197 log_buffer.push("Test log 2".to_string());
1198
1199 let metrics = MetricsCollector::new(1);
1200 let router = Arc::new(BackendSelector::new());
1201 let servers = create_test_servers(1);
1202
1203 let app = TuiAppBuilder::new(metrics, router, servers)
1204 .with_log_buffer(log_buffer)
1205 .build();
1206
1207 let logs = app.log_buffer().recent_lines(2);
1209 assert_eq!(logs.len(), 2);
1210 assert_eq!(logs[0], "Test log 1");
1211 assert_eq!(logs[1], "Test log 2");
1212 }
1213
1214 #[test]
1215 fn test_throughput_point_accessors() {
1216 let point = ThroughputPoint::new_backend(
1217 Timestamp::now(),
1218 Throughput::new(1000.0),
1219 Throughput::new(2000.0),
1220 CommandsPerSecond::new(50.0),
1221 );
1222
1223 assert_f64_eq(point.sent_per_sec().get(), 1000.0);
1224 assert_f64_eq(point.received_per_sec().get(), 2000.0);
1225 assert_f64_eq(point.commands_per_sec().unwrap().get(), 50.0);
1226
1227 let client_point = ThroughputPoint::new_client(
1228 Timestamp::now(),
1229 Throughput::new(500.0),
1230 Throughput::new(1500.0),
1231 );
1232
1233 assert_f64_eq(client_point.sent_per_sec().get(), 500.0);
1234 assert_f64_eq(client_point.received_per_sec().get(), 1500.0);
1235 assert!(client_point.commands_per_sec().is_none());
1236 }
1237
1238 #[test]
1239 fn test_throughput_history_latest() {
1240 let mut history = ThroughputHistory::new(HistorySize::new(10));
1241
1242 assert!(history.latest().is_none());
1243
1244 let point1 = ThroughputPoint::new_client(
1245 Timestamp::now(),
1246 Throughput::new(100.0),
1247 Throughput::new(200.0),
1248 );
1249 history.push(point1);
1250
1251 assert!(history.latest().is_some());
1252 assert_f64_eq(history.latest().unwrap().sent_per_sec().get(), 100.0);
1253
1254 let point2 = ThroughputPoint::new_client(
1255 Timestamp::now(),
1256 Throughput::new(300.0),
1257 Throughput::new(400.0),
1258 );
1259 history.push(point2);
1260
1261 assert_f64_eq(history.latest().unwrap().sent_per_sec().get(), 300.0);
1263 }
1264
1265 #[test]
1267 fn test_builder_basic() {
1268 let metrics = MetricsCollector::new(2);
1269 let router = Arc::new(BackendSelector::new());
1270 let servers = create_test_servers(2);
1271
1272 let app = TuiAppBuilder::new(metrics, router, servers).build();
1273
1274 assert_eq!(app.snapshot().backend_stats.len(), 2);
1275 assert_eq!(app.backend_history.len(), 2);
1276 }
1277
1278 #[test]
1279 fn test_builder_with_log_buffer() {
1280 use crate::tui::log_capture::LogBuffer;
1281
1282 let log_buffer = LogBuffer::new();
1283 log_buffer.push("Test log".to_string());
1284
1285 let metrics = MetricsCollector::new(1);
1286 let router = Arc::new(BackendSelector::new());
1287 let servers = create_test_servers(1);
1288
1289 let app = TuiAppBuilder::new(metrics, router, servers)
1290 .with_log_buffer(log_buffer)
1291 .build();
1292
1293 let logs = app.log_buffer().recent_lines(1);
1294 assert_eq!(logs.len(), 1);
1295 assert_eq!(logs[0], "Test log");
1296 }
1297
1298 #[test]
1299 fn test_builder_chaining() {
1300 use crate::tui::log_capture::LogBuffer;
1301
1302 let log_buffer = LogBuffer::new();
1303 let metrics = MetricsCollector::new(3);
1304 let router = Arc::new(BackendSelector::new());
1305 let servers = create_test_servers(3);
1306
1307 let app = TuiAppBuilder::new(metrics, router, servers)
1308 .with_log_buffer(log_buffer)
1309 .with_history_size(HistorySize::new(90))
1310 .build();
1311
1312 assert_eq!(app.backend_history.len(), 3);
1313 }
1314
1315 #[test]
1316 fn test_snapshot_state_serialization_round_trip() {
1317 use crate::tui::log_capture::LogBuffer;
1318
1319 let log_buffer = LogBuffer::new();
1320 log_buffer.push("Dashboard log line".to_string());
1321
1322 let metrics = MetricsCollector::new(1);
1323 let router = Arc::new(BackendSelector::new());
1324 let servers = Arc::from(vec![
1325 Server::builder("backend.example.com", Port::try_new(119).unwrap())
1326 .name("Backend")
1327 .username("backend-user")
1328 .password("backend-pass")
1329 .build()
1330 .unwrap(),
1331 ]);
1332
1333 let app = TuiAppBuilder::new(metrics, router, servers)
1334 .with_log_buffer(log_buffer)
1335 .build();
1336
1337 let snapshot = app.snapshot_state();
1338 assert_eq!(snapshot.backend_views.len(), 1);
1339 assert_eq!(snapshot.top_users.len(), 0);
1340 assert_eq!(snapshot.client_history.len(), 0);
1341 assert_eq!(snapshot.log_lines, vec!["Dashboard log line".to_string()]);
1342 assert_eq!(snapshot.view_mode, ViewMode::Normal);
1343 assert!(!snapshot.show_details);
1344
1345 let json = serde_json::to_string(&snapshot).expect("snapshot should serialize");
1346 assert!(!json.contains("backend-user"));
1347 assert!(!json.contains("backend-pass"));
1348 let decoded: DashboardState =
1349 serde_json::from_str(&json).expect("snapshot should deserialize");
1350
1351 assert_eq!(decoded.backend_views.len(), 1);
1352 assert_eq!(decoded.top_users.len(), 0);
1353 assert_eq!(decoded.log_lines, vec!["Dashboard log line".to_string()]);
1354 assert_eq!(decoded.view_mode, ViewMode::Normal);
1355 assert!(!decoded.show_details);
1356 assert!(decoded.buffer_pool.is_none());
1357 assert_eq!(decoded.backend_views[0].server.name.as_str(), "Backend");
1358 }
1359
1360 #[test]
1361 fn test_snapshot_state_with_log_limit_keeps_recent_tail() {
1362 use crate::tui::log_capture::LogBuffer;
1363
1364 let log_buffer = LogBuffer::new();
1365 for i in 0..5 {
1366 log_buffer.push(format!("Dashboard log line {i}"));
1367 }
1368
1369 let metrics = MetricsCollector::new(1);
1370 let router = Arc::new(BackendSelector::new());
1371 let servers = create_test_servers(1);
1372
1373 let app = TuiAppBuilder::new(metrics, router, servers)
1374 .with_log_buffer(log_buffer)
1375 .build();
1376
1377 let snapshot = app.snapshot_state_with_log_limit(Some(2));
1378 assert_eq!(
1379 snapshot.log_lines,
1380 vec![
1381 "Dashboard log line 3".to_string(),
1382 "Dashboard log line 4".to_string()
1383 ]
1384 );
1385 }
1386
1387 #[test]
1388 fn test_snapshot_state_collects_history_and_buffer_pool() {
1389 use crate::pool::BufferPool;
1390 use crate::tui::log_capture::LogBuffer;
1391 use crate::types::BufferSize;
1392
1393 let log_buffer = LogBuffer::new();
1394 let metrics = MetricsCollector::new(1);
1395 let router = Arc::new(BackendSelector::new());
1396 let servers = create_test_servers(1);
1397 let buffer_pool = BufferPool::new(BufferSize::try_new(8192).unwrap(), 4);
1398
1399 let mut app = TuiAppBuilder::new(metrics, router, servers)
1400 .with_log_buffer(log_buffer)
1401 .with_buffer_pool(buffer_pool)
1402 .build();
1403
1404 let client_point = ThroughputPoint::new_client(
1405 Timestamp::now(),
1406 Throughput::new(10.0),
1407 Throughput::new(20.0),
1408 );
1409 let backend_point = ThroughputPoint::new_backend(
1410 Timestamp::now(),
1411 Throughput::new(30.0),
1412 Throughput::new(40.0),
1413 CommandsPerSecond::new(5.0),
1414 );
1415
1416 app.client_history.push(client_point.clone());
1417 app.backend_history[0].push(backend_point.clone());
1418
1419 let snapshot = app.snapshot_state();
1420
1421 assert_eq!(snapshot.client_history.len(), 1);
1422 assert_f64_eq(
1423 snapshot.client_history[0].sent_per_sec().get(),
1424 client_point.sent_per_sec().get(),
1425 );
1426 assert_f64_eq(
1427 snapshot.client_history[0].received_per_sec().get(),
1428 client_point.received_per_sec().get(),
1429 );
1430 assert_eq!(snapshot.backend_views.len(), 1);
1431 assert_eq!(snapshot.backend_views[0].history.len(), 1);
1432 assert_eq!(snapshot.top_users.len(), 0);
1433 assert_f64_eq(
1434 snapshot.backend_views[0].history[0].sent_per_sec().get(),
1435 backend_point.sent_per_sec().get(),
1436 );
1437 assert_f64_eq(
1438 snapshot.backend_views[0].history[0]
1439 .received_per_sec()
1440 .get(),
1441 backend_point.received_per_sec().get(),
1442 );
1443 assert_f64_eq(
1444 snapshot.backend_views[0].history[0]
1445 .commands_per_sec()
1446 .unwrap()
1447 .get(),
1448 backend_point.commands_per_sec().unwrap().get(),
1449 );
1450 assert_eq!(
1451 snapshot.buffer_pool.as_ref().map(|pool| pool.total),
1452 Some(4)
1453 );
1454 }
1455
1456 #[test]
1457 fn test_snapshot_state_serializes_dashboard_metrics_and_top_users() {
1458 use crate::metrics::{CommandCount, DiskCacheStats, ErrorCount};
1459
1460 let metrics = MetricsCollector::new(1);
1461 let router = Arc::new(BackendSelector::new());
1462 let servers = create_test_servers(1);
1463 let mut app = TuiAppBuilder::new(metrics, router, servers).build();
1464
1465 app.snapshot = Arc::new(MetricsSnapshot {
1466 total_connections: 42,
1467 active_connections: 3,
1468 stateful_sessions: 2,
1469 client_to_backend_bytes: crate::types::ClientToBackendBytes::new(100),
1470 backend_to_client_bytes: crate::types::BackendToClientBytes::new(250),
1471 uptime: Duration::from_secs(61),
1472 user_stats: vec![crate::metrics::UserStats {
1473 username: "alice".to_string(),
1474 active_connections: 2,
1475 total_connections: crate::types::TotalConnections::new(9),
1476 bytes_sent: crate::types::BytesSent::new(500),
1477 bytes_received: crate::types::BytesReceived::new(700),
1478 bytes_sent_per_sec: crate::types::BytesPerSecondRate::new(11),
1479 bytes_received_per_sec: crate::types::BytesPerSecondRate::new(13),
1480 total_commands: CommandCount::new(17),
1481 errors: ErrorCount::new(1),
1482 }],
1483 cache_entries: 7,
1484 cache_size_bytes: 8192,
1485 cache_hit_rate: 12.5,
1486 disk_cache: Some(DiskCacheStats {
1487 disk_hits: 3,
1488 disk_hit_rate: 50.0,
1489 disk_capacity: 1024,
1490 bytes_written: 2048,
1491 bytes_read: 1024,
1492 write_ios: 4,
1493 read_ios: 5,
1494 }),
1495 pipeline_batches: 6,
1496 pipeline_commands: 12,
1497 pipeline_requests_queued: 8,
1498 pipeline_requests_completed: 7,
1499 ..MetricsSnapshot::default()
1500 });
1501
1502 let snapshot = app.snapshot_state();
1503 let json = serde_json::to_string(&snapshot).expect("snapshot should serialize");
1504 let decoded: DashboardState =
1505 serde_json::from_str(&json).expect("snapshot should deserialize");
1506
1507 assert_eq!(decoded.metrics.total_connections, 42);
1508 assert_eq!(decoded.metrics.active_connections, 3);
1509 assert_eq!(decoded.metrics.stateful_sessions, 2);
1510 assert_eq!(decoded.metrics.cache_entries, 7);
1511 assert_eq!(decoded.metrics.cache_size_bytes, 8192);
1512 assert_eq!(decoded.metrics.cache_hit_rate, 12.5);
1513 assert_eq!(decoded.metrics.pipeline_batches, 6);
1514 assert_eq!(decoded.metrics.pipeline_commands, 12);
1515 assert_eq!(decoded.top_users.len(), 1);
1516 assert_eq!(decoded.top_users[0].username, "alice");
1517 assert_eq!(decoded.top_users[0].active_connections, 2);
1518 assert_eq!(decoded.top_users[0].bytes_sent_per_sec.get(), 11);
1519 assert_eq!(decoded.top_users[0].bytes_received_per_sec.get(), 13);
1520 }
1521
1522 #[test]
1523 fn test_in_flight_requests_use_same_pending_count_as_backend_load() {
1524 let metrics = MetricsCollector::new(1);
1525 let mut router = BackendSelector::new();
1526 let provider = crate::pool::DeadpoolConnectionProvider::new(
1527 "backend.example.com".to_string(),
1528 119,
1529 "Backend".to_string(),
1530 10,
1531 None,
1532 None,
1533 );
1534 let backend_id = crate::types::BackendId::from_index(0);
1535 router.add_backend(
1536 crate::types::ServerName::try_new("Backend".to_string()).unwrap(),
1537 provider,
1538 0,
1539 );
1540 router.mark_backend_pending(backend_id);
1541 router.mark_backend_pending(backend_id);
1542 router.mark_backend_pending(backend_id);
1543 router.mark_backend_pending(backend_id);
1544 router.mark_backend_pending(backend_id);
1545
1546 let servers: Arc<[Server]> = vec![
1547 Server::builder("backend.example.com", Port::try_new(119).unwrap())
1548 .name("Backend")
1549 .build()
1550 .unwrap(),
1551 ]
1552 .into();
1553
1554 let app = TuiApp::new(metrics, Arc::new(router), servers);
1555 let snapshot = app.snapshot_state();
1556
1557 assert_eq!(snapshot.backend_views[0].pending_count, 5);
1558 assert_eq!(snapshot.metrics.in_flight_requests, 5);
1559 }
1560
1561 #[test]
1562 fn test_throughput_point_preserves_raw_and_smoothed_rates_for_dashboard_serde() {
1563 let point = ThroughputPoint::new_backend_estimate(
1564 Timestamp::now(),
1565 Throughput::new(10_000.0),
1566 Throughput::new(2_500.0),
1567 Throughput::new(20_000.0),
1568 Throughput::new(5_000.0),
1569 CommandsPerSecond::new(100.0),
1570 CommandsPerSecond::new(25.0),
1571 );
1572
1573 assert_f64_eq(point.raw_sent_per_sec().get(), 10_000.0);
1574 assert_f64_eq(point.sent_per_sec().get(), 2_500.0);
1575 assert_f64_eq(point.raw_received_per_sec().get(), 20_000.0);
1576 assert_f64_eq(point.received_per_sec().get(), 5_000.0);
1577 assert_f64_eq(point.raw_commands_per_sec().unwrap().get(), 100.0);
1578 assert_f64_eq(point.commands_per_sec().unwrap().get(), 25.0);
1579
1580 let json = serde_json::to_string(&point).expect("point should serialize");
1581 let decoded: ThroughputPoint =
1582 serde_json::from_str(&json).expect("point should deserialize");
1583 assert_f64_eq(decoded.raw_sent_per_sec().get(), 10_000.0);
1584 assert_f64_eq(decoded.sent_per_sec().get(), 2_500.0);
1585 }
1586
1587 #[test]
1588 fn test_tui_update_uses_smoothed_backend_and_user_rates() {
1589 let metrics = MetricsCollector::new(1);
1590 let router = Arc::new(BackendSelector::new());
1591 let servers = create_test_servers(1);
1592 let mut app = TuiApp::new(metrics.clone(), router, servers);
1593
1594 app.update();
1595
1596 std::thread::sleep(Duration::from_millis(20));
1597 metrics.record_backend_to_client_bytes_for(crate::types::BackendId::from_index(0), 1_000);
1598 metrics.user_bytes_received(Some("alice"), 1_000);
1599 app.update();
1600
1601 std::thread::sleep(Duration::from_millis(20));
1602 metrics.record_backend_to_client_bytes_for(crate::types::BackendId::from_index(0), 100_000);
1603 metrics.user_bytes_received(Some("alice"), 100_000);
1604 app.update();
1605
1606 let backend = app.latest_backend_throughput(0).unwrap();
1607 assert!(
1608 backend.received_per_sec().get() < backend.raw_received_per_sec().get(),
1609 "backend display rate should use smoothed value"
1610 );
1611
1612 let user = app
1613 .snapshot()
1614 .user_stats
1615 .iter()
1616 .find(|user| user.username == "alice")
1617 .expect("alice stats should exist");
1618 let raw_received = backend.raw_received_per_sec().get() as u64;
1619 assert!(
1620 user.bytes_received_per_sec.get() < raw_received,
1621 "user display rate should use smoothed value"
1622 );
1623 }
1624}