1mod load_test;
5mod payload;
6
7use anyhow::Result;
8use clap::Parser;
9use payload::AddressQueryType;
10
11use std::error::Error;
12use std::path::PathBuf;
13use std::time::{Duration, SystemTime, UNIX_EPOCH};
14use sui_keys::keystore::{AccountKeystore, FileBasedKeystore, Keystore};
15use sui_types::crypto::{EncodeDecodeBase64, SuiKeyPair};
16use tracing::info;
17
18use crate::load_test::{LoadTest, LoadTestConfig};
19use crate::payload::{
20 Command, RpcCommandProcessor, SignerInfo, load_addresses_from_file, load_digests_from_file,
21 load_objects_from_file,
22};
23
24#[derive(Parser)]
25#[clap(
26 name = "Sui RPC Load Generator",
27 version = "0.1",
28 about = "A load test application for Sui RPC"
29)]
30struct Opts {
31 #[clap(subcommand)]
33 pub command: ClapCommand,
34 #[clap(long, default_value_t = 1)]
35 pub num_threads: usize,
36 #[clap(long, default_value_t = true)]
37 pub cross_validate: bool,
38 #[clap(long, num_args(1..), default_value = "http://127.0.0.1:9000")]
39 pub urls: Vec<String>,
40 #[clap(long, default_value = "~/.sui/sui_config/logs")]
42 logs_directory: String,
43
44 #[clap(long, default_value = "~/.sui/loadgen/data")]
45 data_directory: String,
46}
47
48#[derive(Parser)]
49pub struct CommonOptions {
50 #[clap(short, long, default_value_t = 0)]
51 pub repeat: usize,
52
53 #[clap(short, long, default_value_t = 0)]
54 pub interval_in_ms: u64,
55
56 #[clap(long, default_value_t = 1)]
58 num_chunks_per_thread: usize,
59}
60
61#[derive(Parser)]
62pub enum ClapCommand {
63 #[clap(name = "dry-run")]
64 DryRun {
65 #[clap(flatten)]
66 common: CommonOptions,
67 },
68 #[clap(name = "get-checkpoints")]
69 GetCheckpoints {
70 #[clap(short, long, default_value_t = 0)]
72 start: u64,
73
74 #[clap(short, long)]
76 end: Option<u64>,
77
78 #[clap(long)]
79 skip_verify_transactions: bool,
80
81 #[clap(long)]
82 skip_verify_objects: bool,
83
84 #[clap(long)]
86 skip_record: bool,
87
88 #[clap(flatten)]
89 common: CommonOptions,
90 },
91 #[clap(name = "pay-sui")]
92 PaySui {
93 #[clap(flatten)]
95 common: CommonOptions,
96 },
97 #[clap(name = "query-transaction-blocks")]
98 QueryTransactionBlocks {
99 #[clap(long, ignore_case = true)]
100 address_type: AddressQueryType,
101
102 #[clap(flatten)]
103 common: CommonOptions,
104 },
105 #[clap(name = "multi-get-transaction-blocks")]
106 MultiGetTransactionBlocks {
107 #[clap(flatten)]
108 common: CommonOptions,
109 },
110 #[clap(name = "multi-get-objects")]
111 MultiGetObjects {
112 #[clap(flatten)]
113 common: CommonOptions,
114 },
115 #[clap(name = "get-object")]
116 GetObject {
117 #[clap(long)]
118 chunk_size: usize,
119
120 #[clap(flatten)]
121 common: CommonOptions,
122 },
123 #[clap(name = "get-all-balances")]
124 GetAllBalances {
125 #[clap(long)]
126 chunk_size: usize,
127
128 #[clap(flatten)]
129 common: CommonOptions,
130 },
131 #[clap(name = "get-reference-gas-price")]
132 GetReferenceGasPrice {
133 #[clap(flatten)]
134 common: CommonOptions,
135 },
136}
137
138fn get_keypair() -> Result<SignerInfo> {
139 let keystore_path = get_sui_config_directory().join("sui.keystore");
142 let keystore = Keystore::from(FileBasedKeystore::load_or_create(&keystore_path)?);
143 let active_address = keystore.addresses().pop().unwrap();
144 let keypair: &SuiKeyPair = keystore.export(&active_address)?;
145 println!("using address {active_address} for signing");
146 Ok(SignerInfo::new(keypair.encode_base64()))
147}
148
149fn get_sui_config_directory() -> PathBuf {
150 match dirs::home_dir() {
151 Some(v) => v.join(".sui").join("sui_config"),
152 None => panic!("Cannot obtain home directory path"),
153 }
154}
155
156pub fn expand_path(dir_path: &str) -> String {
157 shellexpand::full(&dir_path)
158 .map(|v| v.into_owned())
159 .unwrap_or_else(|e| panic!("Failed to expand directory '{:?}': {}", dir_path, e))
160}
161
162fn get_log_file_path(dir_path: String) -> String {
163 let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
164 let timestamp = current_time.as_secs();
165 let log_filename = format!("sui-rpc-loadgen.{}.log", timestamp);
167
168 let dir_path = expand_path(&dir_path);
169 format!("{dir_path}/{log_filename}")
170}
171
172#[tokio::main]
173async fn main() -> Result<(), Box<dyn Error>> {
174 let tracing_level = "debug";
175 let network_tracing_level = "info";
176 let log_filter = format!(
177 "{tracing_level},h2={network_tracing_level},tower={network_tracing_level},hyper={network_tracing_level},tonic::transport={network_tracing_level}"
178 );
179 let opts = Opts::parse();
180
181 let log_filename = get_log_file_path(opts.logs_directory);
182
183 let (_guard, _filter_handle) = telemetry_subscribers::TelemetryConfig::new()
185 .with_env()
186 .with_log_level(&log_filter)
187 .with_log_file(&log_filename)
188 .init();
189
190 println!("Logging to {}", &log_filename);
191 info!("Running Load Gen with following urls {:?}", opts.urls);
192
193 let (command, common, need_keystore) = match opts.command {
194 ClapCommand::DryRun { common } => (Command::new_dry_run(), common, false),
195 ClapCommand::PaySui { common } => (Command::new_pay_sui(), common, true),
196 ClapCommand::GetCheckpoints {
197 common,
198 start,
199 end,
200 skip_verify_transactions,
201 skip_verify_objects,
202 skip_record,
203 } => (
204 Command::new_get_checkpoints(
205 start,
206 end,
207 !skip_verify_transactions,
208 !skip_verify_objects,
209 !skip_record,
210 ),
211 common,
212 false,
213 ),
214 ClapCommand::QueryTransactionBlocks {
215 common,
216 address_type,
217 } => {
218 let addresses = load_addresses_from_file(expand_path(&opts.data_directory));
219 (
220 Command::new_query_transaction_blocks(address_type, addresses),
221 common,
222 false,
223 )
224 }
225 ClapCommand::MultiGetTransactionBlocks { common } => {
226 let digests = load_digests_from_file(expand_path(&opts.data_directory));
227 (
228 Command::new_multi_get_transaction_blocks(digests),
229 common,
230 false,
231 )
232 }
233 ClapCommand::GetAllBalances { common, chunk_size } => {
234 let addresses = load_addresses_from_file(expand_path(&opts.data_directory));
235 (
236 Command::new_get_all_balances(addresses, chunk_size),
237 common,
238 false,
239 )
240 }
241 ClapCommand::MultiGetObjects { common } => {
242 let objects = load_objects_from_file(expand_path(&opts.data_directory));
243 (Command::new_multi_get_objects(objects), common, false)
244 }
245 ClapCommand::GetReferenceGasPrice { common } => {
246 let num_repeats = common.num_chunks_per_thread;
247 (
248 Command::new_get_reference_gas_price(num_repeats),
249 common,
250 false,
251 )
252 }
253 ClapCommand::GetObject { common, chunk_size } => {
254 let objects = load_objects_from_file(expand_path(&opts.data_directory));
255 (Command::new_get_object(objects, chunk_size), common, false)
256 }
257 };
258
259 let signer_info = need_keystore.then_some(get_keypair()?);
260
261 let command = command
262 .with_repeat_interval(Duration::from_millis(common.interval_in_ms))
263 .with_repeat_n_times(common.repeat);
264
265 let processor = RpcCommandProcessor::new(&opts.urls, expand_path(&opts.data_directory)).await;
266
267 let load_test = LoadTest {
268 processor,
269 config: LoadTestConfig {
270 command,
271 num_threads: opts.num_threads,
272 divide_tasks: true,
274 signer_info,
275 num_chunks_per_thread: common.num_chunks_per_thread,
276 max_repeat: common.repeat,
277 },
278 };
279 load_test.run().await?;
280
281 Ok(())
282}