1use crate::bench::{Outcome, SpeedTable, Stats};
26use crate::{Config, Reason};
27use std::fmt;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct LeafFailure {
32 pub leaf: String,
34 pub reason: Reason,
36 pub note: String,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum Eligibility {
43 Eligible,
45 Excluded(Vec<LeafFailure>),
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
51#[non_exhaustive]
52pub enum Durability {
53 Volatile,
55 Process,
57 Machine,
60}
61
62impl Durability {
63 pub fn as_str(self) -> &'static str {
65 match self {
66 Durability::Volatile => "volatile",
67 Durability::Process => "process",
68 Durability::Machine => "machine",
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct Declared {
76 pub class: Durability,
78 pub basis: &'static str,
80 pub verified: Option<Durability>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct Need {
90 pub leaves: Vec<String>,
92 pub min_durability: Durability,
94 pub rank_by: String,
96 pub environment: String,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct Candidate {
105 pub backend: String,
107 pub version: String,
109 pub config: Config,
111 pub eligibility: Eligibility,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum Why {
118 Contract(Vec<LeafFailure>),
120 Durability {
122 declared: Durability,
124 wanted: Durability,
126 },
127 DurabilityUnknown {
129 wanted: Durability,
131 },
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct Excluded {
137 pub backend: String,
139 pub version: String,
141 pub why: Why,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct Entry {
148 pub backend: String,
150 pub version: String,
152 pub config: Config,
154 pub stats: Stats,
156 pub date: String,
158 pub digest: Option<String>,
160 pub rerun: String,
162 pub durability: Option<Declared>,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct Unmeasured {
169 pub backend: String,
171 pub version: String,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct Choice {
178 pub need: Need,
180 pub ranked: Vec<Entry>,
182 pub unmeasured: Vec<Unmeasured>,
184 pub excluded: Vec<Excluded>,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct NoneEligible {
192 pub excluded: Vec<Excluded>,
194 pub unmeasured: Vec<Unmeasured>,
196}
197
198impl fmt::Display for NoneEligible {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 write!(
201 f,
202 "no backend satisfies the need: {} excluded, {} eligible but not measured in this environment",
203 self.excluded.len(),
204 self.unmeasured.len()
205 )
206 }
207}
208
209impl std::error::Error for NoneEligible {}
210
211pub fn choose(
220 candidates: &[Candidate],
221 need: &Need,
222 speed: &SpeedTable,
223 declared: &dyn Fn(&str) -> Option<Declared>,
224) -> Result<Choice, NoneEligible> {
225 let mut ranked = Vec::new();
226 let mut unmeasured = Vec::new();
227 let mut excluded = Vec::new();
228 for c in candidates {
229 if let Eligibility::Excluded(ex) = &c.eligibility {
230 excluded.push(Excluded {
231 backend: c.backend.clone(),
232 version: c.version.clone(),
233 why: Why::Contract(ex.clone()),
234 });
235 continue;
236 }
237 let dur = declared(&c.backend);
238 match dur {
239 Some(d) if d.class < need.min_durability => {
240 excluded.push(Excluded {
241 backend: c.backend.clone(),
242 version: c.version.clone(),
243 why: Why::Durability {
244 declared: d.class,
245 wanted: need.min_durability,
246 },
247 });
248 continue;
249 }
250 None if need.min_durability > Durability::Volatile => {
251 excluded.push(Excluded {
252 backend: c.backend.clone(),
253 version: c.version.clone(),
254 why: Why::DurabilityUnknown {
255 wanted: need.min_durability,
256 },
257 });
258 continue;
259 }
260 _ => {}
261 }
262 let row = speed.rows().iter().find(|r| {
263 r.workload == need.rank_by
264 && r.backend == c.backend
265 && r.version == c.version
266 && r.config == c.config
267 && r.env.comparable_key() == need.environment
268 && matches!(r.outcome, Outcome::Measured(_))
269 });
270 match row {
271 Some(r) => {
272 let Outcome::Measured(stats) = r.outcome else {
273 continue;
274 };
275 ranked.push(Entry {
276 backend: c.backend.clone(),
277 version: c.version.clone(),
278 config: c.config.clone(),
279 stats,
280 date: r.env.date.clone(),
281 digest: r.env.digest.clone(),
282 rerun: r.env.rerun.clone(),
283 durability: dur,
284 });
285 }
286 None => unmeasured.push(Unmeasured {
287 backend: c.backend.clone(),
288 version: c.version.clone(),
289 }),
290 }
291 }
292 ranked.sort_by(|a, b| {
293 (a.stats.p50_ns, &a.backend, &a.version).cmp(&(b.stats.p50_ns, &b.backend, &b.version))
294 });
295 if ranked.is_empty() {
296 return Err(NoneEligible {
297 excluded,
298 unmeasured,
299 });
300 }
301 Ok(Choice {
302 need: need.clone(),
303 ranked,
304 unmeasured,
305 excluded,
306 })
307}
308
309impl Choice {
310 pub fn first(&self) -> &Entry {
312 &self.ranked[0]
313 }
314
315 pub fn log_lines(&self) -> Vec<String> {
318 let n = &self.need;
319 let leaves: Vec<_> = n.leaves.iter().map(String::as_str).collect();
320 let mut out = vec![format!(
321 "choose: need leaves=[{}] min_durability={} rank_by={} environment=[{}]",
322 leaves.join(","),
323 n.min_durability.as_str(),
324 n.rank_by,
325 n.environment
326 )];
327 for (i, e) in self.ranked.iter().enumerate() {
328 out.push(format!(
329 "choose: #{} {} {} {} p50={}ns p95={}ns ops/s={} measured {}{} durability={} re-run: {}",
330 i + 1,
331 e.backend,
332 e.version,
333 e.config,
334 e.stats.p50_ns,
335 e.stats.p95_ns,
336 e.stats.ops_per_sec,
337 e.date,
338 e.digest
339 .as_ref()
340 .map(|d| format!(" image {d}"))
341 .unwrap_or_default(),
342 e.durability.map_or_else(
343 || "undeclared".to_string(),
344 |d| format!(
345 "{} (declared: {}; verified: {})",
346 d.class.as_str(),
347 d.basis,
348 d.verified.map_or_else(
349 || "no".to_string(),
350 |v| format!("up to {} by kv.durability.process", v.as_str())
351 )
352 )
353 ),
354 e.rerun
355 ));
356 }
357 for u in &self.unmeasured {
358 out.push(format!(
359 "choose: eligible, not ranked (no measurement of {} here): {} {}",
360 n.rank_by, u.backend, u.version
361 ));
362 }
363 for x in &self.excluded {
364 out.push(format!(
365 "choose: excluded {} {}: {}",
366 x.backend, x.version, x.why
367 ));
368 }
369 out
370 }
371}
372
373impl fmt::Display for Why {
374 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375 match self {
376 Why::Contract(ex) => {
377 let parts: Vec<_> = ex
378 .iter()
379 .map(|e| format!("{} {}", e.leaf, e.reason.as_str()))
380 .collect();
381 write!(f, "failed the contract ({})", parts.join(", "))
382 }
383 Why::Durability { declared, wanted } => write!(
384 f,
385 "declares durability {} but {} is required",
386 declared.as_str(),
387 wanted.as_str()
388 ),
389 Why::DurabilityUnknown { wanted } => write!(
390 f,
391 "no durability declaration and {} is required",
392 wanted.as_str()
393 ),
394 }
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use crate::bench::{Environment, SpeedRow};
402
403 fn env(machine: &str) -> Environment {
404 Environment {
405 machine: machine.to_string(),
406 os: "linux".to_string(),
407 arch: "x86_64".to_string(),
408 date: "2026-10-16".to_string(),
409 digest: None,
410 rerun: "re-run".to_string(),
411 }
412 }
413
414 fn row(backend: &str, machine: &str, p50: u64) -> SpeedRow {
415 SpeedRow {
416 workload: "speed.x.get".to_string(),
417 backend: backend.to_string(),
418 version: "1".to_string(),
419 config: Config::new(),
420 env: env(machine),
421 outcome: Outcome::Measured(Stats::from_samples(vec![p50; 10]).unwrap()),
422 }
423 }
424
425 fn cand(backend: &str, ok: bool) -> Candidate {
426 Candidate {
427 backend: backend.to_string(),
428 version: "1".to_string(),
429 config: Config::new(),
430 eligibility: if ok {
431 Eligibility::Eligible
432 } else {
433 Eligibility::Excluded(vec![LeafFailure {
434 leaf: "x.get".to_string(),
435 reason: Reason::Wrong,
436 note: "lost a write".to_string(),
437 }])
438 },
439 }
440 }
441
442 fn need(min: Durability) -> Need {
443 Need {
444 leaves: vec!["x.get".to_string()],
445 min_durability: min,
446 rank_by: "speed.x.get".to_string(),
447 environment: env("laptop").comparable_key(),
448 }
449 }
450
451 fn table(b: &str) -> Option<Declared> {
452 let class = match b {
453 "file" => Durability::Process,
454 "server" => Durability::Machine,
455 "memory" => Durability::Volatile,
456 _ => return None,
457 };
458 Some(Declared {
459 class,
460 basis: "test table",
461 verified: (class >= Durability::Process).then_some(Durability::Process),
462 })
463 }
464
465 #[test]
466 fn the_fastest_eligible_backend_is_first_and_carries_its_evidence() {
467 let speed = SpeedTable::from_rows(vec![
468 row("file", "laptop", 300),
469 row("server", "laptop", 2000),
470 ]);
471 let cands = [cand("server", true), cand("file", true)];
472 let c = choose(&cands, &need(Durability::Process), &speed, &table).unwrap();
473 assert_eq!(c.first().backend, "file");
474 assert_eq!(c.first().stats.p50_ns, 300);
475 assert_eq!(c.first().rerun, "re-run");
476 let log = c.log_lines().join("\n");
477 assert!(
478 log.contains("min_durability=process") && log.contains("re-run: re-run"),
479 "{log}"
480 );
481 assert!(log.contains("verified: up to process"), "{log}");
482 }
483
484 #[test]
485 fn a_minimum_durability_excludes_a_faster_backend_and_says_so() {
486 let speed = SpeedTable::from_rows(vec![
487 row("file", "laptop", 300),
488 row("server", "laptop", 2000),
489 ]);
490 let cands = [cand("file", true), cand("server", true)];
491 let c = choose(&cands, &need(Durability::Machine), &speed, &table).unwrap();
492 assert_eq!(c.ranked.len(), 1);
493 assert_eq!(c.first().backend, "server");
494 assert!(matches!(
495 c.excluded[0].why,
496 Why::Durability {
497 declared: Durability::Process,
498 wanted: Durability::Machine
499 }
500 ));
501 assert!(
502 c.log_lines()
503 .join("\n")
504 .contains("declares durability process but machine is required")
505 );
506 }
507
508 #[test]
509 fn an_undeclared_backend_is_unknown_not_volatile() {
510 let speed = SpeedTable::from_rows(vec![row("mystery", "laptop", 1)]);
511 let one = [cand("mystery", true)];
512 let c = choose(&one, &need(Durability::Volatile), &speed, &table).unwrap();
513 assert_eq!(
514 c.first().backend,
515 "mystery",
516 "volatile needs no declaration"
517 );
518 let e = choose(&one, &need(Durability::Process), &speed, &table).unwrap_err();
519 assert!(matches!(e.excluded[0].why, Why::DurabilityUnknown { .. }));
520 }
521
522 #[test]
523 fn a_contract_failure_is_excluded_however_fast() {
524 let speed =
525 SpeedTable::from_rows(vec![row("memory", "laptop", 1), row("file", "laptop", 300)]);
526 let cands = [cand("memory", false), cand("file", true)];
527 let c = choose(&cands, &need(Durability::Volatile), &speed, &table).unwrap();
528 assert_eq!(c.first().backend, "file");
529 assert!(matches!(&c.excluded[0].why, Why::Contract(ex) if ex[0].reason == Reason::Wrong));
530 }
531
532 #[test]
533 fn measurements_from_another_machine_are_not_used() {
534 let speed = SpeedTable::from_rows(vec![row("memory", "server", 1)]);
535 let one = [cand("memory", true)];
536 let e = choose(&one, &need(Durability::Volatile), &speed, &table).unwrap_err();
537 assert!(e.excluded.is_empty());
538 assert_eq!(
539 e.unmeasured.len(),
540 1,
541 "eligible, but nothing measured on this machine"
542 );
543 }
544
545 #[test]
546 fn nothing_eligible_is_an_error_with_the_reasons_not_a_fallback() {
547 let speed = SpeedTable::from_rows(vec![row("memory", "laptop", 1)]);
548 let one = [cand("memory", false)];
549 let e = choose(&one, &need(Durability::Volatile), &speed, &table).unwrap_err();
550 assert_eq!(e.excluded.len(), 1);
551 assert!(e.to_string().contains("no backend satisfies"));
552 }
553}