1use std::sync::OnceLock;
26use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
27use std::time::Instant;
28
29#[derive(Debug)]
41pub struct TimingCounter {
42 count: AtomicU64,
43 total_us: AtomicU64,
44 max_us: AtomicU64,
45}
46
47impl TimingCounter {
48 const fn new() -> Self {
49 Self {
50 count: AtomicU64::new(0),
51 total_us: AtomicU64::new(0),
52 max_us: AtomicU64::new(0),
53 }
54 }
55
56 #[inline]
58 pub fn record(&self, elapsed_us: u64) {
59 self.count.fetch_add(1, Ordering::Relaxed);
60 self.total_us.fetch_add(elapsed_us, Ordering::Relaxed);
61 let mut current = self.max_us.load(Ordering::Relaxed);
63 while elapsed_us > current {
64 match self.max_us.compare_exchange_weak(
65 current,
66 elapsed_us,
67 Ordering::Relaxed,
68 Ordering::Relaxed,
69 ) {
70 Ok(_) => break,
71 Err(actual) => current = actual,
72 }
73 }
74 }
75
76 pub fn snapshot(&self) -> TimingSnapshot {
78 let count = self.count.load(Ordering::Relaxed);
79 let total_us = self.total_us.load(Ordering::Relaxed);
80 let max_us = self.max_us.load(Ordering::Relaxed);
81 TimingSnapshot {
82 count,
83 total_us,
84 max_us,
85 avg_us: total_us.checked_div(count).unwrap_or(0),
86 }
87 }
88
89 pub fn reset(&self) {
91 self.count.store(0, Ordering::Relaxed);
92 self.total_us.store(0, Ordering::Relaxed);
93 self.max_us.store(0, Ordering::Relaxed);
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct TimingSnapshot {
100 pub count: u64,
101 pub total_us: u64,
102 pub max_us: u64,
103 pub avg_us: u64,
104}
105
106impl std::fmt::Display for TimingSnapshot {
107 #[allow(clippy::cast_precision_loss)]
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 if self.count == 0 {
110 write!(f, "n=0")
111 } else {
112 write!(
113 f,
114 "n={} avg={:.1}ms max={:.1}ms total={:.1}ms",
115 self.count,
116 self.avg_us as f64 / 1000.0,
117 self.max_us as f64 / 1000.0,
118 self.total_us as f64 / 1000.0,
119 )
120 }
121 }
122}
123
124#[derive(Debug)]
130pub struct ByteCounter {
131 count: AtomicU64,
132 total_bytes: AtomicU64,
133}
134
135impl ByteCounter {
136 const fn new() -> Self {
137 Self {
138 count: AtomicU64::new(0),
139 total_bytes: AtomicU64::new(0),
140 }
141 }
142
143 #[inline]
145 pub fn record(&self, bytes: u64) {
146 self.count.fetch_add(1, Ordering::Relaxed);
147 self.total_bytes.fetch_add(bytes, Ordering::Relaxed);
148 }
149
150 pub fn snapshot(&self) -> ByteSnapshot {
152 let count = self.count.load(Ordering::Relaxed);
153 let total_bytes = self.total_bytes.load(Ordering::Relaxed);
154 ByteSnapshot {
155 count,
156 total_bytes,
157 avg_bytes: total_bytes.checked_div(count).unwrap_or(0),
158 }
159 }
160
161 pub fn reset(&self) {
163 self.count.store(0, Ordering::Relaxed);
164 self.total_bytes.store(0, Ordering::Relaxed);
165 }
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub struct ByteSnapshot {
171 pub count: u64,
172 pub total_bytes: u64,
173 pub avg_bytes: u64,
174}
175
176impl std::fmt::Display for ByteSnapshot {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 if self.count == 0 {
179 write!(f, "n=0")
180 } else {
181 write!(
182 f,
183 "n={} avg={}B total={}B",
184 self.count, self.avg_bytes, self.total_bytes,
185 )
186 }
187 }
188}
189
190pub struct SessionMetrics {
204 enabled: AtomicBool,
205
206 pub jsonl_save: TimingCounter,
209 pub jsonl_serialize: TimingCounter,
211 pub jsonl_io: TimingCounter,
213 pub jsonl_bytes: ByteCounter,
215 pub jsonl_queue_wait: TimingCounter,
217
218 pub sqlite_save: TimingCounter,
221 pub sqlite_append: TimingCounter,
223 pub sqlite_serialize: TimingCounter,
225 pub sqlite_bytes: ByteCounter,
227
228 pub sqlite_load: TimingCounter,
231 pub sqlite_load_meta: TimingCounter,
233
234 pub index_lock: TimingCounter,
237 pub index_upsert: TimingCounter,
239 pub index_list: TimingCounter,
241 pub index_reindex: TimingCounter,
243
244 pub append: TimingCounter,
247}
248
249impl SessionMetrics {
250 const fn new() -> Self {
251 Self {
252 enabled: AtomicBool::new(false),
253 jsonl_save: TimingCounter::new(),
254 jsonl_serialize: TimingCounter::new(),
255 jsonl_io: TimingCounter::new(),
256 jsonl_bytes: ByteCounter::new(),
257 jsonl_queue_wait: TimingCounter::new(),
258 sqlite_save: TimingCounter::new(),
259 sqlite_append: TimingCounter::new(),
260 sqlite_serialize: TimingCounter::new(),
261 sqlite_bytes: ByteCounter::new(),
262 sqlite_load: TimingCounter::new(),
263 sqlite_load_meta: TimingCounter::new(),
264 index_lock: TimingCounter::new(),
265 index_upsert: TimingCounter::new(),
266 index_list: TimingCounter::new(),
267 index_reindex: TimingCounter::new(),
268 append: TimingCounter::new(),
269 }
270 }
271
272 #[inline]
274 pub fn enabled(&self) -> bool {
275 self.enabled.load(Ordering::Relaxed)
276 }
277
278 pub fn enable(&self) {
280 self.enabled.store(true, Ordering::Relaxed);
281 }
282
283 pub fn disable(&self) {
285 self.enabled.store(false, Ordering::Relaxed);
286 }
287
288 #[inline]
291 pub fn start_timer<'a>(&'a self, counter: &'a TimingCounter) -> ScopedTimer<'a> {
292 if self.enabled() {
293 ScopedTimer {
294 counter: Some(counter),
295 start: Instant::now(),
296 finished: false,
297 }
298 } else {
299 ScopedTimer {
300 counter: None,
301 start: Instant::now(), finished: false,
303 }
304 }
305 }
306
307 #[inline]
309 pub fn record_bytes(&self, counter: &ByteCounter, bytes: u64) {
310 if self.enabled() {
311 counter.record(bytes);
312 }
313 }
314
315 pub fn reset_all(&self) {
317 self.jsonl_save.reset();
318 self.jsonl_serialize.reset();
319 self.jsonl_io.reset();
320 self.jsonl_bytes.reset();
321 self.jsonl_queue_wait.reset();
322 self.sqlite_save.reset();
323 self.sqlite_append.reset();
324 self.sqlite_serialize.reset();
325 self.sqlite_bytes.reset();
326 self.sqlite_load.reset();
327 self.sqlite_load_meta.reset();
328 self.index_lock.reset();
329 self.index_upsert.reset();
330 self.index_list.reset();
331 self.index_reindex.reset();
332 self.append.reset();
333 }
334
335 pub fn snapshot(&self) -> MetricsSnapshot {
337 MetricsSnapshot {
338 enabled: self.enabled(),
339 jsonl_save: self.jsonl_save.snapshot(),
340 jsonl_serialize: self.jsonl_serialize.snapshot(),
341 jsonl_io: self.jsonl_io.snapshot(),
342 jsonl_bytes: self.jsonl_bytes.snapshot(),
343 jsonl_queue_wait: self.jsonl_queue_wait.snapshot(),
344 sqlite_save: self.sqlite_save.snapshot(),
345 sqlite_append: self.sqlite_append.snapshot(),
346 sqlite_serialize: self.sqlite_serialize.snapshot(),
347 sqlite_bytes: self.sqlite_bytes.snapshot(),
348 sqlite_load: self.sqlite_load.snapshot(),
349 sqlite_load_meta: self.sqlite_load_meta.snapshot(),
350 index_lock: self.index_lock.snapshot(),
351 index_upsert: self.index_upsert.snapshot(),
352 index_list: self.index_list.snapshot(),
353 index_reindex: self.index_reindex.snapshot(),
354 append: self.append.snapshot(),
355 }
356 }
357
358 pub fn summary(&self) -> String {
360 if !self.enabled() {
361 return "Session telemetry disabled (set PI_PERF_TELEMETRY=1 to enable)".to_string();
362 }
363 let s = self.snapshot();
364 format!(
365 "Session hot-path metrics:\n \
366 JSONL save: {}\n \
367 JSONL serialize: {}\n \
368 JSONL IO: {}\n \
369 JSONL bytes: {}\n \
370 JSONL queue wait: {}\n \
371 SQLite save: {}\n \
372 SQLite append: {}\n \
373 SQLite serialize: {}\n \
374 SQLite bytes: {}\n \
375 SQLite load: {}\n \
376 SQLite load meta: {}\n \
377 Index lock: {}\n \
378 Index upsert: {}\n \
379 Index list: {}\n \
380 Index reindex: {}\n \
381 Append: {}",
382 s.jsonl_save,
383 s.jsonl_serialize,
384 s.jsonl_io,
385 s.jsonl_bytes,
386 s.jsonl_queue_wait,
387 s.sqlite_save,
388 s.sqlite_append,
389 s.sqlite_serialize,
390 s.sqlite_bytes,
391 s.sqlite_load,
392 s.sqlite_load_meta,
393 s.index_lock,
394 s.index_upsert,
395 s.index_list,
396 s.index_reindex,
397 s.append,
398 )
399 }
400
401 pub fn emit(&self) {
403 if self.enabled() {
404 tracing::debug!("{}", self.summary());
405 }
406 }
407}
408
409#[derive(Debug, Clone)]
415pub struct MetricsSnapshot {
416 pub enabled: bool,
417 pub jsonl_save: TimingSnapshot,
418 pub jsonl_serialize: TimingSnapshot,
419 pub jsonl_io: TimingSnapshot,
420 pub jsonl_bytes: ByteSnapshot,
421 pub jsonl_queue_wait: TimingSnapshot,
422 pub sqlite_save: TimingSnapshot,
423 pub sqlite_append: TimingSnapshot,
424 pub sqlite_serialize: TimingSnapshot,
425 pub sqlite_bytes: ByteSnapshot,
426 pub sqlite_load: TimingSnapshot,
427 pub sqlite_load_meta: TimingSnapshot,
428 pub index_lock: TimingSnapshot,
429 pub index_upsert: TimingSnapshot,
430 pub index_list: TimingSnapshot,
431 pub index_reindex: TimingSnapshot,
432 pub append: TimingSnapshot,
433}
434
435pub struct ScopedTimer<'a> {
442 counter: Option<&'a TimingCounter>,
443 start: Instant,
444 finished: bool,
445}
446
447impl ScopedTimer<'_> {
448 #[allow(clippy::cast_possible_truncation)]
451 pub fn finish(mut self) -> u64 {
452 let elapsed_us = self.start.elapsed().as_micros().min(u128::from(u64::MAX)) as u64;
453 if let Some(counter) = self.counter {
454 counter.record(elapsed_us);
455 }
456 self.finished = true;
458 elapsed_us
459 }
460}
461
462impl Drop for ScopedTimer<'_> {
463 #[allow(clippy::cast_possible_truncation)]
464 fn drop(&mut self) {
465 if !self.finished {
466 if let Some(counter) = self.counter {
467 let elapsed_us = self.start.elapsed().as_micros().min(u128::from(u64::MAX)) as u64;
468 counter.record(elapsed_us);
469 }
470 }
471 }
472}
473
474static GLOBAL_METRICS: OnceLock<SessionMetrics> = OnceLock::new();
479
480pub fn global() -> &'static SessionMetrics {
485 GLOBAL_METRICS.get_or_init(|| {
486 let metrics = SessionMetrics::new();
487 let enabled =
488 std::env::var_os("PI_PERF_TELEMETRY").is_some_and(|v| v == "1" || v == "true");
489 if enabled {
490 metrics.enabled.store(true, Ordering::Relaxed);
491 }
492 metrics
493 })
494}
495
496#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[test]
505 fn timing_counter_starts_at_zero() {
506 let counter = TimingCounter::new();
507 let snap = counter.snapshot();
508 assert_eq!(snap.count, 0);
509 assert_eq!(snap.total_us, 0);
510 assert_eq!(snap.max_us, 0);
511 assert_eq!(snap.avg_us, 0);
512 }
513
514 #[test]
515 fn timing_counter_records_single_observation() {
516 let counter = TimingCounter::new();
517 counter.record(500);
518 let snap = counter.snapshot();
519 assert_eq!(snap.count, 1);
520 assert_eq!(snap.total_us, 500);
521 assert_eq!(snap.max_us, 500);
522 assert_eq!(snap.avg_us, 500);
523 }
524
525 #[test]
526 fn timing_counter_records_multiple_observations() {
527 let counter = TimingCounter::new();
528 counter.record(100);
529 counter.record(300);
530 counter.record(200);
531 let snap = counter.snapshot();
532 assert_eq!(snap.count, 3);
533 assert_eq!(snap.total_us, 600);
534 assert_eq!(snap.max_us, 300);
535 assert_eq!(snap.avg_us, 200);
536 }
537
538 #[test]
539 fn timing_counter_max_tracks_peak() {
540 let counter = TimingCounter::new();
541 counter.record(50);
542 counter.record(999);
543 counter.record(100);
544 assert_eq!(counter.snapshot().max_us, 999);
545 }
546
547 #[test]
548 fn timing_counter_reset_clears_all() {
549 let counter = TimingCounter::new();
550 counter.record(100);
551 counter.record(200);
552 counter.reset();
553 let snap = counter.snapshot();
554 assert_eq!(snap.count, 0);
555 assert_eq!(snap.total_us, 0);
556 assert_eq!(snap.max_us, 0);
557 }
558
559 #[test]
560 fn byte_counter_starts_at_zero() {
561 let counter = ByteCounter::new();
562 let snap = counter.snapshot();
563 assert_eq!(snap.count, 0);
564 assert_eq!(snap.total_bytes, 0);
565 assert_eq!(snap.avg_bytes, 0);
566 }
567
568 #[test]
569 fn byte_counter_records_observations() {
570 let counter = ByteCounter::new();
571 counter.record(1024);
572 counter.record(2048);
573 let snap = counter.snapshot();
574 assert_eq!(snap.count, 2);
575 assert_eq!(snap.total_bytes, 3072);
576 assert_eq!(snap.avg_bytes, 1536);
577 }
578
579 #[test]
580 fn byte_counter_reset_clears_all() {
581 let counter = ByteCounter::new();
582 counter.record(512);
583 counter.reset();
584 let snap = counter.snapshot();
585 assert_eq!(snap.count, 0);
586 assert_eq!(snap.total_bytes, 0);
587 }
588
589 #[test]
590 fn scoped_timer_records_on_drop() {
591 let counter = TimingCounter::new();
592 {
593 let _timer = ScopedTimer {
594 counter: Some(&counter),
595 start: Instant::now(),
596 finished: false,
597 };
598 std::thread::sleep(std::time::Duration::from_micros(100));
600 }
601 let snap = counter.snapshot();
602 assert_eq!(snap.count, 1);
603 assert!(
604 snap.total_us > 0,
605 "Timer should record nonzero elapsed time"
606 );
607 }
608
609 #[test]
610 fn scoped_timer_finish_returns_elapsed_and_records() {
611 let counter = TimingCounter::new();
612 let timer = ScopedTimer {
613 counter: Some(&counter),
614 start: Instant::now(),
615 finished: false,
616 };
617 std::thread::sleep(std::time::Duration::from_micros(100));
618 let elapsed = timer.finish();
619 assert!(elapsed > 0);
620 assert_eq!(counter.snapshot().count, 1);
621 }
622
623 #[test]
624 fn scoped_timer_noop_when_disabled() {
625 let counter = TimingCounter::new();
626 {
627 let _timer = ScopedTimer {
628 counter: None,
629 start: Instant::now(),
630 finished: false,
631 };
632 }
633 assert_eq!(counter.snapshot().count, 0);
634 }
635
636 #[test]
637 fn session_metrics_disabled_by_default() {
638 let metrics = SessionMetrics::new();
639 assert!(!metrics.enabled());
640 }
641
642 #[test]
643 fn session_metrics_enable_disable() {
644 let metrics = SessionMetrics::new();
645 metrics.enable();
646 assert!(metrics.enabled());
647 metrics.disable();
648 assert!(!metrics.enabled());
649 }
650
651 #[test]
652 fn session_metrics_start_timer_noop_when_disabled() {
653 let metrics = SessionMetrics::new();
654 assert!(!metrics.enabled());
655 {
656 let _timer = metrics.start_timer(&metrics.sqlite_save);
657 }
658 assert_eq!(metrics.sqlite_save.snapshot().count, 0);
659 }
660
661 #[test]
662 fn session_metrics_start_timer_records_when_enabled() {
663 let metrics = SessionMetrics::new();
664 metrics.enable();
665 {
666 let _timer = metrics.start_timer(&metrics.sqlite_save);
667 std::thread::sleep(std::time::Duration::from_micros(50));
668 }
669 assert_eq!(metrics.sqlite_save.snapshot().count, 1);
670 assert!(metrics.sqlite_save.snapshot().total_us > 0);
671 }
672
673 #[test]
674 fn session_metrics_record_bytes_noop_when_disabled() {
675 let metrics = SessionMetrics::new();
676 metrics.record_bytes(&metrics.jsonl_bytes, 1024);
677 assert_eq!(metrics.jsonl_bytes.snapshot().count, 0);
678 }
679
680 #[test]
681 fn session_metrics_record_bytes_when_enabled() {
682 let metrics = SessionMetrics::new();
683 metrics.enable();
684 metrics.record_bytes(&metrics.jsonl_bytes, 1024);
685 metrics.record_bytes(&metrics.jsonl_bytes, 2048);
686 let snap = metrics.jsonl_bytes.snapshot();
687 assert_eq!(snap.count, 2);
688 assert_eq!(snap.total_bytes, 3072);
689 }
690
691 #[test]
692 fn session_metrics_reset_all() {
693 let metrics = SessionMetrics::new();
694 metrics.enable();
695 metrics.sqlite_save.record(100);
696 metrics.index_upsert.record(200);
697 metrics.jsonl_bytes.record(512);
698 metrics.reset_all();
699 assert_eq!(metrics.sqlite_save.snapshot().count, 0);
700 assert_eq!(metrics.index_upsert.snapshot().count, 0);
701 assert_eq!(metrics.jsonl_bytes.snapshot().count, 0);
702 }
703
704 #[test]
705 fn session_metrics_snapshot_captures_all_counters() {
706 let metrics = SessionMetrics::new();
707 metrics.enable();
708 metrics.sqlite_save.record(100);
709 metrics.sqlite_load.record(200);
710 metrics.index_lock.record(50);
711 metrics.jsonl_bytes.record(4096);
712 let snap = metrics.snapshot();
713 assert!(snap.enabled);
714 assert_eq!(snap.sqlite_save.count, 1);
715 assert_eq!(snap.sqlite_load.count, 1);
716 assert_eq!(snap.index_lock.count, 1);
717 assert_eq!(snap.jsonl_bytes.count, 1);
718 assert_eq!(snap.jsonl_bytes.total_bytes, 4096);
719 }
720
721 #[test]
722 fn session_metrics_summary_disabled() {
723 let metrics = SessionMetrics::new();
724 let summary = metrics.summary();
725 assert!(summary.contains("disabled"));
726 }
727
728 #[test]
729 fn session_metrics_summary_enabled_contains_all_labels() {
730 let metrics = SessionMetrics::new();
731 metrics.enable();
732 metrics.sqlite_save.record(100);
733 let summary = metrics.summary();
734 assert!(summary.contains("JSONL save:"));
735 assert!(summary.contains("JSONL serialize:"));
736 assert!(summary.contains("JSONL IO:"));
737 assert!(summary.contains("JSONL bytes:"));
738 assert!(summary.contains("JSONL queue wait:"));
739 assert!(summary.contains("SQLite save:"));
740 assert!(summary.contains("SQLite append:"));
741 assert!(summary.contains("SQLite serialize:"));
742 assert!(summary.contains("SQLite bytes:"));
743 assert!(summary.contains("SQLite load:"));
744 assert!(summary.contains("SQLite load meta:"));
745 assert!(summary.contains("Index lock:"));
746 assert!(summary.contains("Index upsert:"));
747 assert!(summary.contains("Index list:"));
748 assert!(summary.contains("Index reindex:"));
749 assert!(summary.contains("Append:"));
750 }
751
752 #[test]
753 fn timing_snapshot_display_zero() {
754 let snap = TimingSnapshot {
755 count: 0,
756 total_us: 0,
757 max_us: 0,
758 avg_us: 0,
759 };
760 assert_eq!(format!("{snap}"), "n=0");
761 }
762
763 #[test]
764 fn timing_snapshot_display_nonzero() {
765 let snap = TimingSnapshot {
766 count: 3,
767 total_us: 6000,
768 max_us: 3000,
769 avg_us: 2000,
770 };
771 let display = format!("{snap}");
772 assert!(display.contains("n=3"));
773 assert!(display.contains("avg=2.0ms"));
774 assert!(display.contains("max=3.0ms"));
775 assert!(display.contains("total=6.0ms"));
776 }
777
778 #[test]
779 fn byte_snapshot_display_zero() {
780 let snap = ByteSnapshot {
781 count: 0,
782 total_bytes: 0,
783 avg_bytes: 0,
784 };
785 assert_eq!(format!("{snap}"), "n=0");
786 }
787
788 #[test]
789 fn byte_snapshot_display_nonzero() {
790 let snap = ByteSnapshot {
791 count: 2,
792 total_bytes: 3072,
793 avg_bytes: 1536,
794 };
795 let display = format!("{snap}");
796 assert!(display.contains("n=2"));
797 assert!(display.contains("avg=1536B"));
798 assert!(display.contains("total=3072B"));
799 }
800
801 #[test]
802 fn global_returns_same_instance() {
803 let a = global();
804 let b = global();
805 assert!(std::ptr::eq(a, b));
806 }
807
808 #[test]
809 fn timing_counter_concurrent_recording() {
810 use std::sync::Arc;
811
812 let counter = Arc::new(TimingCounter::new());
813 let threads: Vec<_> = (0..4)
814 .map(|_| {
815 let c = Arc::clone(&counter);
816 std::thread::spawn(move || {
817 for i in 0..100 {
818 c.record(i);
819 }
820 })
821 })
822 .collect();
823 for t in threads {
824 t.join().expect("thread join");
825 }
826 let snap = counter.snapshot();
827 assert_eq!(snap.count, 400);
828 assert_eq!(snap.total_us, 19800);
830 assert_eq!(snap.max_us, 99);
831 }
832
833 mod proptest_session_metrics {
834 use super::*;
835 use proptest::prelude::*;
836
837 proptest! {
838 #[test]
840 fn timing_counter_sum_and_count(
841 values in prop::collection::vec(0u64..10_000, 0..50)
842 ) {
843 let counter = TimingCounter::new();
844 for &v in &values {
845 counter.record(v);
846 }
847 let snap = counter.snapshot();
848 assert_eq!(snap.count, values.len() as u64);
849 assert_eq!(
850 snap.total_us,
851 values.iter().copied().sum::<u64>()
852 );
853 }
854
855 #[test]
857 fn timing_counter_tracks_max(
858 values in prop::collection::vec(0u64..100_000, 1..50)
859 ) {
860 let counter = TimingCounter::new();
861 for &v in &values {
862 counter.record(v);
863 }
864 let snap = counter.snapshot();
865 assert_eq!(snap.max_us, *values.iter().max().unwrap());
866 }
867
868 #[test]
870 fn timing_snapshot_avg_is_floor_division(
871 values in prop::collection::vec(1u64..10_000, 1..50)
872 ) {
873 let counter = TimingCounter::new();
874 for &v in &values {
875 counter.record(v);
876 }
877 let snap = counter.snapshot();
878 let expected = snap.total_us / snap.count;
879 assert_eq!(snap.avg_us, expected);
880 }
881
882 #[test]
884 fn empty_counter_snapshot(_dummy in 0..1u8) {
885 let counter = TimingCounter::new();
886 let snap = counter.snapshot();
887 assert_eq!(snap.count, 0);
888 assert_eq!(snap.total_us, 0);
889 assert_eq!(snap.max_us, 0);
890 assert_eq!(snap.avg_us, 0);
891 }
892
893 #[test]
895 fn timing_reset_clears(values in prop::collection::vec(1u64..1000, 1..20)) {
896 let counter = TimingCounter::new();
897 for &v in &values {
898 counter.record(v);
899 }
900 counter.reset();
901 let snap = counter.snapshot();
902 assert_eq!(snap.count, 0);
903 assert_eq!(snap.total_us, 0);
904 assert_eq!(snap.max_us, 0);
905 }
906
907 #[test]
909 fn byte_counter_sum_and_count(
910 values in prop::collection::vec(0u64..100_000, 0..50)
911 ) {
912 let counter = ByteCounter::new();
913 for &v in &values {
914 counter.record(v);
915 }
916 let snap = counter.snapshot();
917 assert_eq!(snap.count, values.len() as u64);
918 assert_eq!(snap.total_bytes, values.iter().copied().sum::<u64>());
919 }
920
921 #[test]
923 fn byte_counter_avg(
924 values in prop::collection::vec(1u64..10_000, 1..50)
925 ) {
926 let counter = ByteCounter::new();
927 for &v in &values {
928 counter.record(v);
929 }
930 let snap = counter.snapshot();
931 assert_eq!(snap.avg_bytes, snap.total_bytes / snap.count);
932 }
933
934 #[test]
936 fn byte_counter_reset(values in prop::collection::vec(1u64..1000, 1..10)) {
937 let counter = ByteCounter::new();
938 for &v in &values {
939 counter.record(v);
940 }
941 counter.reset();
942 let snap = counter.snapshot();
943 assert_eq!(snap.count, 0);
944 assert_eq!(snap.total_bytes, 0);
945 }
946
947 #[test]
949 fn timing_display_zero(_dummy in 0..1u8) {
950 let snap = TimingSnapshot {
951 count: 0,
952 total_us: 0,
953 max_us: 0,
954 avg_us: 0,
955 };
956 assert_eq!(format!("{snap}"), "n=0");
957 }
958
959 #[test]
961 fn timing_display_nonzero(
962 count in 1u64..1000,
963 total_us in 1u64..1_000_000,
964 max_us in 1u64..1_000_000
965 ) {
966 let snap = TimingSnapshot {
967 count,
968 total_us,
969 max_us,
970 avg_us: total_us / count,
971 };
972 let display = format!("{snap}");
973 assert!(display.contains(&format!("n={count}")));
974 assert!(display.contains("avg="));
975 assert!(display.contains("max="));
976 assert!(display.contains("total="));
977 }
978
979 #[test]
981 fn byte_display_zero(_dummy in 0..1u8) {
982 let snap = ByteSnapshot {
983 count: 0,
984 total_bytes: 0,
985 avg_bytes: 0,
986 };
987 assert_eq!(format!("{snap}"), "n=0");
988 }
989
990 #[test]
992 fn byte_display_nonzero(
993 count in 1u64..1000,
994 total in 1u64..1_000_000
995 ) {
996 let snap = ByteSnapshot {
997 count,
998 total_bytes: total,
999 avg_bytes: total / count,
1000 };
1001 let display = format!("{snap}");
1002 assert!(display.contains(&format!("n={count}")));
1003 assert!(display.contains("avg="));
1004 assert!(display.contains("total="));
1005 }
1006 }
1007 }
1008}