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 }
297 } else {
298 ScopedTimer {
299 counter: None,
300 start: Instant::now(), }
302 }
303 }
304
305 #[inline]
307 pub fn record_bytes(&self, counter: &ByteCounter, bytes: u64) {
308 if self.enabled() {
309 counter.record(bytes);
310 }
311 }
312
313 pub fn reset_all(&self) {
315 self.jsonl_save.reset();
316 self.jsonl_serialize.reset();
317 self.jsonl_io.reset();
318 self.jsonl_bytes.reset();
319 self.jsonl_queue_wait.reset();
320 self.sqlite_save.reset();
321 self.sqlite_append.reset();
322 self.sqlite_serialize.reset();
323 self.sqlite_bytes.reset();
324 self.sqlite_load.reset();
325 self.sqlite_load_meta.reset();
326 self.index_lock.reset();
327 self.index_upsert.reset();
328 self.index_list.reset();
329 self.index_reindex.reset();
330 self.append.reset();
331 }
332
333 pub fn snapshot(&self) -> MetricsSnapshot {
335 MetricsSnapshot {
336 enabled: self.enabled(),
337 jsonl_save: self.jsonl_save.snapshot(),
338 jsonl_serialize: self.jsonl_serialize.snapshot(),
339 jsonl_io: self.jsonl_io.snapshot(),
340 jsonl_bytes: self.jsonl_bytes.snapshot(),
341 jsonl_queue_wait: self.jsonl_queue_wait.snapshot(),
342 sqlite_save: self.sqlite_save.snapshot(),
343 sqlite_append: self.sqlite_append.snapshot(),
344 sqlite_serialize: self.sqlite_serialize.snapshot(),
345 sqlite_bytes: self.sqlite_bytes.snapshot(),
346 sqlite_load: self.sqlite_load.snapshot(),
347 sqlite_load_meta: self.sqlite_load_meta.snapshot(),
348 index_lock: self.index_lock.snapshot(),
349 index_upsert: self.index_upsert.snapshot(),
350 index_list: self.index_list.snapshot(),
351 index_reindex: self.index_reindex.snapshot(),
352 append: self.append.snapshot(),
353 }
354 }
355
356 pub fn summary(&self) -> String {
358 if !self.enabled() {
359 return "Session telemetry disabled (set PI_PERF_TELEMETRY=1 to enable)".to_string();
360 }
361 let s = self.snapshot();
362 format!(
363 "Session hot-path metrics:\n \
364 JSONL save: {}\n \
365 JSONL serialize: {}\n \
366 JSONL IO: {}\n \
367 JSONL bytes: {}\n \
368 JSONL queue wait: {}\n \
369 SQLite save: {}\n \
370 SQLite append: {}\n \
371 SQLite serialize: {}\n \
372 SQLite bytes: {}\n \
373 SQLite load: {}\n \
374 SQLite load meta: {}\n \
375 Index lock: {}\n \
376 Index upsert: {}\n \
377 Index list: {}\n \
378 Index reindex: {}\n \
379 Append: {}",
380 s.jsonl_save,
381 s.jsonl_serialize,
382 s.jsonl_io,
383 s.jsonl_bytes,
384 s.jsonl_queue_wait,
385 s.sqlite_save,
386 s.sqlite_append,
387 s.sqlite_serialize,
388 s.sqlite_bytes,
389 s.sqlite_load,
390 s.sqlite_load_meta,
391 s.index_lock,
392 s.index_upsert,
393 s.index_list,
394 s.index_reindex,
395 s.append,
396 )
397 }
398
399 pub fn emit(&self) {
401 if self.enabled() {
402 tracing::debug!("{}", self.summary());
403 }
404 }
405}
406
407#[derive(Debug, Clone)]
413pub struct MetricsSnapshot {
414 pub enabled: bool,
415 pub jsonl_save: TimingSnapshot,
416 pub jsonl_serialize: TimingSnapshot,
417 pub jsonl_io: TimingSnapshot,
418 pub jsonl_bytes: ByteSnapshot,
419 pub jsonl_queue_wait: TimingSnapshot,
420 pub sqlite_save: TimingSnapshot,
421 pub sqlite_append: TimingSnapshot,
422 pub sqlite_serialize: TimingSnapshot,
423 pub sqlite_bytes: ByteSnapshot,
424 pub sqlite_load: TimingSnapshot,
425 pub sqlite_load_meta: TimingSnapshot,
426 pub index_lock: TimingSnapshot,
427 pub index_upsert: TimingSnapshot,
428 pub index_list: TimingSnapshot,
429 pub index_reindex: TimingSnapshot,
430 pub append: TimingSnapshot,
431}
432
433pub struct ScopedTimer<'a> {
440 counter: Option<&'a TimingCounter>,
441 start: Instant,
442}
443
444impl ScopedTimer<'_> {
445 #[allow(clippy::cast_possible_truncation)]
448 pub fn finish(self) -> u64 {
449 let elapsed_us = self.start.elapsed().as_micros().min(u128::from(u64::MAX)) as u64;
450 if let Some(counter) = self.counter {
451 counter.record(elapsed_us);
452 }
453 std::mem::forget(self);
455 elapsed_us
456 }
457}
458
459impl Drop for ScopedTimer<'_> {
460 #[allow(clippy::cast_possible_truncation)]
461 fn drop(&mut self) {
462 if let Some(counter) = self.counter {
463 let elapsed_us = self.start.elapsed().as_micros().min(u128::from(u64::MAX)) as u64;
464 counter.record(elapsed_us);
465 }
466 }
467}
468
469static GLOBAL_METRICS: OnceLock<SessionMetrics> = OnceLock::new();
474
475pub fn global() -> &'static SessionMetrics {
480 GLOBAL_METRICS.get_or_init(|| {
481 let metrics = SessionMetrics::new();
482 let enabled =
483 std::env::var_os("PI_PERF_TELEMETRY").is_some_and(|v| v == "1" || v == "true");
484 if enabled {
485 metrics.enabled.store(true, Ordering::Relaxed);
486 }
487 metrics
488 })
489}
490
491#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn timing_counter_starts_at_zero() {
501 let counter = TimingCounter::new();
502 let snap = counter.snapshot();
503 assert_eq!(snap.count, 0);
504 assert_eq!(snap.total_us, 0);
505 assert_eq!(snap.max_us, 0);
506 assert_eq!(snap.avg_us, 0);
507 }
508
509 #[test]
510 fn timing_counter_records_single_observation() {
511 let counter = TimingCounter::new();
512 counter.record(500);
513 let snap = counter.snapshot();
514 assert_eq!(snap.count, 1);
515 assert_eq!(snap.total_us, 500);
516 assert_eq!(snap.max_us, 500);
517 assert_eq!(snap.avg_us, 500);
518 }
519
520 #[test]
521 fn timing_counter_records_multiple_observations() {
522 let counter = TimingCounter::new();
523 counter.record(100);
524 counter.record(300);
525 counter.record(200);
526 let snap = counter.snapshot();
527 assert_eq!(snap.count, 3);
528 assert_eq!(snap.total_us, 600);
529 assert_eq!(snap.max_us, 300);
530 assert_eq!(snap.avg_us, 200);
531 }
532
533 #[test]
534 fn timing_counter_max_tracks_peak() {
535 let counter = TimingCounter::new();
536 counter.record(50);
537 counter.record(999);
538 counter.record(100);
539 assert_eq!(counter.snapshot().max_us, 999);
540 }
541
542 #[test]
543 fn timing_counter_reset_clears_all() {
544 let counter = TimingCounter::new();
545 counter.record(100);
546 counter.record(200);
547 counter.reset();
548 let snap = counter.snapshot();
549 assert_eq!(snap.count, 0);
550 assert_eq!(snap.total_us, 0);
551 assert_eq!(snap.max_us, 0);
552 }
553
554 #[test]
555 fn byte_counter_starts_at_zero() {
556 let counter = ByteCounter::new();
557 let snap = counter.snapshot();
558 assert_eq!(snap.count, 0);
559 assert_eq!(snap.total_bytes, 0);
560 assert_eq!(snap.avg_bytes, 0);
561 }
562
563 #[test]
564 fn byte_counter_records_observations() {
565 let counter = ByteCounter::new();
566 counter.record(1024);
567 counter.record(2048);
568 let snap = counter.snapshot();
569 assert_eq!(snap.count, 2);
570 assert_eq!(snap.total_bytes, 3072);
571 assert_eq!(snap.avg_bytes, 1536);
572 }
573
574 #[test]
575 fn byte_counter_reset_clears_all() {
576 let counter = ByteCounter::new();
577 counter.record(512);
578 counter.reset();
579 let snap = counter.snapshot();
580 assert_eq!(snap.count, 0);
581 assert_eq!(snap.total_bytes, 0);
582 }
583
584 #[test]
585 fn scoped_timer_records_on_drop() {
586 let counter = TimingCounter::new();
587 {
588 let _timer = ScopedTimer {
589 counter: Some(&counter),
590 start: Instant::now(),
591 };
592 std::thread::sleep(std::time::Duration::from_micros(100));
594 }
595 let snap = counter.snapshot();
596 assert_eq!(snap.count, 1);
597 assert!(
598 snap.total_us > 0,
599 "Timer should record nonzero elapsed time"
600 );
601 }
602
603 #[test]
604 fn scoped_timer_finish_returns_elapsed_and_records() {
605 let counter = TimingCounter::new();
606 let timer = ScopedTimer {
607 counter: Some(&counter),
608 start: Instant::now(),
609 };
610 std::thread::sleep(std::time::Duration::from_micros(100));
611 let elapsed = timer.finish();
612 assert!(elapsed > 0);
613 assert_eq!(counter.snapshot().count, 1);
614 }
615
616 #[test]
617 fn scoped_timer_noop_when_disabled() {
618 let counter = TimingCounter::new();
619 {
620 let _timer = ScopedTimer {
621 counter: None,
622 start: Instant::now(),
623 };
624 }
625 assert_eq!(counter.snapshot().count, 0);
626 }
627
628 #[test]
629 fn session_metrics_disabled_by_default() {
630 let metrics = SessionMetrics::new();
631 assert!(!metrics.enabled());
632 }
633
634 #[test]
635 fn session_metrics_enable_disable() {
636 let metrics = SessionMetrics::new();
637 metrics.enable();
638 assert!(metrics.enabled());
639 metrics.disable();
640 assert!(!metrics.enabled());
641 }
642
643 #[test]
644 fn session_metrics_start_timer_noop_when_disabled() {
645 let metrics = SessionMetrics::new();
646 assert!(!metrics.enabled());
647 {
648 let _timer = metrics.start_timer(&metrics.sqlite_save);
649 }
650 assert_eq!(metrics.sqlite_save.snapshot().count, 0);
651 }
652
653 #[test]
654 fn session_metrics_start_timer_records_when_enabled() {
655 let metrics = SessionMetrics::new();
656 metrics.enable();
657 {
658 let _timer = metrics.start_timer(&metrics.sqlite_save);
659 std::thread::sleep(std::time::Duration::from_micros(50));
660 }
661 assert_eq!(metrics.sqlite_save.snapshot().count, 1);
662 assert!(metrics.sqlite_save.snapshot().total_us > 0);
663 }
664
665 #[test]
666 fn session_metrics_record_bytes_noop_when_disabled() {
667 let metrics = SessionMetrics::new();
668 metrics.record_bytes(&metrics.jsonl_bytes, 1024);
669 assert_eq!(metrics.jsonl_bytes.snapshot().count, 0);
670 }
671
672 #[test]
673 fn session_metrics_record_bytes_when_enabled() {
674 let metrics = SessionMetrics::new();
675 metrics.enable();
676 metrics.record_bytes(&metrics.jsonl_bytes, 1024);
677 metrics.record_bytes(&metrics.jsonl_bytes, 2048);
678 let snap = metrics.jsonl_bytes.snapshot();
679 assert_eq!(snap.count, 2);
680 assert_eq!(snap.total_bytes, 3072);
681 }
682
683 #[test]
684 fn session_metrics_reset_all() {
685 let metrics = SessionMetrics::new();
686 metrics.enable();
687 metrics.sqlite_save.record(100);
688 metrics.index_upsert.record(200);
689 metrics.jsonl_bytes.record(512);
690 metrics.reset_all();
691 assert_eq!(metrics.sqlite_save.snapshot().count, 0);
692 assert_eq!(metrics.index_upsert.snapshot().count, 0);
693 assert_eq!(metrics.jsonl_bytes.snapshot().count, 0);
694 }
695
696 #[test]
697 fn session_metrics_snapshot_captures_all_counters() {
698 let metrics = SessionMetrics::new();
699 metrics.enable();
700 metrics.sqlite_save.record(100);
701 metrics.sqlite_load.record(200);
702 metrics.index_lock.record(50);
703 metrics.jsonl_bytes.record(4096);
704 let snap = metrics.snapshot();
705 assert!(snap.enabled);
706 assert_eq!(snap.sqlite_save.count, 1);
707 assert_eq!(snap.sqlite_load.count, 1);
708 assert_eq!(snap.index_lock.count, 1);
709 assert_eq!(snap.jsonl_bytes.count, 1);
710 assert_eq!(snap.jsonl_bytes.total_bytes, 4096);
711 }
712
713 #[test]
714 fn session_metrics_summary_disabled() {
715 let metrics = SessionMetrics::new();
716 let summary = metrics.summary();
717 assert!(summary.contains("disabled"));
718 }
719
720 #[test]
721 fn session_metrics_summary_enabled_contains_all_labels() {
722 let metrics = SessionMetrics::new();
723 metrics.enable();
724 metrics.sqlite_save.record(100);
725 let summary = metrics.summary();
726 assert!(summary.contains("JSONL save:"));
727 assert!(summary.contains("JSONL serialize:"));
728 assert!(summary.contains("JSONL IO:"));
729 assert!(summary.contains("JSONL bytes:"));
730 assert!(summary.contains("JSONL queue wait:"));
731 assert!(summary.contains("SQLite save:"));
732 assert!(summary.contains("SQLite append:"));
733 assert!(summary.contains("SQLite serialize:"));
734 assert!(summary.contains("SQLite bytes:"));
735 assert!(summary.contains("SQLite load:"));
736 assert!(summary.contains("SQLite load meta:"));
737 assert!(summary.contains("Index lock:"));
738 assert!(summary.contains("Index upsert:"));
739 assert!(summary.contains("Index list:"));
740 assert!(summary.contains("Index reindex:"));
741 assert!(summary.contains("Append:"));
742 }
743
744 #[test]
745 fn timing_snapshot_display_zero() {
746 let snap = TimingSnapshot {
747 count: 0,
748 total_us: 0,
749 max_us: 0,
750 avg_us: 0,
751 };
752 assert_eq!(format!("{snap}"), "n=0");
753 }
754
755 #[test]
756 fn timing_snapshot_display_nonzero() {
757 let snap = TimingSnapshot {
758 count: 3,
759 total_us: 6000,
760 max_us: 3000,
761 avg_us: 2000,
762 };
763 let display = format!("{snap}");
764 assert!(display.contains("n=3"));
765 assert!(display.contains("avg=2.0ms"));
766 assert!(display.contains("max=3.0ms"));
767 assert!(display.contains("total=6.0ms"));
768 }
769
770 #[test]
771 fn byte_snapshot_display_zero() {
772 let snap = ByteSnapshot {
773 count: 0,
774 total_bytes: 0,
775 avg_bytes: 0,
776 };
777 assert_eq!(format!("{snap}"), "n=0");
778 }
779
780 #[test]
781 fn byte_snapshot_display_nonzero() {
782 let snap = ByteSnapshot {
783 count: 2,
784 total_bytes: 3072,
785 avg_bytes: 1536,
786 };
787 let display = format!("{snap}");
788 assert!(display.contains("n=2"));
789 assert!(display.contains("avg=1536B"));
790 assert!(display.contains("total=3072B"));
791 }
792
793 #[test]
794 fn global_returns_same_instance() {
795 let a = global();
796 let b = global();
797 assert!(std::ptr::eq(a, b));
798 }
799
800 #[test]
801 fn timing_counter_concurrent_recording() {
802 use std::sync::Arc;
803
804 let counter = Arc::new(TimingCounter::new());
805 let threads: Vec<_> = (0..4)
806 .map(|_| {
807 let c = Arc::clone(&counter);
808 std::thread::spawn(move || {
809 for i in 0..100 {
810 c.record(i);
811 }
812 })
813 })
814 .collect();
815 for t in threads {
816 t.join().expect("thread join");
817 }
818 let snap = counter.snapshot();
819 assert_eq!(snap.count, 400);
820 assert_eq!(snap.total_us, 19800);
822 assert_eq!(snap.max_us, 99);
823 }
824
825 mod proptest_session_metrics {
826 use super::*;
827 use proptest::prelude::*;
828
829 proptest! {
830 #[test]
832 fn timing_counter_sum_and_count(
833 values in prop::collection::vec(0u64..10_000, 0..50)
834 ) {
835 let counter = TimingCounter::new();
836 for &v in &values {
837 counter.record(v);
838 }
839 let snap = counter.snapshot();
840 assert_eq!(snap.count, values.len() as u64);
841 assert_eq!(
842 snap.total_us,
843 values.iter().copied().sum::<u64>()
844 );
845 }
846
847 #[test]
849 fn timing_counter_tracks_max(
850 values in prop::collection::vec(0u64..100_000, 1..50)
851 ) {
852 let counter = TimingCounter::new();
853 for &v in &values {
854 counter.record(v);
855 }
856 let snap = counter.snapshot();
857 assert_eq!(snap.max_us, *values.iter().max().unwrap());
858 }
859
860 #[test]
862 fn timing_snapshot_avg_is_floor_division(
863 values in prop::collection::vec(1u64..10_000, 1..50)
864 ) {
865 let counter = TimingCounter::new();
866 for &v in &values {
867 counter.record(v);
868 }
869 let snap = counter.snapshot();
870 let expected = snap.total_us / snap.count;
871 assert_eq!(snap.avg_us, expected);
872 }
873
874 #[test]
876 fn empty_counter_snapshot(_dummy in 0..1u8) {
877 let counter = TimingCounter::new();
878 let snap = counter.snapshot();
879 assert_eq!(snap.count, 0);
880 assert_eq!(snap.total_us, 0);
881 assert_eq!(snap.max_us, 0);
882 assert_eq!(snap.avg_us, 0);
883 }
884
885 #[test]
887 fn timing_reset_clears(values in prop::collection::vec(1u64..1000, 1..20)) {
888 let counter = TimingCounter::new();
889 for &v in &values {
890 counter.record(v);
891 }
892 counter.reset();
893 let snap = counter.snapshot();
894 assert_eq!(snap.count, 0);
895 assert_eq!(snap.total_us, 0);
896 assert_eq!(snap.max_us, 0);
897 }
898
899 #[test]
901 fn byte_counter_sum_and_count(
902 values in prop::collection::vec(0u64..100_000, 0..50)
903 ) {
904 let counter = ByteCounter::new();
905 for &v in &values {
906 counter.record(v);
907 }
908 let snap = counter.snapshot();
909 assert_eq!(snap.count, values.len() as u64);
910 assert_eq!(snap.total_bytes, values.iter().copied().sum::<u64>());
911 }
912
913 #[test]
915 fn byte_counter_avg(
916 values in prop::collection::vec(1u64..10_000, 1..50)
917 ) {
918 let counter = ByteCounter::new();
919 for &v in &values {
920 counter.record(v);
921 }
922 let snap = counter.snapshot();
923 assert_eq!(snap.avg_bytes, snap.total_bytes / snap.count);
924 }
925
926 #[test]
928 fn byte_counter_reset(values in prop::collection::vec(1u64..1000, 1..10)) {
929 let counter = ByteCounter::new();
930 for &v in &values {
931 counter.record(v);
932 }
933 counter.reset();
934 let snap = counter.snapshot();
935 assert_eq!(snap.count, 0);
936 assert_eq!(snap.total_bytes, 0);
937 }
938
939 #[test]
941 fn timing_display_zero(_dummy in 0..1u8) {
942 let snap = TimingSnapshot {
943 count: 0,
944 total_us: 0,
945 max_us: 0,
946 avg_us: 0,
947 };
948 assert_eq!(format!("{snap}"), "n=0");
949 }
950
951 #[test]
953 fn timing_display_nonzero(
954 count in 1u64..1000,
955 total_us in 1u64..1_000_000,
956 max_us in 1u64..1_000_000
957 ) {
958 let snap = TimingSnapshot {
959 count,
960 total_us,
961 max_us,
962 avg_us: total_us / count,
963 };
964 let display = format!("{snap}");
965 assert!(display.contains(&format!("n={count}")));
966 assert!(display.contains("avg="));
967 assert!(display.contains("max="));
968 assert!(display.contains("total="));
969 }
970
971 #[test]
973 fn byte_display_zero(_dummy in 0..1u8) {
974 let snap = ByteSnapshot {
975 count: 0,
976 total_bytes: 0,
977 avg_bytes: 0,
978 };
979 assert_eq!(format!("{snap}"), "n=0");
980 }
981
982 #[test]
984 fn byte_display_nonzero(
985 count in 1u64..1000,
986 total in 1u64..1_000_000
987 ) {
988 let snap = ByteSnapshot {
989 count,
990 total_bytes: total,
991 avg_bytes: total / count,
992 };
993 let display = format!("{snap}");
994 assert!(display.contains(&format!("n={count}")));
995 assert!(display.contains("avg="));
996 assert!(display.contains("total="));
997 }
998 }
999 }
1000}