1use 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
20pub struct Registry {
22 inputs: HashMap<&'static str, input::internal::Input>,
24 benchmarks: Vec<RegisteredBenchmark>,
25}
26
27impl Registry {
28 pub fn new() -> Self {
30 Self {
31 inputs: HashMap::new(),
32 benchmarks: Vec::new(),
33 }
34 }
35
36 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 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 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 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 pub(crate) fn benchmarks(&self) -> &[RegisteredBenchmark] {
142 &self.benchmarks
143 }
144
145 pub(crate) fn has_match(&self, job: &input::internal::Any) -> bool {
147 self.find_best_match(job).is_some()
148 }
149
150 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 pub(crate) fn debug(
174 &self,
175 job: &input::internal::Any,
176 max_methods: usize,
177 ) -> Result<(), Mismatches<'_>> {
178 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 return Ok(());
196 } else {
197 failures.push((entry, score));
198 }
199 }
200 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 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 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 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 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
348pub(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
370pub(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 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#[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 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 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#[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 pub(crate) tolerance: Rc<input::internal::Input>,
573
574 pub(crate) regressions: Vec<RegressionBenchmark<'a>>,
577}
578
579#[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 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 registry
731 .register_input(input::internal::Input::gated(
732 "type-a",
733 Features::new("feature"),
734 ))
735 .unwrap();
736
737 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}