rcp-tools-common 0.33.0

Internal library for RCP file operation tools - shared utilities and core operations (not intended for direct use)
Documentation
//! Common CLI arguments shared by every RCP binary.
//!
//! Each binary flattens [`CommonArgs`] into its own clap struct via
//! `#[command(flatten)]`. Tool-specific arguments live in the binary itself.
//!
//! Fields intentionally NOT in this struct, so each binary can document them
//! accurately:
//! - `chunk_size` — rcp/rcpd parse as `bytesize::ByteSize` (e.g. "16MiB"),
//!   others as bare `u64`.
//! - `summary` — rcpd streams results to the master and never prints a summary.
//! - `max_open_files` — filegen falls back to physical CPU cores instead of
//!   80% of the system rlimit, because random-data generation is CPU-bound.
//! - `quiet` — rcmp's `--quiet` also suppresses stdout differences (not just
//!   error output), so its help text differs from the other tools.

#[derive(Debug, Clone, clap::Args)]
pub struct CommonArgs {
    // Progress & output
    /// Show progress
    #[arg(long, help_heading = "Progress & output")]
    pub progress: bool,
    /// Set the type of progress display
    ///
    /// If specified, --progress flag is implied.
    #[arg(long, value_name = "TYPE", help_heading = "Progress & output")]
    pub progress_type: Option<crate::ProgressType>,
    /// Set delay between progress updates
    ///
    /// Default is 200ms for interactive mode (`ProgressBar`) and 10s for non-interactive
    /// mode (`TextUpdates`). If specified, --progress flag is implied. Accepts
    /// human-readable durations like "200ms", "10s", "5min".
    #[arg(long, value_name = "DELAY", help_heading = "Progress & output")]
    pub progress_delay: Option<String>,
    /// Verbose level (implies "summary"): -v INFO / -vv DEBUG / -vvv TRACE (default: ERROR)
    #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, help_heading = "Progress & output")]
    pub verbose: u8,
    // Performance & throttling
    /// Throttle the number of operations per second (0 = no throttle)
    #[arg(
        long,
        default_value = "0",
        value_name = "N",
        help_heading = "Performance & throttling"
    )]
    pub ops_throttle: usize,
    /// Limit I/O operations per second (0 = no throttle)
    ///
    /// Requires --chunk-size to calculate I/O operations per file: ((`file_size` - 1) / `chunk_size`) + 1
    #[arg(
        long,
        default_value = "0",
        value_name = "N",
        help_heading = "Performance & throttling"
    )]
    pub iops_throttle: usize,
    // Advanced settings
    /// Number of worker threads (0 = number of CPU cores)
    #[arg(
        long,
        default_value = "0",
        value_name = "N",
        help_heading = "Advanced settings"
    )]
    pub max_workers: usize,
    /// Number of blocking worker threads (0 = Tokio default of 512)
    #[arg(
        long,
        default_value = "0",
        value_name = "N",
        help_heading = "Advanced settings"
    )]
    pub max_blocking_threads: usize,
    // Congestion control (experimental, opt-in)
    /// Enable adaptive metadata-ops throttling (latency-ratio controller)
    #[arg(long, help_heading = "Congestion control")]
    pub auto_meta_throttle: bool,
    /// Initial concurrency window for adaptive metadata throttle
    #[arg(
        long,
        default_value = "1",
        value_name = "N",
        help_heading = "Congestion control"
    )]
    pub auto_meta_initial_cwnd: u32,
    /// Minimum concurrency window (floor below which cwnd cannot shrink)
    #[arg(
        long,
        default_value = "1",
        value_name = "N",
        help_heading = "Congestion control (advanced)"
    )]
    pub auto_meta_min_cwnd: u32,
    /// Maximum concurrency window (ceiling on adaptive growth)
    #[arg(
        long,
        default_value = "4096",
        value_name = "N",
        help_heading = "Congestion control"
    )]
    pub auto_meta_max_cwnd: u32,
    /// Latency ratio below which cwnd grows (current / baseline).
    /// Default 1.3, sized to sit just below the steady-state p10/p50
    /// inter-quantile spread of typical metadata syscalls so the
    /// controller climbs only when the spread compresses. `alpha` may
    /// be set below 1.0 in passive matched mode (grow only when recent
    /// is meaningfully faster than baseline). The natural scale depends
    /// on the percentile pair: matched percentiles produce a steady-
    /// state ratio of 1.0; cross percentiles produce a ratio above 1.0
    /// set by the inter-quantile spread of the latency distribution.
    #[arg(
        long,
        default_value = "1.3",
        value_name = "F",
        help_heading = "Congestion control (advanced)"
    )]
    pub auto_meta_alpha: f64,
    /// Latency ratio above which cwnd shrinks. Default 1.8, sized to
    /// sit above the steady-state p10/p50 spread so only genuine
    /// queueing-driven tail growth triggers a backoff.
    #[arg(
        long,
        default_value = "1.8",
        value_name = "F",
        help_heading = "Congestion control (advanced)"
    )]
    pub auto_meta_beta: f64,
    /// Percentile (in `[0.0, 1.0)`) applied to the long-horizon window
    /// to derive the baseline statistic. Default 0.1 (p10): paired with
    /// the p50 current percentile this gives a cross-percentile ratio
    /// whose steady-state level tracks the lower-half spread of the
    /// per-syscall latency distribution and rises with queueing. With
    /// matched percentiles (`baseline == current`) the steady-state
    /// ratio sits near 1.0 instead.
    #[arg(
        long,
        default_value = "0.1",
        value_name = "F",
        help_heading = "Congestion control (advanced)"
    )]
    pub auto_meta_baseline_percentile: f64,
    /// Percentile (in `[0.0, 1.0)`) applied to the short-horizon window
    /// to derive the current statistic. Default 0.5 (p50). Must be
    /// `>= baseline percentile`. See `--auto-meta-baseline-percentile`.
    #[arg(
        long,
        default_value = "0.5",
        value_name = "F",
        help_heading = "Congestion control (advanced)"
    )]
    pub auto_meta_current_percentile: f64,
    /// How much to grow cwnd on each under-shoot tick
    #[arg(
        long,
        default_value = "1",
        value_name = "N",
        help_heading = "Congestion control (advanced)"
    )]
    pub auto_meta_increase_step: u32,
    /// How much to shrink cwnd on each over-shoot tick
    #[arg(
        long,
        default_value = "1",
        value_name = "N",
        help_heading = "Congestion control (advanced)"
    )]
    pub auto_meta_decrease_step: u32,
    /// Long-horizon sample window (e.g. "10s"). Drives the baseline
    /// percentile; samples older than this are evicted on every tick.
    #[arg(
        long,
        default_value = "10s",
        value_name = "DUR",
        help_heading = "Congestion control (advanced)"
    )]
    pub auto_meta_long_window: humantime::Duration,
    /// Short-horizon sample window (e.g. "1s"). Drives the current-state
    /// percentile; must be strictly less than `--auto-meta-long-window`.
    #[arg(
        long,
        default_value = "1s",
        value_name = "DUR",
        help_heading = "Congestion control (advanced)"
    )]
    pub auto_meta_short_window: humantime::Duration,
    /// Control-loop tick interval (e.g. "50ms")
    #[arg(
        long,
        default_value = "50ms",
        value_name = "DUR",
        help_heading = "Congestion control (advanced)"
    )]
    pub auto_meta_tick_interval: humantime::Duration,
    /// Enable in-memory HDR latency histograms per (side, op). Implies
    /// `--auto-meta-throttle`. Adds a distribution panel beneath the
    /// existing one-line-per-controller summary in the progress display.
    #[arg(long, help_heading = "Congestion control")]
    pub auto_meta_histogram: bool,
    /// Write a binary log of per-(side, op) HDR histograms to the given
    /// path. The file is truncated if it already exists — rename or move
    /// logs you want to keep across runs. Format documented in
    /// `docs/congestion_control.md`. Implies `--auto-meta-histogram` and
    /// `--auto-meta-throttle`.
    #[arg(long, value_name = "PATH", help_heading = "Congestion control")]
    pub auto_meta_histogram_log: Option<std::path::PathBuf>,
    /// Snapshot cadence for the histogram logger (e.g. "1s"). Drives both
    /// the panel refresh rate and the log-file record interval. Range
    /// `[100ms, 60s]`.
    #[arg(
        long,
        default_value = "1s",
        value_name = "DUR",
        help_heading = "Congestion control"
    )]
    pub auto_meta_histogram_interval: humantime::Duration,
}

impl CommonArgs {
    /// Build a [`crate::OutputConfig`]. `quiet` and `print_summary` are
    /// supplied by the caller (each binary owns its own `--quiet` and
    /// `--summary` flags so it can document binary-specific semantics).
    #[must_use]
    pub fn output_config(&self, quiet: bool, print_summary: bool) -> crate::OutputConfig {
        crate::OutputConfig {
            quiet,
            verbose: self.verbose,
            print_summary,
            ..Default::default()
        }
    }
    /// Build a [`crate::RuntimeConfig`] from these args.
    #[must_use]
    pub fn runtime_config(&self) -> crate::RuntimeConfig {
        crate::RuntimeConfig {
            max_workers: self.max_workers,
            max_blocking_threads: self.max_blocking_threads,
        }
    }
    /// Build a [`crate::ThrottleConfig`]. `max_open_files` and `chunk_size`
    /// are supplied by the caller (filegen has its own `--max-open-files`
    /// default; chunk_size has different parser types per binary).
    #[must_use]
    pub fn throttle_config(
        &self,
        max_open_files: Option<usize>,
        chunk_size: u64,
    ) -> crate::ThrottleConfig {
        let auto_meta_implied = self.auto_meta_throttle
            || self.auto_meta_histogram
            || self.auto_meta_histogram_log.is_some();
        let auto_meta = auto_meta_implied.then(|| crate::AutoMetaThrottleConfig {
            initial_cwnd: self.auto_meta_initial_cwnd,
            min_cwnd: self.auto_meta_min_cwnd,
            max_cwnd: self.auto_meta_max_cwnd,
            alpha: self.auto_meta_alpha,
            beta: self.auto_meta_beta,
            increase_step: self.auto_meta_increase_step,
            decrease_step: self.auto_meta_decrease_step,
            baseline_percentile: self.auto_meta_baseline_percentile,
            current_percentile: self.auto_meta_current_percentile,
            long_window: self.auto_meta_long_window.into(),
            short_window: self.auto_meta_short_window.into(),
            tick_interval: self.auto_meta_tick_interval.into(),
        });
        crate::ThrottleConfig {
            max_open_files,
            ops_throttle: self.ops_throttle,
            iops_throttle: self.iops_throttle,
            chunk_size,
            auto_meta,
            histogram_enabled: self.auto_meta_histogram || self.auto_meta_histogram_log.is_some(),
            histogram_log_path: self.auto_meta_histogram_log.clone(),
            histogram_interval: self.auto_meta_histogram_interval.into(),
        }
    }
    /// Returns true if any progress-related flag was set.
    ///
    /// `--auto-meta-histogram` implies progress because its sole purpose is
    /// to render a live distribution panel. `--auto-meta-histogram-log` does
    /// NOT imply progress — it writes to a file regardless of progress mode,
    /// and forcing a display would be worse UX for users who only want the
    /// file.
    #[must_use]
    pub fn progress_requested(&self) -> bool {
        self.progress
            || self.progress_type.is_some()
            || self.progress_delay.is_some()
            || self.auto_meta_histogram
    }

    /// Build user-facing [`crate::ProgressSettings`] when any progress flag was
    /// set, else `None`. `kind` selects the tool-specific printer. For `rcp`'s
    /// remote-master and `rcpd`'s remote progress modes, build `ProgressSettings`
    /// directly instead of using this helper.
    #[must_use]
    pub fn user_progress_settings(
        &self,
        kind: crate::progress::LocalProgressKind,
    ) -> Option<crate::ProgressSettings> {
        if !self.progress_requested() {
            return None;
        }
        Some(crate::ProgressSettings {
            progress_type: crate::GeneralProgressType::User {
                progress_type: self.progress_type.unwrap_or_default(),
                kind,
            },
            progress_delay: self.progress_delay.clone(),
        })
    }
}

#[cfg(test)]
mod implies_tests {
    use super::*;
    use clap::Parser;

    #[derive(Parser)]
    struct TestCli {
        #[command(flatten)]
        common: CommonArgs,
    }

    #[test]
    fn auto_meta_histogram_implies_throttle_at_cli() {
        let cli = TestCli::parse_from(["test", "--auto-meta-histogram"]);
        let throttle = cli.common.throttle_config(None, 0);
        assert!(
            throttle.auto_meta.is_some(),
            "histogram flag must imply auto_meta"
        );
        assert!(throttle.histogram_enabled);
    }

    #[test]
    fn auto_meta_histogram_log_implies_throttle_at_cli() {
        let cli = TestCli::parse_from(["test", "--auto-meta-histogram-log", "/tmp/x.hdr"]);
        let throttle = cli.common.throttle_config(None, 0);
        assert!(
            throttle.auto_meta.is_some(),
            "histogram-log flag must imply auto_meta"
        );
        assert!(throttle.histogram_log_path.is_some());
    }

    #[test]
    fn no_auto_meta_flags_means_no_throttle() {
        let cli = TestCli::parse_from(["test"]);
        let throttle = cli.common.throttle_config(None, 0);
        assert!(throttle.auto_meta.is_none());
        assert!(!throttle.histogram_enabled);
    }

    /// `--auto-meta-histogram` (panel-only) sets `auto_meta_throttle = false`.
    ///
    /// The rcp binary uses this distinction when building `RcpdConfig`: it
    /// gates `RcpdConfig::auto_meta` on `auto_meta_throttle || histogram_log
    /// .is_some()`, so that the panel-only flag does NOT silently enable the
    /// throttle pipeline on remote daemons.  This test pins that distinction
    /// at the `CommonArgs` level so a future refactor cannot accidentally
    /// collapse the two flags.
    #[test]
    fn panel_flag_does_not_set_explicit_throttle_field() {
        let cli = TestCli::parse_from(["test", "--auto-meta-histogram"]);
        // `throttle_config()` returns `auto_meta = Some(...)` because the
        // panel needs the throttle pipeline locally — that's intentional.
        assert!(cli.common.throttle_config(None, 0).auto_meta.is_some());
        // But the *explicit* throttle field must stay false so the rcp binary
        // can distinguish "panel only" from "user explicitly asked for throttle".
        assert!(
            !cli.common.auto_meta_throttle,
            "--auto-meta-histogram must not set auto_meta_throttle"
        );
        // And no log path either.
        assert!(cli.common.auto_meta_histogram_log.is_none());
    }

    #[test]
    fn auto_meta_histogram_implies_progress() {
        let cli = TestCli::parse_from(["test", "--auto-meta-histogram"]);
        assert!(
            cli.common.progress_requested(),
            "--auto-meta-histogram alone must imply progress so the panel actually renders",
        );
    }

    #[test]
    fn auto_meta_histogram_log_does_not_imply_progress() {
        // the log file writes regardless of progress; user can opt in to
        // progress separately. don't force it on them.
        let cli = TestCli::parse_from(["test", "--auto-meta-histogram-log", "/tmp/x.hdr"]);
        assert!(
            !cli.common.progress_requested(),
            "--auto-meta-histogram-log alone should NOT imply progress",
        );
    }
}