evobench_util/
evobench-util.rs

1use std::{ffi::OsString, path::PathBuf, sync::Arc};
2
3use ahtml::arc_util::IntoArc;
4use anyhow::{Result, bail};
5use cj_path_util::path_util::AppendToPath;
6use clap::Parser;
7
8use evobench_tools::{
9    ctx,
10    git::GitHash,
11    info,
12    run::{
13        config::{RunConfig, RunConfigBundle},
14        global_app_state_dir::GlobalAppStateDir,
15        output_directory::{
16            html_files::regenerate_index_files,
17            post_process::compress_file_as,
18            structure::{KeyDir, OutputSubdir, RunDir, SubDirs},
19        },
20        working_directory_pool::WorkingDirectoryPoolBaseDir,
21    },
22    serde_types::proper_dirname::ProperDirname,
23    util::grep_diff::GrepDiffRegion,
24    utillib::{
25        get_terminal_width::get_terminal_width,
26        into_arc_path::IntoArcPath,
27        logging::{LogLevelOpts, set_log_level},
28    },
29};
30
31#[derive(clap::Parser, Debug)]
32#[clap(next_line_help = true)]
33#[clap(term_width = get_terminal_width(4))]
34/// Utilities for working with evobench
35struct Opts {
36    #[clap(flatten)]
37    log_level: LogLevelOpts,
38
39    // XX should wrap that help text (COPYPASTE) in a wrapper for flatten
40    /// Override the path to the config file (default: the paths
41    /// `~/.evobench.*` where a single one exists where the `*` is
42    /// the suffix for one of the supported config file formats (run
43    /// `config-formats` to get the list), and if those are missing,
44    /// use compiled-in default config values)
45    #[clap(long)]
46    config: Option<PathBuf>,
47
48    /// The subcommand to run. Use `--help` after the sub-command to
49    /// get a list of the allowed options there.
50    #[clap(subcommand)]
51    subcommand: SubCommand,
52}
53
54#[derive(clap::Subcommand, Debug)]
55enum SubCommand {
56    /// Extract time differences between pairs of lines in log files
57    /// from benchmarking runs--not the evobench.log files, but files
58    /// with captured stdout/stderr, in the working directory pool
59    /// directory (like `$n.output_of_benchmarking_command_at_*`).
60    GrepDiff {
61        /// Filter for commit id
62        #[clap(long, short)]
63        commit: Option<GitHash>,
64
65        /// Filter for target name
66        #[clap(long, short)]
67        target: Option<ProperDirname>,
68
69        /// Filter for custom parameters (environment variables); you
70        /// can provide multiple separated by '/',
71        /// e.g. "FOO=1/BAR=hi"; not all of them need to be provided,
72        /// the filter checks for existance and equality on those
73        /// variables that are provided. NOTE: does not verify correct
74        /// syntax of the variable names and values (currently no
75        /// configuration is read, thus the info is not available)
76        /// except for the basic acceptance rules for custom env var
77        /// names.
78        #[clap(long, short)]
79        params: Option<String>,
80
81        /// The regex to match a log line that starts a timed region
82        regex_start: String,
83
84        /// The regex to match a log line that ends a timed region
85        regex_end: String,
86
87        /// Override the path to the config file (default: the paths
88        /// `~/.evobench.*` where a single one exists where the `*` is
89        /// the suffix for one of the supported config file formats (run
90        /// `config-formats` to get the list), and if those are missing,
91        /// use compiled-in default config values)
92        logfiles: Vec<PathBuf>,
93    },
94
95    /// Do the same "single" post-processing on a single benchmark
96    /// results as `evobench daemon` does--useful in case new
97    /// features were added or the configuration was changed.
98    PostProcessSingle {
99        /// Skip (re)generation of the normal evobench.log Excel and
100        /// flamegraph stats.
101        #[clap(long)]
102        no_stats: bool,
103
104        /// The path to a directory for an individual run, i.e. ending
105        /// in a directory name that is a timestamp
106        run_dir: PathBuf,
107    },
108
109    /// Do the same "summary" post-processing on a set of benchmark
110    /// results as `evobench daemon` does--useful in case new
111    /// features were added or the configuration was changed.
112    PostProcessSummary {
113        /// Run `post-process-single` on all sub-directories for the
114        /// individual runs for this 'key', too.
115        #[clap(long)]
116        single: bool,
117
118        /// Skip (re)generation of the normal evobench.log Excel and
119        /// flamegraph stats. (Only relevant when `--single` is
120        /// given.)
121        #[clap(long)]
122        no_single_stats: bool,
123
124        /// Skip (re)generation of the normal evobench.log Excel and
125        /// flamegraph summary stats.
126        #[clap(long)]
127        no_summary_stats: bool,
128
129        /// The path to a directory for a particular 'key', i.e. a set
130        /// of individual runs: ending in a directory name that is a
131        /// commit id
132        key_dir: PathBuf,
133    },
134
135    /// Development commands; these are meant for app development, not
136    /// for users. Only use when you know what you're doing. .
137    Dev {
138        #[clap(subcommand)]
139        subcommand: DevSubCommand,
140    },
141}
142
143#[derive(clap::Subcommand, Debug)]
144enum DevSubCommand {
145    /// Regenerate index files
146    RegenerateIndexFiles,
147
148    /// List the contents of a folder, structurally
149    ListOutputDir { dir_path: PathBuf },
150}
151
152fn post_process_single(run_dir: &RunDir, run_config: &RunConfig, no_stats: bool) -> Result<()> {
153    let target = run_dir.parent().parent().target_name();
154    let standard_log_path = run_dir.standard_log_path();
155    if !standard_log_path.exists() {
156        info!(
157            "missing {standard_log_path:?} -- try to find and move it \
158             from the working directory pool dir"
159        );
160
161        let date_time_with_offset_str = run_dir.timestamp().as_str();
162
163        // (Is this too involved?)
164        let global_app_state_dir = GlobalAppStateDir::new()?;
165        let pool_base_dir = WorkingDirectoryPoolBaseDir::new(
166            run_config.working_directory_pool.base_dir.clone(),
167            &|| global_app_state_dir.working_directory_pool_base(),
168        )?;
169        let pool_base_dir_path = pool_base_dir.path();
170        // /involved
171
172        let found_log_file_name = {
173            let mut file_names: Vec<OsString> = pool_base_dir_path
174                .read_dir()
175                .map_err(ctx!("reading dir {pool_base_dir_path:?}"))?
176                .map(|entry| -> Result<_> {
177                    let entry = entry?;
178                    let file_name = entry.file_name();
179                    if file_name
180                        .to_string_lossy()
181                        .contains(date_time_with_offset_str)
182                    {
183                        Ok(Some(file_name))
184                    } else {
185                        Ok(None)
186                    }
187                })
188                .filter_map(|v| v.transpose())
189                .collect::<Result<_>>()?;
190            match file_names.len() {
191                1 => file_names.pop().expect("seen"),
192                0 => bail!(
193                    "can't find standard log at {standard_log_path:?} and finding \
194                     {date_time_with_offset_str:?} in {pool_base_dir_path:?} was unsuccessful"
195                ),
196                _ => bail!(
197                    "got more than one match for {date_time_with_offset_str:?} in \
198                     {pool_base_dir_path:?}"
199                ),
200            }
201        };
202
203        let found_log_file_path = pool_base_dir_path.append(&found_log_file_name);
204        info!("found file {found_log_file_path:?}");
205
206        compress_file_as(&found_log_file_path, standard_log_path.clone(), false)?;
207        std::fs::remove_file(&found_log_file_path)?;
208        info!("deleted moved file {found_log_file_path:?}");
209    }
210    run_dir.post_process_single(
211        None,
212        || Ok(()),
213        &target,
214        &standard_log_path,
215        run_config,
216        no_stats,
217    )?;
218    Ok(())
219}
220
221fn main() -> Result<()> {
222    let Opts {
223        config,
224        log_level,
225        subcommand,
226    } = Opts::parse();
227
228    set_log_level(log_level.try_into()?);
229
230    let get_config = {
231        move || -> Result<RunConfigBundle> {
232            let config = config.map(Into::into);
233            Ok(RunConfigBundle::load(
234                config,
235                |msg| bail!("can't load config: {msg}"),
236                GlobalAppStateDir::new()?,
237            )?)
238        }
239    };
240
241    match subcommand {
242        SubCommand::GrepDiff {
243            regex_start,
244            regex_end,
245            logfiles,
246            commit,
247            target,
248            params,
249        } => {
250            let grep_diff_region = GrepDiffRegion::from_strings(&regex_start, &regex_end)?;
251            grep_diff_region.grep_diff(logfiles, commit, target, params)?;
252        }
253        SubCommand::PostProcessSingle { run_dir, no_stats } => {
254            let run_config_bundle = get_config()?;
255            let conf = &run_config_bundle.shareable.run_config;
256
257            let run_dir = RunDir::try_from(run_dir.into_arc_path())?;
258
259            post_process_single(&run_dir, conf, no_stats)?;
260        }
261        SubCommand::PostProcessSummary {
262            single,
263            key_dir,
264            no_single_stats,
265            no_summary_stats,
266        } => {
267            let run_config_bundle = get_config()?;
268            let conf = &run_config_bundle.shareable.run_config;
269
270            let key_dir: Arc<_> = KeyDir::try_from(key_dir.into_arc_path())?.into();
271
272            if single {
273                for run_dir in key_dir.sub_dirs()? {
274                    let run_dir = run_dir?;
275                    post_process_single(&run_dir, conf, no_single_stats)?;
276                }
277            }
278
279            key_dir.generate_summaries_for_key_dir(no_summary_stats)?;
280        }
281        SubCommand::Dev { subcommand } => match subcommand {
282            DevSubCommand::RegenerateIndexFiles => {
283                let run_config_bundle = get_config()?;
284                regenerate_index_files(&run_config_bundle.shareable, None, None)?;
285            }
286            DevSubCommand::ListOutputDir { dir_path } => {
287                let dir_path = dir_path.into_arc_path();
288                let output_subdir = OutputSubdir::try_from(dir_path)?;
289                println!(
290                    "Listing for {} at {:?}:",
291                    output_subdir.type_name(),
292                    output_subdir.to_path()
293                );
294                for subdir in output_subdir.into_arc().sub_dirs()? {
295                    let subdir = subdir?;
296                    println!("{} at {:?}", subdir.type_name(), subdir.to_path());
297                }
298            }
299        },
300    }
301
302    Ok(())
303}