Skip to main content

tycho_consensus/models/
stats.rs

1use std::ops::RangeInclusive;
2
3#[derive(Debug, thiserror::Error)]
4pub enum MempoolStatsMergeError {
5    #[error("stat range {:?} cannot include new out-of-order round {}", .0, .1)]
6    RoundOutOfOrder(RangeInclusive<u32>, u32),
7    #[error("stat range {:?} overlaps with {:?}", .0, .1)]
8    OverlappingRanges(RangeInclusive<u32>, RangeInclusive<u32>),
9}
10
11#[derive(Debug)]
12pub struct MempoolPeerStats {
13    first_round: u32,
14    filled_rounds: u32,
15    cumulative: MempoolPeerCounters,
16}
17
18impl MempoolPeerStats {
19    pub fn new(round: u32) -> Self {
20        Self {
21            first_round: round, // zero round cannot exist
22            filled_rounds: 0,
23            cumulative: MempoolPeerCounters::default(), // last round is zero
24        }
25    }
26
27    fn range(&self) -> RangeInclusive<u32> {
28        self.first_round..=self.cumulative.last_round
29    }
30
31    pub fn filled_rounds(&self) -> u32 {
32        self.filled_rounds
33    }
34
35    pub fn counters(&self) -> Option<&MempoolPeerCounters> {
36        (!self.range().is_empty()).then_some(&self.cumulative)
37    }
38
39    pub fn add_in_order(
40        &mut self,
41        stats: &MempoolPeerCounters,
42    ) -> Result<(), MempoolStatsMergeError> {
43        let old_range = self.range();
44        if stats.last_round < *old_range.start() || old_range.contains(&stats.last_round) {
45            return Err(MempoolStatsMergeError::RoundOutOfOrder(
46                old_range,
47                stats.last_round,
48            ));
49        }
50        self.filled_rounds += 1;
51        self.cumulative.merge(stats);
52        Ok(())
53    }
54
55    // unfortunately this is not filled at the same time with the other stats
56    pub fn add_references_skipped(&mut self, value: u32) {
57        self.cumulative.references_skipped += value;
58    }
59
60    pub fn merge_with(&mut self, other: &Self) -> Result<(), MempoolStatsMergeError> {
61        let stat_overlap = if self.first_round < other.first_round {
62            self.range().contains(&other.first_round)
63        } else {
64            other.range().contains(&self.first_round)
65        };
66        if stat_overlap {
67            return Err(MempoolStatsMergeError::OverlappingRanges(
68                self.range(),
69                other.range(),
70            ));
71        }
72        self.first_round = self.first_round.min(other.first_round);
73        self.filled_rounds += other.filled_rounds;
74        self.cumulative.merge(&other.cumulative);
75        Ok(())
76    }
77}
78
79#[derive(Debug, Default)]
80pub struct MempoolPeerCounters {
81    pub last_round: u32,
82    pub was_leader: u32,
83    pub was_not_leader: u32,
84    pub skipped_rounds: u32,
85    pub valid_points: u32,
86    pub equivocated: u32,
87    pub invalid_points: u32,
88    pub ill_formed_points: u32,
89    pub references_skipped: u32,
90}
91
92impl MempoolPeerCounters {
93    fn merge(&mut self, other: &Self) {
94        self.last_round = self.last_round.max(other.last_round);
95        self.was_leader += other.was_leader;
96        self.was_not_leader += other.was_not_leader;
97        self.skipped_rounds += other.skipped_rounds;
98        self.valid_points += other.valid_points;
99        self.equivocated += other.equivocated;
100        self.invalid_points += other.invalid_points;
101        self.ill_formed_points += other.ill_formed_points;
102        self.references_skipped += other.references_skipped;
103    }
104}