1use 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; const DEFAULT_THROUGHPUT_PROFILE_UPDATE_INTERVAL_SECS: u64 = 60; const DEFAULT_THROUGHPUT_PROFILE_COOL_DOWN_THRESHOLD: u64 = 10; #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)]
21pub struct ThroughputProfile {
22 pub level: Level,
23 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 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 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 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 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 profile: ThroughputProfile,
171 timestamp: TimestampSecs,
173 throughput: u64,
175}
176
177#[derive(Default)]
178struct ConsensusThroughputCalculatorInner {
179 observations: VecDeque<(TimestampSecs, u64)>,
180 total_transactions: u64,
181 last_oldest_timestamp: Option<TimestampSecs>,
183}
184
185pub struct ConsensusThroughputProfiler {
190 throughput_profile_update_interval: TimestampSecs,
196 throughput_profile_cool_down_threshold: u64,
200 profile_ranges: ThroughputProfileRanges,
202 last_throughput_profile: ArcSwap<ThroughputProfileEntry>,
204 metrics: Arc<AuthorityMetrics>,
205 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 }), profile_ranges,
243 metrics,
244 calculator,
245 }
246 }
247
248 pub fn throughput_level(&self) -> (Level, u64) {
251 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 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 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 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 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
326pub struct ConsensusThroughputCalculator {
330 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 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; if let Some((front_ts, transactions)) = inner.observations.front_mut() {
363 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 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 inner.total_transactions = inner.total_transactions.saturating_add(num_of_transactions);
389
390 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 inner.last_oldest_timestamp = Some(last_element_ts);
411
412 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 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 assert_eq!(calculator.current_throughput(), (1000, 4));
499
500 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 calculator.add_transactions(15_000 as TimestampMs, 0);
509
510 assert_eq!(calculator.current_throughput(), (481, 15));
511
512 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 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 calculator.add_transactions(1_000, 0);
537
538 for _ in 0..10 {
541 calculator.add_transactions(2_340, 100);
542 }
543 assert_eq!(calculator.current_throughput(), (0, 0));
544
545 calculator.add_transactions(5_000, 0);
547
548 assert_eq!(calculator.current_throughput(), (250, 5));
549
550 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 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 assert_eq!(profiler.throughput_level(), (High, 0));
590
591 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 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 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 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 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 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 calculator.add_transactions(10_000 as TimestampMs, 1_000);
658
659 assert_eq!(profiler.throughput_level(), (Low, 866));
660
661 calculator.add_transactions(16_000 as TimestampMs, 20_000);
663
664 assert_eq!(profiler.throughput_level(), (High, 2333));
665
666 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 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 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 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}