Skip to main content

diskann_benchmark_runner/
registry.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use std::collections::{hash_map::Entry, HashMap};
7
8use thiserror::Error;
9
10use crate::{
11    benchmark::{self, Benchmark, FailureScore, MatchContext, Regression, Score},
12    input, Checkpoint, Input, Output,
13};
14
15/// A registered benchmark entry: a name paired with a type-erased benchmark.
16pub(crate) struct RegisteredBenchmark {
17    name: String,
18    benchmark: Box<dyn benchmark::internal::Benchmark>,
19}
20
21impl std::fmt::Debug for RegisteredBenchmark {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        let benchmark = self.benchmark.as_string();
24        f.debug_struct("RegisteredBenchmark")
25            .field("name", &self.name)
26            .field("benchmark", &benchmark)
27            .finish()
28    }
29}
30
31impl RegisteredBenchmark {
32    pub(crate) fn name(&self) -> &str {
33        &self.name
34    }
35
36    pub(crate) fn benchmark(&self) -> &dyn benchmark::internal::Benchmark {
37        &*self.benchmark
38    }
39}
40
41/// A collection of registered inputs and benchmarks.
42pub struct Registry {
43    // Inputs keyed by their tag type.
44    inputs: HashMap<&'static str, Box<dyn input::internal::DynInput>>,
45    benchmarks: Vec<RegisteredBenchmark>,
46}
47
48impl Registry {
49    /// Return a new empty registry.
50    pub fn new() -> Self {
51        Self {
52            inputs: HashMap::new(),
53            benchmarks: Vec::new(),
54        }
55    }
56
57    /// Return the input with the registered `tag` if present. Otherwise, return `None`.
58    ///
59    /// Inputs are automatically registered as a side-effect of:
60    ///
61    /// * [`register`](Self::register)
62    /// * [`register_regression`](Self::register_regression)
63    pub fn input(&self, tag: &str) -> Option<input::Registered<'_>> {
64        self._input(tag).map(input::Registered)
65    }
66
67    /// Return an iterator over all registered input tags in an unspecified order.
68    pub fn input_tags(&self) -> impl ExactSizeIterator<Item = &'static str> + use<'_> {
69        self.inputs.keys().copied()
70    }
71
72    /// Register a new `benchmark` with the given `name`.
73    ///
74    /// As a side-effect, the benchmark's [`Input`](Benchmark::Input) type is also registered.
75    /// Duplicate registrations of the same tag and type are allowed; mismatched types for the
76    /// same tag return an error.
77    pub fn register<T>(
78        &mut self,
79        name: impl Into<String>,
80        benchmark: T,
81    ) -> Result<(), RegistryError>
82    where
83        T: Benchmark,
84    {
85        self.register_input::<T::Input>()?;
86
87        self.benchmarks.push(RegisteredBenchmark {
88            name: name.into(),
89            benchmark: Box::new(benchmark::internal::Wrapper::<T, _>::new(
90                benchmark,
91                benchmark::internal::NoRegression,
92            )),
93        });
94        Ok(())
95    }
96
97    /// Return an iterator over registered benchmark names and their descriptions.
98    pub(crate) fn names(&self) -> impl ExactSizeIterator<Item = (&str, String)> {
99        self.benchmarks
100            .iter()
101            .map(|entry| (entry.name.as_str(), entry.benchmark.as_string()))
102    }
103
104    /// Return `true` if `job` matches with any registered benchmark. Otherwise, return `false`.
105    pub(crate) fn has_match(&self, job: &input::internal::Any) -> bool {
106        self.find_best_match(job).is_some()
107    }
108
109    /// Attempt to run the best matching benchmark for `job`.
110    ///
111    /// Returns the results of the benchmark if successful.
112    ///
113    /// Errors if a suitable method could not be found or if the invoked benchmark failed.
114    pub(crate) fn call(
115        &self,
116        job: &input::internal::Any,
117        checkpoint: Checkpoint<'_>,
118        output: &mut dyn Output,
119    ) -> anyhow::Result<serde_json::Value> {
120        match self.find_best_match(job) {
121            Some(entry) => entry.benchmark.run(job, checkpoint, output),
122            None => Err(anyhow::Error::msg(
123                "could not find a matching benchmark for the given input",
124            )),
125        }
126    }
127
128    /// Attempt to debug reasons for a missed dispatch, returning at most `max_methods`
129    /// reasons.
130    ///
131    /// Returns `Ok(())` if a match was found.
132    pub(crate) fn debug(
133        &self,
134        job: &input::internal::Any,
135        max_methods: usize,
136    ) -> Result<(), Vec<Mismatch>> {
137        if self.has_match(job) {
138            return Ok(());
139        }
140
141        // Collect all failures with their scores and reasons in a single pass.
142        let mut failures: Vec<(&RegisteredBenchmark, FailureScore, Score)> = self
143            .benchmarks
144            .iter()
145            .filter_map(|entry| {
146                let score = entry
147                    .benchmark
148                    .try_match(job, &MatchContext::with_reasons());
149                match score.as_raw() {
150                    crate::benchmark::RawScore::Success(_) => None,
151                    crate::benchmark::RawScore::Failure(fail_score) => {
152                        Some((entry, fail_score, score))
153                    }
154                }
155            })
156            .collect();
157
158        failures.sort_by_key(|(_, score, _)| *score);
159        failures.truncate(max_methods);
160
161        let mismatches = failures
162            .into_iter()
163            .map(|(entry, _, score)| Mismatch {
164                method: entry.name.clone(),
165                reason: score.reason().to_string(),
166            })
167            .collect();
168
169        Err(mismatches)
170    }
171
172    /// Find the best matching benchmark for `job` by score.
173    fn find_best_match(&self, job: &input::internal::Any) -> Option<&RegisteredBenchmark> {
174        self.benchmarks
175            .iter()
176            .filter_map(|entry| {
177                entry
178                    .benchmark
179                    .try_match(job, &MatchContext::new())
180                    .match_score()
181                    .map(|score| (entry, score))
182            })
183            .min_by_key(|(_, score)| *score)
184            .map(|(entry, _)| entry)
185    }
186
187    fn _input(&self, tag: &str) -> Option<&dyn input::internal::DynInput> {
188        self.inputs.get(tag).map(|v| &**v)
189    }
190
191    fn register_input<T>(&mut self) -> Result<(), RegistryError>
192    where
193        T: Input + 'static,
194    {
195        let tag = T::tag();
196        let wrapper = crate::input::internal::Wrapper::<T>::new();
197        match self.inputs.entry(tag) {
198            Entry::Vacant(v) => {
199                v.insert(Box::new(wrapper));
200                Ok(())
201            }
202            Entry::Occupied(o) => {
203                use input::internal::DynInput;
204
205                if o.get().as_any().is::<crate::input::internal::Wrapper<T>>() {
206                    Ok(())
207                } else {
208                    Err(RegistryError {
209                        tag,
210                        existing: o.get().type_name(),
211                        new: wrapper.type_name(),
212                    })
213                }
214            }
215        }
216    }
217
218    //-------------------//
219    // Regression Checks //
220    //-------------------//
221
222    /// Register a regression-checkable `benchmark` with the given `name`.
223    ///
224    /// As a side-effect, the benchmark's [`Input`](Benchmark::Input) type is also registered.
225    /// Duplicate registrations of the same tag and type are allowed; mismatched types for the
226    /// same tag return an error.
227    ///
228    /// Upon registration, the associated [`Regression::Tolerances`] input and the benchmark
229    /// itself will be reachable via [`Check`](crate::app::Check).
230    pub fn register_regression<T>(
231        &mut self,
232        name: impl Into<String>,
233        benchmark: T,
234    ) -> Result<(), RegistryError>
235    where
236        T: Regression,
237    {
238        self.register_input::<T::Input>()?;
239
240        let registered = benchmark::internal::Wrapper::<T, _>::new(
241            benchmark,
242            benchmark::internal::WithRegression,
243        );
244        self.benchmarks.push(RegisteredBenchmark {
245            name: name.into(),
246            benchmark: Box::new(registered),
247        });
248
249        Ok(())
250    }
251
252    /// Return a collection of all tolerance related inputs, keyed by the input tag type
253    /// of the tolerance.
254    pub(crate) fn tolerances(&self) -> HashMap<&'static str, RegisteredTolerance<'_>> {
255        let mut tolerances = HashMap::<&'static str, RegisteredTolerance<'_>>::new();
256        for b in self.benchmarks.iter() {
257            if let Some(regression) = b.benchmark.as_regression() {
258                // If a tolerance input already exists - then simply add this benchmark
259                // to the list of benchmarks associated with the tolerance.
260                //
261                // Otherwise, create a new entry.
262                let t = regression.tolerance();
263                let packaged = RegressionBenchmark {
264                    benchmark: b,
265                    regression,
266                };
267
268                match tolerances.entry(t.tag()) {
269                    Entry::Occupied(occupied) => occupied.into_mut().regressions.push(packaged),
270                    Entry::Vacant(vacant) => {
271                        vacant.insert(RegisteredTolerance {
272                            tolerance: input::Registered(t),
273                            regressions: vec![packaged],
274                        });
275                    }
276                }
277            }
278        }
279
280        tolerances
281    }
282}
283
284impl Default for Registry {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290/// Error for [`Registry::register`] or [`Registry::register_regression`].
291#[derive(Debug, Error)]
292#[error(
293    "A different input with tag \"{}\" was already registered. Existing type: \"{}\". New type: \"{}\"",
294    self.tag,
295    self.existing,
296    self.new,
297)]
298pub struct RegistryError {
299    tag: &'static str,
300    existing: &'static str,
301    new: &'static str,
302}
303
304/// Document the reason for a method matching failure.
305pub struct Mismatch {
306    method: String,
307    reason: String,
308}
309
310impl Mismatch {
311    /// Return the name of the benchmark that we failed to match.
312    pub fn method(&self) -> &str {
313        &self.method
314    }
315
316    /// Return the reason why this method was not a match.
317    pub fn reason(&self) -> &str {
318        &self.reason
319    }
320}
321
322//----------//
323// Internal //
324//----------//
325
326#[derive(Debug, Clone, Copy)]
327pub(crate) struct RegressionBenchmark<'a> {
328    benchmark: &'a RegisteredBenchmark,
329    regression: &'a dyn benchmark::internal::Regression,
330}
331
332impl RegressionBenchmark<'_> {
333    pub(crate) fn name(&self) -> &str {
334        self.benchmark.name()
335    }
336
337    pub(crate) fn input_tag(&self) -> &'static str {
338        self.regression.input_tag()
339    }
340
341    pub(crate) fn try_match(&self, input: &input::internal::Any, context: &MatchContext) -> Score {
342        self.benchmark.benchmark().try_match(input, context)
343    }
344
345    pub(crate) fn check(
346        &self,
347        tolerance: &input::internal::Any,
348        input: &input::internal::Any,
349        before: &serde_json::Value,
350        after: &serde_json::Value,
351    ) -> anyhow::Result<benchmark::internal::CheckedPassFail> {
352        self.regression.check(tolerance, input, before, after)
353    }
354}
355
356#[derive(Debug, Clone)]
357pub(crate) struct RegisteredTolerance<'a> {
358    /// The tolerance parser.
359    pub(crate) tolerance: input::Registered<'a>,
360
361    /// A single tolerance input can apply to multiple benchmarks. This field records all
362    /// such benchmarks that are available in the registry that use this tolerance.
363    pub(crate) regressions: Vec<RegressionBenchmark<'a>>,
364}
365
366///////////
367// Tests //
368///////////
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    use crate::{input, Checker};
375
376    macro_rules! input {
377        ($T:ident, $tag:literal) => {
378            #[derive(Debug)]
379            struct $T;
380
381            impl Input for $T {
382                type Raw = ();
383                fn tag() -> &'static str {
384                    $tag
385                }
386                fn from_raw(_raw: Self::Raw, _checker: &mut Checker) -> anyhow::Result<$T> {
387                    unimplemented!("this struct is for test only");
388                }
389                fn serialize(&self) -> anyhow::Result<serde_json::Value> {
390                    unimplemented!("this struct is for test only");
391                }
392                fn example() -> Self::Raw {
393                    unimplemented!("this struct is for test only");
394                }
395            }
396        };
397    }
398
399    // For the types below, `A` and `B` have distinct tags, but `A2`'s tag conflicts with `A`.
400    input!(A, "type-a");
401    input!(B, "type-b");
402    input!(A2, "type-a");
403
404    #[test]
405    fn test_tag_conflicts() {
406        let mut registry = Registry::new();
407        registry.register_input::<A>().unwrap();
408        registry.register_input::<B>().unwrap();
409
410        let mut tags: Vec<_> = registry.input_tags().collect();
411        tags.sort();
412        assert_eq!(tags.as_slice(), ["type-a", "type-b"]);
413
414        {
415            let a = registry._input(A::tag()).unwrap();
416            assert!(a.as_any().is::<input::internal::Wrapper<A>>());
417
418            let name = a.type_name();
419            assert!(name.contains("A"), "{}", name);
420        }
421
422        {
423            let b = registry._input(B::tag()).unwrap();
424            assert!(b.as_any().is::<input::internal::Wrapper<B>>());
425
426            let name = b.type_name();
427            assert!(name.contains("B"), "{}", name);
428        }
429
430        let err = registry.register_input::<A2>().unwrap_err();
431        let msg = err.to_string();
432        assert!(
433            msg.contains("A different input with tag \"type-a\" was already registered"),
434            "FAILED: {}",
435            msg
436        );
437    }
438}