sui_rpc_loadgen/
main.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4mod 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    // TODO(chris): support running multiple commands at once
32    #[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    /// the path to log file directory
41    #[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    /// different chunks will be executed concurrently on the same thread
57    #[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        /// Default to start from checkpoint 0
71        #[clap(short, long, default_value_t = 0)]
72        start: u64,
73
74        /// inclusive, uses `getLatestCheckpointSequenceNumber` if `None`
75        #[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        // Whether to record data from checkpoint
85        #[clap(long)]
86        skip_record: bool,
87
88        #[clap(flatten)]
89        common: CommonOptions,
90    },
91    #[clap(name = "pay-sui")]
92    PaySui {
93        // TODO(chris) customize recipients and amounts
94        #[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    // TODO(chris) allow pass in custom path for keystore
140    // Load keystore from ~/.sui/sui_config/sui.keystore
141    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    // use timestamp to signify which file is newer
166    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    // Initialize logger
184    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            // TODO: pass in from config
273            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}