evobench_tools/run/
benchmarking_job.rs

1use std::sync::Arc;
2
3use anyhow::{Result, anyhow, bail};
4
5use crate::{
6    fallback_to_default, fallback_to_option,
7    git::GitHash,
8    run::{
9        config::RunConfig,
10        key::{BenchmarkingJobParameters, CustomParameters, RunParameters},
11        sub_command::insert::{ForceInvalidOpt, InsertBenchmarkingJobOpts},
12    },
13    serde_types::priority::{NonComparableNumber, Priority},
14    utillib::{arc::CloneArc, fallback::FallingBackTo},
15};
16
17use super::{
18    config::{BenchmarkingCommand, JobTemplate},
19    working_directory_pool::WorkingDirectoryId,
20};
21
22#[derive(Debug, PartialEq, Clone, clap::Args, serde::Serialize, serde::Deserialize)]
23#[serde(deny_unknown_fields)]
24#[serde(rename = "BenchmarkingJobSettings")]
25pub struct BenchmarkingJobSettingsOpts {
26    /// The number of times the job should be run in total (across all
27    /// queues). Default (if not defined elsewhere): 5
28    #[clap(short, long)]
29    count: Option<u8>,
30
31    /// How many times a job is allowed to fail before it is removed
32    /// from the pipeline. Default (if not defined elsewhere): 3
33    #[clap(short, long)]
34    error_budget: Option<u8>,
35}
36
37pub struct BenchmarkingJobSettings {
38    count: u8,
39    error_budget: u8,
40}
41
42impl Default for BenchmarkingJobSettings {
43    fn default() -> Self {
44        Self {
45            count: 5,
46            error_budget: 3,
47        }
48    }
49}
50
51impl FallingBackTo for BenchmarkingJobSettingsOpts {
52    fn falling_back_to(
53        self,
54        fallback: &BenchmarkingJobSettingsOpts,
55    ) -> BenchmarkingJobSettingsOpts {
56        let Self {
57            count,
58            error_budget,
59        } = self;
60        fallback_to_option!(fallback.count);
61        fallback_to_option!(fallback.error_budget);
62        BenchmarkingJobSettingsOpts {
63            count,
64            error_budget,
65        }
66    }
67}
68
69impl From<BenchmarkingJobSettingsOpts> for BenchmarkingJobSettings {
70    fn from(value: BenchmarkingJobSettingsOpts) -> Self {
71        let BenchmarkingJobSettingsOpts {
72            count,
73            error_budget,
74        } = value;
75        let default = BenchmarkingJobSettings::default();
76        fallback_to_default!(default.count);
77        fallback_to_default!(default.error_budget);
78        Self {
79            count,
80            error_budget,
81        }
82    }
83}
84
85#[derive(Debug, PartialEq, Clone, clap::Args)]
86pub struct BenchmarkingJobReasonOpt {
87    /// An optional short context string (should be <= 15 characters)
88    /// describing the reason for or context of the job (e.g. used to
89    /// report which git branch the commit was found on).
90    #[clap(long)]
91    pub reason: Option<String>,
92}
93
94#[derive(Debug)]
95pub struct BenchmarkingJobOpts {
96    /// Optional overrides for what values might come from elsewhere
97    pub insert_benchmarking_job_opts: InsertBenchmarkingJobOpts,
98    pub commit_id: GitHash,
99}
100
101/// Just the public constant parts of a BenchmarkingJob
102#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
103#[serde(deny_unknown_fields)]
104pub struct BenchmarkingJobPublic {
105    pub reason: Option<String>,
106    pub run_parameters: Arc<RunParameters>,
107    pub command: Arc<BenchmarkingCommand>,
108}
109
110/// Just the public changing parts of a BenchmarkingJob
111#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
112#[serde(deny_unknown_fields)]
113pub struct BenchmarkingJobState {
114    pub remaining_count: u8,
115    pub remaining_error_budget: u8,
116    pub last_working_directory: Option<WorkingDirectoryId>,
117}
118
119#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
120#[serde(deny_unknown_fields)]
121pub struct BenchmarkingJob {
122    #[serde(flatten)]
123    pub public: BenchmarkingJobPublic,
124    #[serde(flatten)]
125    pub state: BenchmarkingJobState,
126    priority: Priority,
127    current_boost: Priority,
128}
129
130impl BenchmarkingJob {
131    /// Constructor, since some fields are private (needed for
132    /// migration code).
133    pub fn new(
134        public: BenchmarkingJobPublic,
135        state: BenchmarkingJobState,
136        priority: Priority,
137        current_boost: Priority,
138    ) -> Self {
139        Self {
140            public,
141            state,
142            priority,
143            current_boost,
144        }
145    }
146
147    /// Overwrite values in `self` with those from the given overrides
148    /// (and for values in `BenchmarkingJobSettings`, if not provided,
149    /// take those from the config). Delete
150    /// `last_working_directory`. Then check for allowable values
151    /// (custom variables) via settings from the config, unless
152    /// `force` was given.
153    // (A little wasteful as it initializes a new CustomParameters
154    // that is then dropped again.)
155    pub fn check_and_init(
156        &mut self,
157        config: &RunConfig,
158        override_opts: &InsertBenchmarkingJobOpts,
159        override_commit: Option<&GitHash>,
160        force: &ForceInvalidOpt,
161    ) -> Result<()> {
162        // Init: take the values from the overrides, falling back to
163        // the values from `self`. But can't use FallingBackTo as Self
164        // has no options anymore. Thus code up manually.
165        {
166            let BenchmarkingJobState {
167                remaining_count,
168                remaining_error_budget,
169                last_working_directory,
170            } = &mut self.state;
171
172            *last_working_directory = None;
173
174            let InsertBenchmarkingJobOpts {
175                reason,
176                benchmarking_job_settings,
177                priority,
178                initial_boost,
179            } = override_opts;
180
181            {
182                let BenchmarkingJobSettings {
183                    count,
184                    error_budget,
185                } = benchmarking_job_settings
186                    .clone()
187                    .falling_back_to(&config.benchmarking_job_settings)
188                    .into();
189
190                *remaining_count = count;
191                *remaining_error_budget = error_budget;
192            }
193
194            if let Some(initial_boost) = initial_boost {
195                self.current_boost = initial_boost.clone();
196            }
197            if let Some(priority) = priority {
198                self.priority = priority.clone();
199            }
200
201            if let Some(reason) = &reason.reason {
202                self.public.reason = Some(reason.into());
203            }
204
205            if let Some(override_commit) = override_commit {
206                let mut run_parameters: RunParameters = (*self.public.run_parameters).clone();
207                run_parameters.commit_id = override_commit.clone();
208                self.public.run_parameters = Arc::new(run_parameters);
209            }
210        }
211
212        // Checks
213        if !force.force_invalid {
214            let Self {
215                public:
216                    BenchmarkingJobPublic {
217                        reason: _,
218                        run_parameters,
219                        command,
220                    },
221                state: _,
222                priority: _,
223                current_boost: _,
224            } = self;
225
226            let target_name = &command.target_name;
227            let target = config.targets.get(target_name).ok_or_else(|| {
228                anyhow!("target {:?} not found in the config", target_name.as_str())
229            })?;
230
231            let _ = CustomParameters::checked_from(
232                &run_parameters.custom_parameters.keyvals(),
233                &target.allowed_custom_parameters,
234            )?;
235
236            if *command != target.benchmarking_command {
237                bail!(
238                    "command for target {:?} is expected to be {:?}, but is: {:?}",
239                    target_name.as_str(),
240                    target.benchmarking_command,
241                    command
242                );
243            }
244        }
245
246        Ok(())
247    }
248
249    pub fn priority(&self) -> Result<Priority, NonComparableNumber> {
250        self.priority + self.current_boost
251    }
252
253    /// Clones everything except `current_boost` is set to 0. You can
254    /// change the public fields afterwards.
255    pub fn clone_for_queue_reinsertion(&self, state: BenchmarkingJobState) -> Self {
256        let Self {
257            public,
258            priority,
259            current_boost: _,
260            state: _,
261        } = self;
262        Self {
263            public: public.clone(),
264            state,
265            priority: *priority,
266            current_boost: Priority::NORMAL,
267        }
268    }
269
270    pub fn benchmarking_job_parameters(&self) -> BenchmarkingJobParameters {
271        // Ignore all fields that are not "key" parts (inputs
272        // determining/influencing the output)
273        let BenchmarkingJob {
274            public:
275                BenchmarkingJobPublic {
276                    reason: _,
277                    run_parameters,
278                    command,
279                },
280            state: _,
281            priority: _,
282            current_boost: _,
283        } = self;
284        BenchmarkingJobParameters {
285            run_parameters: run_parameters.clone_arc(),
286            command: command.clone_arc(),
287        }
288    }
289}
290
291impl BenchmarkingJobOpts {
292    /// Make one job per job template, filling the missing values
293    /// (currently just `priority` and `initial_boost`) and all other
294    /// values from the job template.
295    pub fn complete_jobs(&self, job_template_list: &[JobTemplate]) -> Vec<BenchmarkingJob> {
296        let Self {
297            insert_benchmarking_job_opts:
298                InsertBenchmarkingJobOpts {
299                    reason,
300                    benchmarking_job_settings,
301                    priority: opts_priority,
302                    initial_boost: opts_initial_boost,
303                },
304            commit_id,
305        } = self;
306
307        let BenchmarkingJobSettings {
308            count,
309            error_budget,
310        } = benchmarking_job_settings.clone().into();
311
312        job_template_list
313            .iter()
314            .map(|job_template| {
315                let JobTemplate {
316                    priority,
317                    initial_boost,
318                    command,
319                    custom_parameters,
320                } = job_template;
321
322                BenchmarkingJob {
323                    public: BenchmarkingJobPublic {
324                        reason: reason.reason.clone(),
325                        run_parameters: Arc::new(RunParameters {
326                            commit_id: commit_id.clone(),
327                            custom_parameters: custom_parameters.clone_arc(),
328                        }),
329                        command: command.clone_arc(),
330                    },
331                    state: BenchmarkingJobState {
332                        remaining_count: count,
333                        remaining_error_budget: error_budget,
334                        last_working_directory: None,
335                    },
336                    priority: opts_priority.unwrap_or(*priority),
337                    current_boost: opts_initial_boost.unwrap_or(*initial_boost),
338                }
339            })
340            .collect()
341    }
342}