git_perf/
cli.rs

1use anyhow::anyhow;
2use anyhow::Result;
3use clap::{error::ErrorKind::ArgumentConflict, Args, Parser};
4use clap::{CommandFactory, Subcommand};
5use std::path::PathBuf;
6
7use crate::audit;
8use crate::basic_measure::measure;
9use crate::config::bump_epoch;
10use crate::data::ReductionFunc;
11use crate::git_interop::{prune, pull, push};
12use crate::measurement_storage::add;
13use crate::reporting::report;
14
15#[derive(Parser)]
16#[command(version)]
17struct Cli {
18    #[command(subcommand)]
19    command: Commands,
20}
21
22#[derive(Args)]
23struct CliMeasurement {
24    /// Name of the measurement
25    #[arg(short = 'm', long = "measurement", value_parser=parse_spaceless_string)]
26    name: String,
27
28    /// Key-value pairs separated by '='
29    #[arg(short, long, value_parser=parse_key_value)]
30    key_value: Vec<(String, String)>,
31}
32
33#[derive(Args)]
34struct CliReportHistory {
35    /// Limit the number of previous commits considered.
36    /// HEAD is included in this count.
37    #[arg(short = 'n', long, default_value = "40")]
38    max_count: usize,
39}
40
41#[derive(Subcommand)]
42enum Commands {
43    /// Measure the runtime of the supplied command (in nanoseconds)
44    Measure {
45        /// Repetitions
46        #[arg(short = 'n', long, value_parser=clap::value_parser!(u16).range(1..), default_value = "1")]
47        repetitions: u16,
48
49        #[command(flatten)]
50        measurement: CliMeasurement,
51
52        /// Command to measure
53        #[arg(required(true), last(true))]
54        command: Vec<String>,
55    },
56
57    /// Add single measurement
58    Add {
59        /// Measured value to be added
60        value: f64,
61
62        #[command(flatten)]
63        measurement: CliMeasurement,
64    },
65
66    /// Publish performance results to remote
67    Push {},
68
69    /// Pull performance results from remote
70    Pull {},
71
72    /// Create an HTML performance report
73    Report {
74        /// HTML output file
75        #[arg(short, long, default_value = "output.html")]
76        output: PathBuf,
77
78        #[command(flatten)]
79        report_history: CliReportHistory,
80
81        /// Select an individual measurements instead of all
82        #[arg(short, long)]
83        measurement: Vec<String>,
84
85        /// Key-value pairs separated by '=', select only matching measurements
86        #[arg(short, long, value_parser=parse_key_value)]
87        key_value: Vec<(String, String)>,
88
89        /// Create individual traces in the graph by grouping with the value of this selector
90        #[arg(short, long, value_parser=parse_spaceless_string)]
91        separate_by: Option<String>,
92
93        /// What to aggregate the measurements in each group with
94        #[arg(short, long)]
95        aggregate_by: Option<ReductionFunc>,
96    },
97
98    /// For a given measurement, check perfomance deviations of the HEAD commit
99    /// against `<n>` previous commits. Group previous results and aggregate their
100    /// results before comparison.
101    Audit {
102        #[arg(short, long, value_parser=parse_spaceless_string)]
103        measurement: String,
104
105        #[command(flatten)]
106        report_history: CliReportHistory,
107
108        /// Key-value pair separated by "=" with no whitespaces to subselect measurements
109        #[arg(short, long, value_parser=parse_key_value)]
110        selectors: Vec<(String, String)>,
111
112        /// Minimum number of measurements needed. If less, pass test and assume
113        /// more measurements are needed.
114        /// A minimum of two historic measurements are needed for proper evaluation of standard
115        /// deviation.
116        // TODO(kaihowl) fix up min value and default_value
117        #[arg(long, value_parser=clap::value_parser!(u16).range(1..), default_value="2")]
118        min_measurements: u16,
119
120        /// What to aggregate the measurements in each group with
121        #[arg(short, long, default_value = "min")]
122        aggregate_by: ReductionFunc,
123
124        /// Multiple of the stddev after which a outlier is detected.
125        /// If the HEAD measurement is within `[mean-<d>*sigma; mean+<d>*sigma]`,
126        /// it is considered acceptable.
127        #[arg(short = 'd', long, default_value = "4.0")]
128        sigma: f64,
129    },
130
131    /// Accept HEAD commit's measurement for audit, even if outside of range.
132    /// This is allows to accept expected performance changes.
133    /// This is accomplished by starting a new epoch for the given measurement.
134    /// The epoch is configured in the git perf config file.
135    /// A change to the epoch therefore has to be committed and will result in a new HEAD for which
136    /// new measurements have to be taken.
137    BumpEpoch {
138        #[arg(short = 'm', long = "measurement", value_parser=parse_spaceless_string)]
139        measurement: String,
140    },
141
142    /// Remove all performance measurements for non-existent/unreachable objects.
143    /// Will refuse to work if run on a shallow clone.
144    Prune {},
145
146    /// Generate the manpage content
147    #[command(hide = true)]
148    Manpage {},
149}
150
151fn parse_key_value(s: &str) -> Result<(String, String)> {
152    let pos = s
153        .find('=')
154        .ok_or_else(|| anyhow!("invalid key=value: no '=' found in '{}'", s))?;
155    let key = parse_spaceless_string(&s[..pos])?;
156    let value = parse_spaceless_string(&s[pos + 1..])?;
157    Ok((key, value))
158}
159
160fn parse_spaceless_string(s: &str) -> Result<String> {
161    if s.split_whitespace().count() > 1 {
162        Err(anyhow!("invalid string/key/value: found space in '{}'", s))
163    } else {
164        Ok(String::from(s))
165    }
166}
167
168pub fn handle_calls() -> Result<()> {
169    let cli = Cli::parse();
170
171    match cli.command {
172        Commands::Measure {
173            repetitions,
174            command,
175            measurement,
176        } => Ok(measure(
177            &measurement.name,
178            repetitions,
179            &command,
180            &measurement.key_value,
181        )?),
182        Commands::Add { value, measurement } => {
183            Ok(add(&measurement.name, value, &measurement.key_value)?)
184        }
185        Commands::Push {} => Ok(push(None)?),
186        Commands::Pull {} => Ok(pull(None)?),
187        Commands::Report {
188            output,
189            separate_by,
190            report_history,
191            measurement,
192            key_value,
193            aggregate_by,
194        } => Ok(report(
195            output,
196            separate_by,
197            report_history.max_count,
198            &measurement,
199            &key_value,
200            aggregate_by,
201        )?),
202        Commands::Audit {
203            measurement,
204            report_history,
205            selectors,
206            min_measurements,
207            aggregate_by,
208            sigma,
209        } => {
210            if report_history.max_count < min_measurements.into() {
211                Cli::command().error(ArgumentConflict, format!("The minimal number of measurements ({}) cannot be more than the maximum number of measurements ({})", min_measurements, report_history.max_count)).exit()
212            }
213            Ok(audit::audit(
214                &measurement,
215                report_history.max_count,
216                min_measurements,
217                &selectors,
218                aggregate_by,
219                sigma,
220            )?)
221        }
222        Commands::BumpEpoch { measurement } => Ok(bump_epoch(&measurement)?),
223        Commands::Prune {} => Ok(prune()?),
224        Commands::Manpage {} => {
225            generate_manpage().expect("Man page generation failed");
226            Ok(())
227        }
228    }
229}
230
231fn generate_manpage() -> Result<()> {
232    let man = clap_mangen::Man::new(Cli::command());
233    man.render(&mut std::io::stdout())?;
234
235    // TODO(kaihowl) this does not look very nice. Fix it.
236    for command in Cli::command()
237        .get_subcommands()
238        .filter(|c| !c.is_hide_set())
239    {
240        let man = clap_mangen::Man::new(command.clone());
241        man.render(&mut std::io::stdout())?
242    }
243
244    Ok(())
245}
246
247#[cfg(test)]
248mod test {
249    use super::*;
250
251    #[test]
252    fn verify_cli() {
253        Cli::command().debug_assert()
254    }
255}