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::{
7    collections::{HashMap, hash_map::Entry},
8    rc::Rc,
9};
10
11use thiserror::Error;
12
13use crate::{
14    Checkpoint, Features, Input, Output,
15    benchmark::{self, Benchmark, MatchContext, Regression, Score, internal::AnnotatedMatch},
16    input,
17    internal::visibility::Visibility,
18};
19
20/// A collection of registered inputs and benchmarks.
21pub struct Registry {
22    // Inputs keyed by their tag type.
23    inputs: HashMap<&'static str, input::internal::Input>,
24    benchmarks: Vec<RegisteredBenchmark>,
25}
26
27impl Registry {
28    /// Return a new empty registry.
29    pub fn new() -> Self {
30        Self {
31            inputs: HashMap::new(),
32            benchmarks: Vec::new(),
33        }
34    }
35
36    /// Return the input with the registered `tag` if present. Otherwise, return `None`.
37    ///
38    /// Inputs are automatically registered as a side-effect of:
39    ///
40    /// * [`register`](Self::register)
41    /// * [`register_regression`](Self::register_regression)
42    pub(crate) fn input(&self, tag: &str) -> Option<&input::internal::Input> {
43        self.inputs.get(tag)
44    }
45
46    pub(crate) fn inputs(&self) -> impl ExactSizeIterator<Item = &input::internal::Input> {
47        self.inputs.values()
48    }
49
50    //--------------//
51    // Registration //
52    //--------------//
53
54    /// Register a new `benchmark` with the given `name`.
55    ///
56    /// As a side-effect, the benchmark's [`Input`](Benchmark::Input) type is also registered.
57    /// Duplicate registrations of the same tag and type are allowed; mismatched types for the
58    /// same tag return an error.
59    pub fn register<T>(
60        &mut self,
61        name: impl Into<String>,
62        benchmark: T,
63    ) -> Result<(), RegistryError>
64    where
65        T: Benchmark,
66    {
67        self.register_input(input::internal::Input::new::<T::Input>())?;
68
69        self.benchmarks.push(RegisteredBenchmark {
70            name: name.into(),
71            benchmark: Box::new(benchmark::internal::Wrapper::<T, _>::new(
72                benchmark,
73                benchmark::internal::NoRegression,
74            )),
75        });
76        Ok(())
77    }
78
79    /// Register a non-existent benchmark that is gated by `features` with an input `tag` that
80    /// is also gated.
81    ///
82    /// The resulting benchmark will not be callable, but it and its input will be made
83    /// discoverable. When used with [`crate::App`], this will provide much better diagnostics
84    /// when an input with `tag` is discovered (pointing the user towards `features`).
85    ///
86    /// # Errors
87    ///
88    /// Errors if a concrete input with `tag` is already registered, or another gated input
89    /// has registered `tag` with a **different** feature set.
90    pub fn register_gated(
91        &mut self,
92        tag: &'static str,
93        name: impl Into<String>,
94        features: Features,
95        description: impl Into<String>,
96    ) -> Result<(), RegistryError> {
97        self.register_input(input::internal::Input::gated(tag, features.clone()))?;
98
99        self.benchmarks.push(RegisteredBenchmark {
100            name: name.into(),
101            benchmark: Box::new(benchmark::internal::FullyGated::new(
102                tag,
103                features,
104                description.into(),
105            )),
106        });
107        Ok(())
108    }
109
110    /// Register a non-existent benchmark that is gated by `features` with a compiled [`Input`].
111    ///
112    /// The resulting benchmark will not be callable, but it will be discoverable. When used
113    /// with [`crate::App`], this will provide much better diagnostics when an input with
114    /// `tag` is parsed by the front-end and dispatched to the back-end.
115    ///
116    /// # Errors
117    ///
118    /// Errors if an input with tag but different type has already been registered.
119    pub fn register_partially_gated<I>(
120        &mut self,
121        name: impl Into<String>,
122        features: Features,
123        description: impl Into<String>,
124    ) -> Result<(), RegistryError>
125    where
126        I: Input,
127    {
128        self.register_input(input::internal::Input::new::<I>())?;
129
130        self.benchmarks.push(RegisteredBenchmark {
131            name: name.into(),
132            benchmark: Box::new(benchmark::internal::PartiallyGated::<I>::new(
133                features,
134                description.into(),
135            )),
136        });
137        Ok(())
138    }
139
140    /// Return an iterator over registered benchmark names and their descriptions.
141    pub(crate) fn benchmarks(&self) -> &[RegisteredBenchmark] {
142        &self.benchmarks
143    }
144
145    /// Return `true` if `job` matches with any registered benchmark. Otherwise, return `false`.
146    pub(crate) fn has_match(&self, job: &input::internal::Any) -> bool {
147        self.find_best_match(job).is_some()
148    }
149
150    /// Attempt to run the best matching benchmark for `job`.
151    ///
152    /// Returns the results of the benchmark if successful.
153    ///
154    /// Errors if a suitable method could not be found or if the invoked benchmark failed.
155    pub(crate) fn call(
156        &self,
157        job: &input::internal::Any,
158        checkpoint: Checkpoint<'_>,
159        output: &mut dyn Output,
160    ) -> anyhow::Result<serde_json::Value> {
161        match self.find_best_match(job) {
162            Some(entry) => entry.benchmark.run(job, checkpoint, output),
163            None => Err(anyhow::Error::msg(
164                "could not find a matching benchmark for the given input",
165            )),
166        }
167    }
168
169    /// Attempt to debug reasons for a missed dispatch, returning at most `max_methods`
170    /// reasons.
171    ///
172    /// Returns `Ok(())` if a match was found.
173    pub(crate) fn debug(
174        &self,
175        job: &input::internal::Any,
176        max_methods: usize,
177    ) -> Result<(), Mismatches<'_>> {
178        // Simple `has_match` will minimally allocate if an actual match is found, saving
179        // the work done below to put together a comprehensive summary of the closest matches.
180        if self.has_match(job) {
181            return Ok(());
182        }
183
184        let mut gated = Vec::<(&RegisteredBenchmark, &Features)>::new();
185        let mut failures = Vec::<(&RegisteredBenchmark, Score)>::new();
186
187        for entry in self.benchmarks() {
188            match entry
189                .benchmark
190                .try_match(job, &MatchContext::with_reasons())
191            {
192                AnnotatedMatch::User(score) => {
193                    if score.is_success() {
194                        // This should be unreachable from the earlier `has_match` check.
195                        return Ok(());
196                    } else {
197                        failures.push((entry, score));
198                    }
199                }
200                // Don't report anything for a wrong tag. It's not very useful.
201                AnnotatedMatch::WrongTag => {}
202                AnnotatedMatch::Gated(features) => gated.push((entry, features)),
203            }
204        }
205
206        failures.sort_by(|(_, left), (_, right)| Score::order(left, right));
207        failures.truncate(max_methods);
208
209        let mismatches = Mismatches {
210            tag: job.tag(),
211            user: failures,
212            gated,
213        };
214
215        Err(mismatches)
216    }
217
218    /// Find the best matching benchmark for `job` by score.
219    fn find_best_match(&self, job: &input::internal::Any) -> Option<&RegisteredBenchmark> {
220        find_best_match(job, self.benchmarks())
221    }
222
223    fn register_input(&mut self, input: input::internal::Input) -> Result<(), RegistryError> {
224        let tag = input.tag();
225        match self.inputs.entry(tag) {
226            Entry::Vacant(v) => {
227                v.insert(input);
228                Ok(())
229            }
230            Entry::Occupied(o) => {
231                use Visibility::{Available, Gated};
232
233                let o = o.get();
234                match (o.visibility(), input.visibility()) {
235                    (Available, Available) => {
236                        if o.as_any().type_id() == input.as_any().type_id() {
237                            Ok(())
238                        } else {
239                            Err(RegistryError {
240                                tag,
241                                existing: Kind::Available(o.type_name()),
242                                new: Kind::Available(input.type_name()),
243                            })
244                        }
245                    }
246                    (Available, Gated { features }) => Err(RegistryError {
247                        tag,
248                        existing: Kind::Available(o.type_name()),
249                        new: Kind::Gated(features.to_string()),
250                    }),
251                    (Gated { features }, Available) => Err(RegistryError {
252                        tag,
253                        existing: Kind::Gated(features.to_string()),
254                        new: Kind::Available(input.type_name()),
255                    }),
256                    (Gated { features: existing }, Gated { features: new }) => {
257                        if existing == new {
258                            Ok(())
259                        } else {
260                            Err(RegistryError {
261                                tag,
262                                existing: Kind::Gated(existing.to_string()),
263                                new: Kind::Gated(new.to_string()),
264                            })
265                        }
266                    }
267                }
268            }
269        }
270    }
271
272    //-------------------//
273    // Regression Checks //
274    //-------------------//
275
276    /// Register a regression-checkable `benchmark` with the given `name`.
277    ///
278    /// As a side-effect, the benchmark's [`Input`](Benchmark::Input) type is also registered.
279    /// Duplicate registrations of the same tag and type are allowed; mismatched types for the
280    /// same tag return an error.
281    ///
282    /// Upon registration, the associated [`Regression::Tolerances`] input and the benchmark
283    /// itself will be reachable via [`Check`](crate::app::Check).
284    pub fn register_regression<T>(
285        &mut self,
286        name: impl Into<String>,
287        benchmark: T,
288    ) -> Result<(), RegistryError>
289    where
290        T: Regression,
291    {
292        self.register_input(input::internal::Input::new::<T::Input>())?;
293
294        let registered = benchmark::internal::Wrapper::<T, _>::new(
295            benchmark,
296            benchmark::internal::WithRegression,
297        );
298        self.benchmarks.push(RegisteredBenchmark {
299            name: name.into(),
300            benchmark: Box::new(registered),
301        });
302
303        Ok(())
304    }
305
306    /// Return a collection of all tolerance related inputs, keyed by the input tag type
307    /// of the tolerance.
308    pub(crate) fn tolerances(&self) -> HashMap<&'static str, RegisteredTolerance<'_>> {
309        let mut tolerances = HashMap::<&'static str, RegisteredTolerance<'_>>::new();
310        for b in self.benchmarks.iter() {
311            if let Some(regression) = b.benchmark.as_regression() {
312                // If a tolerance input already exists - then simply add this benchmark
313                // to the list of benchmarks associated with the tolerance.
314                //
315                // Otherwise, create a new entry.
316                let t = regression.tolerance();
317                let packaged = RegressionBenchmark {
318                    benchmark: b,
319                    regression,
320                };
321
322                match tolerances.entry(t.tag()) {
323                    Entry::Occupied(occupied) => occupied.into_mut().regressions.push(packaged),
324                    Entry::Vacant(vacant) => {
325                        vacant.insert(RegisteredTolerance {
326                            tolerance: Rc::new(t),
327                            regressions: vec![packaged],
328                        });
329                    }
330                }
331            }
332        }
333
334        tolerances
335    }
336}
337
338impl Default for Registry {
339    fn default() -> Self {
340        Self::new()
341    }
342}
343
344pub(crate) trait CommonTryMatch {
345    fn common_try_match(&self, input: &input::internal::Any) -> AnnotatedMatch<'_>;
346}
347
348/// Return the entry in `benchmarks` that has the best successful match to `job`.
349///
350/// Returns `None` if no entries in `benchmarks` matches `job`.
351pub(crate) fn find_best_match<'a, T>(
352    job: &input::internal::Any,
353    benchmarks: &'a [T],
354) -> Option<&'a T>
355where
356    T: CommonTryMatch,
357{
358    benchmarks
359        .iter()
360        .filter_map(|entry| {
361            let score: benchmark::Score = entry.common_try_match(job).try_into_score()?;
362            score
363                .match_score()
364                .map(|s: benchmark::SuccessScore| (entry, s))
365        })
366        .min_by_key(|(_, score)| *score)
367        .map(|(entry, _)| entry)
368}
369
370/// A registered benchmark entry: a name paired with a type-erased benchmark.
371pub(crate) struct RegisteredBenchmark {
372    name: String,
373    benchmark: Box<dyn benchmark::internal::Benchmark>,
374}
375
376impl std::fmt::Debug for RegisteredBenchmark {
377    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378        let benchmark = self.benchmark.as_string();
379        f.debug_struct("RegisteredBenchmark")
380            .field("name", &self.name)
381            .field("benchmark", &benchmark)
382            .finish()
383    }
384}
385
386impl RegisteredBenchmark {
387    pub(crate) fn name(&self) -> &str {
388        &self.name
389    }
390
391    pub(crate) fn benchmark(&self) -> &dyn benchmark::internal::Benchmark {
392        &*self.benchmark
393    }
394
395    pub(crate) fn visibility(&self) -> Visibility<'_> {
396        self.benchmark.visibility()
397    }
398
399    pub(crate) fn display(&self) -> Display<'_> {
400        Display(self)
401    }
402
403    pub(crate) fn description(&self) -> String {
404        self.benchmark.as_string()
405    }
406
407    /// Order available benchmarks first (alphabetically) followed by gated benchmarks
408    /// (also alphabetically).
409    pub(crate) fn order(this: &&Self, other: &&Self) -> std::cmp::Ordering {
410        use Visibility::{Available, Gated};
411
412        match (this.visibility(), other.visibility()) {
413            (Available, Available) => this.name().cmp(other.name()),
414            (Available, Gated { .. }) => std::cmp::Ordering::Less,
415            (Gated { .. }, Available) => std::cmp::Ordering::Greater,
416            (
417                Gated {
418                    features: this_features,
419                },
420                Gated {
421                    features: other_features,
422                },
423            ) => this
424                .name()
425                .cmp(other.name())
426                .then_with(|| this_features.cmp(other_features)),
427        }
428    }
429}
430
431impl CommonTryMatch for RegisteredBenchmark {
432    fn common_try_match(&self, input: &input::internal::Any) -> AnnotatedMatch<'_> {
433        self.benchmark().try_match(input, &MatchContext::new())
434    }
435}
436
437pub(crate) struct Display<'a>(&'a RegisteredBenchmark);
438
439impl std::fmt::Display for Display<'_> {
440    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441        let name = self.0.name();
442        match self.0.visibility() {
443            Visibility::Available => writeln!(f, "{name}:")?,
444            Visibility::Gated { features } => writeln!(f, "{name} (requires the {}):", features)?,
445        };
446
447        write!(
448            f,
449            "{}",
450            crate::utils::fmt::Indent::new(&self.0.description(), 4)
451        )
452    }
453}
454
455/// Error for [`Registry::register`] or [`Registry::register_regression`].
456#[derive(Debug, Error)]
457#[error(
458    "A different input with tag \"{}\" was already registered. Existing {}. New {}",
459    self.tag,
460    self.existing,
461    self.new,
462)]
463pub struct RegistryError {
464    tag: &'static str,
465    existing: Kind,
466    new: Kind,
467}
468
469#[derive(Debug)]
470enum Kind {
471    Available(&'static str),
472    Gated(String),
473}
474
475impl std::fmt::Display for Kind {
476    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477        match self {
478            Self::Available(type_name) => write!(f, "type \"{}\"", type_name),
479            Self::Gated(features) => write!(f, "gated input for {}", features),
480        }
481    }
482}
483
484#[derive(Debug)]
485pub(crate) struct Mismatches<'a> {
486    tag: &'static str,
487    user: Vec<(&'a RegisteredBenchmark, Score)>,
488    gated: Vec<(&'a RegisteredBenchmark, &'a Features)>,
489}
490
491impl std::fmt::Display for Mismatches<'_> {
492    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493        // Print out user mismatches
494        if self.user.is_empty() {
495            writeln!(
496                f,
497                "No active benchmarks are registered for tag \"{}\"",
498                self.tag
499            )?;
500        } else {
501            writeln!(f, "Closest matches for tag \"{}\":\n", self.tag)?;
502            for (i, (entry, score)) in self.user.iter().enumerate() {
503                writeln!(f, "    {}. \"{}\":", i + 1, entry.name())?;
504                writeln!(
505                    f,
506                    "{}\n",
507                    crate::utils::fmt::Indent::new(&score.reason().to_string(), 8)
508                )?;
509            }
510        }
511
512        // Print out gated benchmarks (if any).
513        if !self.gated.is_empty() {
514            writeln!(
515                f,
516                "\nFound {} gated {} matching this input:\n",
517                self.gated.len(),
518                if self.gated.len() == 1 {
519                    "benchmark"
520                } else {
521                    "benchmarks"
522                },
523            )?;
524            for (entry, features) in self.gated.iter() {
525                writeln!(f, "    * \"{}\" (requires the {})", entry.name(), features)?;
526            }
527        }
528
529        Ok(())
530    }
531}
532
533//----------//
534// Internal //
535//----------//
536
537#[derive(Debug, Clone, Copy)]
538pub(crate) struct RegressionBenchmark<'a> {
539    benchmark: &'a RegisteredBenchmark,
540    regression: &'a dyn benchmark::internal::Regression,
541}
542
543impl RegressionBenchmark<'_> {
544    pub(crate) fn name(&self) -> &str {
545        self.benchmark.name()
546    }
547
548    pub(crate) fn input_tag(&self) -> &'static str {
549        self.regression.input_tag()
550    }
551
552    pub(crate) fn check(
553        &self,
554        tolerance: &input::internal::Any,
555        input: &input::internal::Any,
556        before: &serde_json::Value,
557        after: &serde_json::Value,
558    ) -> anyhow::Result<benchmark::internal::CheckedPassFail> {
559        self.regression.check(tolerance, input, before, after)
560    }
561}
562
563impl CommonTryMatch for RegressionBenchmark<'_> {
564    fn common_try_match(&self, input: &input::internal::Any) -> AnnotatedMatch<'_> {
565        self.benchmark.common_try_match(input)
566    }
567}
568
569#[derive(Debug, Clone)]
570pub(crate) struct RegisteredTolerance<'a> {
571    /// The tolerance parser.
572    pub(crate) tolerance: Rc<input::internal::Input>,
573
574    /// A single tolerance input can apply to multiple benchmarks. This field records all
575    /// such benchmarks that are available in the registry that use this tolerance.
576    pub(crate) regressions: Vec<RegressionBenchmark<'a>>,
577}
578
579///////////
580// Tests //
581///////////
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586
587    use crate::{Checker, input};
588
589    macro_rules! input {
590        ($T:ident, $tag:literal) => {
591            #[derive(Debug)]
592            struct $T;
593
594            impl Input for $T {
595                type Raw = ();
596                fn tag() -> &'static str {
597                    $tag
598                }
599                fn from_raw(_raw: Self::Raw, _checker: &mut Checker) -> anyhow::Result<$T> {
600                    unimplemented!("this struct is for test only");
601                }
602                fn serialize(&self) -> anyhow::Result<serde_json::Value> {
603                    unimplemented!("this struct is for test only");
604                }
605                fn example() -> Self::Raw {
606                    unimplemented!("this struct is for test only");
607                }
608            }
609        };
610    }
611
612    // For the types below, `A` and `B` have distinct tags, but `A2`'s tag conflicts with `A`.
613    input!(A, "type-a");
614    input!(B, "type-b");
615    input!(A2, "type-a");
616
617    #[test]
618    fn test_tag_conflicts() {
619        let mut registry = Registry::new();
620        registry
621            .register_input(input::internal::Input::new::<A>())
622            .unwrap();
623        registry
624            .register_input(input::internal::Input::new::<B>())
625            .unwrap();
626
627        let mut tags: Vec<_> = registry.inputs().map(|i| i.tag()).collect();
628        tags.sort();
629        assert_eq!(tags.as_slice(), ["type-a", "type-b"]);
630
631        {
632            let a = registry.inputs.get(A::tag()).unwrap();
633            assert!(a.is_concrete::<A>());
634
635            let name = a.type_name();
636            assert!(name.contains("A"), "{}", name);
637        }
638
639        {
640            let b = registry.inputs.get(B::tag()).unwrap();
641            assert!(b.is_concrete::<B>());
642
643            let name = b.type_name();
644            assert!(name.contains("B"), "{}", name);
645        }
646
647        let err = registry
648            .register_input(input::internal::Input::new::<A2>())
649            .unwrap_err();
650        let msg = err.to_string();
651        assert!(
652            msg.contains("A different input with tag \"type-a\" was already registered"),
653            "FAILED: {}",
654            msg
655        );
656    }
657
658    #[test]
659    fn test_gated_to_real_conflict() {
660        let mut registry = Registry::new();
661        registry
662            .register_input(input::internal::Input::gated(
663                "type-a",
664                Features::new("feature"),
665            ))
666            .unwrap();
667
668        registry
669            .register_input(input::internal::Input::new::<B>())
670            .unwrap();
671
672        let mut tags: Vec<_> = registry.inputs().map(|i| i.tag()).collect();
673        tags.sort();
674        assert_eq!(tags.as_slice(), ["type-a", "type-b"]);
675
676        {
677            let a = registry.inputs.get(A::tag()).unwrap();
678            assert!(a.is_gated());
679        }
680
681        let err = registry
682            .register_input(input::internal::Input::new::<A>())
683            .unwrap_err();
684        let msg = err.to_string();
685        assert!(
686            msg.contains("A different input with tag \"type-a\" was already registered"),
687            "FAILED: {}",
688            msg
689        );
690    }
691
692    #[test]
693    fn test_real_to_gated_conflict() {
694        let mut registry = Registry::new();
695
696        registry
697            .register_input(input::internal::Input::new::<A>())
698            .unwrap();
699        registry
700            .register_input(input::internal::Input::new::<B>())
701            .unwrap();
702
703        let err = registry
704            .register_input(input::internal::Input::gated(
705                "type-a",
706                Features::new("feature"),
707            ))
708            .unwrap_err();
709
710        let msg = err.to_string();
711        assert!(
712            msg.contains("A different input with tag \"type-a\" was already registered"),
713            "FAILED: {}",
714            msg
715        );
716    }
717
718    #[test]
719    fn test_gated_to_gated() {
720        let mut registry = Registry::new();
721
722        registry
723            .register_input(input::internal::Input::gated(
724                "type-a",
725                Features::new("feature"),
726            ))
727            .unwrap();
728
729        // If we register with the same feature set, there is no error.
730        registry
731            .register_input(input::internal::Input::gated(
732                "type-a",
733                Features::new("feature"),
734            ))
735            .unwrap();
736
737        // If we register with a different feature-set, that is an error.
738        let err = registry
739            .register_input(input::internal::Input::gated(
740                "type-a",
741                Features::any(["feature", "another"]),
742            ))
743            .unwrap_err();
744
745        let msg = err.to_string();
746        assert!(
747            msg.contains("A different input with tag \"type-a\" was already registered"),
748            "FAILED: {}",
749            msg
750        );
751    }
752}