linera_service/cli/validator_benchmark/
mod.rs1mod bulk_download;
9mod config;
10mod latency;
11mod partial_sync;
12mod preflight;
13mod progress;
14mod read_latency;
15mod report;
16mod rpc;
17mod tip_lag;
18
19use std::{io::IsTerminal as _, time::Duration};
20
21use anyhow::Result;
22use chrono::Utc;
23pub use config::Benchmark;
24use linera_base::identifiers::ChainId;
25use linera_client::client_context::ClientContext;
26use linera_core::{
27 data_types::ChainInfoQuery,
28 node::{ValidatorNode, ValidatorNodeProvider as _},
29};
30
31use self::{
32 progress::Progress,
33 report::{Candidate, Layers, Metadata, Observer, OutputSpec, Report, Writer},
34};
35
36impl Benchmark {
37 pub async fn run(
39 &self,
40 context: &mut ClientContext<
41 impl linera_core::Environment<ValidatorNode = linera_rpc::Client>,
42 >,
43 ) -> Result<()> {
44 let output_specs = OutputSpec::parse_all(&self.output)?;
46
47 let progress = Progress::new(!self.no_progress && std::io::stderr().is_terminal());
48 let rpc_timeout = Duration::from_secs(self.rpc_timeout_secs);
49
50 let started_at = Utc::now();
51 let node = context.make_node_provider().make_node(&self.address)?;
52 let writer = Writer::new(output_specs);
53
54 let mut report = Report {
57 metadata: Metadata {
58 tool_version: env!("CARGO_PKG_VERSION").to_string(),
59 candidate: Candidate {
60 address: self.address.clone(),
61 public_key: self.public_key.map(|k| k.to_string()),
62 version_info: None,
63 network_description: None,
64 },
65 observer: Observer {
66 location: self.observer_location.clone(),
67 hostname: std::env::var("HOSTNAME").unwrap_or_default(),
68 started_at: started_at.to_rfc3339(),
69 ended_at: None,
70 duration_secs: None,
71 },
72 config: serde_json::to_value(self)?,
73 chains_tested: self.chain.iter().map(|c| c.to_string()).collect(),
74 complete: false,
75 },
76 layers: Layers::default(),
77 };
78
79 if !self.skip_preflight {
82 let outcome = preflight::run(&node, rpc_timeout, &progress).await;
83 if self.abort_on_preflight_fail
84 && outcome.report.status == report::PreflightStatus::Fail
85 {
86 progress.clear();
87 anyhow::bail!(
88 "preflight failed for {}: {:?}",
89 self.address,
90 outcome.report.errors
91 );
92 }
93 report.metadata.candidate.version_info = outcome.version_info;
94 report.metadata.candidate.network_description = outcome.network_description;
95 report.layers.preflight = Some(outcome.report);
96 } else {
97 report.metadata.candidate.version_info =
98 rpc::timed(rpc_timeout, node.get_version_info())
99 .await
100 .ok()
101 .map(|v| format!("{v:?}"));
102 report.metadata.candidate.network_description =
103 rpc::timed(rpc_timeout, node.get_network_description())
104 .await
105 .ok()
106 .and_then(|nd| serde_json::to_value(nd).ok());
107 }
108 writer.write_files(&report)?;
109
110 let deep_chain = self.deep.then(|| self.deep_chain.unwrap_or(self.chain[0]));
114 warn_unheld_chains(&node, &self.chain, deep_chain, rpc_timeout).await;
115 if let Some(deep_chain) = deep_chain {
116 report.layers.partial_sync = Some(
117 Box::pin(partial_sync::run(
118 &node,
119 context,
120 deep_chain,
121 self.deep_blocks,
122 rpc_timeout,
123 &progress,
124 ))
125 .await?,
126 );
127 writer.write_files(&report)?;
128 }
129
130 if !self.skip_read_baseline {
132 report.layers.read_baseline = Some(
133 Box::pin(read_latency::run_baseline(
134 &node,
135 &self.chain,
136 self.baseline_requests,
137 rpc_timeout,
138 &progress,
139 ))
140 .await,
141 );
142 writer.write_files(&report)?;
143 }
144
145 if !self.skip_read_stress {
146 report.layers.read_stress = Some(
147 Box::pin(read_latency::run_stress(
148 &node,
149 &self.chain,
150 &self.stress_levels,
151 Duration::from_secs(self.stress_duration_secs),
152 rpc_timeout,
153 &progress,
154 ))
155 .await,
156 );
157 writer.write_files(&report)?;
158 }
159
160 if !self.skip_bulk_download {
161 report.layers.bulk_download = Some(
162 Box::pin(bulk_download::run(
163 &node,
164 &self.chain,
165 self.bulk_batch_size,
166 &self.bulk_concurrency,
167 &self.bulk_height_range,
168 rpc_timeout,
169 &progress,
170 ))
171 .await?,
172 );
173 writer.write_files(&report)?;
174 }
175
176 if !self.skip_tip_lag {
177 report.layers.tip_lag = Some(
178 Box::pin(tip_lag::run(
179 &node,
180 context,
181 &self.chain,
182 self.tip_lag_samples,
183 Duration::from_secs(self.tip_lag_interval_secs),
184 rpc_timeout,
185 &progress,
186 ))
187 .await?,
188 );
189 writer.write_files(&report)?;
190 }
191
192 let ended_at = Utc::now();
193 report.metadata.observer.ended_at = Some(ended_at.to_rfc3339());
194 report.metadata.observer.duration_secs =
195 Some(u64::try_from((ended_at - started_at).num_seconds()).unwrap_or(0));
196 report.metadata.complete = true;
197 progress.clear();
198 writer.emit(&report)?;
199 Ok(())
200 }
201}
202
203async fn warn_unheld_chains(
207 node: &impl ValidatorNode,
208 chains: &[ChainId],
209 seeded: Option<ChainId>,
210 rpc_timeout: Duration,
211) {
212 let mut unheld = Vec::new();
213 for &chain in chains {
214 if Some(chain) == seeded {
215 continue;
216 }
217 let held = rpc::timed(
218 rpc_timeout,
219 node.handle_chain_info_query(ChainInfoQuery::new(chain)),
220 )
221 .await
222 .is_ok_and(|response| response.info.next_block_height.0 > 0);
223 if !held {
224 unheld.push(chain.to_string());
225 }
226 }
227 if !unheld.is_empty() {
228 tracing::warn!(
229 "candidate does not hold chain(s) [{}]; read layers (L3-L5) will be shallow. \
230 Pre-sync them (`linera validator sync`) or pass `--deep` to seed blocks first.",
231 unheld.join(", ")
232 );
233 }
234}