1use serde::{Deserialize, Serialize};
16use std::collections::{BTreeMap, HashMap, VecDeque};
17use std::time::Duration;
18use tor_netdir::params::NetParameters;
19
20use super::Action;
21use tor_persist::JsonValue;
22
23const TIME_HISTORY_LEN: usize = 1000;
25
26const SUCCESS_HISTORY_DEFAULT_LEN: usize = 20;
29
30const BUCKET_WIDTH_MSEC: u32 = 10;
32
33#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
38#[serde(transparent)]
39struct MsecDuration(u32);
40
41impl MsecDuration {
42 fn new_saturating(d: &Duration) -> Self {
45 let msec = std::cmp::min(d.as_millis(), u128::from(u32::MAX)) as u32;
46 MsecDuration(msec)
47 }
48}
49
50#[allow(clippy::checked_conversions)]
54mod assertion {
55 const _: () = assert!(super::TIME_HISTORY_LEN <= u16::MAX as usize);
58}
59
60#[derive(Debug, Clone)]
63struct History {
64 time_history: BoundedDeque<MsecDuration>,
70
71 time_histogram: BTreeMap<MsecDuration, u16>,
78
79 success_history: BoundedDeque<bool>,
85}
86
87impl History {
88 fn new_empty() -> Self {
90 History {
91 time_history: BoundedDeque::new(TIME_HISTORY_LEN),
92 time_histogram: BTreeMap::new(),
93 success_history: BoundedDeque::new(SUCCESS_HISTORY_DEFAULT_LEN),
94 }
95 }
96
97 fn clear(&mut self) {
99 self.time_history.clear();
100 self.time_histogram.clear();
101 self.success_history.clear();
102 }
103
104 fn set_success_history_len(&mut self, n: usize) {
107 self.success_history.set_max_len(n);
108 }
109
110 #[cfg(test)]
121 fn set_time_history_len(&mut self, n: usize) {
122 self.time_history.set_max_len(n);
123 }
124
125 fn from_sparse_histogram<I>(iter: I) -> Self
135 where
136 I: Iterator<Item = (MsecDuration, u16)>,
137 {
138 use rand::seq::{IteratorRandom, SliceRandom};
139 let mut rng = rand::rng();
140
141 let mut observations = iter
145 .take(TIME_HISTORY_LEN) .flat_map(|(dur, n)| std::iter::repeat_n(dur, n as usize))
147 .sample(&mut rng, TIME_HISTORY_LEN);
148 observations.shuffle(&mut rng);
150
151 let mut result = History::new_empty();
152 for obs in observations {
153 result.add_time(obs);
154 }
155
156 result
157 }
158
159 fn sparse_histogram(&self) -> impl Iterator<Item = (MsecDuration, u16)> + '_ {
168 self.time_histogram.iter().map(|(d, n)| (*d, *n))
169 }
170
171 fn bucket_center(time: MsecDuration) -> MsecDuration {
173 let idx = time.0 / BUCKET_WIDTH_MSEC;
174 let msec = (idx * BUCKET_WIDTH_MSEC) + (BUCKET_WIDTH_MSEC) / 2;
175 MsecDuration(msec)
176 }
177
178 fn inc_bucket(&mut self, time: MsecDuration) {
180 let center = History::bucket_center(time);
181 *self.time_histogram.entry(center).or_insert(0) += 1;
182 }
183
184 fn dec_bucket(&mut self, time: MsecDuration) {
187 use std::collections::btree_map::Entry;
188 let center = History::bucket_center(time);
189 match self.time_histogram.entry(center) {
190 Entry::Vacant(_) => {
191 }
193 Entry::Occupied(e) if e.get() <= &1 => {
194 e.remove();
195 }
196 Entry::Occupied(mut e) => {
197 *e.get_mut() -= 1;
198 }
199 }
200 }
201
202 fn add_time(&mut self, time: MsecDuration) {
205 match self.time_history.push_back(time) {
206 None => {}
207 Some(removed_time) => {
208 self.dec_bucket(removed_time);
211 }
212 }
213 self.inc_bucket(time);
214 }
215
216 fn n_times(&self) -> usize {
220 self.time_history.len()
221 }
222
223 fn add_success(&mut self, succeeded: bool) {
226 self.success_history.push_back(succeeded);
227 }
228
229 fn n_recent_timeouts(&self) -> usize {
231 self.success_history.iter().filter(|x| !**x).count()
232 }
233
234 fn n_most_frequent_bins(&self, n: usize) -> Vec<(MsecDuration, u16)> {
236 use itertools::Itertools;
237 use std::cmp::Reverse;
240
241 self.sparse_histogram()
247 .map(|(center, count)| (Reverse(count), center))
248 .k_smallest(n)
250 .map(|(Reverse(count), center)| (center, count))
251 .collect()
252 }
253
254 fn estimate_xm(&self, n_modes: usize) -> Option<u32> {
264 let bins = self.n_most_frequent_bins(n_modes);
271 let n_observations: u16 = bins.iter().map(|(_, n)| n).sum();
273 let total_observations: u64 = bins
275 .iter()
276 .map(|(d, n)| u64::from(d.0 * u32::from(*n)))
277 .sum();
278
279 if n_observations == 0 {
280 None
281 } else {
282 Some((total_observations / u64::from(n_observations)) as u32)
283 }
284 }
285
286 fn pareto_estimate(&self, n_modes: usize) -> Option<ParetoDist> {
292 let xm = self.estimate_xm(n_modes)?;
293
294 let n = self.time_history.len();
298 let sum_of_log_observations: f64 = self
299 .time_history
300 .iter()
301 .map(|m| f64::from(std::cmp::max(m.0, xm)).ln())
302 .sum();
303 let sum_of_log_xm = (n as f64) * f64::from(xm).ln();
304
305 let inv_alpha = (sum_of_log_observations - sum_of_log_xm) / (n as f64);
309
310 Some(ParetoDist {
311 x_m: f64::from(xm),
312 inv_alpha,
313 })
314 }
315}
316
317#[derive(Debug)]
321struct ParetoDist {
322 x_m: f64,
324 inv_alpha: f64,
328}
329
330impl ParetoDist {
331 fn quantile(&self, q: f64) -> f64 {
339 let q = q.clamp(0.0, 1.0);
340 self.x_m / ((1.0 - q).powf(self.inv_alpha))
341 }
342}
343
344#[derive(Clone, Debug)]
348pub(crate) struct Params {
349 use_estimates: bool,
353 min_observations: u16,
356 significant_hop: u8,
359 timeout_quantile: f64,
366 abandon_quantile: f64,
373 default_thresholds: (Duration, Duration),
376 n_modes_for_xm: usize,
380 success_history_len: usize,
382 reset_after_timeouts: usize,
386 min_timeout: Duration,
388}
389
390impl Default for Params {
391 fn default() -> Self {
392 Params {
393 use_estimates: true,
394 min_observations: 100,
395 significant_hop: 2,
396 timeout_quantile: 0.80,
397 abandon_quantile: 0.99,
398 default_thresholds: (Duration::from_secs(60), Duration::from_secs(60)),
399 n_modes_for_xm: 10,
400 success_history_len: SUCCESS_HISTORY_DEFAULT_LEN,
401 reset_after_timeouts: 18,
402 min_timeout: Duration::from_millis(10),
403 }
404 }
405}
406
407impl From<&NetParameters> for Params {
408 fn from(p: &NetParameters) -> Params {
409 let timeout = p
414 .cbt_initial_timeout
415 .try_into()
416 .unwrap_or_else(|_| Duration::from_secs(60));
417 let learning_disabled: bool = p.cbt_learning_disabled.into();
418 Params {
419 use_estimates: !learning_disabled,
420 min_observations: p.cbt_min_circs_for_estimate.get() as u16,
421 significant_hop: 2,
422 timeout_quantile: p.cbt_timeout_quantile.as_fraction(),
423 abandon_quantile: p.cbt_abandon_quantile.as_fraction(),
424 default_thresholds: (timeout, timeout),
425 n_modes_for_xm: p.cbt_num_xm_modes.get() as usize,
426 success_history_len: p.cbt_success_count.get() as usize,
427 reset_after_timeouts: p.cbt_max_timeouts.get() as usize,
428 min_timeout: p
429 .cbt_min_timeout
430 .try_into()
431 .unwrap_or_else(|_| Duration::from_millis(10)),
432 }
433 }
434}
435
436pub(crate) struct ParetoTimeoutEstimator {
445 history: History,
448
449 timeouts: Option<(Duration, Duration)>,
454
455 fallback_timeouts: (Duration, Duration),
461
462 p: Params,
465}
466
467impl Default for ParetoTimeoutEstimator {
468 fn default() -> Self {
469 Self::from_history(History::new_empty())
470 }
471}
472
473#[derive(Clone, Debug, Serialize, Deserialize, Default)]
475#[serde(default)]
476pub(crate) struct ParetoTimeoutState {
477 #[allow(dead_code)]
479 version: usize,
480 histogram: Vec<(MsecDuration, u16)>,
482 current_timeout: Option<MsecDuration>,
484
485 #[serde(flatten)]
488 unknown_fields: HashMap<String, JsonValue>,
489}
490
491impl ParetoTimeoutState {
492 pub(crate) fn latest_estimate(&self) -> Option<Duration> {
494 self.current_timeout
495 .map(|m| Duration::from_millis(m.0.into()))
496 }
497}
498
499impl ParetoTimeoutEstimator {
500 fn from_history(history: History) -> Self {
503 let p = Params::default();
504 ParetoTimeoutEstimator {
505 history,
506 timeouts: None,
507 fallback_timeouts: p.default_thresholds,
508 p,
509 }
510 }
511
512 pub(crate) fn from_state(state: ParetoTimeoutState) -> Self {
515 let history = History::from_sparse_histogram(state.histogram.into_iter());
516 Self::from_history(history)
517 }
518
519 fn base_timeouts(&mut self) -> (Duration, Duration) {
525 if let Some(x) = self.timeouts {
526 return x;
528 }
529
530 if self.history.n_times() < self.p.min_observations as usize {
531 return self.fallback_timeouts;
533 }
534
535 let dist = match self.history.pareto_estimate(self.p.n_modes_for_xm) {
538 Some(dist) => dist,
539 None => {
540 return self.fallback_timeouts;
541 }
542 };
543 let timeout_threshold = dist.quantile(self.p.timeout_quantile);
544 let abandon_threshold = dist
545 .quantile(self.p.abandon_quantile)
546 .max(timeout_threshold);
547
548 let timeouts = (
549 Duration::from_secs_f64(timeout_threshold / 1000.0).max(self.p.min_timeout),
550 Duration::from_secs_f64(abandon_threshold / 1000.0).max(self.p.min_timeout),
551 );
552 self.timeouts = Some(timeouts);
553
554 timeouts
555 }
556}
557
558impl super::TimeoutEstimator for ParetoTimeoutEstimator {
559 fn update_params(&mut self, p: &NetParameters) {
560 let parameters = p.into();
561 self.p = parameters;
562 let new_success_len = self.p.success_history_len;
563 self.history.set_success_history_len(new_success_len);
564 }
565
566 fn note_hop_completed(&mut self, hop: u8, delay: Duration, is_last: bool) {
567 if hop == self.p.significant_hop {
568 let time = MsecDuration::new_saturating(&delay);
569 self.history.add_time(time);
570 self.timeouts.take();
571 }
572 if is_last {
573 tracing::trace!(%hop, ?delay, "Circuit creation success");
574 self.history.add_success(true);
575 }
576 }
577
578 fn note_circ_timeout(&mut self, hop: u8, delay: Duration) {
579 let have_seen_recent_activity =
582 if let Some(last_traffic) = tor_proto::time_since_last_incoming_traffic() {
583 last_traffic < delay
584 } else {
585 true
587 };
588
589 tracing::trace!(%hop, ?delay, %have_seen_recent_activity, "Circuit timeout");
590
591 if hop > 0 && have_seen_recent_activity {
592 self.history.add_success(false);
593 if self.history.n_recent_timeouts() > self.p.reset_after_timeouts {
594 tracing::debug!("Multiple connections failed, resetting timeouts...");
595 let base_timeouts = self.base_timeouts();
596 self.history.clear();
597 self.timeouts.take();
598 if base_timeouts.0 >= self.fallback_timeouts.0 {
602 const MAX_FALLBACK_TIMEOUT: Duration = Duration::from_secs(7200);
608 self.fallback_timeouts.0 =
609 (self.fallback_timeouts.0 * 2).min(MAX_FALLBACK_TIMEOUT);
610 self.fallback_timeouts.1 =
611 (self.fallback_timeouts.1 * 2).min(MAX_FALLBACK_TIMEOUT);
612 }
613 }
614 }
615 }
616
617 fn timeouts(&mut self, action: &Action) -> (Duration, Duration) {
618 let (base_t, base_a) = if self.p.use_estimates {
619 self.base_timeouts()
620 } else {
621 return self.p.default_thresholds;
624 };
625
626 let reference_action = Action::BuildCircuit {
627 length: self.p.significant_hop as usize + 1,
628 };
629 debug_assert!(reference_action.timeout_scale() > 0);
630
631 let multiplier =
632 (action.timeout_scale() as f64) / (reference_action.timeout_scale() as f64);
633
634 use super::mul_duration_f64_saturating as mul;
638 (mul(base_t, multiplier), mul(base_a, multiplier))
639 }
640
641 fn learning_timeouts(&self) -> bool {
642 self.p.use_estimates && self.history.n_times() < usize::from(self.p.min_observations)
643 }
644
645 fn build_state(&mut self) -> Option<ParetoTimeoutState> {
646 let cur_timeout = MsecDuration::new_saturating(&self.base_timeouts().0);
647 Some(ParetoTimeoutState {
648 version: 1,
649 histogram: self.history.sparse_histogram().collect(),
650 current_timeout: Some(cur_timeout),
651 unknown_fields: Default::default(),
652 })
653 }
654}
655
656#[derive(Clone, Debug)]
658struct BoundedDeque<T> {
659 inner: VecDeque<T>,
664
665 limit: usize,
667}
668impl<T> BoundedDeque<T> {
669 fn new(limit: usize) -> Self {
671 Self {
672 inner: VecDeque::with_capacity(limit),
673 limit,
674 }
675 }
676
677 fn clear(&mut self) {
679 self.inner.clear();
680 }
681
682 fn len(&self) -> usize {
684 self.inner.len()
685 }
686
687 fn push_back(&mut self, item: T) -> Option<T> {
691 if self.limit == 0 {
692 return None;
693 }
694 let removed = if self.len() == self.limit {
695 self.inner.pop_front()
696 } else {
697 None
698 };
699 self.inner.push_back(item);
700 removed
701 }
702
703 fn iter(&self) -> impl Iterator<Item = &T> {
705 self.inner.iter()
706 }
707
708 fn set_max_len(&mut self, new_limit: usize) {
715 if new_limit < self.limit {
716 let n_to_drain = self.inner.len().saturating_sub(new_limit);
717 self.inner.drain(0..n_to_drain);
718 self.inner.shrink_to_fit();
719 }
720 self.limit = new_limit;
721 }
722}
723
724#[cfg(test)]
725mod test {
726 #![allow(clippy::bool_assert_comparison)]
728 #![allow(clippy::clone_on_copy)]
729 #![allow(clippy::dbg_macro)]
730 #![allow(clippy::mixed_attributes_style)]
731 #![allow(clippy::print_stderr)]
732 #![allow(clippy::print_stdout)]
733 #![allow(clippy::single_char_pattern)]
734 #![allow(clippy::unwrap_used)]
735 #![allow(clippy::unchecked_time_subtraction)]
736 #![allow(clippy::useless_vec)]
737 #![allow(clippy::needless_pass_by_value)]
738 #![allow(clippy::string_slice)] use super::*;
741 use crate::timeouts::TimeoutEstimator;
742 use tor_basic_utils::RngExt as _;
743 use tor_basic_utils::test_rng::testing_rng;
744
745 fn b3() -> Action {
747 Action::BuildCircuit { length: 3 }
748 }
749
750 impl From<u32> for MsecDuration {
751 fn from(v: u32) -> Self {
752 Self(v)
753 }
754 }
755
756 #[test]
757 fn ms_partial_cmp() {
758 #![allow(clippy::eq_op)]
759 let myriad: MsecDuration = 10_000.into();
760 let lakh: MsecDuration = 100_000.into();
761 let crore: MsecDuration = 10_000_000.into();
762
763 assert!(myriad < lakh);
764 assert!(myriad == myriad);
765 assert!(crore > lakh);
766 assert!(crore >= crore);
767 assert!(crore <= crore);
768 }
769
770 #[test]
771 fn history_lowlev() {
772 assert_eq!(History::bucket_center(1.into()), 5.into());
773 assert_eq!(History::bucket_center(903.into()), 905.into());
774 assert_eq!(History::bucket_center(0.into()), 5.into());
775 assert_eq!(History::bucket_center(u32::MAX.into()), 4294967295.into());
776
777 let mut h = History::new_empty();
778 h.inc_bucket(7.into());
779 h.inc_bucket(8.into());
780 h.inc_bucket(9.into());
781 h.inc_bucket(10.into());
782 h.inc_bucket(11.into());
783 h.inc_bucket(12.into());
784 h.inc_bucket(13.into());
785 h.inc_bucket(299.into());
786 assert_eq!(h.time_histogram.get(&5.into()), Some(&3));
787 assert_eq!(h.time_histogram.get(&15.into()), Some(&4));
788 assert_eq!(h.time_histogram.get(&25.into()), None);
789 assert_eq!(h.time_histogram.get(&295.into()), Some(&1));
790
791 h.dec_bucket(299.into());
792 h.dec_bucket(24.into());
793 h.dec_bucket(12.into());
794
795 assert_eq!(h.time_histogram.get(&15.into()), Some(&3));
796 assert_eq!(h.time_histogram.get(&25.into()), None);
797 assert_eq!(h.time_histogram.get(&295.into()), None);
798
799 h.add_success(true);
800 h.add_success(false);
801 assert_eq!(h.success_history.len(), 2);
802
803 h.clear();
804 assert_eq!(h.time_histogram.len(), 0);
805 assert_eq!(h.time_history.len(), 0);
806 assert_eq!(h.success_history.len(), 0);
807 }
808
809 #[test]
810 fn time_observation_management() {
811 let mut h = History::new_empty();
812 h.set_time_history_len(8); h.add_time(300.into());
815 h.add_time(500.into());
816 h.add_time(542.into());
817 h.add_time(305.into());
818 h.add_time(543.into());
819 h.add_time(307.into());
820
821 assert_eq!(h.n_times(), 6);
822 let v = h.n_most_frequent_bins(10);
823 assert_eq!(&v[..], [(305.into(), 3), (545.into(), 2), (505.into(), 1)]);
824 let v = h.n_most_frequent_bins(2);
825 assert_eq!(&v[..], [(305.into(), 3), (545.into(), 2)]);
826
827 let v: Vec<_> = h.sparse_histogram().collect();
828 assert_eq!(&v[..], [(305.into(), 3), (505.into(), 1), (545.into(), 2)]);
829
830 h.add_time(212.into());
831 h.add_time(203.into());
832 h.add_time(617.into());
834 h.add_time(413.into());
835
836 assert_eq!(h.n_times(), 8);
837
838 let v: Vec<_> = h.sparse_histogram().collect();
839 assert_eq!(
840 &v[..],
841 [
842 (205.into(), 1),
843 (215.into(), 1),
844 (305.into(), 2),
845 (415.into(), 1),
846 (545.into(), 2),
847 (615.into(), 1)
848 ]
849 );
850
851 let h2 = History::from_sparse_histogram(v.clone().into_iter());
852 let v2: Vec<_> = h2.sparse_histogram().collect();
853 assert_eq!(v, v2);
854 }
855
856 #[test]
857 fn success_observation_mechanism() {
858 let mut h = History::new_empty();
859 h.set_success_history_len(20);
860
861 assert_eq!(h.n_recent_timeouts(), 0);
862 h.add_success(true);
863 assert_eq!(h.n_recent_timeouts(), 0);
864 h.add_success(false);
865 assert_eq!(h.n_recent_timeouts(), 1);
866 for _ in 0..200 {
867 h.add_success(false);
868 }
869 assert_eq!(h.n_recent_timeouts(), 20);
870 h.add_success(true);
871 h.add_success(true);
872 h.add_success(true);
873 assert_eq!(h.n_recent_timeouts(), 20 - 3);
874
875 h.set_success_history_len(10);
876 assert_eq!(h.n_recent_timeouts(), 10 - 3);
877 }
878
879 #[test]
880 fn xm_calculation() {
881 let mut h = History::new_empty();
882 assert_eq!(h.estimate_xm(2), None);
883
884 for n in &[300, 500, 542, 305, 543, 307, 212, 203, 617, 413] {
885 h.add_time(MsecDuration(*n));
886 }
887
888 let v = h.n_most_frequent_bins(2);
889 assert_eq!(&v[..], [(305.into(), 3), (545.into(), 2)]);
890 let est = (305 * 3 + 545 * 2) / 5;
891 assert_eq!(h.estimate_xm(2), Some(est));
892 assert_eq!(est, 401);
893 }
894
895 #[test]
896 fn pareto_estimate() {
897 let mut h = History::new_empty();
898 assert!(h.pareto_estimate(2).is_none());
899
900 for n in &[300, 500, 542, 305, 543, 307, 212, 203, 617, 413] {
901 h.add_time(MsecDuration(*n));
902 }
903 let expected_log_sum: f64 = [401, 500, 542, 401, 543, 401, 401, 401, 617, 413]
904 .iter()
905 .map(|x| f64::from(*x).ln())
906 .sum();
907 let expected_log_xm: f64 = (401_f64).ln() * 10.0;
908 let expected_alpha = 10.0 / (expected_log_sum - expected_log_xm);
909 let expected_inv_alpha = 1.0 / expected_alpha;
910
911 let p = h.pareto_estimate(2).unwrap();
912
913 assert!((401.0 - p.x_m).abs() < 1.0e-9);
915 assert!((expected_inv_alpha - p.inv_alpha).abs() < 1.0e-9);
916
917 let q60 = p.quantile(0.60);
918 let q99 = p.quantile(0.99);
919
920 assert!((q60 - 451.127) < 0.001);
921 assert!((q99 - 724.841) < 0.001);
922 }
923
924 #[test]
925 fn pareto_estimate_timeout() {
926 let mut est = ParetoTimeoutEstimator::default();
927
928 assert_eq!(
929 est.timeouts(&b3()),
930 (Duration::from_secs(60), Duration::from_secs(60))
931 );
932 est.p.min_observations = 0;
935 est.p.n_modes_for_xm = 2;
936 assert_eq!(
937 est.timeouts(&b3()),
938 (Duration::from_secs(60), Duration::from_secs(60))
939 );
940
941 for msec in &[300, 500, 542, 305, 543, 307, 212, 203, 617, 413] {
942 let d = Duration::from_millis(*msec);
943 est.note_hop_completed(2, d, true);
944 }
945
946 let t = est.timeouts(&b3());
947 assert_eq!(t.0.as_micros(), 493_169);
948 assert_eq!(t.1.as_micros(), 724_841);
949
950 let t2 = est.timeouts(&b3());
951 assert_eq!(t2, t);
952
953 let t2 = est.timeouts(&Action::BuildCircuit { length: 4 });
954 assert_eq!(t2.0, t.0.mul_f64(10.0 / 6.0));
955 assert_eq!(t2.1, t.1.mul_f64(10.0 / 6.0));
956 }
957
958 #[test]
959 fn pareto_estimate_clear() {
960 let mut est = ParetoTimeoutEstimator::default();
961
962 let params = NetParameters::from_map(&"cbtmincircs=1 cbtnummodes=2".parse().unwrap());
965 est.update_params(¶ms);
966
967 assert_eq!(est.timeouts(&b3()).0.as_micros(), 60_000_000);
968 assert!(est.learning_timeouts());
969
970 for msec in &[300, 500, 542, 305, 543, 307, 212, 203, 617, 413] {
971 let d = Duration::from_millis(*msec);
972 est.note_hop_completed(2, d, true);
973 }
974 assert_ne!(est.timeouts(&b3()).0.as_micros(), 60_000_000);
975 assert!(!est.learning_timeouts());
976 assert_eq!(est.history.n_recent_timeouts(), 0);
977
978 for _ in 0..18 {
980 est.note_circ_timeout(2, Duration::from_secs(2000));
981 }
982 assert_ne!(est.timeouts(&b3()).0.as_micros(), 60_000_000);
983
984 est.note_circ_timeout(2, Duration::from_secs(2000));
986 assert_eq!(est.timeouts(&b3()).0.as_micros(), 60_000_000);
987
988 for _ in 0..20 {
990 est.note_circ_timeout(2, Duration::from_secs(2000));
991 }
992 assert_eq!(est.timeouts(&b3()).0.as_micros(), 120_000_000);
993 }
994
995 #[test]
996 fn default_params() {
997 let p1 = Params::default();
998 let p2 = Params::from(&tor_netdir::params::NetParameters::default());
999 assert_eq!(format!("{:?}", p1), format!("{:?}", p2));
1001 }
1002
1003 #[test]
1004 fn state_conversion() {
1005 let mut est = ParetoTimeoutEstimator::default();
1010 let mut rng = testing_rng();
1011 for _ in 0..1000 {
1012 let d = Duration::from_millis(rng.gen_range_checked(10..3_000).unwrap());
1013 est.note_hop_completed(2, d, true);
1014 }
1015
1016 let state = est.build_state().unwrap();
1017 assert_eq!(state.version, 1);
1018 assert!(state.current_timeout.is_some());
1019
1020 let mut est2 = ParetoTimeoutEstimator::from_state(state);
1021 let act = Action::BuildCircuit { length: 3 };
1022 let ms1 = est.timeouts(&act).0.as_millis() as i32;
1025 let ms2 = est2.timeouts(&act).0.as_millis() as i32;
1026 assert!((ms1 - ms2).abs() < 50);
1027 }
1028
1029 #[test]
1030 fn validate_iterator_sample() {
1031 use rand::seq::IteratorRandom as _;
1036 let mut rng = testing_rng();
1037 let mut ten_elements = (1..=10).sample(&mut rng, 100);
1038 ten_elements.sort();
1039 assert_eq!(ten_elements.len(), 10);
1040 assert_eq!(ten_elements, (1..=10).collect::<Vec<_>>());
1041 }
1042
1043 }