Skip to main content

sui_core/
consensus_throughput_calculator.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use arc_swap::ArcSwap;
5use parking_lot::Mutex;
6use std::collections::{BTreeMap, VecDeque};
7use std::num::NonZeroU64;
8use std::sync::Arc;
9use sui_protocol_config::Chain;
10use sui_types::digests::ChainIdentifier;
11use sui_types::messages_consensus::TimestampMs;
12use tracing::{debug, warn};
13
14use crate::authority::AuthorityMetrics;
15
16const DEFAULT_OBSERVATIONS_WINDOW: u64 = 120; // number of observations to use to calculate the past throughput
17const DEFAULT_THROUGHPUT_PROFILE_UPDATE_INTERVAL_SECS: u64 = 60; // seconds that need to pass between two consecutive throughput profile updates
18const DEFAULT_THROUGHPUT_PROFILE_COOL_DOWN_THRESHOLD: u64 = 10; // 10% of throughput
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)]
21pub struct ThroughputProfile {
22    pub level: Level,
23    /// The lower range of the throughput that this profile is referring to. For example, if
24    /// `throughput = 1_000`, then for values >= 1_000 this throughput profile applies.
25    pub throughput: u64,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)]
29pub enum Level {
30    Low,
31    Medium,
32    High,
33}
34
35impl From<usize> for Level {
36    fn from(value: usize) -> Self {
37        if value == 0 {
38            Level::Low
39        } else if value == 1 {
40            Level::Medium
41        } else {
42            Level::High
43        }
44    }
45}
46
47impl From<Level> for usize {
48    fn from(value: Level) -> Self {
49        match value {
50            Level::Low => 0,
51            Level::Medium => 1,
52            Level::High => 2,
53        }
54    }
55}
56
57#[derive(Debug)]
58pub struct ThroughputProfileRanges {
59    /// Holds the throughput profiles by the throughput range (upper_throughput, cool_down_threshold)
60    profiles: BTreeMap<u64, ThroughputProfile>,
61}
62
63impl ThroughputProfileRanges {
64    pub fn from_chain(chain_id: ChainIdentifier) -> ThroughputProfileRanges {
65        let to_profiles = |medium: u64, high: u64| -> Vec<ThroughputProfile> {
66            vec![
67                ThroughputProfile {
68                    level: Level::Low,
69                    throughput: 0,
70                },
71                ThroughputProfile {
72                    level: Level::Medium,
73                    throughput: medium,
74                },
75                ThroughputProfile {
76                    level: Level::High,
77                    throughput: high,
78                },
79            ]
80        };
81
82        match chain_id.chain() {
83            Chain::Mainnet => ThroughputProfileRanges::new(&to_profiles(500, 2_000)),
84            Chain::Testnet => ThroughputProfileRanges::new(&to_profiles(500, 2_000)),
85            Chain::Unknown => ThroughputProfileRanges::new(&to_profiles(1_000, 2_000)),
86        }
87    }
88
89    pub fn new(profiles: &[ThroughputProfile]) -> Self {
90        let mut p: BTreeMap<u64, ThroughputProfile> = BTreeMap::new();
91
92        for profile in profiles {
93            assert!(
94                !p.iter().any(|(_, pr)| pr.level == profile.level),
95                "Attempted to insert profile with same level"
96            );
97            assert!(
98                p.insert(profile.throughput, *profile).is_none(),
99                "Attempted to insert profile with same throughput"
100            );
101        }
102
103        // By default the Low profile should exist with throughput 0
104        assert_eq!(
105            *p.get(&0).unwrap(),
106            ThroughputProfile {
107                level: Level::Low,
108                throughput: 0
109            }
110        );
111
112        Self { profiles: p }
113    }
114
115    pub fn lowest_profile(&self) -> ThroughputProfile {
116        *self
117            .profiles
118            .first_key_value()
119            .expect("Should contain at least one throughput profile")
120            .1
121    }
122
123    pub fn highest_profile(&self) -> ThroughputProfile {
124        *self
125            .profiles
126            .last_key_value()
127            .expect("Should contain at least one throughput profile")
128            .1
129    }
130    /// Resolves the throughput profile that corresponds to the provided throughput.
131    pub fn resolve(&self, current_throughput: u64) -> ThroughputProfile {
132        let mut iter = self.profiles.iter();
133        while let Some((threshold, profile)) = iter.next_back() {
134            if current_throughput >= *threshold {
135                return *profile;
136            }
137        }
138
139        warn!(
140            "Could not resolve throughput profile for throughput {} - we shouldn't end up here. Fallback to lowest profile as default.",
141            current_throughput
142        );
143
144        // If not found, then we should return the lowest possible profile as default to stay on safe side.
145        self.highest_profile()
146    }
147}
148
149impl Default for ThroughputProfileRanges {
150    fn default() -> Self {
151        let profiles = vec![
152            ThroughputProfile {
153                level: Level::Low,
154                throughput: 0,
155            },
156            ThroughputProfile {
157                level: Level::High,
158                throughput: 2_000,
159            },
160        ];
161        ThroughputProfileRanges::new(&profiles)
162    }
163}
164
165pub type TimestampSecs = u64;
166
167#[derive(Debug, Copy, Clone)]
168pub struct ThroughputProfileEntry {
169    /// The throughput profile
170    profile: ThroughputProfile,
171    /// The time when this throughput profile was created
172    timestamp: TimestampSecs,
173    /// The calculated throughput when this profile created
174    throughput: u64,
175}
176
177#[derive(Default)]
178struct ConsensusThroughputCalculatorInner {
179    observations: VecDeque<(TimestampSecs, u64)>,
180    total_transactions: u64,
181    /// The last timestamp that we considered as oldest to calculate the throughput over the observations window.
182    last_oldest_timestamp: Option<TimestampSecs>,
183}
184
185/// The ConsensusThroughputProfiler is responsible for assigning the right throughput profile by polling
186/// the measured consensus throughput. It is important to rely on the ConsensusThroughputCalculator to measure
187/// throughput as we need to make sure that validators will see an as possible consistent view to assign
188/// the right profile.
189pub struct ConsensusThroughputProfiler {
190    /// The throughput profile will be eligible for update every `throughput_profile_update_interval` seconds.
191    /// A bucketing approach is followed where the throughput timestamp is used in order to calculate on which
192    /// seconds bucket is assigned to. When we detect a change on that bucket then an update is triggered (if a different
193    /// profile is calculated). That allows validators to align on the update timing and ensure they will eventually
194    /// converge as the consensus timestamps are used.
195    throughput_profile_update_interval: TimestampSecs,
196    /// When current calculated throughput (A) is lower than previous, and the assessed profile is now a lower than previous,
197    /// we'll change to the lower profile only when (A) <= (previous_profile.throughput) * (100 - throughput_profile_cool_down_threshold) / 100.
198    /// Otherwise we'll stick to the previous profile. We want to do that to avoid any jittery behaviour that alternates between two profiles.
199    throughput_profile_cool_down_threshold: u64,
200    /// The profile ranges to use to profile the throughput
201    profile_ranges: ThroughputProfileRanges,
202    /// The most recently calculated throughput profile
203    last_throughput_profile: ArcSwap<ThroughputProfileEntry>,
204    metrics: Arc<AuthorityMetrics>,
205    /// The throughput calculator to use to derive the current throughput.
206    calculator: Arc<ConsensusThroughputCalculator>,
207}
208
209impl ConsensusThroughputProfiler {
210    pub fn new(
211        calculator: Arc<ConsensusThroughputCalculator>,
212        throughput_profile_update_interval: Option<TimestampSecs>,
213        throughput_profile_cool_down_threshold: Option<u64>,
214        metrics: Arc<AuthorityMetrics>,
215        profile_ranges: ThroughputProfileRanges,
216    ) -> Self {
217        let throughput_profile_update_interval = throughput_profile_update_interval
218            .unwrap_or(DEFAULT_THROUGHPUT_PROFILE_UPDATE_INTERVAL_SECS);
219        let throughput_profile_cool_down_threshold = throughput_profile_cool_down_threshold
220            .unwrap_or(DEFAULT_THROUGHPUT_PROFILE_COOL_DOWN_THRESHOLD);
221
222        assert!(
223            throughput_profile_update_interval > 0,
224            "throughput_profile_update_interval should be >= 0"
225        );
226
227        assert!(
228            (0..=30).contains(&throughput_profile_cool_down_threshold),
229            "Out of bounds provided cool down threshold offset"
230        );
231
232        debug!("Profile ranges used: {:?}", profile_ranges);
233
234        Self {
235            throughput_profile_update_interval,
236            throughput_profile_cool_down_threshold,
237            last_throughput_profile: ArcSwap::from_pointee(ThroughputProfileEntry {
238                profile: profile_ranges.highest_profile(),
239                timestamp: 0,
240                throughput: 0,
241            }), // assume high throughput so the node is more conservative on bootstrap
242            profile_ranges,
243            metrics,
244            calculator,
245        }
246    }
247
248    // Return the current throughput level and the corresponding throughput when this was last updated.
249    // If that is not set yet then as default the High profile is returned and the throughput will be None.
250    pub fn throughput_level(&self) -> (Level, u64) {
251        // Update throughput profile if necessary time has passed
252        let (throughput, timestamp) = self.calculator.current_throughput();
253        let profile = self.update_and_fetch_throughput_profile(throughput, timestamp);
254
255        (profile.profile.level, profile.throughput)
256    }
257
258    // Calculate and update the throughput profile based on the provided throughput. The throughput profile
259    // will only get updated when a different value has been calculated. For example, if the
260    // `last_throughput_profile` is `Low` , and again we calculate it as `Low` based on input, then we'll
261    // not update the profile or the timestamp. We do care to perform updates only when profiles differ.
262    // To ensure that we are protected against throughput profile change fluctuations, we update a
263    // throughput profile every `throughput_profile_update_interval` seconds based on the provided unix timestamps.
264    // The last throughput profile entry is returned.
265    fn update_and_fetch_throughput_profile(
266        &self,
267        throughput: u64,
268        timestamp: TimestampSecs,
269    ) -> ThroughputProfileEntry {
270        let last_profile = self.last_throughput_profile.load();
271
272        // Skip any processing if provided timestamp is older than the last used one. Also return existing
273        // profile when provided timestamp is 0 - this avoids triggering an immediate update eventually overriding
274        // the default value.
275        if timestamp == 0 || timestamp < last_profile.timestamp {
276            return **last_profile;
277        }
278
279        let profile = self.profile_ranges.resolve(throughput);
280
281        let current_seconds_bucket = timestamp / self.throughput_profile_update_interval;
282        let last_profile_seconds_bucket =
283            last_profile.timestamp / self.throughput_profile_update_interval;
284
285        // Update only when we minimum time has been passed since last update.
286        // We allow the edge case to update on the same bucket when a different profile has been
287        // computed for the exact same timestamp.
288        let should_update_profile = if current_seconds_bucket > last_profile_seconds_bucket
289            || (profile != last_profile.profile && last_profile.timestamp == timestamp)
290        {
291            if profile < last_profile.profile {
292                // If new profile is smaller than previous one, then make sure the cool down threshold is respected.
293                let min_throughput = last_profile
294                    .profile
295                    .throughput
296                    .saturating_mul(100 - self.throughput_profile_cool_down_threshold)
297                    / 100;
298                throughput <= min_throughput
299            } else {
300                true
301            }
302        } else {
303            false
304        };
305
306        if should_update_profile {
307            let p = ThroughputProfileEntry {
308                profile,
309                timestamp,
310                throughput,
311            };
312            debug!("Updating throughput profile to {:?}", p);
313            self.last_throughput_profile.store(Arc::new(p));
314
315            self.metrics
316                .consensus_calculated_throughput_profile
317                .set(usize::from(profile.level) as i64);
318
319            p
320        } else {
321            **last_profile
322        }
323    }
324}
325
326/// ConsensusThroughputCalculator is calculating the transaction throughput as this is coming out from
327/// consensus. The throughput is calculated using a sliding window approach and leveraging the timestamps
328/// provided by consensus.
329pub struct ConsensusThroughputCalculator {
330    /// The number of transaction throughput observations that should be stored within the observations
331    /// vector in the ConsensusThroughputCalculatorInner. Those observations will be used to calculate
332    /// the current transactions throughput. We want to select a number that give us enough observations
333    /// so we better calculate the throughput and protected against spikes. A large enough value though
334    /// will make us less reactive to throughput changes.
335    observations_window: u64,
336    inner: Mutex<ConsensusThroughputCalculatorInner>,
337    current_throughput: ArcSwap<(u64, TimestampSecs)>,
338    metrics: Arc<AuthorityMetrics>,
339}
340
341impl ConsensusThroughputCalculator {
342    pub fn new(observations_window: Option<NonZeroU64>, metrics: Arc<AuthorityMetrics>) -> Self {
343        let observations_window = observations_window
344            .unwrap_or(NonZeroU64::new(DEFAULT_OBSERVATIONS_WINDOW).unwrap())
345            .get();
346
347        Self {
348            observations_window,
349            inner: Mutex::new(ConsensusThroughputCalculatorInner::default()),
350            current_throughput: ArcSwap::from_pointee((0, 0)),
351            metrics,
352        }
353    }
354
355    // Adds an observation of the number of transactions that have been sequenced after deduplication
356    // and the corresponding leader timestamp. The observation timestamps should be monotonically
357    // incremented otherwise observation will be ignored.
358    pub fn add_transactions(&self, timestamp_ms: TimestampMs, num_of_transactions: u64) {
359        let mut inner = self.inner.lock();
360        let timestamp_secs: TimestampSecs = timestamp_ms / 1_000; // lowest bucket we care is seconds
361
362        if let Some((front_ts, transactions)) = inner.observations.front_mut() {
363            // First check that the timestamp is monotonically incremented - ignore any observation that is not
364            // later from previous one (it shouldn't really happen).
365            if timestamp_secs < *front_ts {
366                warn!(
367                    "Ignoring observation of transactions:{} as has earlier timestamp than last observation {}s < {}s",
368                    num_of_transactions, timestamp_secs, front_ts
369                );
370                return;
371            }
372
373            // Not very likely, but if transactions refer to same second we add to the last element.
374            if timestamp_secs == *front_ts {
375                *transactions = transactions.saturating_add(num_of_transactions);
376            } else {
377                inner
378                    .observations
379                    .push_front((timestamp_secs, num_of_transactions));
380            }
381        } else {
382            inner
383                .observations
384                .push_front((timestamp_secs, num_of_transactions));
385        }
386
387        // update total number of transactions in the observations list
388        inner.total_transactions = inner.total_transactions.saturating_add(num_of_transactions);
389
390        // If we have more values on our window of max values, remove the last one, and calculate throughput.
391        // If we have the exact same values on our window of max values, then still calculate the throughput to ensure
392        // that we are taking into account the case where the last bucket gets updated because it falls into the same second.
393        if inner.observations.len() as u64 >= self.observations_window {
394            let last_element_ts = if inner.observations.len() as u64 == self.observations_window {
395                if let Some(ts) = inner.last_oldest_timestamp {
396                    ts
397                } else {
398                    warn!(
399                        "Skip calculation - we still don't have enough elements to pop the last observation"
400                    );
401                    return;
402                }
403            } else {
404                let (ts, txes) = inner.observations.pop_back().unwrap();
405                inner.total_transactions = inner.total_transactions.saturating_sub(txes);
406                ts
407            };
408
409            // update the last oldest timestamp
410            inner.last_oldest_timestamp = Some(last_element_ts);
411
412            // get the first element's timestamp to calculate the transaction rate
413            let (first_element_ts, _first_element_transactions) = inner
414                .observations
415                .front()
416                .expect("There should be at least on element in the list");
417
418            let period = first_element_ts.saturating_sub(last_element_ts);
419
420            if let Some(current_throughput) = inner.total_transactions.checked_div(period) {
421                self.metrics
422                    .consensus_calculated_throughput
423                    .set(current_throughput as i64);
424
425                self.current_throughput
426                    .store(Arc::new((current_throughput, timestamp_secs)));
427            } else {
428                warn!(
429                    "Skip calculating throughput as time period is {}. This is very unlikely to happen, should investigate.",
430                    period
431                );
432            }
433        }
434    }
435
436    // Returns the current (live calculated) throughput and the corresponding timestamp of when this got updated.
437    pub fn current_throughput(&self) -> (u64, TimestampSecs) {
438        *self.current_throughput.load().as_ref()
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445    use crate::consensus_throughput_calculator::Level::{High, Low};
446    use prometheus::Registry;
447
448    #[test]
449    pub fn test_throughput_profile_ranges() {
450        let ranges = ThroughputProfileRanges::default();
451
452        assert_eq!(
453            ranges.resolve(0),
454            ThroughputProfile {
455                level: Low,
456                throughput: 0
457            }
458        );
459        assert_eq!(
460            ranges.resolve(1_000),
461            ThroughputProfile {
462                level: Low,
463                throughput: 0
464            }
465        );
466        assert_eq!(
467            ranges.resolve(2_000),
468            ThroughputProfile {
469                level: High,
470                throughput: 2_000
471            }
472        );
473        assert_eq!(
474            ranges.resolve(u64::MAX),
475            ThroughputProfile {
476                level: High,
477                throughput: 2_000
478            }
479        );
480    }
481
482    #[test]
483    #[cfg_attr(msim, ignore)]
484    pub fn test_consensus_throughput_calculator() {
485        let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));
486        let max_observation_points: NonZeroU64 = NonZeroU64::new(3).unwrap();
487
488        let calculator = ConsensusThroughputCalculator::new(Some(max_observation_points), metrics);
489
490        assert_eq!(calculator.current_throughput(), (0, 0));
491
492        calculator.add_transactions(1000 as TimestampMs, 1_000);
493        calculator.add_transactions(2000 as TimestampMs, 1_000);
494        calculator.add_transactions(3000 as TimestampMs, 1_000);
495        calculator.add_transactions(4000 as TimestampMs, 1_000);
496
497        // We expect to have a rate of 1K tx/sec with last update timestamp the 4th second
498        assert_eq!(calculator.current_throughput(), (1000, 4));
499
500        // We are adding more transactions to get over 2K tx/sec
501        calculator.add_transactions(5_000 as TimestampMs, 2_500);
502        calculator.add_transactions(6_000 as TimestampMs, 2_800);
503        assert_eq!(calculator.current_throughput(), (2100, 6));
504
505        // Let's now add 0 transactions after 5 seconds. Since 5 seconds have passed since the last
506        // update and now the transactions are 0 we expect the throughput to be calculate as:
507        // 2800 + 2500 + 0 = 5300 / (15sec - 4sec) = 5300 / 11sec = 481 tx/sec
508        calculator.add_transactions(15_000 as TimestampMs, 0);
509
510        assert_eq!(calculator.current_throughput(), (481, 15));
511
512        // Adding zero transactions for the next 5 seconds will make throughput zero
513        calculator.add_transactions(17_000 as TimestampMs, 0);
514        assert_eq!(calculator.current_throughput(), (233, 17));
515
516        calculator.add_transactions(19_000 as TimestampMs, 0);
517        calculator.add_transactions(20_000 as TimestampMs, 0);
518        assert_eq!(calculator.current_throughput(), (0, 20));
519
520        // By adding now a few entries with lots of transactions increase again the throughput
521        calculator.add_transactions(21_000 as TimestampMs, 1_000);
522        calculator.add_transactions(22_000 as TimestampMs, 2_000);
523        calculator.add_transactions(23_000 as TimestampMs, 3_100);
524        assert_eq!(calculator.current_throughput(), (2033, 23));
525    }
526
527    #[test]
528    #[cfg_attr(msim, ignore)]
529    pub fn test_throughput_calculator_same_timestamp_observations() {
530        let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));
531        let max_observation_points: NonZeroU64 = NonZeroU64::new(2).unwrap();
532
533        let calculator = ConsensusThroughputCalculator::new(Some(max_observation_points), metrics);
534
535        // adding one observation
536        calculator.add_transactions(1_000, 0);
537
538        // Adding observations with same timestamp should fall under the same bucket and won't lead
539        // to throughput update.
540        for _ in 0..10 {
541            calculator.add_transactions(2_340, 100);
542        }
543        assert_eq!(calculator.current_throughput(), (0, 0));
544
545        // Adding now one observation on a different second bucket will change throughput
546        calculator.add_transactions(5_000, 0);
547
548        assert_eq!(calculator.current_throughput(), (250, 5));
549
550        // Updating further the last bucket with more transactions it keeps updating the throughput
551        calculator.add_transactions(5_000, 400);
552        assert_eq!(calculator.current_throughput(), (350, 5));
553
554        calculator.add_transactions(5_000, 300);
555        assert_eq!(calculator.current_throughput(), (425, 5));
556    }
557
558    #[test]
559    #[cfg_attr(msim, ignore)]
560    pub fn test_consensus_throughput_profiler() {
561        let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));
562        let throughput_profile_update_interval: TimestampSecs = 5;
563        let max_observation_points: NonZeroU64 = NonZeroU64::new(3).unwrap();
564        let throughput_profile_cool_down_threshold: u64 = 10;
565
566        let ranges = ThroughputProfileRanges::default();
567
568        let calculator = Arc::new(ConsensusThroughputCalculator::new(
569            Some(max_observation_points),
570            metrics.clone(),
571        ));
572        let profiler = ConsensusThroughputProfiler::new(
573            calculator.clone(),
574            Some(throughput_profile_update_interval),
575            Some(throughput_profile_cool_down_threshold),
576            metrics,
577            ranges,
578        );
579
580        // When no transactions exists, the calculator will return by default "High" to err on the
581        // assumption that there is lots of load.
582        assert_eq!(profiler.throughput_level(), (High, 0));
583
584        calculator.add_transactions(1000 as TimestampMs, 1_000);
585        calculator.add_transactions(2000 as TimestampMs, 1_000);
586        calculator.add_transactions(3000 as TimestampMs, 1_000);
587
588        // We expect to have a rate of 1K tx/sec, that's < 2K limit , so throughput profile remains to "High" - nothing gets updated
589        assert_eq!(profiler.throughput_level(), (High, 0));
590
591        // We are adding more transactions to get over 2K tx/sec, so throughput profile should now be categorised
592        // as "high"
593        calculator.add_transactions(4000 as TimestampMs, 2_500);
594        calculator.add_transactions(5000 as TimestampMs, 2_800);
595        assert_eq!(profiler.throughput_level(), (High, 2100));
596
597        // Let's now add 0 transactions after at least 5 seconds. Since the update should happen every 5 seconds
598        // now the transactions are 0 we expect the throughput to be calculate as:
599        // 2800 + 2800 + 0 = 5300 / 15 - 4sec = 5600 / 11sec = 509 tx/sec
600        calculator.add_transactions(7_000 as TimestampMs, 2_800);
601        calculator.add_transactions(15_000 as TimestampMs, 0);
602
603        assert_eq!(profiler.throughput_level(), (Low, 509));
604
605        // Adding zero transactions for the next 5 seconds will make throughput zero.
606        // Profile will remain Low and throughput will get updated
607        calculator.add_transactions(17_000 as TimestampMs, 0);
608        calculator.add_transactions(19_000 as TimestampMs, 0);
609        calculator.add_transactions(20_000 as TimestampMs, 0);
610
611        assert_eq!(profiler.throughput_level(), (Low, 0));
612
613        // By adding a few entries with lots of transactions for the exact same last timestamp it will
614        // trigger a throughput profile update.
615        calculator.add_transactions(20_000 as TimestampMs, 4_000);
616        calculator.add_transactions(20_000 as TimestampMs, 4_000);
617        calculator.add_transactions(20_000 as TimestampMs, 4_000);
618        assert_eq!(profiler.throughput_level(), (High, 2400));
619
620        // no further updates will happen until the next 5sec bucket update.
621        calculator.add_transactions(22_000 as TimestampMs, 0);
622        calculator.add_transactions(23_000 as TimestampMs, 0);
623        assert_eq!(profiler.throughput_level(), (High, 2400));
624    }
625
626    #[test]
627    #[cfg_attr(msim, ignore)]
628    pub fn test_consensus_throughput_profiler_update_interval() {
629        let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));
630        let throughput_profile_update_interval: TimestampSecs = 5;
631        let max_observation_points: NonZeroU64 = NonZeroU64::new(2).unwrap();
632
633        let ranges = ThroughputProfileRanges::default();
634
635        let calculator = Arc::new(ConsensusThroughputCalculator::new(
636            Some(max_observation_points),
637            metrics.clone(),
638        ));
639        let profiler = ConsensusThroughputProfiler::new(
640            calculator.clone(),
641            Some(throughput_profile_update_interval),
642            None,
643            metrics,
644            ranges,
645        );
646
647        // Current setup is `throughput_profile_update_interval` = 5sec, which means that throughput profile
648        // should get updated every 5 seconds (based on the provided unix timestamp).
649
650        calculator.add_transactions(3_000 as TimestampMs, 2_200);
651        calculator.add_transactions(4_000 as TimestampMs, 4_200);
652        calculator.add_transactions(7_000 as TimestampMs, 4_200);
653
654        assert_eq!(profiler.throughput_level(), (High, 2_100));
655
656        // When adding transactions at timestamp 10s the bucket changes and the profile should get updated
657        calculator.add_transactions(10_000 as TimestampMs, 1_000);
658
659        assert_eq!(profiler.throughput_level(), (Low, 866));
660
661        // Now adding transactions at timestamp 16s the bucket changes and profile should get updated
662        calculator.add_transactions(16_000 as TimestampMs, 20_000);
663
664        assert_eq!(profiler.throughput_level(), (High, 2333));
665
666        // Keep adding transactions that fall under the same timestamp as the previous one, even though
667        // traffic should be marked as low it doesn't until the bucket of 20s is updated.
668        calculator.add_transactions(17_000 as TimestampMs, 0);
669        calculator.add_transactions(18_000 as TimestampMs, 0);
670        calculator.add_transactions(19_000 as TimestampMs, 0);
671
672        assert_eq!(profiler.throughput_level(), (High, 2333));
673
674        calculator.add_transactions(20_000 as TimestampMs, 0);
675
676        assert_eq!(profiler.throughput_level(), (Low, 0));
677    }
678
679    #[test]
680    #[cfg_attr(msim, ignore)]
681    pub fn test_consensus_throughput_profiler_cool_down() {
682        let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));
683        let throughput_profile_update_window: TimestampSecs = 3;
684        let max_observation_points: NonZeroU64 = NonZeroU64::new(3).unwrap();
685        let throughput_profile_cool_down_threshold: u64 = 10;
686
687        let ranges = ThroughputProfileRanges::default();
688
689        let calculator = Arc::new(ConsensusThroughputCalculator::new(
690            Some(max_observation_points),
691            metrics.clone(),
692        ));
693        let profiler = ConsensusThroughputProfiler::new(
694            calculator.clone(),
695            Some(throughput_profile_update_window),
696            Some(throughput_profile_cool_down_threshold),
697            metrics,
698            ranges,
699        );
700
701        // Adding 4 observations of 3_000 tx/sec, so in the end throughput profile should be flagged as high
702        for i in 1..=4 {
703            calculator.add_transactions(i * 1_000, 3_000);
704        }
705        assert_eq!(profiler.throughput_level(), (High, 3_000));
706
707        // Now let's add some transactions to bring throughput little bit bellow the upper Low threshold (2000 tx/sec)
708        // but still above the 10% offset which is 1800 tx/sec.
709        calculator.add_transactions(5_000, 1_900);
710        calculator.add_transactions(6_000, 1_900);
711        calculator.add_transactions(7_000, 1_900);
712
713        assert_eq!(calculator.current_throughput(), (1_900, 7));
714        assert_eq!(profiler.throughput_level(), (High, 3_000));
715
716        // Let's bring down more throughput - now the throughput profile should get updated
717        calculator.add_transactions(8_000, 1_500);
718        calculator.add_transactions(9_000, 1_500);
719        calculator.add_transactions(10_000, 1_500);
720
721        assert_eq!(profiler.throughput_level(), (Low, 1500));
722    }
723}