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