1use std::{
17 sync::atomic::{AtomicU64, AtomicUsize, Ordering},
18 time::Duration,
19};
20
21use nautilus_common::{
22 messages::{DataEvent, ExecutionEvent, data::DataCommand},
23 runner::{SystemChannel, TimeEventMessage, TradingCommandMessage},
24};
25
26#[non_exhaustive]
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub struct RunnerChannelMetricsSnapshot {
30 pub dispatched: u64,
32 pub dispatch_busy_ns: u64,
34 pub queue_depth: usize,
36 pub last_dispatch_at_ns: u64,
38}
39
40#[non_exhaustive]
47#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
48pub struct RunnerMetricsSnapshot {
49 pub time_events: RunnerChannelMetricsSnapshot,
51 pub exec_events: RunnerChannelMetricsSnapshot,
53 pub exec_commands: RunnerChannelMetricsSnapshot,
55 pub data_events: RunnerChannelMetricsSnapshot,
57 pub data_commands: RunnerChannelMetricsSnapshot,
59 pub dispatch_busy_ns: u64,
61 pub maintenance_busy_ns: u64,
63 pub external_msgbus_busy_ns: u64,
65 pub elapsed_ns: u64,
67}
68
69#[non_exhaustive]
74#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
75pub struct RunnerMetricsDelta {
76 pub time_events: u64,
78 pub exec_events: u64,
80 pub exec_commands: u64,
82 pub data_events: u64,
84 pub data_commands: u64,
86 pub time_events_busy_ns: u64,
88 pub exec_events_busy_ns: u64,
90 pub exec_commands_busy_ns: u64,
92 pub data_events_busy_ns: u64,
94 pub data_commands_busy_ns: u64,
96 pub dispatch_busy_ns: u64,
98 pub maintenance_busy_ns: u64,
100 pub external_msgbus_busy_ns: u64,
102 pub elapsed_ns: u64,
104}
105
106impl RunnerMetricsDelta {
107 #[must_use]
109 pub fn from_snapshots(before: RunnerMetricsSnapshot, after: RunnerMetricsSnapshot) -> Self {
110 let (time_events, time_events_busy_ns) =
111 channel_dispatch_delta(before.time_events, after.time_events);
112 let (exec_events, exec_events_busy_ns) =
113 channel_dispatch_delta(before.exec_events, after.exec_events);
114 let (exec_commands, exec_commands_busy_ns) =
115 channel_dispatch_delta(before.exec_commands, after.exec_commands);
116 let (data_events, data_events_busy_ns) =
117 channel_dispatch_delta(before.data_events, after.data_events);
118 let (data_commands, data_commands_busy_ns) =
119 channel_dispatch_delta(before.data_commands, after.data_commands);
120
121 Self {
122 time_events,
123 exec_events,
124 exec_commands,
125 data_events,
126 data_commands,
127 time_events_busy_ns,
128 exec_events_busy_ns,
129 exec_commands_busy_ns,
130 data_events_busy_ns,
131 data_commands_busy_ns,
132 dispatch_busy_ns: after
133 .dispatch_busy_ns
134 .saturating_sub(before.dispatch_busy_ns),
135 maintenance_busy_ns: after
136 .maintenance_busy_ns
137 .saturating_sub(before.maintenance_busy_ns),
138 external_msgbus_busy_ns: after
139 .external_msgbus_busy_ns
140 .saturating_sub(before.external_msgbus_busy_ns),
141 elapsed_ns: after.elapsed_ns.saturating_sub(before.elapsed_ns),
142 }
143 }
144
145 #[must_use]
147 pub const fn total_dispatched(&self) -> u64 {
148 self.time_events
149 .saturating_add(self.exec_events)
150 .saturating_add(self.exec_commands)
151 .saturating_add(self.data_events)
152 .saturating_add(self.data_commands)
153 }
154
155 #[must_use]
159 #[expect(
160 clippy::cast_precision_loss,
161 reason = "sample-window utilization is an approximate ratio"
162 )]
163 pub fn dispatch_utilization(&self) -> f64 {
164 if self.elapsed_ns == 0 {
165 0.0
166 } else {
167 self.dispatch_busy_ns as f64 / self.elapsed_ns as f64
168 }
169 }
170
171 #[must_use]
176 #[expect(
177 clippy::cast_precision_loss,
178 reason = "sample-window utilization is an approximate ratio"
179 )]
180 pub fn loop_utilization(&self) -> f64 {
181 if self.elapsed_ns == 0 {
182 0.0
183 } else {
184 self.total_busy_ns() as f64 / self.elapsed_ns as f64
185 }
186 }
187
188 #[must_use]
192 pub fn mean_dispatch_ns(&self) -> u64 {
193 self.dispatch_busy_ns
194 .checked_div(self.total_dispatched())
195 .unwrap_or(0)
196 }
197
198 #[must_use]
202 pub fn channel_mean_dispatch_ns(&self, channel: SystemChannel) -> u64 {
203 let (dispatched, dispatch_busy_ns) = match channel {
204 SystemChannel::TimeEvents => (self.time_events, self.time_events_busy_ns),
205 SystemChannel::ExecEvents => (self.exec_events, self.exec_events_busy_ns),
206 SystemChannel::ExecCommands => (self.exec_commands, self.exec_commands_busy_ns),
207 SystemChannel::DataEvents => (self.data_events, self.data_events_busy_ns),
208 SystemChannel::DataCommands => (self.data_commands, self.data_commands_busy_ns),
209 };
210
211 dispatch_busy_ns.checked_div(dispatched).unwrap_or(0)
212 }
213
214 #[must_use]
216 pub const fn total_busy_ns(&self) -> u64 {
217 self.dispatch_busy_ns
218 .saturating_add(self.maintenance_busy_ns)
219 .saturating_add(self.external_msgbus_busy_ns)
220 }
221}
222
223fn channel_dispatch_delta(
224 before: RunnerChannelMetricsSnapshot,
225 after: RunnerChannelMetricsSnapshot,
226) -> (u64, u64) {
227 (
228 after.dispatched.saturating_sub(before.dispatched),
229 after
230 .dispatch_busy_ns
231 .saturating_sub(before.dispatch_busy_ns),
232 )
233}
234
235#[derive(Debug, Default)]
236pub(crate) struct RunnerMetrics {
237 time_events: RunnerChannelMetrics,
238 exec_events: RunnerChannelMetrics,
239 exec_commands: RunnerChannelMetrics,
240 data_events: RunnerChannelMetrics,
241 data_commands: RunnerChannelMetrics,
242 maintenance_busy_ns: AtomicU64,
243 external_msgbus_busy_ns: AtomicU64,
244 elapsed_ns: AtomicU64,
245}
246
247impl RunnerMetrics {
248 pub(crate) fn reset(&self) {
249 self.time_events.reset();
250 self.exec_events.reset();
251 self.exec_commands.reset();
252 self.data_events.reset();
253 self.data_commands.reset();
254 self.maintenance_busy_ns.store(0, Ordering::Relaxed);
255 self.external_msgbus_busy_ns.store(0, Ordering::Relaxed);
256 self.elapsed_ns.store(0, Ordering::Relaxed);
257 }
258
259 pub(crate) fn snapshot(&self) -> RunnerMetricsSnapshot {
260 let time_events = self.time_events.snapshot();
261 let exec_events = self.exec_events.snapshot();
262 let exec_commands = self.exec_commands.snapshot();
263 let data_events = self.data_events.snapshot();
264 let data_commands = self.data_commands.snapshot();
265
266 RunnerMetricsSnapshot {
267 time_events,
268 exec_events,
269 exec_commands,
270 data_events,
271 data_commands,
272 dispatch_busy_ns: time_events
273 .dispatch_busy_ns
274 .saturating_add(exec_events.dispatch_busy_ns)
275 .saturating_add(exec_commands.dispatch_busy_ns)
276 .saturating_add(data_events.dispatch_busy_ns)
277 .saturating_add(data_commands.dispatch_busy_ns),
278 maintenance_busy_ns: self.maintenance_busy_ns.load(Ordering::Relaxed),
279 external_msgbus_busy_ns: self.external_msgbus_busy_ns.load(Ordering::Relaxed),
280 elapsed_ns: self.elapsed_ns.load(Ordering::Relaxed),
281 }
282 }
283
284 pub(crate) fn record_dispatch(
285 &self,
286 channel: SystemChannel,
287 dispatch_elapsed: Duration,
288 elapsed_since_start: Duration,
289 ) {
290 let elapsed_ns = duration_ns(elapsed_since_start);
291 self.channel(channel)
292 .record_dispatch(duration_ns(dispatch_elapsed), elapsed_ns);
293 self.elapsed_ns.store(elapsed_ns, Ordering::Relaxed);
294 }
295
296 pub(crate) fn record_maintenance(&self, work_elapsed: Duration, elapsed_since_start: Duration) {
297 self.record_loop_work(&self.maintenance_busy_ns, work_elapsed, elapsed_since_start);
298 }
299
300 pub(crate) fn record_external_msgbus(
301 &self,
302 work_elapsed: Duration,
303 elapsed_since_start: Duration,
304 ) {
305 self.record_loop_work(
306 &self.external_msgbus_busy_ns,
307 work_elapsed,
308 elapsed_since_start,
309 );
310 }
311
312 pub(crate) fn publish_queue_depths(
313 &self,
314 depths: RunnerChannelQueueDepths,
315 elapsed_since_start: Duration,
316 ) {
317 self.time_events.set_queue_depth(depths.time_events);
318 self.exec_events.set_queue_depth(depths.exec_events);
319 self.exec_commands.set_queue_depth(depths.exec_commands);
320 self.data_events.set_queue_depth(depths.data_events);
321 self.data_commands.set_queue_depth(depths.data_commands);
322 self.elapsed_ns
323 .store(duration_ns(elapsed_since_start), Ordering::Relaxed);
324 }
325
326 fn channel(&self, channel: SystemChannel) -> &RunnerChannelMetrics {
327 match channel {
328 SystemChannel::TimeEvents => &self.time_events,
329 SystemChannel::ExecEvents => &self.exec_events,
330 SystemChannel::ExecCommands => &self.exec_commands,
331 SystemChannel::DataEvents => &self.data_events,
332 SystemChannel::DataCommands => &self.data_commands,
333 }
334 }
335
336 fn record_loop_work(
337 &self,
338 busy_ns: &AtomicU64,
339 work_elapsed: Duration,
340 elapsed_since_start: Duration,
341 ) {
342 saturating_fetch_add(busy_ns, duration_ns(work_elapsed));
343 self.elapsed_ns
344 .store(duration_ns(elapsed_since_start), Ordering::Relaxed);
345 }
346}
347
348#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
349pub(crate) struct RunnerChannelQueueDepths {
350 time_events: usize,
351 exec_events: usize,
352 exec_commands: usize,
353 data_events: usize,
354 data_commands: usize,
355}
356
357impl RunnerChannelQueueDepths {
358 pub(crate) fn from_receivers(
359 time_events: &tokio::sync::mpsc::UnboundedReceiver<TimeEventMessage>,
360 exec_events: &tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
361 exec_commands: &tokio::sync::mpsc::UnboundedReceiver<TradingCommandMessage>,
362 data_events: &tokio::sync::mpsc::UnboundedReceiver<DataEvent>,
363 data_commands: &tokio::sync::mpsc::UnboundedReceiver<DataCommand>,
364 ) -> Self {
365 Self {
366 time_events: time_events.len(),
367 exec_events: exec_events.len(),
368 exec_commands: exec_commands.len(),
369 data_events: data_events.len(),
370 data_commands: data_commands.len(),
371 }
372 }
373}
374
375#[derive(Debug, Default)]
376struct RunnerChannelMetrics {
377 dispatched: AtomicU64,
378 dispatch_busy_ns: AtomicU64,
379 queue_depth: AtomicUsize,
380 last_dispatch_at_ns: AtomicU64,
381}
382
383impl RunnerChannelMetrics {
384 fn reset(&self) {
385 self.dispatched.store(0, Ordering::Relaxed);
386 self.dispatch_busy_ns.store(0, Ordering::Relaxed);
387 self.queue_depth.store(0, Ordering::Relaxed);
388 self.last_dispatch_at_ns.store(0, Ordering::Relaxed);
389 }
390
391 fn snapshot(&self) -> RunnerChannelMetricsSnapshot {
392 RunnerChannelMetricsSnapshot {
393 dispatched: self.dispatched.load(Ordering::Relaxed),
394 dispatch_busy_ns: self.dispatch_busy_ns.load(Ordering::Relaxed),
395 queue_depth: self.queue_depth.load(Ordering::Relaxed),
396 last_dispatch_at_ns: self.last_dispatch_at_ns.load(Ordering::Relaxed),
397 }
398 }
399
400 fn record_dispatch(&self, dispatch_busy_ns: u64, last_dispatch_at_ns: u64) {
401 self.dispatched.fetch_add(1, Ordering::Relaxed);
402 saturating_fetch_add(&self.dispatch_busy_ns, dispatch_busy_ns);
403 self.last_dispatch_at_ns
404 .store(last_dispatch_at_ns, Ordering::Relaxed);
405 }
406
407 fn set_queue_depth(&self, queue_depth: usize) {
408 self.queue_depth.store(queue_depth, Ordering::Relaxed);
409 }
410}
411
412fn duration_ns(duration: Duration) -> u64 {
413 u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
414}
415
416fn saturating_fetch_add(atomic: &AtomicU64, value: u64) {
417 atomic
418 .try_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
419 Some(current.saturating_add(value))
420 })
421 .expect("try_update closure returns Some");
422}
423
424#[cfg(test)]
425mod tests {
426 use std::time::Duration;
427
428 use nautilus_common::{
429 messages::{
430 data::{SubscribeCommand, subscribe::SubscribeInstruments},
431 execution::{QueryAccount, TradingCommand},
432 system::{QueueCondition, QueueState},
433 },
434 msgbus::MessagingSwitchboard,
435 timer::{TimeEvent, TimeEventCallback},
436 };
437 use nautilus_core::{UUID4, UnixNanos};
438 use nautilus_model::{
439 enums::AccountType,
440 events::account::state::AccountState,
441 identifiers::{AccountId, TraderId, Venue},
442 instruments::{InstrumentAny, stubs::crypto_perpetual_ethusdt},
443 };
444 use rstest::rstest;
445 use ustr::Ustr;
446
447 use super::{
448 super::queue::{QueueMonitor, QueueMonitorConfig, QueueStateTransition},
449 *,
450 };
451
452 #[rstest]
453 fn test_runner_metrics_default_snapshot_is_zero() {
454 let metrics = RunnerMetrics::default();
455
456 assert_eq!(metrics.snapshot(), RunnerMetricsSnapshot::default());
457 }
458
459 #[rstest]
460 fn test_runner_metrics_delta_saturates_when_after_is_lower_than_before() {
461 let before = runner_snapshot([10, 9, 8, 7, 6], [20, 20, 20, 20, 20], 90, 80, 70);
462 let after = runner_snapshot([5, 4, 3, 2, 1], [10, 10, 10, 10, 10], 40, 30, 20);
463
464 let delta = RunnerMetricsDelta::from_snapshots(before, after);
465
466 assert_eq!(delta, RunnerMetricsDelta::default());
467 }
468
469 #[rstest]
470 fn test_runner_metrics_delta_zero_elapsed_window_returns_zero_utilization() {
471 let delta = RunnerMetricsDelta::from_snapshots(
472 RunnerMetricsSnapshot::default(),
473 runner_snapshot([1, 0, 0, 0, 0], [10, 0, 0, 0, 0], 20, 30, 0),
474 );
475
476 assert!(delta.dispatch_utilization().abs() < f64::EPSILON);
477 assert!(delta.loop_utilization().abs() < f64::EPSILON);
478 }
479
480 #[rstest]
481 fn test_runner_metrics_delta_zero_dispatched_returns_zero_mean_dispatch_time() {
482 let delta = RunnerMetricsDelta::from_snapshots(
483 RunnerMetricsSnapshot::default(),
484 runner_snapshot([0, 0, 0, 0, 0], [100, 0, 0, 0, 0], 0, 0, 100),
485 );
486
487 assert_eq!(delta.mean_dispatch_ns(), 0);
488 assert_eq!(delta.channel_mean_dispatch_ns(SystemChannel::TimeEvents), 0);
489 }
490
491 #[rstest]
492 fn test_runner_metrics_delta_total_dispatched_sums_all_channels() {
493 let delta = RunnerMetricsDelta::from_snapshots(
494 RunnerMetricsSnapshot::default(),
495 runner_snapshot([1, 2, 3, 4, 5], [0, 0, 0, 0, 0], 0, 0, 100),
496 );
497
498 assert_eq!(delta.total_dispatched(), 15);
499 }
500
501 #[rstest]
502 fn test_runner_metrics_delta_derived_metrics_use_sample_window_values() {
503 let before = runner_snapshot([1, 2, 0, 0, 0], [40, 20, 30, 10, 0], 10, 5, 200);
504 let after = runner_snapshot([4, 3, 2, 1, 0], [70, 40, 40, 10, 0], 30, 15, 300);
505
506 let delta = RunnerMetricsDelta::from_snapshots(before, after);
507
508 assert_eq!(delta.total_dispatched(), 7);
509 assert_eq!(delta.dispatch_busy_ns, 60);
510 assert_eq!(delta.total_busy_ns(), 90);
511 assert_eq!(delta.mean_dispatch_ns(), 8);
512 assert!((delta.dispatch_utilization() - 0.6).abs() < f64::EPSILON);
513 assert!((delta.loop_utilization() - 0.9).abs() < f64::EPSILON);
514 }
515
516 #[rstest]
517 #[case(SystemChannel::TimeEvents, 10)]
518 #[case(SystemChannel::ExecEvents, 20)]
519 #[case(SystemChannel::ExecCommands, 5)]
520 #[case(SystemChannel::DataEvents, 4)]
521 #[case(SystemChannel::DataCommands, 0)]
522 fn test_runner_metrics_delta_channel_mean_dispatch_ns_divides_selected_channel(
523 #[case] channel: SystemChannel,
524 #[case] expected_mean_ns: u64,
525 ) {
526 let before = runner_snapshot([1, 2, 3, 4, 5], [10, 20, 30, 40, 50], 0, 0, 100);
527 let after = runner_snapshot([4, 3, 5, 9, 5], [40, 40, 40, 60, 90], 0, 0, 200);
528
529 let delta = RunnerMetricsDelta::from_snapshots(before, after);
530
531 assert_eq!(delta.channel_mean_dispatch_ns(channel), expected_mean_ns);
532 }
533
534 #[rstest]
535 fn test_runner_metrics_delta_channel_busy_ns_sums_to_dispatch_busy_ns() {
536 let before = runner_snapshot([1, 2, 3, 4, 5], [10, 20, 30, 40, 50], 0, 0, 100);
537 let after = runner_snapshot([4, 3, 5, 9, 5], [40, 40, 40, 60, 90], 0, 0, 200);
538
539 let delta = RunnerMetricsDelta::from_snapshots(before, after);
540
541 assert_eq!(delta.time_events_busy_ns, 30);
542 assert_eq!(delta.exec_events_busy_ns, 20);
543 assert_eq!(delta.exec_commands_busy_ns, 10);
544 assert_eq!(delta.data_events_busy_ns, 20);
545 assert_eq!(delta.data_commands_busy_ns, 40);
546 assert_eq!(delta.dispatch_busy_ns, 120);
547 }
548
549 #[rstest]
550 fn test_queue_monitor_uses_successive_snapshot_delta_and_crossing_values() {
551 let previous = with_queue_depths(
552 runner_snapshot([10, 0, 0, 0, 0], [1_000, 0, 0, 0, 0], 0, 0, 100),
553 [999, 0, 0, 0, 0],
554 );
555 let mut monitor = QueueMonitor::new(&queue_monitor_config(), previous);
556 let snapshot = with_queue_depths(
557 runner_snapshot([12, 0, 0, 0, 0], [1_300, 0, 0, 0, 0], 0, 0, 200),
558 [10, 0, 0, 0, 0],
559 );
560
561 let transitions = monitor.evaluate(snapshot);
562
563 assert_eq!(
564 transitions,
565 vec![
566 QueueStateTransition {
567 channel: SystemChannel::TimeEvents,
568 condition: QueueCondition::Backlogged,
569 state: QueueState::Triggered,
570 queue_depth: 10,
571 mean_dispatch_ns: 150,
572 },
573 QueueStateTransition {
574 channel: SystemChannel::TimeEvents,
575 condition: QueueCondition::Slow,
576 state: QueueState::Triggered,
577 queue_depth: 10,
578 mean_dispatch_ns: 150,
579 },
580 ]
581 );
582 }
583
584 #[rstest]
585 fn test_queue_monitor_hysteresis_does_not_flap_between_thresholds() {
586 let mut monitor =
587 QueueMonitor::new(&queue_monitor_config(), RunnerMetricsSnapshot::default());
588 let triggered = with_queue_depths(
589 runner_snapshot([1, 0, 0, 0, 0], [100, 0, 0, 0, 0], 0, 0, 100),
590 [10, 0, 0, 0, 0],
591 );
592 let between = with_queue_depths(
593 runner_snapshot([2, 0, 0, 0, 0], [175, 0, 0, 0, 0], 0, 0, 200),
594 [7, 0, 0, 0, 0],
595 );
596 let cleared = with_queue_depths(
597 runner_snapshot([3, 0, 0, 0, 0], [225, 0, 0, 0, 0], 0, 0, 300),
598 [5, 0, 0, 0, 0],
599 );
600
601 assert_eq!(monitor.evaluate(triggered).len(), 2);
602 assert!(monitor.evaluate(between).is_empty());
603 assert_eq!(
604 monitor.evaluate(cleared),
605 vec![
606 QueueStateTransition {
607 channel: SystemChannel::TimeEvents,
608 condition: QueueCondition::Backlogged,
609 state: QueueState::Cleared,
610 queue_depth: 5,
611 mean_dispatch_ns: 50,
612 },
613 QueueStateTransition {
614 channel: SystemChannel::TimeEvents,
615 condition: QueueCondition::Slow,
616 state: QueueState::Cleared,
617 queue_depth: 5,
618 mean_dispatch_ns: 50,
619 },
620 ]
621 );
622 }
623
624 #[rstest]
625 fn test_queue_monitor_holds_slow_state_without_dispatch_sample() {
626 let mut monitor =
627 QueueMonitor::new(&queue_monitor_config(), RunnerMetricsSnapshot::default());
628 let triggered = runner_snapshot([1, 0, 0, 0, 0], [100, 0, 0, 0, 0], 0, 0, 100);
629 let idle = runner_snapshot([1, 0, 0, 0, 0], [100, 0, 0, 0, 0], 0, 0, 200);
630 let cleared = runner_snapshot([2, 0, 0, 0, 0], [150, 0, 0, 0, 0], 0, 0, 300);
631
632 assert_eq!(
633 monitor.evaluate(triggered),
634 vec![QueueStateTransition {
635 channel: SystemChannel::TimeEvents,
636 condition: QueueCondition::Slow,
637 state: QueueState::Triggered,
638 queue_depth: 0,
639 mean_dispatch_ns: 100,
640 }]
641 );
642 assert!(monitor.evaluate(idle).is_empty());
643 assert_eq!(
644 monitor.evaluate(cleared),
645 vec![QueueStateTransition {
646 channel: SystemChannel::TimeEvents,
647 condition: QueueCondition::Slow,
648 state: QueueState::Cleared,
649 queue_depth: 0,
650 mean_dispatch_ns: 50,
651 }]
652 );
653 }
654
655 #[rstest]
656 fn test_queue_monitor_conditions_trigger_and_clear_independently() {
657 let mut monitor =
658 QueueMonitor::new(&queue_monitor_config(), RunnerMetricsSnapshot::default());
659 let triggered = with_queue_depths(
660 runner_snapshot([1, 0, 0, 0, 0], [100, 0, 0, 0, 0], 0, 0, 100),
661 [10, 0, 0, 0, 0],
662 );
663 let slow_cleared = with_queue_depths(
664 runner_snapshot([2, 0, 0, 0, 0], [150, 0, 0, 0, 0], 0, 0, 200),
665 [7, 0, 0, 0, 0],
666 );
667 let backlog_cleared = with_queue_depths(
668 runner_snapshot([3, 0, 0, 0, 0], [225, 0, 0, 0, 0], 0, 0, 300),
669 [5, 0, 0, 0, 0],
670 );
671
672 assert_eq!(monitor.evaluate(triggered).len(), 2);
673 assert_eq!(
674 monitor.evaluate(slow_cleared),
675 vec![QueueStateTransition {
676 channel: SystemChannel::TimeEvents,
677 condition: QueueCondition::Slow,
678 state: QueueState::Cleared,
679 queue_depth: 7,
680 mean_dispatch_ns: 50,
681 }]
682 );
683 assert_eq!(
684 monitor.evaluate(backlog_cleared),
685 vec![QueueStateTransition {
686 channel: SystemChannel::TimeEvents,
687 condition: QueueCondition::Backlogged,
688 state: QueueState::Cleared,
689 queue_depth: 5,
690 mean_dispatch_ns: 75,
691 }]
692 );
693 }
694
695 #[rstest]
696 fn test_queue_monitor_keeps_channel_state_isolated() {
697 let mut monitor =
698 QueueMonitor::new(&queue_monitor_config(), RunnerMetricsSnapshot::default());
699 let first = with_queue_depths(
700 runner_snapshot([0, 0, 0, 1, 0], [0, 0, 0, 100, 0], 0, 0, 100),
701 [0, 0, 0, 10, 0],
702 );
703 let second = with_queue_depths(
704 runner_snapshot([0, 1, 0, 2, 0], [0, 100, 0, 175, 0], 0, 0, 200),
705 [0, 10, 0, 7, 0],
706 );
707
708 assert_eq!(
709 monitor
710 .evaluate(first)
711 .iter()
712 .map(|transition| transition.channel)
713 .collect::<Vec<_>>(),
714 vec![SystemChannel::DataEvents, SystemChannel::DataEvents]
715 );
716 assert_eq!(
717 monitor.evaluate(second),
718 vec![
719 QueueStateTransition {
720 channel: SystemChannel::ExecEvents,
721 condition: QueueCondition::Backlogged,
722 state: QueueState::Triggered,
723 queue_depth: 10,
724 mean_dispatch_ns: 100,
725 },
726 QueueStateTransition {
727 channel: SystemChannel::ExecEvents,
728 condition: QueueCondition::Slow,
729 state: QueueState::Triggered,
730 queue_depth: 10,
731 mean_dispatch_ns: 100,
732 },
733 ]
734 );
735 }
736
737 #[rstest]
738 fn test_runner_metrics_snapshot_reflects_dispatch_updates() {
739 let metrics = RunnerMetrics::default();
740
741 metrics.record_dispatch(
742 SystemChannel::ExecCommands,
743 Duration::from_nanos(10),
744 Duration::from_nanos(50),
745 );
746 metrics.record_dispatch(
747 SystemChannel::DataEvents,
748 Duration::from_nanos(7),
749 Duration::from_nanos(90),
750 );
751
752 let snapshot = metrics.snapshot();
753
754 assert_eq!(snapshot.exec_commands.dispatched, 1);
755 assert_eq!(snapshot.exec_commands.dispatch_busy_ns, 10);
756 assert_eq!(snapshot.exec_commands.last_dispatch_at_ns, 50);
757 assert_eq!(snapshot.data_events.dispatched, 1);
758 assert_eq!(snapshot.data_events.dispatch_busy_ns, 7);
759 assert_eq!(snapshot.data_events.last_dispatch_at_ns, 90);
760 assert_eq!(snapshot.dispatch_busy_ns, 17);
761 assert_eq!(snapshot.maintenance_busy_ns, 0);
762 assert_eq!(snapshot.external_msgbus_busy_ns, 0);
763 assert_eq!(snapshot.elapsed_ns, 90);
764 }
765
766 #[rstest]
767 #[case(SystemChannel::TimeEvents, [1, 0, 0, 0, 0], [10, 0, 0, 0, 0], [50, 0, 0, 0, 0])]
768 #[case(SystemChannel::ExecEvents, [0, 1, 0, 0, 0], [0, 10, 0, 0, 0], [0, 50, 0, 0, 0])]
769 #[case(SystemChannel::ExecCommands, [0, 0, 1, 0, 0], [0, 0, 10, 0, 0], [0, 0, 50, 0, 0])]
770 #[case(SystemChannel::DataEvents, [0, 0, 0, 1, 0], [0, 0, 0, 10, 0], [0, 0, 0, 50, 0])]
771 #[case(SystemChannel::DataCommands, [0, 0, 0, 0, 1], [0, 0, 0, 0, 10], [0, 0, 0, 0, 50])]
772 fn test_runner_metrics_record_dispatch_updates_selected_channel(
773 #[case] channel: SystemChannel,
774 #[case] expected_dispatched: [u64; 5],
775 #[case] expected_dispatch_busy_ns: [u64; 5],
776 #[case] expected_last_dispatch: [u64; 5],
777 ) {
778 let metrics = RunnerMetrics::default();
779
780 metrics.record_dispatch(channel, Duration::from_nanos(10), Duration::from_nanos(50));
781 let snapshot = metrics.snapshot();
782
783 assert_eq!(snapshot_dispatch_counts(snapshot), expected_dispatched);
784 assert_eq!(
785 snapshot_dispatch_busy_ns(snapshot),
786 expected_dispatch_busy_ns
787 );
788 assert_eq!(
789 snapshot_last_dispatch_at_ns(snapshot),
790 expected_last_dispatch
791 );
792 assert_eq!(snapshot.dispatch_busy_ns, 10);
793 assert_eq!(snapshot.elapsed_ns, 50);
794 }
795
796 #[rstest]
797 fn test_runner_metrics_snapshot_reflects_loop_work_updates() {
798 let metrics = RunnerMetrics::default();
799
800 metrics.record_maintenance(Duration::from_nanos(10), Duration::from_nanos(50));
801 metrics.record_external_msgbus(Duration::from_nanos(7), Duration::from_nanos(90));
802
803 let snapshot = metrics.snapshot();
804
805 assert_eq!(snapshot.dispatch_busy_ns, 0);
806 assert_eq!(snapshot.maintenance_busy_ns, 10);
807 assert_eq!(snapshot.external_msgbus_busy_ns, 7);
808 assert_eq!(snapshot.elapsed_ns, 90);
809 }
810
811 #[rstest]
812 fn test_runner_metrics_reset_clears_populated_snapshot() {
813 let metrics = RunnerMetrics::default();
814
815 metrics.record_dispatch(
816 SystemChannel::TimeEvents,
817 Duration::from_nanos(10),
818 Duration::from_nanos(30),
819 );
820 metrics.record_maintenance(Duration::from_nanos(5), Duration::from_nanos(40));
821 metrics.record_external_msgbus(Duration::from_nanos(7), Duration::from_nanos(45));
822 metrics.publish_queue_depths(
823 RunnerChannelQueueDepths {
824 time_events: 1,
825 exec_events: 2,
826 exec_commands: 3,
827 data_events: 4,
828 data_commands: 5,
829 },
830 Duration::from_nanos(50),
831 );
832 metrics.reset();
833
834 assert_eq!(metrics.snapshot(), RunnerMetricsSnapshot::default());
835 }
836
837 #[rstest]
838 fn test_runner_metrics_queue_depths_use_receiver_lengths() {
839 let (time_tx, time_rx) = tokio::sync::mpsc::unbounded_channel::<TimeEventMessage>();
840 let (exec_evt_tx, exec_evt_rx) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
841 let (exec_cmd_tx, exec_cmd_rx) =
842 tokio::sync::mpsc::unbounded_channel::<TradingCommandMessage>();
843 let (data_evt_tx, data_evt_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
844 let (data_cmd_tx, data_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<DataCommand>();
845 let metrics = RunnerMetrics::default();
846
847 time_tx.send(stub_time_event_handler()).unwrap();
848 for _ in 0..2 {
849 exec_evt_tx.send(stub_exec_event()).unwrap();
850 }
851
852 for _ in 0..3 {
853 exec_cmd_tx
854 .send(TradingCommandMessage::new(
855 MessagingSwitchboard::exec_engine_execute(),
856 stub_trading_command(),
857 ))
858 .unwrap();
859 }
860
861 for _ in 0..4 {
862 data_evt_tx.send(stub_data_event()).unwrap();
863 }
864
865 for _ in 0..5 {
866 data_cmd_tx.send(stub_data_command()).unwrap();
867 }
868
869 metrics.publish_queue_depths(
870 RunnerChannelQueueDepths::from_receivers(
871 &time_rx,
872 &exec_evt_rx,
873 &exec_cmd_rx,
874 &data_evt_rx,
875 &data_cmd_rx,
876 ),
877 Duration::from_nanos(25),
878 );
879 let snapshot = metrics.snapshot();
880
881 assert_eq!(snapshot.time_events.queue_depth, 1);
882 assert_eq!(snapshot.exec_events.queue_depth, 2);
883 assert_eq!(snapshot.exec_commands.queue_depth, 3);
884 assert_eq!(snapshot.data_events.queue_depth, 4);
885 assert_eq!(snapshot.data_commands.queue_depth, 5);
886 assert_eq!(snapshot.elapsed_ns, 25);
887 }
888
889 fn runner_snapshot(
890 dispatched: [u64; 5],
891 dispatch_busy_ns: [u64; 5],
892 maintenance_busy_ns: u64,
893 external_msgbus_busy_ns: u64,
894 elapsed_ns: u64,
895 ) -> RunnerMetricsSnapshot {
896 let total_dispatch_busy_ns = dispatch_busy_ns.into_iter().fold(0, u64::saturating_add);
897 let [
898 time_events,
899 exec_events,
900 exec_commands,
901 data_events,
902 data_commands,
903 ] = dispatched;
904 let [
905 time_events_busy_ns,
906 exec_events_busy_ns,
907 exec_commands_busy_ns,
908 data_events_busy_ns,
909 data_commands_busy_ns,
910 ] = dispatch_busy_ns;
911
912 RunnerMetricsSnapshot {
913 time_events: channel_snapshot(time_events, time_events_busy_ns),
914 exec_events: channel_snapshot(exec_events, exec_events_busy_ns),
915 exec_commands: channel_snapshot(exec_commands, exec_commands_busy_ns),
916 data_events: channel_snapshot(data_events, data_events_busy_ns),
917 data_commands: channel_snapshot(data_commands, data_commands_busy_ns),
918 dispatch_busy_ns: total_dispatch_busy_ns,
919 maintenance_busy_ns,
920 external_msgbus_busy_ns,
921 elapsed_ns,
922 }
923 }
924
925 fn with_queue_depths(
926 mut snapshot: RunnerMetricsSnapshot,
927 depths: [usize; 5],
928 ) -> RunnerMetricsSnapshot {
929 let [
930 time_events,
931 exec_events,
932 exec_commands,
933 data_events,
934 data_commands,
935 ] = depths;
936 snapshot.time_events.queue_depth = time_events;
937 snapshot.exec_events.queue_depth = exec_events;
938 snapshot.exec_commands.queue_depth = exec_commands;
939 snapshot.data_events.queue_depth = data_events;
940 snapshot.data_commands.queue_depth = data_commands;
941 snapshot
942 }
943
944 fn queue_monitor_config() -> QueueMonitorConfig {
945 QueueMonitorConfig {
946 queue_depth_trigger: 10,
947 queue_depth_clear: 5,
948 mean_dispatch_ns_trigger: 100,
949 mean_dispatch_ns_clear: 50,
950 }
951 }
952
953 fn channel_snapshot(dispatched: u64, dispatch_busy_ns: u64) -> RunnerChannelMetricsSnapshot {
954 RunnerChannelMetricsSnapshot {
955 dispatched,
956 dispatch_busy_ns,
957 ..Default::default()
958 }
959 }
960
961 fn snapshot_dispatch_counts(snapshot: RunnerMetricsSnapshot) -> [u64; 5] {
962 [
963 snapshot.time_events.dispatched,
964 snapshot.exec_events.dispatched,
965 snapshot.exec_commands.dispatched,
966 snapshot.data_events.dispatched,
967 snapshot.data_commands.dispatched,
968 ]
969 }
970
971 fn snapshot_dispatch_busy_ns(snapshot: RunnerMetricsSnapshot) -> [u64; 5] {
972 [
973 snapshot.time_events.dispatch_busy_ns,
974 snapshot.exec_events.dispatch_busy_ns,
975 snapshot.exec_commands.dispatch_busy_ns,
976 snapshot.data_events.dispatch_busy_ns,
977 snapshot.data_commands.dispatch_busy_ns,
978 ]
979 }
980
981 fn snapshot_last_dispatch_at_ns(snapshot: RunnerMetricsSnapshot) -> [u64; 5] {
982 [
983 snapshot.time_events.last_dispatch_at_ns,
984 snapshot.exec_events.last_dispatch_at_ns,
985 snapshot.exec_commands.last_dispatch_at_ns,
986 snapshot.data_events.last_dispatch_at_ns,
987 snapshot.data_commands.last_dispatch_at_ns,
988 ]
989 }
990
991 fn stub_time_event_handler() -> TimeEventMessage {
992 TimeEventMessage::new(
993 TimeEvent::new(
994 Ustr::from("test-timer"),
995 UUID4::new(),
996 UnixNanos::default(),
997 UnixNanos::default(),
998 ),
999 TimeEventCallback::from(|_| {}),
1000 )
1001 }
1002
1003 fn stub_exec_event() -> ExecutionEvent {
1004 ExecutionEvent::Account(AccountState::new(
1005 AccountId::from("TEST-001"),
1006 AccountType::Cash,
1007 vec![],
1008 vec![],
1009 true,
1010 UUID4::new(),
1011 UnixNanos::default(),
1012 UnixNanos::default(),
1013 None,
1014 ))
1015 }
1016
1017 fn stub_trading_command() -> TradingCommand {
1018 TradingCommand::QueryAccount(QueryAccount::new(
1019 TraderId::from("TESTER-001"),
1020 None,
1021 AccountId::from("TEST-001"),
1022 UUID4::new(),
1023 UnixNanos::default(),
1024 None,
1025 None,
1026 ))
1027 }
1028
1029 fn stub_data_event() -> DataEvent {
1030 DataEvent::Instrument(InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt()))
1031 }
1032
1033 fn stub_data_command() -> DataCommand {
1034 DataCommand::Subscribe(SubscribeCommand::Instruments(SubscribeInstruments::new(
1035 None,
1036 Venue::from("TEST"),
1037 UUID4::new(),
1038 UnixNanos::default(),
1039 None,
1040 None,
1041 )))
1042 }
1043}