1pub const FILES_IN_FLIGHT_HELP: &str = "Maximum concurrent file-like operations. Local automatic defaults resolve from available CPU parallelism on this host, with a floor of 4. Use a positive integer for a finite ceiling or 'unlimited' to remove the user ceiling; lower internal safety ceilings may still apply. This applies to file copy, link, comparison, removal, chmod, and generation work.";
16
17#[derive(Debug, Clone, clap::Args)]
18pub struct CommonArgs {
19 #[arg(long, help_heading = "Progress & output")]
22 pub progress: bool,
23 #[arg(long, value_name = "TYPE", help_heading = "Progress & output")]
27 pub progress_type: Option<crate::ProgressType>,
28 #[arg(long, value_name = "DELAY", help_heading = "Progress & output")]
34 pub progress_delay: Option<String>,
35 #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, help_heading = "Progress & output")]
37 pub verbose: u8,
38 #[arg(
41 long,
42 default_value = "0",
43 value_name = "N",
44 help_heading = "Performance & throttling"
45 )]
46 pub ops_throttle: usize,
47 #[arg(
51 long,
52 default_value = "0",
53 value_name = "N",
54 help_heading = "Performance & throttling"
55 )]
56 pub iops_throttle: usize,
57 #[arg(long, value_name = "N|unlimited", value_parser = parse_files_in_flight_limit,
59 help = FILES_IN_FLIGHT_HELP,
60 help_heading = "Performance & throttling",
61 conflicts_with = "max_open_files")]
62 pub max_files_in_flight: Option<crate::ConcurrencyLimit>,
63 #[arg(
64 long,
65 value_name = "N",
66 hide = true,
67 conflicts_with = "max_files_in_flight"
68 )]
69 pub max_open_files: Option<usize>,
70 #[arg(
73 long,
74 default_value = "0",
75 value_name = "N",
76 help_heading = "Advanced settings"
77 )]
78 pub max_workers: usize,
79 #[arg(
81 long,
82 default_value = "0",
83 value_name = "N",
84 help_heading = "Advanced settings"
85 )]
86 pub max_blocking_threads: usize,
87 #[arg(long, help_heading = "Congestion control")]
90 pub auto_meta_throttle: bool,
91 #[arg(
93 long,
94 default_value = "1",
95 value_name = "N",
96 help_heading = "Congestion control"
97 )]
98 pub auto_meta_initial_cwnd: u32,
99 #[arg(
101 long,
102 default_value = "1",
103 value_name = "N",
104 help_heading = "Congestion control (advanced)"
105 )]
106 pub auto_meta_min_cwnd: u32,
107 #[arg(
109 long,
110 default_value = "4096",
111 value_name = "N",
112 help_heading = "Congestion control"
113 )]
114 pub auto_meta_max_cwnd: u32,
115 #[arg(
125 long,
126 default_value = "1.3",
127 value_name = "F",
128 help_heading = "Congestion control (advanced)"
129 )]
130 pub auto_meta_alpha: f64,
131 #[arg(
135 long,
136 default_value = "1.8",
137 value_name = "F",
138 help_heading = "Congestion control (advanced)"
139 )]
140 pub auto_meta_beta: f64,
141 #[arg(
149 long,
150 default_value = "0.1",
151 value_name = "F",
152 help_heading = "Congestion control (advanced)"
153 )]
154 pub auto_meta_baseline_percentile: f64,
155 #[arg(
159 long,
160 default_value = "0.5",
161 value_name = "F",
162 help_heading = "Congestion control (advanced)"
163 )]
164 pub auto_meta_current_percentile: f64,
165 #[arg(
167 long,
168 default_value = "1",
169 value_name = "N",
170 help_heading = "Congestion control (advanced)"
171 )]
172 pub auto_meta_increase_step: u32,
173 #[arg(
175 long,
176 default_value = "1",
177 value_name = "N",
178 help_heading = "Congestion control (advanced)"
179 )]
180 pub auto_meta_decrease_step: u32,
181 #[arg(
184 long,
185 default_value = "10s",
186 value_name = "DUR",
187 help_heading = "Congestion control (advanced)"
188 )]
189 pub auto_meta_long_window: humantime::Duration,
190 #[arg(
193 long,
194 default_value = "1s",
195 value_name = "DUR",
196 help_heading = "Congestion control (advanced)"
197 )]
198 pub auto_meta_short_window: humantime::Duration,
199 #[arg(
201 long,
202 default_value = "50ms",
203 value_name = "DUR",
204 help_heading = "Congestion control (advanced)"
205 )]
206 pub auto_meta_tick_interval: humantime::Duration,
207 #[arg(long, help_heading = "Congestion control")]
211 pub auto_meta_histogram: bool,
212 #[arg(long, value_name = "PATH", help_heading = "Congestion control")]
218 pub auto_meta_histogram_log: Option<std::path::PathBuf>,
219 #[arg(
223 long,
224 default_value = "1s",
225 value_name = "DUR",
226 help_heading = "Congestion control"
227 )]
228 pub auto_meta_histogram_interval: humantime::Duration,
229}
230
231impl CommonArgs {
232 #[must_use]
233 pub fn resolve_files_in_flight(&self) -> crate::ResolvedFilesInFlight {
234 match (self.max_files_in_flight, self.max_open_files) {
235 (Some(value), None) => crate::ResolvedFilesInFlight::explicit_limit(value),
236 (None, Some(value)) => crate::ResolvedFilesInFlight::legacy(value),
237 (None, None) => crate::ResolvedFilesInFlight::automatic(),
238 (Some(_), Some(_)) => unreachable!("clap rejects conflicting file limits"),
239 }
240 }
241 #[must_use]
245 pub fn output_config(&self, quiet: bool, print_summary: bool) -> crate::OutputConfig {
246 crate::OutputConfig {
247 quiet,
248 verbose: self.verbose,
249 print_summary,
250 ..Default::default()
251 }
252 }
253 #[must_use]
255 pub fn runtime_config(&self) -> crate::RuntimeConfig {
256 crate::RuntimeConfig {
257 max_workers: self.max_workers,
258 max_blocking_threads: self.max_blocking_threads,
259 }
260 }
261 #[must_use]
263 pub fn throttle_config(
264 &self,
265 files_in_flight: crate::ResolvedFilesInFlight,
266 chunk_size: u64,
267 ) -> crate::ThrottleConfig {
268 let auto_meta_implied = self.auto_meta_throttle
269 || self.auto_meta_histogram
270 || self.auto_meta_histogram_log.is_some();
271 let auto_meta = auto_meta_implied.then(|| crate::AutoMetaThrottleConfig {
272 initial_cwnd: self.auto_meta_initial_cwnd,
273 min_cwnd: self.auto_meta_min_cwnd,
274 max_cwnd: self.auto_meta_max_cwnd,
275 alpha: self.auto_meta_alpha,
276 beta: self.auto_meta_beta,
277 increase_step: self.auto_meta_increase_step,
278 decrease_step: self.auto_meta_decrease_step,
279 baseline_percentile: self.auto_meta_baseline_percentile,
280 current_percentile: self.auto_meta_current_percentile,
281 long_window: self.auto_meta_long_window.into(),
282 short_window: self.auto_meta_short_window.into(),
283 tick_interval: self.auto_meta_tick_interval.into(),
284 });
285 crate::ThrottleConfig {
286 files_in_flight,
287 apply_files_in_flight: true,
288 ops_throttle: self.ops_throttle,
289 iops_throttle: self.iops_throttle,
290 chunk_size,
291 auto_meta,
292 histogram_enabled: self.auto_meta_histogram || self.auto_meta_histogram_log.is_some(),
293 histogram_log_path: self.auto_meta_histogram_log.clone(),
294 histogram_interval: self.auto_meta_histogram_interval.into(),
295 }
296 }
297 #[must_use]
305 pub fn progress_requested(&self) -> bool {
306 self.progress
307 || self.progress_type.is_some()
308 || self.progress_delay.is_some()
309 || self.auto_meta_histogram
310 }
311
312 #[must_use]
317 pub fn user_progress_settings(
318 &self,
319 kind: crate::progress::LocalProgressKind,
320 ) -> Option<crate::ProgressSettings> {
321 if !self.progress_requested() {
322 return None;
323 }
324 Some(crate::ProgressSettings {
325 progress_type: crate::GeneralProgressType::User {
326 progress_type: self.progress_type.unwrap_or_default(),
327 kind,
328 },
329 progress_delay: self.progress_delay.clone(),
330 })
331 }
332}
333
334pub fn parse_positive_usize(value: &str) -> Result<std::num::NonZeroUsize, String> {
335 let value = value.parse::<usize>().map_err(|error| error.to_string())?;
336 std::num::NonZeroUsize::new(value).ok_or_else(|| "value must be at least 1".to_string())
337}
338
339pub fn parse_files_in_flight_limit(value: &str) -> Result<crate::ConcurrencyLimit, String> {
340 if value == "unlimited" {
341 return Ok(crate::ConcurrencyLimit::Unlimited);
342 }
343 parse_positive_usize(value).map(crate::ConcurrencyLimit::Limited)
344}
345
346pub fn parse_positive_u64(value: &str) -> Result<u64, String> {
347 let value = value.parse::<u64>().map_err(|error| error.to_string())?;
348 (value > 0)
349 .then_some(value)
350 .ok_or_else(|| "value must be at least 1".to_string())
351}
352
353#[cfg(test)]
354mod implies_tests {
355 use super::*;
356 use clap::Parser;
357 use std::num::NonZeroUsize;
358
359 #[derive(Debug, Parser)]
360 struct TestCli {
361 #[command(flatten)]
362 common: CommonArgs,
363 }
364
365 #[test]
366 fn max_files_in_flight_resolves_cli_sources() {
367 let explicit = TestCli::try_parse_from(["test", "--max-files-in-flight=1"])
368 .expect("positive max-files-in-flight must parse")
369 .common
370 .resolve_files_in_flight();
371 assert_eq!(
372 explicit,
373 crate::ResolvedFilesInFlight::explicit(NonZeroUsize::new(1).unwrap())
374 );
375 let explicit_unlimited =
376 TestCli::try_parse_from(["test", "--max-files-in-flight=unlimited"])
377 .expect("explicit unlimited max-files-in-flight must parse")
378 .common
379 .resolve_files_in_flight();
380 assert_eq!(
381 explicit_unlimited,
382 crate::ResolvedFilesInFlight::unlimited()
383 );
384 let automatic = TestCli::try_parse_from(["test"])
385 .expect("omitted max-files-in-flight must parse")
386 .common
387 .resolve_files_in_flight();
388 assert_eq!(automatic.source(), crate::FilesInFlightSource::Automatic);
389 let legacy_zero = TestCli::try_parse_from(["test", "--max-open-files=0"])
390 .expect("legacy zero must parse")
391 .common
392 .resolve_files_in_flight();
393 assert_eq!(legacy_zero.limit(), crate::ConcurrencyLimit::Unlimited);
394 assert_eq!(
395 legacy_zero.source(),
396 crate::FilesInFlightSource::DeprecatedMaxOpenFiles
397 );
398 let legacy_positive = TestCli::try_parse_from(["test", "--max-open-files=7"])
399 .expect("positive legacy value must parse")
400 .common
401 .resolve_files_in_flight();
402 assert_eq!(legacy_positive, crate::ResolvedFilesInFlight::legacy(7));
403 }
404
405 #[test]
406 fn max_files_in_flight_rejects_invalid_and_conflicting_values() {
407 let zero = TestCli::try_parse_from(["test", "--max-files-in-flight=0"])
408 .expect_err("zero max-files-in-flight must be rejected");
409 assert!(zero.to_string().contains("value must be at least 1"));
410 assert!(TestCli::try_parse_from(["test", "--max-files-in-flight=nope"]).is_err());
411 assert!(
412 TestCli::try_parse_from(["test", "--max-files-in-flight=1", "--max-open-files=1",])
413 .is_err()
414 );
415 }
416
417 #[test]
418 fn max_files_in_flight_help_stays_local_to_shared_tools() {
419 let help = TestCli::try_parse_from(["test", "--help"])
420 .expect_err("help must stop clap parsing")
421 .to_string();
422 assert!(help.contains(
423 "Local automatic defaults resolve from available CPU parallelism on this host"
424 ));
425 assert!(
426 !help.contains("source rcpd"),
427 "shared CLI help must not describe remote negotiation: {help}"
428 );
429 }
430
431 #[test]
432 fn long_help_only_advertises_max_files_in_flight() {
433 let help = TestCli::try_parse_from(["test", "--help"])
434 .expect_err("help must stop clap parsing")
435 .to_string();
436 assert!(help.contains("--max-files-in-flight"));
437 assert!(!help.contains("--max-open-files"));
438 }
439
440 #[test]
441 fn auto_meta_histogram_implies_throttle_at_cli() {
442 let cli = TestCli::parse_from(["test", "--auto-meta-histogram"]);
443 let throttle = cli
444 .common
445 .throttle_config(cli.common.resolve_files_in_flight(), 0);
446 assert!(
447 throttle.auto_meta.is_some(),
448 "histogram flag must imply auto_meta"
449 );
450 assert!(throttle.histogram_enabled);
451 }
452
453 #[test]
454 fn auto_meta_histogram_log_implies_throttle_at_cli() {
455 let cli = TestCli::parse_from(["test", "--auto-meta-histogram-log", "/tmp/x.hdr"]);
456 let throttle = cli
457 .common
458 .throttle_config(cli.common.resolve_files_in_flight(), 0);
459 assert!(
460 throttle.auto_meta.is_some(),
461 "histogram-log flag must imply auto_meta"
462 );
463 assert!(throttle.histogram_log_path.is_some());
464 }
465
466 #[test]
467 fn no_auto_meta_flags_means_no_throttle() {
468 let cli = TestCli::parse_from(["test"]);
469 let throttle = cli
470 .common
471 .throttle_config(cli.common.resolve_files_in_flight(), 0);
472 assert!(throttle.auto_meta.is_none());
473 assert!(!throttle.histogram_enabled);
474 }
475
476 #[test]
485 fn panel_flag_does_not_set_explicit_throttle_field() {
486 let cli = TestCli::parse_from(["test", "--auto-meta-histogram"]);
487 assert!(
490 cli.common
491 .throttle_config(cli.common.resolve_files_in_flight(), 0)
492 .auto_meta
493 .is_some()
494 );
495 assert!(
498 !cli.common.auto_meta_throttle,
499 "--auto-meta-histogram must not set auto_meta_throttle"
500 );
501 assert!(cli.common.auto_meta_histogram_log.is_none());
503 }
504
505 #[test]
506 fn auto_meta_histogram_implies_progress() {
507 let cli = TestCli::parse_from(["test", "--auto-meta-histogram"]);
508 assert!(
509 cli.common.progress_requested(),
510 "--auto-meta-histogram alone must imply progress so the panel actually renders",
511 );
512 }
513
514 #[test]
515 fn auto_meta_histogram_log_does_not_imply_progress() {
516 let cli = TestCli::parse_from(["test", "--auto-meta-histogram-log", "/tmp/x.hdr"]);
519 assert!(
520 !cli.common.progress_requested(),
521 "--auto-meta-histogram-log alone should NOT imply progress",
522 );
523 }
524}