common/cli.rs
1//! Common CLI arguments shared by every RCP binary.
2//!
3//! Each binary flattens [`CommonArgs`] into its own clap struct via
4//! `#[command(flatten)]`. Tool-specific arguments live in the binary itself.
5//!
6//! Fields intentionally NOT in this struct, so each binary can document them
7//! accurately:
8//! - `chunk_size` — rcp/rcpd parse as `bytesize::ByteSize` (e.g. "16MiB"),
9//! others as bare `u64`.
10//! - `summary` — rcpd streams results to the master and never prints a summary.
11//! - `max_open_files` — filegen falls back to physical CPU cores instead of
12//! 80% of the system rlimit, because random-data generation is CPU-bound.
13//! - `quiet` — rcmp's `--quiet` also suppresses stdout differences (not just
14//! error output), so its help text differs from the other tools.
15
16#[derive(Debug, Clone, clap::Args)]
17pub struct CommonArgs {
18 // Progress & output
19 /// Show progress
20 #[arg(long, help_heading = "Progress & output")]
21 pub progress: bool,
22 /// Set the type of progress display
23 ///
24 /// If specified, --progress flag is implied.
25 #[arg(long, value_name = "TYPE", help_heading = "Progress & output")]
26 pub progress_type: Option<crate::ProgressType>,
27 /// Set delay between progress updates
28 ///
29 /// Default is 200ms for interactive mode (`ProgressBar`) and 10s for non-interactive
30 /// mode (`TextUpdates`). If specified, --progress flag is implied. Accepts
31 /// human-readable durations like "200ms", "10s", "5min".
32 #[arg(long, value_name = "DELAY", help_heading = "Progress & output")]
33 pub progress_delay: Option<String>,
34 /// Verbose level (implies "summary"): -v INFO / -vv DEBUG / -vvv TRACE (default: ERROR)
35 #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, help_heading = "Progress & output")]
36 pub verbose: u8,
37 // Performance & throttling
38 /// Throttle the number of operations per second (0 = no throttle)
39 #[arg(
40 long,
41 default_value = "0",
42 value_name = "N",
43 help_heading = "Performance & throttling"
44 )]
45 pub ops_throttle: usize,
46 /// Limit I/O operations per second (0 = no throttle)
47 ///
48 /// Requires --chunk-size to calculate I/O operations per file: ((`file_size` - 1) / `chunk_size`) + 1
49 #[arg(
50 long,
51 default_value = "0",
52 value_name = "N",
53 help_heading = "Performance & throttling"
54 )]
55 pub iops_throttle: usize,
56 // Advanced settings
57 /// Number of worker threads (0 = number of CPU cores)
58 #[arg(
59 long,
60 default_value = "0",
61 value_name = "N",
62 help_heading = "Advanced settings"
63 )]
64 pub max_workers: usize,
65 /// Number of blocking worker threads (0 = Tokio default of 512)
66 #[arg(
67 long,
68 default_value = "0",
69 value_name = "N",
70 help_heading = "Advanced settings"
71 )]
72 pub max_blocking_threads: usize,
73 // Congestion control (experimental, opt-in)
74 /// Enable adaptive metadata-ops throttling (latency-ratio controller)
75 #[arg(long, help_heading = "Congestion control")]
76 pub auto_meta_throttle: bool,
77 /// Initial concurrency window for adaptive metadata throttle
78 #[arg(
79 long,
80 default_value = "1",
81 value_name = "N",
82 help_heading = "Congestion control"
83 )]
84 pub auto_meta_initial_cwnd: u32,
85 /// Minimum concurrency window (floor below which cwnd cannot shrink)
86 #[arg(
87 long,
88 default_value = "1",
89 value_name = "N",
90 help_heading = "Congestion control (advanced)"
91 )]
92 pub auto_meta_min_cwnd: u32,
93 /// Maximum concurrency window (ceiling on adaptive growth)
94 #[arg(
95 long,
96 default_value = "4096",
97 value_name = "N",
98 help_heading = "Congestion control"
99 )]
100 pub auto_meta_max_cwnd: u32,
101 /// Latency ratio below which cwnd grows (current / baseline).
102 /// Default 1.3, sized to sit just below the steady-state p10/p50
103 /// inter-quantile spread of typical metadata syscalls so the
104 /// controller climbs only when the spread compresses. `alpha` may
105 /// be set below 1.0 in passive matched mode (grow only when recent
106 /// is meaningfully faster than baseline). The natural scale depends
107 /// on the percentile pair: matched percentiles produce a steady-
108 /// state ratio of 1.0; cross percentiles produce a ratio above 1.0
109 /// set by the inter-quantile spread of the latency distribution.
110 #[arg(
111 long,
112 default_value = "1.3",
113 value_name = "F",
114 help_heading = "Congestion control (advanced)"
115 )]
116 pub auto_meta_alpha: f64,
117 /// Latency ratio above which cwnd shrinks. Default 1.8, sized to
118 /// sit above the steady-state p10/p50 spread so only genuine
119 /// queueing-driven tail growth triggers a backoff.
120 #[arg(
121 long,
122 default_value = "1.8",
123 value_name = "F",
124 help_heading = "Congestion control (advanced)"
125 )]
126 pub auto_meta_beta: f64,
127 /// Percentile (in `[0.0, 1.0)`) applied to the long-horizon window
128 /// to derive the baseline statistic. Default 0.1 (p10): paired with
129 /// the p50 current percentile this gives a cross-percentile ratio
130 /// whose steady-state level tracks the lower-half spread of the
131 /// per-syscall latency distribution and rises with queueing. With
132 /// matched percentiles (`baseline == current`) the steady-state
133 /// ratio sits near 1.0 instead.
134 #[arg(
135 long,
136 default_value = "0.1",
137 value_name = "F",
138 help_heading = "Congestion control (advanced)"
139 )]
140 pub auto_meta_baseline_percentile: f64,
141 /// Percentile (in `[0.0, 1.0)`) applied to the short-horizon window
142 /// to derive the current statistic. Default 0.5 (p50). Must be
143 /// `>= baseline percentile`. See `--auto-meta-baseline-percentile`.
144 #[arg(
145 long,
146 default_value = "0.5",
147 value_name = "F",
148 help_heading = "Congestion control (advanced)"
149 )]
150 pub auto_meta_current_percentile: f64,
151 /// How much to grow cwnd on each under-shoot tick
152 #[arg(
153 long,
154 default_value = "1",
155 value_name = "N",
156 help_heading = "Congestion control (advanced)"
157 )]
158 pub auto_meta_increase_step: u32,
159 /// How much to shrink cwnd on each over-shoot tick
160 #[arg(
161 long,
162 default_value = "1",
163 value_name = "N",
164 help_heading = "Congestion control (advanced)"
165 )]
166 pub auto_meta_decrease_step: u32,
167 /// Long-horizon sample window (e.g. "10s"). Drives the baseline
168 /// percentile; samples older than this are evicted on every tick.
169 #[arg(
170 long,
171 default_value = "10s",
172 value_name = "DUR",
173 help_heading = "Congestion control (advanced)"
174 )]
175 pub auto_meta_long_window: humantime::Duration,
176 /// Short-horizon sample window (e.g. "1s"). Drives the current-state
177 /// percentile; must be strictly less than `--auto-meta-long-window`.
178 #[arg(
179 long,
180 default_value = "1s",
181 value_name = "DUR",
182 help_heading = "Congestion control (advanced)"
183 )]
184 pub auto_meta_short_window: humantime::Duration,
185 /// Control-loop tick interval (e.g. "50ms")
186 #[arg(
187 long,
188 default_value = "50ms",
189 value_name = "DUR",
190 help_heading = "Congestion control (advanced)"
191 )]
192 pub auto_meta_tick_interval: humantime::Duration,
193 /// Enable in-memory HDR latency histograms per (side, op). Implies
194 /// `--auto-meta-throttle`. Adds a distribution panel beneath the
195 /// existing one-line-per-controller summary in the progress display.
196 #[arg(long, help_heading = "Congestion control")]
197 pub auto_meta_histogram: bool,
198 /// Write a binary log of per-(side, op) HDR histograms to the given
199 /// path. The file is truncated if it already exists — rename or move
200 /// logs you want to keep across runs. Format documented in
201 /// `docs/congestion_control.md`. Implies `--auto-meta-histogram` and
202 /// `--auto-meta-throttle`.
203 #[arg(long, value_name = "PATH", help_heading = "Congestion control")]
204 pub auto_meta_histogram_log: Option<std::path::PathBuf>,
205 /// Snapshot cadence for the histogram logger (e.g. "1s"). Drives both
206 /// the panel refresh rate and the log-file record interval. Range
207 /// `[100ms, 60s]`.
208 #[arg(
209 long,
210 default_value = "1s",
211 value_name = "DUR",
212 help_heading = "Congestion control"
213 )]
214 pub auto_meta_histogram_interval: humantime::Duration,
215}
216
217impl CommonArgs {
218 /// Build a [`crate::OutputConfig`]. `quiet` and `print_summary` are
219 /// supplied by the caller (each binary owns its own `--quiet` and
220 /// `--summary` flags so it can document binary-specific semantics).
221 #[must_use]
222 pub fn output_config(&self, quiet: bool, print_summary: bool) -> crate::OutputConfig {
223 crate::OutputConfig {
224 quiet,
225 verbose: self.verbose,
226 print_summary,
227 ..Default::default()
228 }
229 }
230 /// Build a [`crate::RuntimeConfig`] from these args.
231 #[must_use]
232 pub fn runtime_config(&self) -> crate::RuntimeConfig {
233 crate::RuntimeConfig {
234 max_workers: self.max_workers,
235 max_blocking_threads: self.max_blocking_threads,
236 }
237 }
238 /// Build a [`crate::ThrottleConfig`]. `max_open_files` and `chunk_size`
239 /// are supplied by the caller (filegen has its own `--max-open-files`
240 /// default; chunk_size has different parser types per binary).
241 #[must_use]
242 pub fn throttle_config(
243 &self,
244 max_open_files: Option<usize>,
245 chunk_size: u64,
246 ) -> crate::ThrottleConfig {
247 let auto_meta_implied = self.auto_meta_throttle
248 || self.auto_meta_histogram
249 || self.auto_meta_histogram_log.is_some();
250 let auto_meta = auto_meta_implied.then(|| crate::AutoMetaThrottleConfig {
251 initial_cwnd: self.auto_meta_initial_cwnd,
252 min_cwnd: self.auto_meta_min_cwnd,
253 max_cwnd: self.auto_meta_max_cwnd,
254 alpha: self.auto_meta_alpha,
255 beta: self.auto_meta_beta,
256 increase_step: self.auto_meta_increase_step,
257 decrease_step: self.auto_meta_decrease_step,
258 baseline_percentile: self.auto_meta_baseline_percentile,
259 current_percentile: self.auto_meta_current_percentile,
260 long_window: self.auto_meta_long_window.into(),
261 short_window: self.auto_meta_short_window.into(),
262 tick_interval: self.auto_meta_tick_interval.into(),
263 });
264 crate::ThrottleConfig {
265 max_open_files,
266 ops_throttle: self.ops_throttle,
267 iops_throttle: self.iops_throttle,
268 chunk_size,
269 auto_meta,
270 histogram_enabled: self.auto_meta_histogram || self.auto_meta_histogram_log.is_some(),
271 histogram_log_path: self.auto_meta_histogram_log.clone(),
272 histogram_interval: self.auto_meta_histogram_interval.into(),
273 }
274 }
275 /// Returns true if any progress-related flag was set.
276 ///
277 /// `--auto-meta-histogram` implies progress because its sole purpose is
278 /// to render a live distribution panel. `--auto-meta-histogram-log` does
279 /// NOT imply progress — it writes to a file regardless of progress mode,
280 /// and forcing a display would be worse UX for users who only want the
281 /// file.
282 #[must_use]
283 pub fn progress_requested(&self) -> bool {
284 self.progress
285 || self.progress_type.is_some()
286 || self.progress_delay.is_some()
287 || self.auto_meta_histogram
288 }
289
290 /// Build user-facing [`crate::ProgressSettings`] when any progress flag was
291 /// set, else `None`. `kind` selects the tool-specific printer. For `rcp`'s
292 /// remote-master and `rcpd`'s remote progress modes, build `ProgressSettings`
293 /// directly instead of using this helper.
294 #[must_use]
295 pub fn user_progress_settings(
296 &self,
297 kind: crate::progress::LocalProgressKind,
298 ) -> Option<crate::ProgressSettings> {
299 if !self.progress_requested() {
300 return None;
301 }
302 Some(crate::ProgressSettings {
303 progress_type: crate::GeneralProgressType::User {
304 progress_type: self.progress_type.unwrap_or_default(),
305 kind,
306 },
307 progress_delay: self.progress_delay.clone(),
308 })
309 }
310}
311
312#[cfg(test)]
313mod implies_tests {
314 use super::*;
315 use clap::Parser;
316
317 #[derive(Parser)]
318 struct TestCli {
319 #[command(flatten)]
320 common: CommonArgs,
321 }
322
323 #[test]
324 fn auto_meta_histogram_implies_throttle_at_cli() {
325 let cli = TestCli::parse_from(["test", "--auto-meta-histogram"]);
326 let throttle = cli.common.throttle_config(None, 0);
327 assert!(
328 throttle.auto_meta.is_some(),
329 "histogram flag must imply auto_meta"
330 );
331 assert!(throttle.histogram_enabled);
332 }
333
334 #[test]
335 fn auto_meta_histogram_log_implies_throttle_at_cli() {
336 let cli = TestCli::parse_from(["test", "--auto-meta-histogram-log", "/tmp/x.hdr"]);
337 let throttle = cli.common.throttle_config(None, 0);
338 assert!(
339 throttle.auto_meta.is_some(),
340 "histogram-log flag must imply auto_meta"
341 );
342 assert!(throttle.histogram_log_path.is_some());
343 }
344
345 #[test]
346 fn no_auto_meta_flags_means_no_throttle() {
347 let cli = TestCli::parse_from(["test"]);
348 let throttle = cli.common.throttle_config(None, 0);
349 assert!(throttle.auto_meta.is_none());
350 assert!(!throttle.histogram_enabled);
351 }
352
353 /// `--auto-meta-histogram` (panel-only) sets `auto_meta_throttle = false`.
354 ///
355 /// The rcp binary uses this distinction when building `RcpdConfig`: it
356 /// gates `RcpdConfig::auto_meta` on `auto_meta_throttle || histogram_log
357 /// .is_some()`, so that the panel-only flag does NOT silently enable the
358 /// throttle pipeline on remote daemons. This test pins that distinction
359 /// at the `CommonArgs` level so a future refactor cannot accidentally
360 /// collapse the two flags.
361 #[test]
362 fn panel_flag_does_not_set_explicit_throttle_field() {
363 let cli = TestCli::parse_from(["test", "--auto-meta-histogram"]);
364 // `throttle_config()` returns `auto_meta = Some(...)` because the
365 // panel needs the throttle pipeline locally — that's intentional.
366 assert!(cli.common.throttle_config(None, 0).auto_meta.is_some());
367 // But the *explicit* throttle field must stay false so the rcp binary
368 // can distinguish "panel only" from "user explicitly asked for throttle".
369 assert!(
370 !cli.common.auto_meta_throttle,
371 "--auto-meta-histogram must not set auto_meta_throttle"
372 );
373 // And no log path either.
374 assert!(cli.common.auto_meta_histogram_log.is_none());
375 }
376
377 #[test]
378 fn auto_meta_histogram_implies_progress() {
379 let cli = TestCli::parse_from(["test", "--auto-meta-histogram"]);
380 assert!(
381 cli.common.progress_requested(),
382 "--auto-meta-histogram alone must imply progress so the panel actually renders",
383 );
384 }
385
386 #[test]
387 fn auto_meta_histogram_log_does_not_imply_progress() {
388 // the log file writes regardless of progress; user can opt in to
389 // progress separately. don't force it on them.
390 let cli = TestCli::parse_from(["test", "--auto-meta-histogram-log", "/tmp/x.hdr"]);
391 assert!(
392 !cli.common.progress_requested(),
393 "--auto-meta-histogram-log alone should NOT imply progress",
394 );
395 }
396}