1use std::{
5 collections::HashMap,
6 path::Path,
7 sync::{
8 atomic::{AtomicUsize, Ordering},
9 Arc,
10 },
11};
12
13use linera_base::{
14 data_types::{Amount, Timestamp},
15 identifiers::{Account, AccountOwner, ApplicationId, ChainId},
16 time::Instant,
17};
18use linera_core::{
19 client::chain_client::{self, ChainClient},
20 data_types::ClientOutcome,
21 Environment,
22};
23use linera_execution::{system::SystemOperation, Operation};
24use linera_sdk::abis::fungible::FungibleOperation;
25use num_format::{Locale, ToFormattedString};
26use prometheus_parse::{HistogramCount, Scrape, Value};
27use rand::{rngs::SmallRng, seq::SliceRandom, thread_rng, SeedableRng};
28use serde::{Deserialize, Serialize};
29use tokio::{
30 sync::{mpsc, Barrier, Notify},
31 task, time,
32};
33use tokio_util::sync::CancellationToken;
34use tracing::{debug, error, info, warn, Instrument as _};
35
36use crate::chain_listener::{ChainListener, ClientContext};
37
38pub trait OperationGenerator: Send + 'static {
47 fn generate_operations(&mut self, owner: AccountOwner, count: usize) -> Vec<Operation>;
49}
50
51pub struct NativeFungibleTransferGenerator {
53 source_chain_id: ChainId,
54 destination_chains: Vec<ChainId>,
55 destination_index: usize,
56 rng: SmallRng,
57 single_destination_per_block: bool,
58}
59
60impl NativeFungibleTransferGenerator {
61 pub fn new(
63 source_chain_id: ChainId,
64 mut destination_chains: Vec<ChainId>,
65 single_destination_per_block: bool,
66 ) -> Result<Self, BenchmarkError> {
67 if destination_chains.is_empty() {
69 destination_chains.push(source_chain_id);
70 }
71 let mut rng = SmallRng::from_rng(thread_rng())?;
72 destination_chains.shuffle(&mut rng);
73 Ok(Self {
74 source_chain_id,
75 destination_chains,
76 destination_index: 0,
77 rng,
78 single_destination_per_block,
79 })
80 }
81
82 fn next_destination(&mut self) -> ChainId {
83 if self.destination_index >= self.destination_chains.len() {
84 self.destination_chains.shuffle(&mut self.rng);
85 self.destination_index = 0;
86 }
87 let destination_chain_id = self.destination_chains[self.destination_index];
88 self.destination_index += 1;
89 if destination_chain_id == self.source_chain_id && self.destination_chains.len() > 1 {
91 self.next_destination()
92 } else {
93 destination_chain_id
94 }
95 }
96}
97
98impl OperationGenerator for NativeFungibleTransferGenerator {
99 fn generate_operations(&mut self, _owner: AccountOwner, count: usize) -> Vec<Operation> {
100 let amount = Amount::from_attos(1);
101 if self.single_destination_per_block {
102 let recipient = self.next_destination();
103 (0..count)
104 .map(|_| {
105 Operation::system(SystemOperation::Transfer {
106 owner: AccountOwner::CHAIN,
107 recipient: Account::chain(recipient),
108 amount,
109 })
110 })
111 .collect()
112 } else {
113 (0..count)
114 .map(|_| {
115 let recipient = self.next_destination();
116 Operation::system(SystemOperation::Transfer {
117 owner: AccountOwner::CHAIN,
118 recipient: Account::chain(recipient),
119 amount,
120 })
121 })
122 .collect()
123 }
124 }
125}
126
127pub struct FungibleTransferGenerator {
129 application_id: ApplicationId,
130 source_chain_id: ChainId,
131 destination_chains: Vec<ChainId>,
132 destination_index: usize,
133 rng: SmallRng,
134 single_destination_per_block: bool,
135}
136
137impl FungibleTransferGenerator {
138 pub fn new(
140 application_id: ApplicationId,
141 source_chain_id: ChainId,
142 mut destination_chains: Vec<ChainId>,
143 single_destination_per_block: bool,
144 ) -> Result<Self, BenchmarkError> {
145 if destination_chains.is_empty() {
147 destination_chains.push(source_chain_id);
148 }
149 let mut rng = SmallRng::from_rng(thread_rng())?;
150 destination_chains.shuffle(&mut rng);
151 Ok(Self {
152 application_id,
153 source_chain_id,
154 destination_chains,
155 destination_index: 0,
156 rng,
157 single_destination_per_block,
158 })
159 }
160
161 fn next_destination(&mut self) -> ChainId {
162 if self.destination_index >= self.destination_chains.len() {
163 self.destination_chains.shuffle(&mut self.rng);
164 self.destination_index = 0;
165 }
166 let destination_chain_id = self.destination_chains[self.destination_index];
167 self.destination_index += 1;
168 if destination_chain_id == self.source_chain_id && self.destination_chains.len() > 1 {
170 self.next_destination()
171 } else {
172 destination_chain_id
173 }
174 }
175}
176
177impl OperationGenerator for FungibleTransferGenerator {
178 fn generate_operations(&mut self, owner: AccountOwner, count: usize) -> Vec<Operation> {
179 let amount = Amount::from_attos(1);
180 if self.single_destination_per_block {
181 let recipient = self.next_destination();
182 (0..count)
183 .map(|_| fungible_transfer(self.application_id, recipient, owner, owner, amount))
184 .collect()
185 } else {
186 (0..count)
187 .map(|_| {
188 let recipient = self.next_destination();
189 fungible_transfer(self.application_id, recipient, owner, owner, amount)
190 })
191 .collect()
192 }
193 }
194}
195
196const PROXY_LATENCY_P99_THRESHOLD: f64 = 400.0;
197const LATENCY_METRIC_PREFIX: &str = "linera_proxy_request_latency";
198
199#[derive(Debug, thiserror::Error)]
201#[allow(missing_docs)]
202pub enum BenchmarkError {
203 #[error("Failed to join task: {0}")]
204 JoinError(#[from] task::JoinError),
205 #[error("Chain client error: {0}")]
206 ChainClient(#[from] chain_client::Error),
207 #[error("Current histogram count is less than previous histogram count")]
208 HistogramCountMismatch,
209 #[error("Expected histogram value, got {0:?}")]
210 ExpectedHistogramValue(Value),
211 #[error("Expected untyped value, got {0:?}")]
212 ExpectedUntypedValue(Value),
213 #[error("Incomplete histogram data")]
214 IncompleteHistogramData,
215 #[error("Could not compute quantile")]
216 CouldNotComputeQuantile,
217 #[error("Bucket boundaries do not match: {0} vs {1}")]
218 BucketBoundariesDoNotMatch(f64, f64),
219 #[error("Reqwest error: {0}")]
220 Reqwest(#[from] reqwest::Error),
221 #[error("Io error: {0}")]
222 IoError(#[from] std::io::Error),
223 #[error("Previous histogram snapshot does not exist: {0}")]
224 PreviousHistogramSnapshotDoesNotExist(String),
225 #[error("No data available yet to calculate p99")]
226 NoDataYetForP99Calculation,
227 #[error("Unexpected empty bucket")]
228 UnexpectedEmptyBucket,
229 #[error("Failed to send unit message: {0}")]
230 TokioSendUnitError(#[from] mpsc::error::SendError<()>),
231 #[error("Config file not found: {0}")]
232 ConfigFileNotFound(std::path::PathBuf),
233 #[error("Failed to load config file: {0}")]
234 ConfigLoadError(#[from] anyhow::Error),
235 #[error("Could not find enough chains in wallet alone: needed {0}, but only found {1}")]
236 NotEnoughChainsInWallet(usize, usize),
237 #[error("Random number generator error: {0}")]
238 RandError(#[from] rand::Error),
239 #[error("Chain listener startup error")]
240 ChainListenerStartupError,
241}
242
243#[derive(Debug)]
244struct HistogramSnapshot {
245 buckets: Vec<HistogramCount>,
246 count: f64,
247 sum: f64,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
251#[serde(rename_all = "kebab-case")]
252pub struct BenchmarkConfig {
254 pub chain_ids: Vec<ChainId>,
256}
257
258impl BenchmarkConfig {
259 pub fn load_from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
261 let content = std::fs::read_to_string(path)?;
262 let config = serde_yaml::from_str(&content)?;
263 Ok(config)
264 }
265
266 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
268 let content = serde_yaml::to_string(self)?;
269 std::fs::write(path, content)?;
270 Ok(())
271 }
272}
273
274pub struct Benchmark<Env: Environment> {
276 _phantom: std::marker::PhantomData<Env>,
277}
278
279impl<Env: Environment> Benchmark<Env> {
280 #[expect(clippy::too_many_arguments)]
285 pub async fn run_benchmark<C: ClientContext<Environment = Env> + 'static>(
286 bps: usize,
287 chain_clients: Vec<ChainClient<Env>>,
288 generators: Vec<Box<dyn OperationGenerator>>,
289 transactions_per_block: usize,
290 health_check_endpoints: Option<String>,
291 runtime_in_seconds: Option<u64>,
292 delay_between_chains_ms: Option<u64>,
293 chain_listener: ChainListener<C>,
294 shutdown_notifier: &CancellationToken,
295 ) -> Result<(), BenchmarkError> {
296 assert_eq!(
297 chain_clients.len(),
298 generators.len(),
299 "Must have one generator per chain client"
300 );
301 let num_chains = chain_clients.len();
302 let bps_counts = (0..num_chains)
303 .map(|_| Arc::new(AtomicUsize::new(0)))
304 .collect::<Vec<_>>();
305 let notifier = Arc::new(Notify::new());
306 let barrier = Arc::new(Barrier::new(num_chains + 1));
307
308 let chain_listener_future = chain_listener
309 .run()
310 .await
311 .map_err(|_| BenchmarkError::ChainListenerStartupError)?;
312 let chain_listener_handle = tokio::spawn(chain_listener_future.in_current_span());
313
314 let bps_control_task = Self::bps_control_task(
315 &barrier,
316 shutdown_notifier,
317 &bps_counts,
318 ¬ifier,
319 transactions_per_block,
320 bps,
321 );
322
323 let (runtime_control_task, runtime_control_sender) =
324 Self::runtime_control_task(shutdown_notifier, runtime_in_seconds, num_chains);
325
326 let bps_initial_share = bps / num_chains;
327 let mut bps_remainder = bps % num_chains;
328 let mut join_set = task::JoinSet::<Result<(), BenchmarkError>>::new();
329 for (chain_idx, (chain_client, generator)) in
330 chain_clients.into_iter().zip(generators).enumerate()
331 {
332 let chain_id = chain_client.chain_id();
333 let shutdown_notifier_clone = shutdown_notifier.clone();
334 let barrier_clone = barrier.clone();
335 let bps_count_clone = bps_counts[chain_idx].clone();
336 let notifier_clone = notifier.clone();
337 let runtime_control_sender_clone = runtime_control_sender.clone();
338 let bps_share = if bps_remainder > 0 {
339 bps_remainder -= 1;
340 bps_initial_share + 1
341 } else {
342 bps_initial_share
343 };
344 join_set.spawn(
345 async move {
346 Box::pin(Self::run_benchmark_internal(
347 chain_idx,
348 chain_id,
349 bps_share,
350 chain_client,
351 generator,
352 transactions_per_block,
353 shutdown_notifier_clone,
354 bps_count_clone,
355 barrier_clone,
356 notifier_clone,
357 runtime_control_sender_clone,
358 delay_between_chains_ms,
359 ))
360 .await?;
361
362 Ok(())
363 }
364 .instrument(tracing::info_span!("chain_id", chain_id = ?chain_id)),
365 );
366 }
367
368 let metrics_watcher =
369 Self::metrics_watcher(health_check_endpoints, shutdown_notifier).await?;
370
371 while let Some(result) = join_set.join_next().await {
373 let inner_result = result?;
374 if let Err(e) = inner_result {
375 error!("Benchmark task failed: {}", e);
376 shutdown_notifier.cancel();
377 join_set.abort_all();
378 return Err(e);
379 }
380 }
381 info!("All benchmark tasks completed successfully");
382
383 bps_control_task.await?;
384 if let Some(metrics_watcher) = metrics_watcher {
385 metrics_watcher.await??;
386 }
387 if let Some(runtime_control_task) = runtime_control_task {
388 runtime_control_task.await?;
389 }
390
391 if let Err(e) = chain_listener_handle.await? {
392 tracing::error!("chain listener error: {e}");
393 }
394
395 Ok(())
396 }
397
398 fn bps_control_task(
400 barrier: &Arc<Barrier>,
401 shutdown_notifier: &CancellationToken,
402 bps_counts: &[Arc<AtomicUsize>],
403 notifier: &Arc<Notify>,
404 transactions_per_block: usize,
405 bps: usize,
406 ) -> task::JoinHandle<()> {
407 let shutdown_notifier = shutdown_notifier.clone();
408 let bps_counts = bps_counts.to_vec();
409 let notifier = notifier.clone();
410 let barrier = barrier.clone();
411 task::spawn(
412 async move {
413 barrier.wait().await;
414 let mut one_second_interval = time::interval(time::Duration::from_secs(1));
415 loop {
416 if shutdown_notifier.is_cancelled() {
417 info!("Shutdown signal received in bps control task");
418 break;
419 }
420 one_second_interval.tick().await;
421 let current_bps_count: usize = bps_counts
422 .iter()
423 .map(|count| count.swap(0, Ordering::Relaxed))
424 .sum();
425 notifier.notify_waiters();
426 let formatted_current_bps = current_bps_count.to_formatted_string(&Locale::en);
427 let formatted_current_tps = (current_bps_count * transactions_per_block)
428 .to_formatted_string(&Locale::en);
429 let formatted_tps_goal =
430 (bps * transactions_per_block).to_formatted_string(&Locale::en);
431 let formatted_bps_goal = bps.to_formatted_string(&Locale::en);
432 if current_bps_count >= bps {
433 info!(
434 "Achieved {} BPS/{} TPS",
435 formatted_current_bps, formatted_current_tps
436 );
437 } else {
438 warn!(
439 "Failed to achieve {} BPS/{} TPS, only achieved {} BPS/{} TPS",
440 formatted_bps_goal,
441 formatted_tps_goal,
442 formatted_current_bps,
443 formatted_current_tps,
444 );
445 }
446 }
447
448 info!("Exiting bps control task");
449 }
450 .instrument(tracing::info_span!("bps_control")),
451 )
452 }
453
454 async fn metrics_watcher(
455 health_check_endpoints: Option<String>,
456 shutdown_notifier: &CancellationToken,
457 ) -> Result<Option<task::JoinHandle<Result<(), BenchmarkError>>>, BenchmarkError> {
458 if let Some(health_check_endpoints) = health_check_endpoints {
459 let metrics_addresses = health_check_endpoints
460 .split(',')
461 .map(|address| format!("http://{}/metrics", address.trim()))
462 .collect::<Vec<_>>();
463
464 let mut previous_histogram_snapshots: HashMap<String, HistogramSnapshot> =
465 HashMap::new();
466 let scrapes = Self::get_scrapes(&metrics_addresses).await?;
467 for (metrics_address, scrape) in scrapes {
468 previous_histogram_snapshots.insert(
469 metrics_address,
470 Self::parse_histogram(&scrape, LATENCY_METRIC_PREFIX)?,
471 );
472 }
473
474 let shutdown_notifier = shutdown_notifier.clone();
475 let metrics_watcher: task::JoinHandle<Result<(), BenchmarkError>> = tokio::spawn(
476 async move {
477 let mut health_interval = time::interval(time::Duration::from_secs(5));
478 let mut shutdown_interval = time::interval(time::Duration::from_secs(1));
479 loop {
480 tokio::select! {
481 biased;
482 _ = health_interval.tick() => {
483 let result = Self::validators_healthy(&metrics_addresses, &mut previous_histogram_snapshots).await;
484 if let Err(ref err) = result {
485 info!("Shutting down benchmark due to error: {}", err);
486 shutdown_notifier.cancel();
487 break;
488 } else if !result? {
489 info!("Shutting down benchmark due to unhealthy validators");
490 shutdown_notifier.cancel();
491 break;
492 }
493 }
494 _ = shutdown_interval.tick() => {
495 if shutdown_notifier.is_cancelled() {
496 info!("Shutdown signal received, stopping metrics watcher");
497 break;
498 }
499 }
500 }
501 }
502
503 Ok(())
504 }
505 .instrument(tracing::info_span!("metrics_watcher")),
506 );
507
508 Ok(Some(metrics_watcher))
509 } else {
510 Ok(None)
511 }
512 }
513
514 fn runtime_control_task(
515 shutdown_notifier: &CancellationToken,
516 runtime_in_seconds: Option<u64>,
517 num_chain_groups: usize,
518 ) -> (Option<task::JoinHandle<()>>, Option<mpsc::Sender<()>>) {
519 if let Some(runtime_in_seconds) = runtime_in_seconds {
520 let (runtime_control_sender, mut runtime_control_receiver) =
521 mpsc::channel(num_chain_groups);
522 let shutdown_notifier = shutdown_notifier.clone();
523 let runtime_control_task = task::spawn(
524 async move {
525 let mut chains_started = 0;
526 while runtime_control_receiver.recv().await.is_some() {
527 chains_started += 1;
528 if chains_started == num_chain_groups {
529 break;
530 }
531 }
532 time::sleep(time::Duration::from_secs(runtime_in_seconds)).await;
533 shutdown_notifier.cancel();
534 }
535 .instrument(tracing::info_span!("runtime_control")),
536 );
537 (Some(runtime_control_task), Some(runtime_control_sender))
538 } else {
539 (None, None)
540 }
541 }
542
543 async fn validators_healthy(
544 metrics_addresses: &[String],
545 previous_histogram_snapshots: &mut HashMap<String, HistogramSnapshot>,
546 ) -> Result<bool, BenchmarkError> {
547 let scrapes = Self::get_scrapes(metrics_addresses).await?;
548 for (metrics_address, scrape) in scrapes {
549 let histogram = Self::parse_histogram(&scrape, LATENCY_METRIC_PREFIX)?;
550 let diff = Self::diff_histograms(
551 previous_histogram_snapshots.get(&metrics_address).ok_or(
552 BenchmarkError::PreviousHistogramSnapshotDoesNotExist(metrics_address.clone()),
553 )?,
554 &histogram,
555 )?;
556 let p99 = match Self::compute_quantile(&diff.buckets, diff.count, 0.99) {
557 Ok(p99) => p99,
558 Err(BenchmarkError::NoDataYetForP99Calculation) => {
559 info!(
560 "No data available yet to calculate p99 for {}",
561 metrics_address
562 );
563 continue;
564 }
565 Err(e) => {
566 error!("Error computing p99 for {}: {}", metrics_address, e);
567 return Err(e);
568 }
569 };
570
571 let last_bucket_boundary = diff.buckets[diff.buckets.len() - 2].less_than;
572 if p99 == f64::INFINITY {
573 info!(
574 "{} -> Estimated p99 for {} is higher than the last bucket boundary of {:?} ms",
575 metrics_address, LATENCY_METRIC_PREFIX, last_bucket_boundary
576 );
577 } else {
578 info!(
579 "{} -> Estimated p99 for {}: {:.2} ms",
580 metrics_address, LATENCY_METRIC_PREFIX, p99
581 );
582 }
583 if p99 > PROXY_LATENCY_P99_THRESHOLD {
584 if p99 == f64::INFINITY {
585 error!(
586 "Proxy of validator {} unhealthy! Latency p99 is too high, it is higher than \
587 the last bucket boundary of {:.2} ms",
588 metrics_address, last_bucket_boundary
589 );
590 } else {
591 error!(
592 "Proxy of validator {} unhealthy! Latency p99 is too high: {:.2} ms",
593 metrics_address, p99
594 );
595 }
596 return Ok(false);
597 }
598 previous_histogram_snapshots.insert(metrics_address.clone(), histogram);
599 }
600
601 Ok(true)
602 }
603
604 fn diff_histograms(
605 previous: &HistogramSnapshot,
606 current: &HistogramSnapshot,
607 ) -> Result<HistogramSnapshot, BenchmarkError> {
608 if current.count < previous.count {
609 return Err(BenchmarkError::HistogramCountMismatch);
610 }
611 let total_diff = current.count - previous.count;
612 let mut buckets_diff: Vec<HistogramCount> = Vec::new();
613 for (before, after) in previous.buckets.iter().zip(current.buckets.iter()) {
614 let bound_before = before.less_than;
615 let bound_after = after.less_than;
616 let cumulative_before = before.count;
617 let cumulative_after = after.count;
618 if (bound_before - bound_after).abs() > f64::EPSILON {
619 return Err(BenchmarkError::BucketBoundariesDoNotMatch(
620 bound_before,
621 bound_after,
622 ));
623 }
624 let diff = (cumulative_after - cumulative_before).max(0.0);
625 buckets_diff.push(HistogramCount {
626 less_than: bound_after,
627 count: diff,
628 });
629 }
630 Ok(HistogramSnapshot {
631 buckets: buckets_diff,
632 count: total_diff,
633 sum: current.sum - previous.sum,
634 })
635 }
636
637 async fn get_scrapes(
638 metrics_addresses: &[String],
639 ) -> Result<Vec<(String, Scrape)>, BenchmarkError> {
640 let mut scrapes = Vec::new();
641 for metrics_address in metrics_addresses {
642 let response = reqwest::get(metrics_address)
643 .await
644 .map_err(BenchmarkError::Reqwest)?;
645 let metrics = response.text().await.map_err(BenchmarkError::Reqwest)?;
646 let scrape = Scrape::parse(metrics.lines().map(|line| Ok(line.to_owned())))
647 .map_err(BenchmarkError::IoError)?;
648 scrapes.push((metrics_address.clone(), scrape));
649 }
650 Ok(scrapes)
651 }
652
653 fn parse_histogram(
654 scrape: &Scrape,
655 metric_prefix: &str,
656 ) -> Result<HistogramSnapshot, BenchmarkError> {
657 let mut buckets: Vec<HistogramCount> = Vec::new();
658 let mut total_count: Option<f64> = None;
659 let mut total_sum: Option<f64> = None;
660
661 for sample in &scrape.samples {
663 if sample.metric == metric_prefix {
664 if let Value::Histogram(histogram) = &sample.value {
665 buckets.extend(histogram.iter().cloned());
666 } else {
667 return Err(BenchmarkError::ExpectedHistogramValue(sample.value.clone()));
668 }
669 } else if sample.metric == format!("{metric_prefix}_count") {
670 if let Value::Untyped(count) = sample.value {
671 total_count = Some(count);
672 } else {
673 return Err(BenchmarkError::ExpectedUntypedValue(sample.value.clone()));
674 }
675 } else if sample.metric == format!("{metric_prefix}_sum") {
676 if let Value::Untyped(sum) = sample.value {
677 total_sum = Some(sum);
678 } else {
679 return Err(BenchmarkError::ExpectedUntypedValue(sample.value.clone()));
680 }
681 }
682 }
683
684 match (total_count, total_sum) {
685 (Some(count), Some(sum)) if !buckets.is_empty() => {
686 buckets.sort_by(|a, b| {
687 a.less_than
688 .partial_cmp(&b.less_than)
689 .expect("Comparison should not fail")
690 });
691 Ok(HistogramSnapshot {
692 buckets,
693 count,
694 sum,
695 })
696 }
697 _ => Err(BenchmarkError::IncompleteHistogramData),
698 }
699 }
700
701 fn compute_quantile(
702 buckets: &[HistogramCount],
703 total_count: f64,
704 quantile: f64,
705 ) -> Result<f64, BenchmarkError> {
706 if total_count == 0.0 {
707 return Err(BenchmarkError::NoDataYetForP99Calculation);
709 }
710 let target = (quantile * total_count).ceil();
712 let mut prev_cumulative = 0.0;
713 let mut prev_bound = 0.0;
714 for bucket in buckets {
715 if bucket.count >= target {
716 let bucket_count = bucket.count - prev_cumulative;
717 if bucket_count == 0.0 {
718 return Err(BenchmarkError::UnexpectedEmptyBucket);
720 }
721 let fraction = (target - prev_cumulative) / bucket_count;
722 return Ok(prev_bound + (bucket.less_than - prev_bound) * fraction);
723 }
724 prev_cumulative = bucket.count;
725 prev_bound = bucket.less_than;
726 }
727 Err(BenchmarkError::CouldNotComputeQuantile)
728 }
729
730 #[expect(clippy::too_many_arguments)]
731 async fn run_benchmark_internal(
732 chain_idx: usize,
733 chain_id: ChainId,
734 bps: usize,
735 chain_client: ChainClient<Env>,
736 mut generator: Box<dyn OperationGenerator>,
737 transactions_per_block: usize,
738 shutdown_notifier: CancellationToken,
739 bps_count: Arc<AtomicUsize>,
740 barrier: Arc<Barrier>,
741 notifier: Arc<Notify>,
742 runtime_control_sender: Option<mpsc::Sender<()>>,
743 delay_between_chains_ms: Option<u64>,
744 ) -> Result<(), BenchmarkError> {
745 barrier.wait().await;
746 if let Some(delay_between_chains_ms) = delay_between_chains_ms {
747 time::sleep(time::Duration::from_millis(
748 (chain_idx as u64) * delay_between_chains_ms,
749 ))
750 .await;
751 }
752 info!("Starting benchmark for chain {:?}", chain_id);
753
754 if let Some(runtime_control_sender) = runtime_control_sender {
755 runtime_control_sender.send(()).await?;
756 }
757
758 let owner = chain_client
759 .identity()
760 .await
761 .map_err(BenchmarkError::ChainClient)?;
762
763 loop {
764 tokio::select! {
765 biased;
766
767 _ = shutdown_notifier.cancelled() => {
768 info!("Shutdown signal received, stopping benchmark");
769 break;
770 }
771 result = chain_client.execute_operations(
772 generator.generate_operations(owner, transactions_per_block),
773 vec![]
774 ) => {
775 result
776 .map_err(BenchmarkError::ChainClient)?
777 .expect("should execute block with operations");
778
779 let current_bps_count = bps_count.fetch_add(1, Ordering::Relaxed) + 1;
780 if current_bps_count >= bps {
781 notifier.notified().await;
782 }
783 }
784 }
785 }
786
787 info!("Exiting task...");
788 Ok(())
789 }
790
791 pub async fn close_benchmark_chain(
793 chain_client: &ChainClient<Env>,
794 ) -> Result<(), BenchmarkError> {
795 let start = Instant::now();
796 loop {
797 let result = chain_client
798 .execute_operation(Operation::system(SystemOperation::CloseChain))
799 .await?;
800 match result {
801 ClientOutcome::Committed(_) => break,
802 ClientOutcome::Conflict(certificate) => {
803 info!(
804 "Conflict while closing chain {:?}: {}. Retrying...",
805 chain_client.chain_id(),
806 certificate.hash()
807 );
808 }
809 ClientOutcome::WaitForTimeout(timeout) => {
810 info!(
811 "Waiting for timeout while closing chain {:?}: {}",
812 chain_client.chain_id(),
813 timeout
814 );
815 linera_base::time::timer::sleep(
816 timeout.timestamp.duration_since(Timestamp::now()),
817 )
818 .await;
819 }
820 }
821 }
822
823 debug!(
824 "Closed chain {:?} in {} ms",
825 chain_client.chain_id(),
826 start.elapsed().as_millis()
827 );
828
829 Ok(())
830 }
831
832 pub fn get_all_chains(
834 chains_config_path: Option<&Path>,
835 benchmark_chains: &[(ChainId, AccountOwner)],
836 ) -> Result<Vec<ChainId>, BenchmarkError> {
837 let all_chains = if let Some(config_path) = chains_config_path {
838 if !config_path.exists() {
839 return Err(BenchmarkError::ConfigFileNotFound(
840 config_path.to_path_buf(),
841 ));
842 }
843 let config = BenchmarkConfig::load_from_file(config_path)
844 .map_err(BenchmarkError::ConfigLoadError)?;
845 config.chain_ids
846 } else {
847 benchmark_chains.iter().map(|(id, _)| *id).collect()
848 };
849
850 Ok(all_chains)
851 }
852}
853
854pub fn fungible_transfer(
856 application_id: ApplicationId,
857 chain_id: ChainId,
858 sender: AccountOwner,
859 receiver: AccountOwner,
860 amount: Amount,
861) -> Operation {
862 let target_account = Account {
863 chain_id,
864 owner: receiver,
865 };
866 let bytes = bcs::to_bytes(&FungibleOperation::Transfer {
867 owner: sender,
868 amount,
869 target_account,
870 })
871 .expect("should serialize fungible token operation");
872 Operation::User {
873 application_id,
874 bytes,
875 }
876}