1use std::collections::{hash_map::Entry, HashMap};
7
8use thiserror::Error;
9
10use crate::{
11 benchmark::{self, internal::AnnotatedMatch, Benchmark, MatchContext, Regression, Score},
12 input,
13 internal::visibility::Visibility,
14 Checkpoint, Features, Input, Output,
15};
16
17pub struct Registry {
19 inputs: HashMap<&'static str, Box<dyn input::internal::DynInput>>,
21 benchmarks: Vec<RegisteredBenchmark>,
22}
23
24impl Registry {
25 pub fn new() -> Self {
27 Self {
28 inputs: HashMap::new(),
29 benchmarks: Vec::new(),
30 }
31 }
32
33 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 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 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 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 pub(crate) fn benchmarks(&self) -> &[RegisteredBenchmark] {
139 &self.benchmarks
140 }
141
142 pub(crate) fn has_match(&self, job: &input::internal::Any) -> bool {
144 self.find_best_match(job).is_some()
145 }
146
147 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 pub(crate) fn debug(
171 &self,
172 job: &input::internal::Any,
173 max_methods: usize,
174 ) -> Result<(), Mismatches<'_>> {
175 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 return Ok(());
193 } else {
194 failures.push((entry, score));
195 }
196 }
197 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 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 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 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 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
373pub(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
395pub(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 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#[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 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 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#[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 pub(crate) tolerance: input::Registered<'a>,
598
599 pub(crate) regressions: Vec<RegressionBenchmark<'a>>,
602}
603
604#[cfg(test)]
609mod tests {
610 use super::*;
611
612 use crate::{input, Checker};
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 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 registry
742 .register_gated_input(crate::input::internal::Gated::new(
743 "type-a",
744 Features::new("feature"),
745 ))
746 .unwrap();
747
748 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}