1use std::{
5 collections::HashMap,
6 hash::{Hash, Hasher},
7 num::NonZeroUsize,
8 sync::{Arc, Weak},
9 time::{Duration, SystemTime},
10};
11
12use serde::{Deserialize, Serialize};
13
14use super::authority_per_epoch_store::AuthorityPerEpochStore;
15use super::weighted_moving_average::WeightedMovingAverage;
16use crate::consensus_adapter::SubmitToConsensus;
17use governor::{Quota, RateLimiter, clock::MonotonicClock};
18use itertools::Itertools;
19use lru::LruCache;
20#[cfg(not(msim))]
21use mysten_common::in_antithesis;
22use mysten_common::{assert_reachable, debug_fatal, in_test_configuration};
23use mysten_metrics::{monitored_scope, spawn_monitored_task};
24use rand::{Rng, SeedableRng, random, rngs, thread_rng};
25use simple_moving_average::{SMA, SingleSumSMA};
26use sui_config::node::ExecutionTimeObserverConfig;
27use sui_protocol_config::{ExecutionTimeEstimateParams, PerObjectCongestionControlMode};
28use sui_types::{
29 base_types::ObjectID,
30 committee::Committee,
31 error::SuiErrorKind,
32 execution::{ExecutionTimeObservationKey, ExecutionTiming},
33 messages_consensus::{AuthorityIndex, ConsensusTransaction, ExecutionTimeObservation},
34 transaction::{
35 Command, ProgrammableTransaction, StoredExecutionTimeObservations, TransactionData,
36 TransactionDataAPI, TransactionKind,
37 },
38};
39use tokio::{sync::mpsc, time::Instant};
40use tracing::{debug, info, trace, warn};
41
42const SMA_LOCAL_OBSERVATION_WINDOW_SIZE: usize = 20;
45const OBJECT_UTILIZATION_METRIC_HASH_MODULUS: u8 = 32;
46
47#[cfg(not(msim))]
54fn antithesis_enable_injecting_synthetic_execution_time() -> bool {
55 use std::sync::OnceLock;
56 static ENABLE_INJECTION: OnceLock<bool> = OnceLock::new();
57 *ENABLE_INJECTION.get_or_init(|| {
58 if !in_antithesis() {
59 return false;
60 }
61
62 std::env::var("ANTITHESIS_ENABLE_EXECUTION_TIME_INJECTION")
63 .map(|v| v.to_lowercase() == "true" || v == "1")
64 .unwrap_or(true)
65 })
66}
67
68pub struct ExecutionTimeObserver {
70 epoch_store: Weak<AuthorityPerEpochStore>,
71 consensus_adapter: Box<dyn SubmitToConsensus>,
72
73 protocol_params: ExecutionTimeEstimateParams,
74 config: ExecutionTimeObserverConfig,
75
76 local_observations: LruCache<ExecutionTimeObservationKey, LocalObservations>,
77
78 object_utilization_tracker: LruCache<ObjectID, ObjectUtilization>,
82
83 tracked_object_counters: HashMap<ObjectID, prometheus::Counter>,
86
87 indebted_objects: Vec<ObjectID>,
89
90 sharing_rate_limiter: RateLimiter<
91 governor::state::NotKeyed,
92 governor::state::InMemoryState,
93 governor::clock::MonotonicClock,
94 governor::middleware::NoOpMiddleware<
95 <governor::clock::MonotonicClock as governor::clock::Clock>::Instant,
96 >,
97 >,
98
99 next_generation_number: u64,
100}
101
102#[derive(Debug, Clone)]
103pub struct LocalObservations {
104 moving_average: SingleSumSMA<Duration, u32, SMA_LOCAL_OBSERVATION_WINDOW_SIZE>,
105 weighted_moving_average: WeightedMovingAverage,
106 last_shared: Option<(Duration, Instant)>,
107 config: ExecutionTimeObserverConfig,
108}
109
110impl LocalObservations {
111 fn new(config: ExecutionTimeObserverConfig, default_duration: Duration) -> Self {
112 let window_size = config.weighted_moving_average_window_size();
113 Self {
114 moving_average: SingleSumSMA::from_zero(Duration::ZERO),
115 weighted_moving_average: WeightedMovingAverage::new(
116 default_duration.as_micros() as u64,
117 window_size,
118 ),
119 last_shared: None,
120 config,
121 }
122 }
123
124 fn add_sample(&mut self, duration: Duration, gas_price: u64) {
125 self.moving_average.add_sample(duration);
126 self.weighted_moving_average
127 .add_sample(duration.as_micros() as u64, gas_price);
128 }
129
130 fn get_average(&self) -> Duration {
131 if self.config.enable_gas_price_weighting() {
132 Duration::from_micros(self.weighted_moving_average.get_weighted_average())
133 } else {
134 self.moving_average.get_average()
135 }
136 }
137
138 fn diff_exceeds_threshold(
139 &self,
140 new_average: Duration,
141 threshold: f64,
142 min_interval: Duration,
143 ) -> bool {
144 let Some((last_shared, last_shared_timestamp)) = self.last_shared else {
145 return true;
147 };
148
149 if last_shared_timestamp.elapsed() < min_interval {
150 return false;
151 }
152
153 if threshold >= 0.0 {
154 new_average
156 .checked_sub(last_shared)
157 .is_some_and(|diff| diff > last_shared.mul_f64(threshold))
158 } else {
159 last_shared
161 .checked_sub(new_average)
162 .is_some_and(|diff| diff > last_shared.mul_f64(-threshold))
163 }
164 }
165}
166
167#[derive(Debug, Clone)]
168pub struct ObjectUtilization {
169 excess_execution_time: Duration,
170 last_measured: Option<Instant>,
171 was_overutilized: bool, }
173
174impl ObjectUtilization {
175 pub fn overutilized(&self, config: &ExecutionTimeObserverConfig) -> bool {
176 self.excess_execution_time > config.observation_sharing_object_utilization_threshold()
177 }
178}
179
180fn tracked_object_counters(
181 config: &ExecutionTimeObserverConfig,
182 metrics: &crate::epoch::epoch_metrics::EpochMetrics,
183) -> HashMap<ObjectID, prometheus::Counter> {
184 config
185 .object_utilization_metric_tracked_ids()
186 .map(|(id, name)| {
187 let counter = metrics
188 .epoch_execution_time_observer_tracked_object_utilization
189 .with_label_values(&[id.to_string().as_str(), name]);
190 (*id, counter)
191 })
192 .collect()
193}
194
195impl ExecutionTimeObserver {
197 pub fn spawn(
198 epoch_store: Arc<AuthorityPerEpochStore>,
199 consensus_adapter: Box<dyn SubmitToConsensus>,
200 config: ExecutionTimeObserverConfig,
201 ) {
202 let PerObjectCongestionControlMode::ExecutionTimeEstimate(protocol_params) = epoch_store
203 .protocol_config()
204 .per_object_congestion_control_mode()
205 else {
206 info!(
207 "ExecutionTimeObserver disabled because per-object congestion control mode is not ExecutionTimeEstimate"
208 );
209 return;
210 };
211
212 let (tx_local_execution_time, mut rx_local_execution_time) =
213 mpsc::channel(config.observation_channel_capacity().into());
214 let (tx_object_debts, mut rx_object_debts) =
215 mpsc::channel(config.object_debt_channel_capacity().into());
216 epoch_store.set_local_execution_time_channels(tx_local_execution_time, tx_object_debts);
217
218 let mut observer = Self {
220 epoch_store: Arc::downgrade(&epoch_store),
221 consensus_adapter,
222 local_observations: LruCache::new(config.observation_cache_size()),
223 object_utilization_tracker: LruCache::new(config.object_utilization_cache_size()),
224 tracked_object_counters: tracked_object_counters(&config, &epoch_store.metrics),
225 indebted_objects: Vec::new(),
226 sharing_rate_limiter: RateLimiter::direct_with_clock(
227 Quota::per_second(config.observation_sharing_rate_limit())
228 .allow_burst(config.observation_sharing_burst_limit()),
229 &MonotonicClock,
230 ),
231 protocol_params,
232 config,
233 next_generation_number: SystemTime::now()
234 .duration_since(std::time::UNIX_EPOCH)
235 .expect("Sui did not exist prior to 1970")
236 .as_micros()
237 .try_into()
238 .expect("This build of sui is not supported in the year 500,000"),
239 };
240 spawn_monitored_task!(epoch_store.within_alive_epoch(async move {
241 loop {
242 tokio::select! {
243 Some(object_debts) = rx_object_debts.recv() => {
245 observer.update_indebted_objects(object_debts);
246 }
247 Some((tx, timings, total_duration, gas_price)) = rx_local_execution_time.recv() => {
248 observer
249 .record_local_observations(&tx, &timings, total_duration, gas_price);
250 }
251 else => { break }
252 }
253 }
254 info!("shutting down ExecutionTimeObserver");
255 }));
256 }
257
258 #[cfg(test)]
259 fn new_for_testing(
260 epoch_store: Arc<AuthorityPerEpochStore>,
261 consensus_adapter: Box<dyn SubmitToConsensus>,
262 observation_sharing_object_utilization_threshold: Duration,
263 enable_gas_price_weighting: bool,
264 ) -> Self {
265 let PerObjectCongestionControlMode::ExecutionTimeEstimate(protocol_params) = epoch_store
266 .protocol_config()
267 .per_object_congestion_control_mode()
268 else {
269 panic!(
270 "tried to construct test ExecutionTimeObserver when congestion control mode is not ExecutionTimeEstimate"
271 );
272 };
273 Self {
274 epoch_store: Arc::downgrade(&epoch_store),
275 consensus_adapter,
276 protocol_params,
277 config: ExecutionTimeObserverConfig {
278 observation_sharing_object_utilization_threshold: Some(
279 observation_sharing_object_utilization_threshold,
280 ),
281 enable_gas_price_weighting: Some(enable_gas_price_weighting),
282 ..ExecutionTimeObserverConfig::default()
283 },
284 local_observations: LruCache::new(NonZeroUsize::new(10000).unwrap()),
285 object_utilization_tracker: LruCache::new(NonZeroUsize::new(50000).unwrap()),
286 tracked_object_counters: HashMap::new(),
287 indebted_objects: Vec::new(),
288 sharing_rate_limiter: RateLimiter::direct_with_clock(
289 Quota::per_hour(std::num::NonZeroU32::MAX),
290 &MonotonicClock,
291 ),
292 next_generation_number: SystemTime::now()
293 .duration_since(std::time::UNIX_EPOCH)
294 .expect("Sui did not exist prior to 1970")
295 .as_micros()
296 .try_into()
297 .expect("This build of sui is not supported in the year 500,000"),
298 }
299 }
300
301 fn record_local_observations(
306 &mut self,
307 tx: &ProgrammableTransaction,
308 timings: &[ExecutionTiming],
309 total_duration: Duration,
310 gas_price: u64,
311 ) {
312 let _scope = monitored_scope("ExecutionTimeObserver::record_local_observations");
313
314 #[cfg(msim)]
316 let should_inject = self.config.inject_synthetic_execution_time();
317 #[cfg(not(msim))]
318 let should_inject = antithesis_enable_injecting_synthetic_execution_time();
319
320 if should_inject {
321 let (generated_timings, generated_duration) = self.generate_test_timings(tx, timings);
322 self.record_local_observations_timing(
323 tx,
324 &generated_timings,
325 generated_duration,
326 gas_price,
327 )
328 } else {
329 self.record_local_observations_timing(tx, timings, total_duration, gas_price)
330 }
331 }
332
333 fn record_local_observations_timing(
334 &mut self,
335 tx: &ProgrammableTransaction,
336 timings: &[ExecutionTiming],
337 total_duration: Duration,
338 gas_price: u64,
339 ) {
340 let Some(epoch_store) = self.epoch_store.upgrade() else {
341 debug!("epoch is ending, dropping execution time observation");
342 return;
343 };
344 let timings = if timings.len() > tx.commands.len() {
345 warn!(
346 executed_commands = timings.len(),
347 original_commands = tx.commands.len(),
348 "execution produced more timings than the original PTB commands; using the trailing timings for local execution-time observations"
349 );
350 &timings[timings.len() - tx.commands.len()..]
351 } else {
352 timings
353 };
354
355 let mut uses_indebted_object = false;
356
357 let max_excess_per_object_execution_time = tx
360 .shared_input_objects()
361 .filter_map(|obj| obj.is_accessed_exclusively().then_some(obj.id))
362 .map(|id| {
363 if !uses_indebted_object && self.indebted_objects.binary_search(&id).is_ok() {
365 uses_indebted_object = true;
366 }
367
368 let now = Instant::now();
378 let utilization =
379 self.object_utilization_tracker
380 .get_or_insert_mut(id, || ObjectUtilization {
381 excess_execution_time: Duration::ZERO,
382 last_measured: None,
383 was_overutilized: false,
384 });
385 let overutilized_at_start = utilization.overutilized(&self.config);
386 utilization.excess_execution_time += total_duration;
387 utilization.excess_execution_time =
388 utilization.excess_execution_time.saturating_sub(
389 utilization
390 .last_measured
391 .map(|last_measured| {
392 now.duration_since(last_measured)
393 .mul_f64(self.protocol_params.target_utilization as f64 / 100.0)
394 })
395 .unwrap_or(Duration::MAX),
396 );
397 utilization.last_measured = Some(now);
398 if utilization.overutilized(&self.config) {
399 utilization.was_overutilized = true;
400 }
401
402 if !overutilized_at_start && utilization.overutilized(&self.config) {
404 trace!("object {id:?} is overutilized");
405 epoch_store
406 .metrics
407 .epoch_execution_time_observer_overutilized_objects
408 .inc();
409 } else if overutilized_at_start && !utilization.overutilized(&self.config) {
410 epoch_store
411 .metrics
412 .epoch_execution_time_observer_overutilized_objects
413 .dec();
414 }
415 if utilization.was_overutilized {
416 let key = if self.config.report_object_utilization_metric_with_full_id() {
417 id.to_string()
418 } else {
419 let key_lsb = id.into_bytes()[ObjectID::LENGTH - 1];
420 let hash = key_lsb % OBJECT_UTILIZATION_METRIC_HASH_MODULUS;
421 format!("{:x}", hash)
422 };
423
424 epoch_store
425 .metrics
426 .epoch_execution_time_observer_object_utilization
427 .with_label_values(&[key.as_str()])
428 .inc_by(total_duration.as_secs_f64());
429 }
430 if let Some(counter) = self.tracked_object_counters.get(&id) {
431 counter.inc_by(total_duration.as_secs_f64());
432 }
433
434 utilization.excess_execution_time
435 })
436 .max()
437 .unwrap_or(Duration::ZERO);
438 epoch_store
439 .metrics
440 .epoch_execution_time_observer_utilization_cache_size
441 .set(self.object_utilization_tracker.len() as i64);
442
443 let total_command_duration: Duration = timings.iter().map(|t| t.duration()).sum();
444 let extra_overhead = total_duration - total_command_duration;
445
446 let mut to_share = Vec::with_capacity(tx.commands.len());
447 for (i, timing) in timings.iter().enumerate() {
448 let command = &tx.commands[i];
449
450 if matches!(command, Command::Publish(_, _)) {
452 continue;
453 }
454
455 let mut command_duration = timing.duration();
457
458 let overhead_factor = if total_command_duration > Duration::ZERO {
460 command_duration.as_secs_f64() / total_command_duration.as_secs_f64()
461 } else {
462 1.0 / (tx.commands.len() as f64)
464 };
465 command_duration += extra_overhead.mul_f64(overhead_factor);
466
467 command_duration = command_duration.div_f64(command_length(command).get() as f64);
470
471 let key = ExecutionTimeObservationKey::from_command(command);
473 let local_observation = self.local_observations.get_or_insert_mut(key.clone(), || {
474 LocalObservations::new(self.config.clone(), Duration::ZERO)
475 });
476 local_observation.add_sample(command_duration, gas_price);
477
478 let new_average = local_observation.get_average();
483 let mut should_share = false;
484
485 if max_excess_per_object_execution_time
487 >= self
488 .config
489 .observation_sharing_object_utilization_threshold()
490 && local_observation.diff_exceeds_threshold(
491 new_average,
492 self.config.observation_sharing_diff_threshold(),
493 self.config.observation_sharing_min_interval(),
494 )
495 {
496 should_share = true;
497 epoch_store
498 .metrics
499 .epoch_execution_time_observations_sharing_reason
500 .with_label_values(&["utilization"])
501 .inc();
502 };
503
504 if uses_indebted_object
506 && local_observation.diff_exceeds_threshold(
507 new_average,
508 -self.config.observation_sharing_diff_threshold(),
509 self.config.observation_sharing_min_interval(),
510 )
511 {
512 should_share = true;
513 epoch_store
514 .metrics
515 .epoch_execution_time_observations_sharing_reason
516 .with_label_values(&["indebted"])
517 .inc();
518 }
519
520 if should_share {
521 debug!("sharing new execution time observation for {key:?}: {new_average:?}");
522 to_share.push((key, new_average));
523 local_observation.last_shared = Some((new_average, Instant::now()));
524 }
525 }
526
527 self.share_observations(to_share);
529 }
530
531 fn generate_test_timings(
532 &self,
533 tx: &ProgrammableTransaction,
534 timings: &[ExecutionTiming],
535 ) -> (Vec<ExecutionTiming>, Duration) {
536 #[allow(clippy::disallowed_methods)]
537 let generated_timings: Vec<_> = tx
538 .commands
539 .iter()
540 .zip(timings.iter())
542 .map(|(command, timing)| {
543 let key = ExecutionTimeObservationKey::from_command(command);
544 let duration = self.get_test_duration(&key);
545 if timing.is_abort() {
546 ExecutionTiming::Abort(duration)
547 } else {
548 ExecutionTiming::Success(duration)
549 }
550 })
551 .collect();
552
553 let total_duration = generated_timings
554 .iter()
555 .map(|t| t.duration())
556 .sum::<Duration>()
557 + thread_rng().gen_range(Duration::from_millis(10)..Duration::from_millis(50));
558
559 (generated_timings, total_duration)
560 }
561
562 fn get_test_duration(&self, key: &ExecutionTimeObservationKey) -> Duration {
563 #[cfg(msim)]
564 let should_inject = self.config.inject_synthetic_execution_time();
565 #[cfg(not(msim))]
566 let should_inject = false;
567
568 if !in_test_configuration() && !should_inject {
569 panic!("get_test_duration called in non-test configuration");
570 }
571
572 static PER_TEST_SEED: std::sync::OnceLock<u64> = std::sync::OnceLock::new();
573
574 let mut hasher = std::collections::hash_map::DefaultHasher::new();
575
576 let checkpoint_digest_used = self
577 .epoch_store
578 .upgrade()
579 .and_then(|store| {
580 store
581 .get_lowest_non_genesis_checkpoint_summary()
582 .ok()
583 .flatten()
584 })
585 .map(|summary| summary.content_digest.hash(&mut hasher))
586 .is_some();
587
588 if !checkpoint_digest_used {
589 PER_TEST_SEED.get_or_init(random::<u64>).hash(&mut hasher);
590 }
591
592 key.hash(&mut hasher);
593 let mut rng = rngs::StdRng::seed_from_u64(hasher.finish());
594 rng.gen_range(Duration::from_millis(100)..Duration::from_millis(600))
595 }
596
597 fn share_observations(&mut self, to_share: Vec<(ExecutionTimeObservationKey, Duration)>) {
598 if to_share.is_empty() {
599 return;
600 }
601 let Some(epoch_store) = self.epoch_store.upgrade() else {
602 debug!("epoch is ending, dropping execution time observation");
603 return;
604 };
605
606 let num_observations = to_share.len() as u64;
607
608 if let Err(e) = self.sharing_rate_limiter.check() {
610 epoch_store
611 .metrics
612 .epoch_execution_time_observations_dropped
613 .with_label_values(&["global_rate_limit"])
614 .inc_by(num_observations);
615 debug!("rate limit exceeded, dropping execution time observation; {e:?}");
616 return;
617 }
618
619 let epoch_store = epoch_store.clone();
620 let transaction = ConsensusTransaction::new_execution_time_observation(
621 ExecutionTimeObservation::new(epoch_store.name, self.next_generation_number, to_share),
622 );
623 self.next_generation_number += 1;
624
625 if let Err(e) = self.consensus_adapter.submit_best_effort(
626 &transaction,
627 &epoch_store,
628 Duration::from_secs(5),
629 ) {
630 if !matches!(e.as_inner(), SuiErrorKind::EpochEnded(_)) {
631 epoch_store
632 .metrics
633 .epoch_execution_time_observations_dropped
634 .with_label_values(&["submit_to_consensus"])
635 .inc_by(num_observations);
636 warn!("failed to submit execution time observation: {e:?}");
637 }
638 } else {
639 assert_reachable!("successfully shares execution time observations");
643 epoch_store
644 .metrics
645 .epoch_execution_time_observations_shared
646 .inc_by(num_observations);
647 }
648 }
649
650 fn update_indebted_objects(&mut self, mut object_debts: Vec<ObjectID>) {
651 let _scope = monitored_scope("ExecutionTimeObserver::update_indebted_objects");
652
653 let Some(epoch_store) = self.epoch_store.upgrade() else {
654 debug!("epoch is ending, dropping indebted object update");
655 return;
656 };
657
658 object_debts.sort_unstable();
659 object_debts.dedup();
660 self.indebted_objects = object_debts;
661 epoch_store
662 .metrics
663 .epoch_execution_time_observer_indebted_objects
664 .set(self.indebted_objects.len() as i64);
665 }
666}
667
668pub const EXTRA_FIELD_EXECUTION_TIME_ESTIMATES_KEY: u64 = 0;
671
672pub const EXTRA_FIELD_EXECUTION_TIME_ESTIMATES_CHUNK_COUNT_KEY: u64 = 1;
674
675pub struct ExecutionTimeEstimator {
678 committee: Arc<Committee>,
679 protocol_params: ExecutionTimeEstimateParams,
680
681 consensus_observations: HashMap<ExecutionTimeObservationKey, ConsensusObservations>,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize)]
685pub struct ConsensusObservations {
686 observations: Vec<(u64 , Option<Duration>)>, stake_weighted_median: Option<Duration>, }
689
690impl ConsensusObservations {
691 fn update_stake_weighted_median(
692 &mut self,
693 committee: &Committee,
694 config: &ExecutionTimeEstimateParams,
695 ) {
696 let mut stake_with_observations = 0;
697 let sorted_observations: Vec<_> = self
698 .observations
699 .iter()
700 .enumerate()
701 .filter_map(|(i, (_, duration))| {
702 duration.map(|duration| {
703 let authority_index: AuthorityIndex = i.try_into().unwrap();
704 stake_with_observations += committee.stake_by_index(authority_index).unwrap();
705 (duration, authority_index)
706 })
707 })
708 .sorted()
709 .collect();
710
711 if stake_with_observations < config.stake_weighted_median_threshold {
713 self.stake_weighted_median = None;
714 return;
715 }
716
717 let median_stake = stake_with_observations / 2;
719 let mut running_stake = 0;
720 for (duration, authority_index) in sorted_observations {
721 running_stake += committee.stake_by_index(authority_index).unwrap();
722 if running_stake > median_stake {
723 self.stake_weighted_median = Some(duration);
724 break;
725 }
726 }
727 }
728}
729
730impl ExecutionTimeEstimator {
731 pub fn new(
732 committee: Arc<Committee>,
733 protocol_params: ExecutionTimeEstimateParams,
734 initial_observations: impl Iterator<
735 Item = (
736 AuthorityIndex,
737 Option<u64>,
738 ExecutionTimeObservationKey,
739 Duration,
740 ),
741 >,
742 ) -> Self {
743 let mut estimator = Self {
744 committee,
745 protocol_params,
746 consensus_observations: HashMap::new(),
747 };
748 for (source, generation, key, duration) in initial_observations {
749 estimator.process_observation_from_consensus(
750 source,
751 generation,
752 key.to_owned(),
753 duration,
754 true,
755 );
756 }
757 for observation in estimator.consensus_observations.values_mut() {
758 observation
759 .update_stake_weighted_median(&estimator.committee, &estimator.protocol_params);
760 }
761 estimator
762 }
763
764 #[cfg(test)]
765 pub fn new_for_testing() -> Self {
766 let (committee, _) = Committee::new_simple_test_committee_of_size(1);
767 Self {
768 committee: Arc::new(committee),
769 protocol_params: ExecutionTimeEstimateParams {
770 target_utilization: 100,
771 max_estimate_us: u64::MAX,
772 ..ExecutionTimeEstimateParams::default()
773 },
774 consensus_observations: HashMap::new(),
775 }
776 }
777
778 pub fn process_observations_from_consensus(
779 &mut self,
780 source: AuthorityIndex,
781 generation: Option<u64>,
782 observations: &[(ExecutionTimeObservationKey, Duration)],
783 ) {
784 for (key, duration) in observations {
785 self.process_observation_from_consensus(
786 source,
787 generation,
788 key.to_owned(),
789 *duration,
790 false,
791 );
792 }
793 }
794
795 fn process_observation_from_consensus(
796 &mut self,
797 source: AuthorityIndex,
798 generation: Option<u64>,
799 observation_key: ExecutionTimeObservationKey,
800 duration: Duration,
801 skip_update: bool,
802 ) {
803 if matches!(observation_key, ExecutionTimeObservationKey::Publish) {
804 warn!(
806 "dropping Publish observation received from possibly-Byzanitine authority {source}"
807 );
808 return;
809 }
810
811 assert_reachable!("receives some valid execution time observations");
812
813 let observations = self
814 .consensus_observations
815 .entry(observation_key)
816 .or_insert_with(|| {
817 let len = self.committee.num_members();
818 let mut empty_observations = Vec::with_capacity(len);
819 empty_observations.resize(len, (0, None));
820 ConsensusObservations {
821 observations: empty_observations,
822 stake_weighted_median: if self
823 .protocol_params
824 .default_none_duration_for_new_keys
825 {
826 None
827 } else {
828 Some(Duration::ZERO)
829 },
830 }
831 });
832
833 let (obs_generation, obs_duration) =
834 &mut observations.observations[TryInto::<usize>::try_into(source).unwrap()];
835 if generation.is_some_and(|generation| *obs_generation >= generation) {
836 return;
838 }
839 *obs_generation = generation.unwrap_or(0);
840 *obs_duration = Some(duration);
841 if !skip_update {
842 observations.update_stake_weighted_median(&self.committee, &self.protocol_params);
843 }
844 }
845
846 pub fn get_estimate(&self, tx: &TransactionData) -> Duration {
847 let TransactionKind::ProgrammableTransaction(tx) = tx.kind() else {
848 debug_fatal!("get_estimate called on non-ProgrammableTransaction");
849 return Duration::ZERO;
850 };
851 tx.commands
852 .iter()
853 .map(|command| {
854 let key = ExecutionTimeObservationKey::from_command(command);
855 self.consensus_observations
856 .get(&key)
857 .and_then(|obs| obs.stake_weighted_median)
858 .unwrap_or_else(|| key.default_duration())
859 .mul_f64(command_length(command).get() as f64)
862 })
863 .sum::<Duration>()
864 .min(Duration::from_micros(self.protocol_params.max_estimate_us))
865 }
866
867 pub fn take_observations(&mut self) -> StoredExecutionTimeObservations {
868 StoredExecutionTimeObservations::V1(
869 self.consensus_observations
870 .drain()
871 .map(|(key, observations)| {
872 let observations = observations
873 .observations
874 .into_iter()
875 .enumerate()
876 .filter_map(|(idx, (_, duration))| {
877 duration.map(|d| {
878 (
879 self.committee
880 .authority_by_index(idx.try_into().unwrap())
881 .cloned()
882 .unwrap(),
883 d,
884 )
885 })
886 })
887 .collect();
888 (key, observations)
889 })
890 .collect(),
891 )
892 }
893
894 pub fn get_observations(&self) -> Vec<(ExecutionTimeObservationKey, ConsensusObservations)> {
895 self.consensus_observations
896 .iter()
897 .map(|(key, observations)| (key.clone(), observations.clone()))
898 .collect()
899 }
900}
901
902fn command_length(command: &Command) -> NonZeroUsize {
903 NonZeroUsize::new(match command {
906 Command::MoveCall(_) => 1,
907 Command::TransferObjects(src, _) => src.len() + 1,
908 Command::SplitCoins(_, amts) => amts.len() + 1,
909 Command::MergeCoins(_, src) => src.len() + 1,
910 Command::Publish(_, _) => 1,
911 Command::MakeMoveVec(_, src) => src.len() + 1,
912 Command::Upgrade(_, _, _, _) => 1,
913 })
914 .unwrap()
915}
916
917#[cfg(test)]
918mod tests {
919 use super::*;
920 use crate::authority::test_authority_builder::TestAuthorityBuilder;
921 use crate::checkpoints::CheckpointStore;
922 use crate::consensus_adapter::{
923 ConsensusAdapter, ConsensusAdapterMetrics, MockConsensusClient,
924 };
925 use std::collections::BTreeMap;
926 use sui_protocol_config::ProtocolConfig;
927 use sui_types::base_types::{ObjectID, SequenceNumber, SuiAddress};
928 use sui_types::transaction::{
929 Argument, CallArg, ObjectArg, ProgrammableMoveCall, SharedObjectMutability,
930 };
931 use {
932 rand::{Rng, SeedableRng},
933 sui_protocol_config::ProtocolVersion,
934 sui_types::supported_protocol_versions::Chain,
935 };
936
937 #[tokio::test]
938 async fn test_record_local_observations() {
939 telemetry_subscribers::init_for_testing();
940
941 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
942 config.set_per_object_congestion_control_mode_for_testing(
943 PerObjectCongestionControlMode::ExecutionTimeEstimate(
944 ExecutionTimeEstimateParams {
945 target_utilization: 100,
946 allowed_txn_cost_overage_burst_limit_us: 0,
947 randomness_scalar: 100,
948 max_estimate_us: u64::MAX,
949 stored_observations_num_included_checkpoints: 10,
950 stored_observations_limit: u64::MAX,
951 stake_weighted_median_threshold: 0,
952 default_none_duration_for_new_keys: true,
953 observations_chunk_size: Some(18),
954 },
955 ),
956 );
957 config
958 });
959
960 let mock_consensus_client = MockConsensusClient::new();
961 let authority = TestAuthorityBuilder::new().build().await;
962 let epoch_store = authority.epoch_store_for_testing();
963 let consensus_adapter = Arc::new(ConsensusAdapter::new(
964 Arc::new(mock_consensus_client),
965 CheckpointStore::new_for_tests(),
966 authority.name,
967 100_000,
968 100_000,
969 ConsensusAdapterMetrics::new_test(),
970 Arc::new(tokio::sync::Notify::new()),
971 ));
972 let mut observer = ExecutionTimeObserver::new_for_testing(
973 epoch_store.clone(),
974 Box::new(consensus_adapter.clone()),
975 Duration::ZERO, false, );
978
979 let package = ObjectID::random();
981 let module = "test_module".to_string();
982 let function = "test_function".to_string();
983 let ptb = ProgrammableTransaction {
984 inputs: vec![],
985 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
986 package,
987 module: module.clone(),
988 function: function.clone(),
989 type_arguments: vec![],
990 arguments: vec![],
991 }))],
992 };
993
994 let timings = vec![ExecutionTiming::Success(Duration::from_millis(100))];
996 let total_duration = Duration::from_millis(110);
997 observer.record_local_observations(&ptb, &timings, total_duration, 1);
998
999 let key = ExecutionTimeObservationKey::MoveEntryPoint {
1000 package,
1001 module: module.clone(),
1002 function: function.clone(),
1003 type_arguments: vec![],
1004 };
1005
1006 let local_obs = observer.local_observations.get(&key).unwrap();
1008 assert_eq!(
1009 local_obs.get_average(),
1010 Duration::from_millis(110)
1012 );
1013 assert_eq!(local_obs.last_shared.unwrap().0, Duration::from_millis(110));
1014
1015 let timings = vec![ExecutionTiming::Success(Duration::from_millis(110))];
1017 let total_duration = Duration::from_millis(120);
1018 observer.record_local_observations(&ptb, &timings, total_duration, 1);
1019
1020 let local_obs = observer.local_observations.get(&key).unwrap();
1022 assert_eq!(
1023 local_obs.get_average(),
1024 Duration::from_millis(115)
1026 );
1027 assert_eq!(local_obs.last_shared.unwrap().0, Duration::from_millis(110));
1029
1030 let timings = vec![ExecutionTiming::Success(Duration::from_millis(120))];
1032 let total_duration = Duration::from_millis(130);
1033 observer.record_local_observations(&ptb, &timings, total_duration, 1);
1034
1035 let local_obs = observer.local_observations.get(&key).unwrap();
1037 assert_eq!(
1038 local_obs.get_average(),
1039 Duration::from_millis(120)
1041 );
1042 assert_eq!(local_obs.last_shared.unwrap().0, Duration::from_millis(110));
1045
1046 observer
1048 .local_observations
1049 .get_mut(&key)
1050 .unwrap()
1051 .last_shared = Some((
1052 Duration::from_millis(110),
1053 Instant::now() - Duration::from_secs(60),
1054 ));
1055
1056 let timings = vec![ExecutionTiming::Success(Duration::from_millis(120))];
1058 let total_duration = Duration::from_millis(160);
1059 observer.record_local_observations(&ptb, &timings, total_duration, 1);
1060
1061 let local_obs = observer.local_observations.get(&key).unwrap();
1064 assert_eq!(
1065 local_obs.get_average(),
1066 Duration::from_millis(130)
1068 );
1069 assert_eq!(local_obs.last_shared.unwrap().0, Duration::from_millis(130));
1070 }
1071
1072 #[tokio::test]
1073 async fn test_record_local_observations_with_gas_price_weighting() {
1074 telemetry_subscribers::init_for_testing();
1075
1076 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1077 config.set_per_object_congestion_control_mode_for_testing(
1078 PerObjectCongestionControlMode::ExecutionTimeEstimate(
1079 ExecutionTimeEstimateParams {
1080 target_utilization: 100,
1081 allowed_txn_cost_overage_burst_limit_us: 0,
1082 randomness_scalar: 100,
1083 max_estimate_us: u64::MAX,
1084 stored_observations_num_included_checkpoints: 10,
1085 stored_observations_limit: u64::MAX,
1086 stake_weighted_median_threshold: 0,
1087 default_none_duration_for_new_keys: true,
1088 observations_chunk_size: Some(18),
1089 },
1090 ),
1091 );
1092 config
1093 });
1094
1095 let mock_consensus_client = MockConsensusClient::new();
1096 let authority = TestAuthorityBuilder::new().build().await;
1097 let epoch_store = authority.epoch_store_for_testing();
1098 let consensus_adapter = Arc::new(ConsensusAdapter::new(
1099 Arc::new(mock_consensus_client),
1100 CheckpointStore::new_for_tests(),
1101 authority.name,
1102 100_000,
1103 100_000,
1104 ConsensusAdapterMetrics::new_test(),
1105 Arc::new(tokio::sync::Notify::new()),
1106 ));
1107 let mut observer = ExecutionTimeObserver::new_for_testing(
1108 epoch_store.clone(),
1109 Box::new(consensus_adapter.clone()),
1110 Duration::ZERO, true, );
1113
1114 let package = ObjectID::random();
1116 let module = "test_module".to_string();
1117 let function = "test_function".to_string();
1118 let ptb = ProgrammableTransaction {
1119 inputs: vec![],
1120 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
1121 package,
1122 module: module.clone(),
1123 function: function.clone(),
1124 type_arguments: vec![],
1125 arguments: vec![],
1126 }))],
1127 };
1128
1129 let timings = vec![ExecutionTiming::Success(Duration::from_millis(100))];
1131 let total_duration = Duration::from_millis(110);
1132 observer.record_local_observations(&ptb, &timings, total_duration, 1);
1133
1134 let key = ExecutionTimeObservationKey::MoveEntryPoint {
1135 package,
1136 module: module.clone(),
1137 function: function.clone(),
1138 type_arguments: vec![],
1139 };
1140
1141 let local_obs = observer.local_observations.get(&key).unwrap();
1143 assert_eq!(
1144 local_obs.get_average(),
1145 Duration::from_millis(110)
1147 );
1148 assert_eq!(local_obs.last_shared.unwrap().0, Duration::from_millis(110));
1149
1150 let timings = vec![ExecutionTiming::Success(Duration::from_millis(110))];
1152 let total_duration = Duration::from_millis(120);
1153 observer.record_local_observations(&ptb, &timings, total_duration, 2);
1154
1155 let local_obs = observer.local_observations.get(&key).unwrap();
1157 assert_eq!(
1158 local_obs.get_average(),
1159 Duration::from_micros(116_666)
1162 );
1163 }
1164
1165 #[tokio::test]
1166 async fn test_record_local_observations_with_multiple_commands() {
1167 telemetry_subscribers::init_for_testing();
1168
1169 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1170 config.set_per_object_congestion_control_mode_for_testing(
1171 PerObjectCongestionControlMode::ExecutionTimeEstimate(
1172 ExecutionTimeEstimateParams {
1173 target_utilization: 100,
1174 allowed_txn_cost_overage_burst_limit_us: 0,
1175 randomness_scalar: 0,
1176 max_estimate_us: u64::MAX,
1177 stored_observations_num_included_checkpoints: 10,
1178 stored_observations_limit: u64::MAX,
1179 stake_weighted_median_threshold: 0,
1180 default_none_duration_for_new_keys: true,
1181 observations_chunk_size: Some(18),
1182 },
1183 ),
1184 );
1185 config
1186 });
1187
1188 let mock_consensus_client = MockConsensusClient::new();
1189 let authority = TestAuthorityBuilder::new().build().await;
1190 let epoch_store = authority.epoch_store_for_testing();
1191 let consensus_adapter = Arc::new(ConsensusAdapter::new(
1192 Arc::new(mock_consensus_client),
1193 CheckpointStore::new_for_tests(),
1194 authority.name,
1195 100_000,
1196 100_000,
1197 ConsensusAdapterMetrics::new_test(),
1198 Arc::new(tokio::sync::Notify::new()),
1199 ));
1200 let mut observer = ExecutionTimeObserver::new_for_testing(
1201 epoch_store.clone(),
1202 Box::new(consensus_adapter.clone()),
1203 Duration::ZERO, false, );
1206
1207 let package = ObjectID::random();
1209 let module = "test_module".to_string();
1210 let function = "test_function".to_string();
1211 let ptb = ProgrammableTransaction {
1212 inputs: vec![],
1213 commands: vec![
1214 Command::MoveCall(Box::new(ProgrammableMoveCall {
1215 package,
1216 module: module.clone(),
1217 function: function.clone(),
1218 type_arguments: vec![],
1219 arguments: vec![],
1220 })),
1221 Command::TransferObjects(
1222 vec![Argument::Input(1), Argument::Input(2)],
1224 Argument::Input(0),
1225 ),
1226 ],
1227 };
1228 let timings = vec![
1229 ExecutionTiming::Success(Duration::from_millis(100)),
1230 ExecutionTiming::Success(Duration::from_millis(50)),
1231 ];
1232 let total_duration = Duration::from_millis(180);
1233 observer.record_local_observations(&ptb, &timings, total_duration, 1);
1234
1235 let move_key = ExecutionTimeObservationKey::MoveEntryPoint {
1237 package,
1238 module: module.clone(),
1239 function: function.clone(),
1240 type_arguments: vec![],
1241 };
1242 let move_obs = observer.local_observations.get(&move_key).unwrap();
1243 assert_eq!(
1244 move_obs.get_average(),
1245 Duration::from_millis(120)
1247 );
1248
1249 let transfer_obs = observer
1250 .local_observations
1251 .get(&ExecutionTimeObservationKey::TransferObjects)
1252 .unwrap();
1253 assert_eq!(
1254 transfer_obs.get_average(),
1255 Duration::from_millis(20)
1259 );
1260 }
1261
1262 #[tokio::test]
1263 async fn test_record_local_observations_with_object_utilization_threshold() {
1264 telemetry_subscribers::init_for_testing();
1265
1266 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1267 config.set_per_object_congestion_control_mode_for_testing(
1268 PerObjectCongestionControlMode::ExecutionTimeEstimate(
1269 ExecutionTimeEstimateParams {
1270 target_utilization: 100,
1271 allowed_txn_cost_overage_burst_limit_us: 0,
1272 randomness_scalar: 0,
1273 max_estimate_us: u64::MAX,
1274 stored_observations_num_included_checkpoints: 10,
1275 stored_observations_limit: u64::MAX,
1276 stake_weighted_median_threshold: 0,
1277 default_none_duration_for_new_keys: true,
1278 observations_chunk_size: Some(18),
1279 },
1280 ),
1281 );
1282 config
1283 });
1284
1285 let mock_consensus_client = MockConsensusClient::new();
1286 let authority = TestAuthorityBuilder::new().build().await;
1287 let epoch_store = authority.epoch_store_for_testing();
1288 let consensus_adapter = Arc::new(ConsensusAdapter::new(
1289 Arc::new(mock_consensus_client),
1290 CheckpointStore::new_for_tests(),
1291 authority.name,
1292 100_000,
1293 100_000,
1294 ConsensusAdapterMetrics::new_test(),
1295 Arc::new(tokio::sync::Notify::new()),
1296 ));
1297 let mut observer = ExecutionTimeObserver::new_for_testing(
1298 epoch_store.clone(),
1299 Box::new(consensus_adapter.clone()),
1300 Duration::from_millis(500), false, );
1303
1304 let package = ObjectID::random();
1306 let module = "test_module".to_string();
1307 let function = "test_function".to_string();
1308 let shared_object_id = ObjectID::random();
1309 let ptb = ProgrammableTransaction {
1310 inputs: vec![CallArg::Object(ObjectArg::SharedObject {
1311 id: shared_object_id,
1312 initial_shared_version: SequenceNumber::new(),
1313 mutability: SharedObjectMutability::Mutable,
1314 })],
1315 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
1316 package,
1317 module: module.clone(),
1318 function: function.clone(),
1319 type_arguments: vec![],
1320 arguments: vec![],
1321 }))],
1322 };
1323 let key = ExecutionTimeObservationKey::MoveEntryPoint {
1324 package,
1325 module: module.clone(),
1326 function: function.clone(),
1327 type_arguments: vec![],
1328 };
1329
1330 tokio::time::pause();
1331
1332 let timings = vec![ExecutionTiming::Success(Duration::from_secs(1))];
1334 observer.record_local_observations(&ptb, &timings, Duration::from_secs(2), 1);
1335 assert!(
1336 observer
1337 .local_observations
1338 .get(&key)
1339 .unwrap()
1340 .last_shared
1341 .is_none()
1342 );
1343
1344 let timings = vec![ExecutionTiming::Success(Duration::from_secs(1))];
1346 observer.record_local_observations(&ptb, &timings, Duration::from_secs(2), 1);
1347 assert_eq!(
1348 observer
1349 .local_observations
1350 .get(&key)
1351 .unwrap()
1352 .last_shared
1353 .unwrap()
1354 .0,
1355 Duration::from_secs(2)
1356 );
1357
1358 tokio::time::advance(Duration::from_secs(5)).await;
1360 let timings = vec![ExecutionTiming::Success(Duration::from_secs(3))];
1361 observer.record_local_observations(&ptb, &timings, Duration::from_secs(5), 1);
1362 assert_eq!(
1363 observer
1364 .local_observations
1365 .get(&key)
1366 .unwrap()
1367 .last_shared
1368 .unwrap()
1369 .0,
1370 Duration::from_secs(3)
1371 );
1372
1373 tokio::time::advance(Duration::from_millis(150)).await;
1376 let timings = vec![ExecutionTiming::Success(Duration::from_millis(100))];
1377 observer.record_local_observations(&ptb, &timings, Duration::from_millis(500), 1);
1378 assert_eq!(
1379 observer
1380 .local_observations
1381 .get(&key)
1382 .unwrap()
1383 .last_shared
1384 .unwrap()
1385 .0,
1386 Duration::from_secs(3) );
1388
1389 tokio::time::advance(Duration::from_secs(60)).await;
1391 let timings = vec![ExecutionTiming::Success(Duration::from_secs(11))];
1392 observer.record_local_observations(&ptb, &timings, Duration::from_secs(11), 1);
1393 assert_eq!(
1394 observer
1395 .local_observations
1396 .get(&key)
1397 .unwrap()
1398 .last_shared
1399 .unwrap()
1400 .0,
1401 Duration::from_secs(3) );
1403 }
1404
1405 #[tokio::test]
1406 async fn test_object_utilization_metric_tracked_ids() {
1407 telemetry_subscribers::init_for_testing();
1408
1409 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1410 config.set_per_object_congestion_control_mode_for_testing(
1411 PerObjectCongestionControlMode::ExecutionTimeEstimate(
1412 ExecutionTimeEstimateParams {
1413 target_utilization: 100,
1414 allowed_txn_cost_overage_burst_limit_us: 0,
1415 randomness_scalar: 0,
1416 max_estimate_us: u64::MAX,
1417 stored_observations_num_included_checkpoints: 10,
1418 stored_observations_limit: u64::MAX,
1419 stake_weighted_median_threshold: 0,
1420 default_none_duration_for_new_keys: true,
1421 observations_chunk_size: Some(18),
1422 },
1423 ),
1424 );
1425 config
1426 });
1427
1428 let mock_consensus_client = MockConsensusClient::new();
1429 let authority = TestAuthorityBuilder::new().build().await;
1430 let epoch_store = authority.epoch_store_for_testing();
1431 let consensus_adapter = Arc::new(ConsensusAdapter::new(
1432 Arc::new(mock_consensus_client),
1433 CheckpointStore::new_for_tests(),
1434 authority.name,
1435 100_000,
1436 100_000,
1437 ConsensusAdapterMetrics::new_test(),
1438 Arc::new(tokio::sync::Notify::new()),
1439 ));
1440 let mut observer = ExecutionTimeObserver::new_for_testing(
1441 epoch_store.clone(),
1442 Box::new(consensus_adapter.clone()),
1443 Duration::from_millis(500),
1444 false,
1445 );
1446
1447 let mut bytes = [0u8; ObjectID::LENGTH];
1449 bytes[ObjectID::LENGTH - 1] = 0x05;
1450 bytes[0] = 1;
1451 let tracked_id = ObjectID::new(bytes);
1452 bytes[0] = 2;
1453 let untracked_id = ObjectID::new(bytes);
1454 let bucket_key = "5";
1455 let tracked_name = "tracked-object";
1456 observer.config.object_utilization_metric_tracked_ids =
1457 Some(BTreeMap::from([(tracked_id, tracked_name.to_string())]));
1458 observer.tracked_object_counters =
1459 tracked_object_counters(&observer.config, &epoch_store.metrics);
1460
1461 let package = ObjectID::random();
1462 let make_ptb = |id: ObjectID| ProgrammableTransaction {
1463 inputs: vec![CallArg::Object(ObjectArg::SharedObject {
1464 id,
1465 initial_shared_version: SequenceNumber::new(),
1466 mutability: SharedObjectMutability::Mutable,
1467 })],
1468 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
1469 package,
1470 module: "test_module".to_string(),
1471 function: "test_function".to_string(),
1472 type_arguments: vec![],
1473 arguments: vec![],
1474 }))],
1475 };
1476 let tracked_ptb = make_ptb(tracked_id);
1477 let untracked_ptb = make_ptb(untracked_id);
1478 let aggregate_metric = &epoch_store
1479 .metrics
1480 .epoch_execution_time_observer_object_utilization;
1481 let tracked_metric = &epoch_store
1482 .metrics
1483 .epoch_execution_time_observer_tracked_object_utilization;
1484 let timings = vec![ExecutionTiming::Success(Duration::from_secs(1))];
1485
1486 tokio::time::pause();
1487
1488 observer.record_local_observations(&tracked_ptb, &timings, Duration::from_secs(2), 1);
1491 observer.record_local_observations(&untracked_ptb, &timings, Duration::from_secs(2), 1);
1492 assert_eq!(
1493 tracked_metric
1494 .with_label_values(&[tracked_id.to_string().as_str(), tracked_name])
1495 .get(),
1496 2.0
1497 );
1498 assert_eq!(aggregate_metric.with_label_values(&[bucket_key]).get(), 0.0);
1499
1500 observer.record_local_observations(&tracked_ptb, &timings, Duration::from_secs(2), 1);
1503 observer.record_local_observations(&untracked_ptb, &timings, Duration::from_secs(2), 1);
1504 assert_eq!(
1505 tracked_metric
1506 .with_label_values(&[tracked_id.to_string().as_str(), tracked_name])
1507 .get(),
1508 4.0
1509 );
1510 assert_eq!(
1511 tracked_metric
1512 .with_label_values(&[untracked_id.to_string().as_str(), tracked_name])
1513 .get(),
1514 0.0
1515 );
1516 assert_eq!(aggregate_metric.with_label_values(&[bucket_key]).get(), 4.0);
1517 }
1518
1519 #[tokio::test]
1520 async fn test_record_local_observations_with_indebted_objects() {
1521 telemetry_subscribers::init_for_testing();
1522
1523 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1524 config.set_per_object_congestion_control_mode_for_testing(
1525 PerObjectCongestionControlMode::ExecutionTimeEstimate(
1526 ExecutionTimeEstimateParams {
1527 target_utilization: 100,
1528 allowed_txn_cost_overage_burst_limit_us: 0,
1529 randomness_scalar: 0,
1530 max_estimate_us: u64::MAX,
1531 stored_observations_num_included_checkpoints: 10,
1532 stored_observations_limit: u64::MAX,
1533 stake_weighted_median_threshold: 0,
1534 default_none_duration_for_new_keys: true,
1535 observations_chunk_size: Some(18),
1536 },
1537 ),
1538 );
1539 config
1540 });
1541
1542 let mock_consensus_client = MockConsensusClient::new();
1543 let authority = TestAuthorityBuilder::new().build().await;
1544 let epoch_store = authority.epoch_store_for_testing();
1545 let consensus_adapter = Arc::new(ConsensusAdapter::new(
1546 Arc::new(mock_consensus_client),
1547 CheckpointStore::new_for_tests(),
1548 authority.name,
1549 100_000,
1550 100_000,
1551 ConsensusAdapterMetrics::new_test(),
1552 Arc::new(tokio::sync::Notify::new()),
1553 ));
1554 let mut observer = ExecutionTimeObserver::new_for_testing(
1555 epoch_store.clone(),
1556 Box::new(consensus_adapter.clone()),
1557 Duration::from_millis(500), false, );
1560
1561 let package = ObjectID::random();
1563 let module = "test_module".to_string();
1564 let function = "test_function".to_string();
1565 let shared_object_id = ObjectID::random();
1566 let ptb = ProgrammableTransaction {
1567 inputs: vec![CallArg::Object(ObjectArg::SharedObject {
1568 id: shared_object_id,
1569 initial_shared_version: SequenceNumber::new(),
1570 mutability: SharedObjectMutability::Mutable,
1571 })],
1572 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
1573 package,
1574 module: module.clone(),
1575 function: function.clone(),
1576 type_arguments: vec![],
1577 arguments: vec![],
1578 }))],
1579 };
1580 let key = ExecutionTimeObservationKey::MoveEntryPoint {
1581 package,
1582 module: module.clone(),
1583 function: function.clone(),
1584 type_arguments: vec![],
1585 };
1586
1587 tokio::time::pause();
1588
1589 let timings = vec![ExecutionTiming::Success(Duration::from_secs(1))];
1591 observer.record_local_observations(&ptb, &timings, Duration::from_secs(1), 1);
1592 assert!(
1593 observer
1594 .local_observations
1595 .get(&key)
1596 .unwrap()
1597 .last_shared
1598 .is_none()
1599 );
1600
1601 let timings = vec![ExecutionTiming::Success(Duration::from_secs(2))];
1603 observer.record_local_observations(&ptb, &timings, Duration::from_secs(2), 1);
1604 assert_eq!(
1605 observer
1606 .local_observations
1607 .get(&key)
1608 .unwrap()
1609 .last_shared
1610 .unwrap()
1611 .0,
1612 Duration::from_millis(1500) );
1614
1615 observer.update_indebted_objects(vec![shared_object_id]);
1617 observer
1618 .config
1619 .observation_sharing_object_utilization_threshold = Some(Duration::from_secs(1000));
1620
1621 tokio::time::advance(Duration::from_secs(60)).await;
1624 let timings = vec![ExecutionTiming::Success(Duration::from_millis(300))];
1625 observer.record_local_observations(&ptb, &timings, Duration::from_millis(300), 1);
1626
1627 assert_eq!(
1630 observer
1631 .local_observations
1632 .get(&key)
1633 .unwrap()
1634 .last_shared
1635 .unwrap()
1636 .0,
1637 Duration::from_millis(1100)
1638 );
1639 }
1640
1641 #[tokio::test]
1642 async fn test_stake_weighted_median() {
1643 telemetry_subscribers::init_for_testing();
1644
1645 let (committee, _) =
1646 Committee::new_simple_test_committee_with_normalized_voting_power(vec![10, 20, 30, 40]);
1647
1648 let params = ExecutionTimeEstimateParams {
1649 stake_weighted_median_threshold: 0,
1650 ..Default::default()
1651 };
1652
1653 let mut tracker = ConsensusObservations {
1654 observations: vec![
1655 (0, Some(Duration::from_secs(1))), (0, Some(Duration::from_secs(2))), (0, Some(Duration::from_secs(3))), (0, Some(Duration::from_secs(4))), ],
1660 stake_weighted_median: None,
1661 };
1662 tracker.update_stake_weighted_median(&committee, ¶ms);
1663 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(3)));
1670
1671 let mut tracker = ConsensusObservations {
1673 observations: vec![
1674 (0, Some(Duration::from_secs(3))), (0, Some(Duration::from_secs(4))), (0, Some(Duration::from_secs(1))), (0, Some(Duration::from_secs(2))), ],
1679 stake_weighted_median: None,
1680 };
1681 tracker.update_stake_weighted_median(&committee, ¶ms);
1682 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(2)));
1689
1690 let mut tracker = ConsensusObservations {
1692 observations: vec![
1693 (0, Some(Duration::from_secs(1))), (0, None), (0, Some(Duration::from_secs(3))), (0, Some(Duration::from_secs(4))), ],
1698 stake_weighted_median: None,
1699 };
1700 tracker.update_stake_weighted_median(&committee, ¶ms);
1701 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(4)));
1707
1708 let mut tracker = ConsensusObservations {
1710 observations: vec![
1711 (0, Some(Duration::from_secs(1))), (0, Some(Duration::from_secs(2))), (0, None), (0, None), ],
1716 stake_weighted_median: None,
1717 };
1718 tracker.update_stake_weighted_median(&committee, ¶ms);
1719 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(2)));
1724
1725 let mut tracker = ConsensusObservations {
1727 observations: vec![
1728 (0, None), (0, None), (0, Some(Duration::from_secs(3))), (0, None), ],
1733 stake_weighted_median: None,
1734 };
1735 tracker.update_stake_weighted_median(&committee, ¶ms);
1736 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(3)));
1738
1739 let mut tracker = ConsensusObservations {
1741 observations: vec![
1742 (0, Some(Duration::from_secs(5))), (0, Some(Duration::from_secs(5))), (0, Some(Duration::from_secs(5))), (0, Some(Duration::from_secs(5))), ],
1747 stake_weighted_median: None,
1748 };
1749 tracker.update_stake_weighted_median(&committee, ¶ms);
1750 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(5)));
1751 }
1752
1753 #[tokio::test]
1754 async fn test_stake_weighted_median_threshold() {
1755 telemetry_subscribers::init_for_testing();
1756
1757 let (committee, _) =
1758 Committee::new_simple_test_committee_with_normalized_voting_power(vec![10, 20, 30, 40]);
1759
1760 let params = ExecutionTimeEstimateParams {
1762 stake_weighted_median_threshold: 5000,
1763 ..Default::default()
1764 };
1765
1766 let mut tracker = ConsensusObservations {
1768 observations: vec![
1769 (0, Some(Duration::from_secs(1))), (0, Some(Duration::from_secs(2))), (0, None), (0, None), ],
1774 stake_weighted_median: None,
1775 };
1776 tracker.update_stake_weighted_median(&committee, ¶ms);
1777 assert_eq!(tracker.stake_weighted_median, None);
1779
1780 let mut tracker = ConsensusObservations {
1782 observations: vec![
1783 (0, Some(Duration::from_secs(1))), (0, Some(Duration::from_secs(2))), (0, Some(Duration::from_secs(3))), (0, None), ],
1788 stake_weighted_median: None,
1789 };
1790 tracker.update_stake_weighted_median(&committee, ¶ms);
1791 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(3)));
1793 }
1794
1795 #[tokio::test]
1796 async fn test_execution_time_estimator() {
1797 telemetry_subscribers::init_for_testing();
1798
1799 let (committee, _) =
1800 Committee::new_simple_test_committee_with_normalized_voting_power(vec![10, 20, 30, 40]);
1801 let mut estimator = ExecutionTimeEstimator::new(
1802 Arc::new(committee),
1803 ExecutionTimeEstimateParams {
1804 target_utilization: 50,
1805 max_estimate_us: 1_500_000,
1806
1807 allowed_txn_cost_overage_burst_limit_us: 0,
1809 randomness_scalar: 0,
1810 stored_observations_num_included_checkpoints: 10,
1811 stored_observations_limit: u64::MAX,
1812 stake_weighted_median_threshold: 0,
1813 default_none_duration_for_new_keys: true,
1814 observations_chunk_size: Some(18),
1815 },
1816 std::iter::empty(),
1817 );
1818 let package = ObjectID::random();
1820 let module = "test_module".to_string();
1821 let function = "test_function".to_string();
1822 let move_key = ExecutionTimeObservationKey::MoveEntryPoint {
1823 package,
1824 module: module.clone(),
1825 function: function.clone(),
1826 type_arguments: vec![],
1827 };
1828 let transfer_key = ExecutionTimeObservationKey::TransferObjects;
1829
1830 estimator.process_observation_from_consensus(
1833 0,
1834 Some(1),
1835 move_key.clone(),
1836 Duration::from_millis(1000),
1837 false,
1838 );
1839 estimator.process_observation_from_consensus(
1840 1,
1841 Some(1),
1842 move_key.clone(),
1843 Duration::from_millis(1000),
1844 false,
1845 );
1846 estimator.process_observation_from_consensus(
1847 2,
1848 Some(1),
1849 move_key.clone(),
1850 Duration::from_millis(1000),
1851 false,
1852 );
1853
1854 estimator.process_observation_from_consensus(
1855 0,
1856 Some(1),
1857 transfer_key.clone(),
1858 Duration::from_millis(500),
1859 false,
1860 );
1861 estimator.process_observation_from_consensus(
1862 1,
1863 Some(1),
1864 transfer_key.clone(),
1865 Duration::from_millis(500),
1866 false,
1867 );
1868 estimator.process_observation_from_consensus(
1869 2,
1870 Some(1),
1871 transfer_key.clone(),
1872 Duration::from_millis(500),
1873 false,
1874 );
1875
1876 estimator.process_observation_from_consensus(
1878 0,
1879 Some(2),
1880 move_key.clone(),
1881 Duration::from_millis(100),
1882 false,
1883 );
1884 estimator.process_observation_from_consensus(
1885 1,
1886 Some(2),
1887 move_key.clone(),
1888 Duration::from_millis(200),
1889 false,
1890 );
1891 estimator.process_observation_from_consensus(
1892 2,
1893 Some(2),
1894 move_key.clone(),
1895 Duration::from_millis(300),
1896 false,
1897 );
1898
1899 estimator.process_observation_from_consensus(
1900 0,
1901 Some(2),
1902 transfer_key.clone(),
1903 Duration::from_millis(50),
1904 false,
1905 );
1906 estimator.process_observation_from_consensus(
1907 1,
1908 Some(2),
1909 transfer_key.clone(),
1910 Duration::from_millis(60),
1911 false,
1912 );
1913 estimator.process_observation_from_consensus(
1914 2,
1915 Some(2),
1916 transfer_key.clone(),
1917 Duration::from_millis(70),
1918 false,
1919 );
1920
1921 estimator.process_observation_from_consensus(
1923 0,
1924 Some(1),
1925 move_key.clone(),
1926 Duration::from_millis(1000),
1927 false,
1928 );
1929 estimator.process_observation_from_consensus(
1930 1,
1931 Some(1),
1932 transfer_key.clone(),
1933 Duration::from_millis(500),
1934 false,
1935 );
1936 estimator.process_observation_from_consensus(
1937 2,
1938 Some(1),
1939 move_key.clone(),
1940 Duration::from_millis(1000),
1941 false,
1942 );
1943
1944 let single_move_tx = TransactionData::new_programmable(
1946 SuiAddress::ZERO,
1947 vec![],
1948 ProgrammableTransaction {
1949 inputs: vec![],
1950 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
1951 package,
1952 module: module.clone(),
1953 function: function.clone(),
1954 type_arguments: vec![],
1955 arguments: vec![],
1956 }))],
1957 },
1958 100,
1959 100,
1960 );
1961
1962 assert_eq!(
1964 estimator.get_estimate(&single_move_tx),
1965 Duration::from_millis(300)
1966 );
1967
1968 let multi_command_tx = TransactionData::new_programmable(
1970 SuiAddress::ZERO,
1971 vec![],
1972 ProgrammableTransaction {
1973 inputs: vec![],
1974 commands: vec![
1975 Command::MoveCall(Box::new(ProgrammableMoveCall {
1976 package,
1977 module: module.clone(),
1978 function: function.clone(),
1979 type_arguments: vec![],
1980 arguments: vec![],
1981 })),
1982 Command::TransferObjects(
1983 vec![Argument::Input(1), Argument::Input(2)],
1984 Argument::Input(0),
1985 ),
1986 ],
1987 },
1988 100,
1989 100,
1990 );
1991
1992 assert_eq!(
1995 estimator.get_estimate(&multi_command_tx),
1996 Duration::from_millis(510)
1997 );
1998 }
1999
2000 #[derive(Debug, Clone, Serialize, Deserialize)]
2001 struct ExecutionTimeObserverSnapshot {
2002 protocol_version: u64,
2003 consensus_observations: Vec<(ExecutionTimeObservationKey, ConsensusObservations)>,
2004 transaction_estimates: Vec<(String, Duration)>, }
2006
2007 fn generate_test_inputs(
2008 seed: u64,
2009 num_validators: usize,
2010 generation_override: Option<u64>,
2011 ) -> Vec<(
2012 AuthorityIndex,
2013 Option<u64>,
2014 ExecutionTimeObservationKey,
2015 Duration,
2016 )> {
2017 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
2018
2019 let observation_keys = [
2020 ExecutionTimeObservationKey::MoveEntryPoint {
2021 package: ObjectID::from_hex_literal("0x1").unwrap(),
2022 module: "coin".to_string(),
2023 function: "transfer".to_string(),
2024 type_arguments: vec![],
2025 },
2026 ExecutionTimeObservationKey::MoveEntryPoint {
2027 package: ObjectID::from_hex_literal("0x2").unwrap(),
2028 module: "nft".to_string(),
2029 function: "mint".to_string(),
2030 type_arguments: vec![],
2031 },
2032 ExecutionTimeObservationKey::TransferObjects,
2033 ExecutionTimeObservationKey::SplitCoins,
2034 ExecutionTimeObservationKey::MergeCoins,
2035 ExecutionTimeObservationKey::MakeMoveVec,
2036 ExecutionTimeObservationKey::Upgrade,
2037 ];
2038
2039 let mut inputs = Vec::new();
2040 let target_samples = 25;
2041
2042 for _ in 0..target_samples {
2043 let key = observation_keys[rng.gen_range(0..observation_keys.len())].clone();
2044 let authority_index =
2045 AuthorityIndex::try_from(rng.gen_range(0..num_validators)).unwrap();
2046
2047 let generation = generation_override.unwrap_or_else(|| rng.gen_range(1..=10));
2049
2050 let base_duration = if rng.gen_ratio(1, 20) {
2053 0
2055 } else {
2056 match &key {
2057 ExecutionTimeObservationKey::MoveEntryPoint { .. } => rng.gen_range(50..=500),
2058 ExecutionTimeObservationKey::TransferObjects => rng.gen_range(10..=100),
2059 ExecutionTimeObservationKey::SplitCoins => rng.gen_range(20..=80),
2060 ExecutionTimeObservationKey::MergeCoins => rng.gen_range(15..=70),
2061 ExecutionTimeObservationKey::MakeMoveVec => rng.gen_range(5..=30),
2062 ExecutionTimeObservationKey::Upgrade => rng.gen_range(100..=1000),
2063 ExecutionTimeObservationKey::Publish => rng.gen_range(200..=2000),
2064 }
2065 };
2066
2067 let duration = Duration::from_millis(base_duration);
2068
2069 inputs.push((authority_index, Some(generation), key, duration));
2070 }
2071
2072 inputs
2073 }
2074
2075 fn generate_test_transactions(seed: u64) -> Vec<(String, TransactionData)> {
2076 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
2077 let mut transactions = Vec::new();
2078
2079 let package3 = ObjectID::from_hex_literal("0x3").unwrap();
2080 transactions.push((
2081 "coin_transfer_call".to_string(),
2082 TransactionData::new_programmable(
2083 SuiAddress::ZERO,
2084 vec![],
2085 ProgrammableTransaction {
2086 inputs: vec![],
2087 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
2088 package: ObjectID::from_hex_literal("0x1").unwrap(),
2089 module: "coin".to_string(),
2090 function: "transfer".to_string(),
2091 type_arguments: vec![],
2092 arguments: vec![],
2093 }))],
2094 },
2095 rng.gen_range(100..1000),
2096 rng.gen_range(100..1000),
2097 ),
2098 ));
2099
2100 transactions.push((
2101 "mixed_move_calls".to_string(),
2102 TransactionData::new_programmable(
2103 SuiAddress::ZERO,
2104 vec![],
2105 ProgrammableTransaction {
2106 inputs: vec![],
2107 commands: vec![
2108 Command::MoveCall(Box::new(ProgrammableMoveCall {
2109 package: ObjectID::from_hex_literal("0x1").unwrap(),
2110 module: "coin".to_string(),
2111 function: "transfer".to_string(),
2112 type_arguments: vec![],
2113 arguments: vec![],
2114 })),
2115 Command::MoveCall(Box::new(ProgrammableMoveCall {
2116 package: ObjectID::from_hex_literal("0x2").unwrap(),
2117 module: "nft".to_string(),
2118 function: "mint".to_string(),
2119 type_arguments: vec![],
2120 arguments: vec![],
2121 })),
2122 ],
2123 },
2124 rng.gen_range(100..1000),
2125 rng.gen_range(100..1000),
2126 ),
2127 ));
2128
2129 transactions.push((
2130 "native_commands_with_observations".to_string(),
2131 TransactionData::new_programmable(
2132 SuiAddress::ZERO,
2133 vec![],
2134 ProgrammableTransaction {
2135 inputs: vec![],
2136 commands: vec![
2137 Command::TransferObjects(vec![Argument::Input(0)], Argument::Input(1)),
2138 Command::SplitCoins(Argument::Input(2), vec![Argument::Input(3)]),
2139 Command::MergeCoins(Argument::Input(4), vec![Argument::Input(5)]),
2140 Command::MakeMoveVec(None, vec![Argument::Input(6)]),
2141 ],
2142 },
2143 rng.gen_range(100..1000),
2144 rng.gen_range(100..1000),
2145 ),
2146 ));
2147
2148 let num_objects = rng.gen_range(1..=5);
2149 transactions.push((
2150 format!("transfer_objects_{}_items", num_objects),
2151 TransactionData::new_programmable(
2152 SuiAddress::ZERO,
2153 vec![],
2154 ProgrammableTransaction {
2155 inputs: vec![],
2156 commands: vec![Command::TransferObjects(
2157 (0..num_objects).map(Argument::Input).collect(),
2158 Argument::Input(num_objects),
2159 )],
2160 },
2161 rng.gen_range(100..1000),
2162 rng.gen_range(100..1000),
2163 ),
2164 ));
2165
2166 let num_amounts = rng.gen_range(1..=4);
2167 transactions.push((
2168 format!("split_coins_{}_amounts", num_amounts),
2169 TransactionData::new_programmable(
2170 SuiAddress::ZERO,
2171 vec![],
2172 ProgrammableTransaction {
2173 inputs: vec![],
2174 commands: vec![Command::SplitCoins(
2175 Argument::Input(0),
2176 (1..=num_amounts).map(Argument::Input).collect(),
2177 )],
2178 },
2179 rng.gen_range(100..1000),
2180 rng.gen_range(100..1000),
2181 ),
2182 ));
2183
2184 let num_sources = rng.gen_range(1..=3);
2185 transactions.push((
2186 format!("merge_coins_{}_sources", num_sources),
2187 TransactionData::new_programmable(
2188 SuiAddress::ZERO,
2189 vec![],
2190 ProgrammableTransaction {
2191 inputs: vec![],
2192 commands: vec![Command::MergeCoins(
2193 Argument::Input(0),
2194 (1..=num_sources).map(Argument::Input).collect(),
2195 )],
2196 },
2197 rng.gen_range(100..1000),
2198 rng.gen_range(100..1000),
2199 ),
2200 ));
2201
2202 let num_elements = rng.gen_range(0..=6);
2203 transactions.push((
2204 format!("make_move_vec_{}_elements", num_elements),
2205 TransactionData::new_programmable(
2206 SuiAddress::ZERO,
2207 vec![],
2208 ProgrammableTransaction {
2209 inputs: vec![],
2210 commands: vec![Command::MakeMoveVec(
2211 None,
2212 (0..num_elements).map(Argument::Input).collect(),
2213 )],
2214 },
2215 rng.gen_range(100..1000),
2216 rng.gen_range(100..1000),
2217 ),
2218 ));
2219
2220 transactions.push((
2221 "mixed_commands".to_string(),
2222 TransactionData::new_programmable(
2223 SuiAddress::ZERO,
2224 vec![],
2225 ProgrammableTransaction {
2226 inputs: vec![],
2227 commands: vec![
2228 Command::MoveCall(Box::new(ProgrammableMoveCall {
2229 package: package3,
2230 module: "game".to_string(),
2231 function: "play".to_string(),
2232 type_arguments: vec![],
2233 arguments: vec![],
2234 })),
2235 Command::TransferObjects(
2236 vec![Argument::Input(1), Argument::Input(2)],
2237 Argument::Input(0),
2238 ),
2239 Command::SplitCoins(Argument::Input(3), vec![Argument::Input(4)]),
2240 ],
2241 },
2242 rng.gen_range(100..1000),
2243 rng.gen_range(100..1000),
2244 ),
2245 ));
2246
2247 transactions.push((
2248 "upgrade_package".to_string(),
2249 TransactionData::new_programmable(
2250 SuiAddress::ZERO,
2251 vec![],
2252 ProgrammableTransaction {
2253 inputs: vec![],
2254 commands: vec![Command::Upgrade(
2255 vec![],
2256 vec![],
2257 package3,
2258 Argument::Input(0),
2259 )],
2260 },
2261 rng.gen_range(100..1000),
2262 rng.gen_range(100..1000),
2263 ),
2264 ));
2265
2266 transactions
2267 }
2268
2269 #[test]
2281 fn snapshot_tests() {
2282 println!("\n============================================================================");
2283 println!("! !");
2284 println!("! IMPORTANT: never update snapshots from this test. only add new versions! !");
2285 println!("! !");
2286 println!("============================================================================\n");
2287
2288 let max_version = ProtocolVersion::MAX.as_u64();
2289
2290 let test_versions: Vec<u64> = (max_version.saturating_sub(9)..=max_version).collect();
2291
2292 for version in test_versions {
2293 let protocol_version = ProtocolVersion::new(version);
2294 let protocol_config = ProtocolConfig::get_for_version(protocol_version, Chain::Unknown);
2295 let (committee, _) = Committee::new_simple_test_committee_of_size(4);
2296 let committee = Arc::new(committee);
2297
2298 let initial_generation =
2299 if let PerObjectCongestionControlMode::ExecutionTimeEstimate(params) =
2300 protocol_config.per_object_congestion_control_mode()
2301 {
2302 if params.default_none_duration_for_new_keys {
2303 None
2304 } else {
2305 Some(0)
2306 }
2307 } else {
2308 Some(0) };
2310
2311 let initial_observations =
2312 generate_test_inputs(0, committee.num_members(), initial_generation);
2313 let mut estimator = ExecutionTimeEstimator::new(
2314 committee.clone(),
2315 ExecutionTimeEstimateParams {
2316 max_estimate_us: u64::MAX, ..ExecutionTimeEstimateParams::default()
2318 },
2319 initial_observations.into_iter(),
2320 );
2321
2322 let test_inputs = generate_test_inputs(version, committee.num_members(), None);
2323
2324 for (source, generation, observation_key, duration) in test_inputs {
2325 estimator.process_observation_from_consensus(
2326 source,
2327 generation,
2328 observation_key,
2329 duration,
2330 false,
2331 );
2332 }
2333
2334 let mut final_observations = estimator.get_observations();
2335 final_observations.sort_by_key(|a| a.0.to_string());
2336
2337 let test_transactions = generate_test_transactions(version);
2338 let mut transaction_estimates = Vec::new();
2339 for (description, tx_data) in test_transactions {
2340 let estimate = estimator.get_estimate(&tx_data);
2341 transaction_estimates.push((description, estimate));
2342 }
2343
2344 let snapshot_data = ExecutionTimeObserverSnapshot {
2345 protocol_version: version,
2346 consensus_observations: final_observations.clone(),
2347 transaction_estimates,
2348 };
2349 insta::assert_yaml_snapshot!(
2350 format!("execution_time_observer_v{}", version),
2351 snapshot_data
2352 );
2353 }
2354 }
2355}