Skip to main content

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}