//! The partitioner's measured tiers against the harness that measured them. //! //! `codegen/perf_tiers.rs` is generated by tools/perfbench from its newest results CSV, and //! `codegen::partition` takes two latency classes' tails from it (a call under a static //! schedule, and the generated ROS 2 node's publish) instead of constants. These tests read //! that CSV themselves, with their own parser and their own rounding, so a hand-edited table, //! a table left behind by a newer run, or a partitioner that stopped using the table fails. //! //! The stated tolerance: a tier's recorded measurement equals the CSV's largest accepted //! p99.9 exactly, and its tail is that value rounded up to two significant figures, which is //! never below the measurement and at most 10% above it. use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use flowrs_core::codegen::partition::{ CALL_TAIL_NS, DDS_PUBLISH_NS, LOOP_MARGIN, LatencyClass, Options, analyze, }; use flowrs_core::codegen::perf_tiers::{CALL, DDS_PUBLISH, SOURCE_CSV, Tier}; use flowrs_core::graph::WireMode; use flowrs_core::graph::ir::Graph; use flowrs_core::graph::placement::KEY; type Row = BTreeMap; fn results_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tools/perfbench/results") } /// RFC 4180 records (quoted fields may hold commas, quotes and newlines). fn records(text: &str) -> Vec> { let mut out = Vec::new(); let (mut rec, mut field, mut quoted) = (Vec::new(), String::new(), false); let mut it = text.chars().peekable(); while let Some(c) = it.next() { match (quoted, c) { (true, '"') if it.peek() == Some(&'"') => { field.push('"'); it.next(); } (true, '"') => quoted = false, (true, c) => field.push(c), (false, '"') => quoted = true, (false, ',') => rec.push(std::mem::take(&mut field)), (false, '\n') => { rec.push(std::mem::take(&mut field)); out.push(std::mem::take(&mut rec)); } (false, '\r') => {} (false, c) => field.push(c), } } if !field.is_empty() || !rec.is_empty() { rec.push(field); out.push(rec); } out } fn rows(path: &Path) -> Vec { let text = std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); let mut recs = records(&text).into_iter(); let head = recs.next().expect("a header"); recs.filter(|r| r.len() == head.len()) .map(|r| head.iter().cloned().zip(r).collect()) .collect() } /// The newest results CSV: the one whose rows carry the latest `date_utc`; the file name /// breaks ties. fn newest() -> (String, Vec) { let mut best: Option<(String, String, Vec)> = None; for e in std::fs::read_dir(results_dir()).expect("tools/perfbench/results exists") { let p = e.expect("a directory entry").path(); if p.extension().and_then(|x| x.to_str()) != Some("csv") { continue; } let name = p.file_name().unwrap().to_string_lossy().into_owned(); let t = rows(&p); let date = t .iter() .filter_map(|r| r.get("date_utc").cloned()) .max() .unwrap_or_default(); let rank = format!("{date}|{name}"); if best.as_ref().is_none_or(|(b, _, _)| rank > *b) { best = Some((rank, name, t)); } } let (_, name, t) = best.expect("at least one results CSV"); (name, t) } fn key(r: &Row) -> String { ["family", "bench", "dataset", "contender", "op"] .map(|k| r.get(k).map_or("", String::as_str)) .join("|") } /// The largest p99.9 over the accepted rows of `row` (over all of them when none was /// accepted), ns, rounded up to a whole ns. fn measured(t: &[Row], row: &str) -> u64 { let all: Vec<&Row> = t.iter().filter(|r| key(r) == row).collect(); assert!(!all.is_empty(), "the CSV has no row {row}"); let accepted: Vec<&Row> = all .iter() .copied() .filter(|r| r.get("accepted").is_some_and(|a| a == "1")) .collect(); let pool = if accepted.is_empty() { all } else { accepted }; pool.iter() .map(|r| { r["p999"] .parse::() .unwrap_or_else(|_| panic!("{row}: p999 {:?}", r["p999"])) .ceil() as u64 }) .max() .unwrap() } /// Rounded up to two significant figures. fn two_figures_up(x: u64) -> u64 { let digits = x.checked_ilog10().map_or(1, |d| d + 1); if digits <= 2 { return x; } let unit = 10u64.pow(digits - 2); x.div_ceil(unit) * unit } #[test] fn the_tier_table_is_generated_from_the_newest_csv() { let (name, t) = newest(); assert_eq!( SOURCE_CSV, name, "codegen/perf_tiers.rs was generated from {SOURCE_CSV}, but the newest CSV in \ tools/perfbench/results is {name}: re-run tools/perfbench/run.sh, which regenerates it" ); for (what, tier) in [("CALL", CALL), ("DDS_PUBLISH", DDS_PUBLISH)] { let m = measured(&t, tier.row); assert_eq!( tier.measured_p999_ns, m, "{what}: the table records {} ns, the CSV's largest accepted p99.9 is {m} ns", tier.measured_p999_ns ); // The tolerance: never below the measurement, at most 10% above it. assert!(tier.tail_ns >= m, "{what}: tail {} below {m}", tier.tail_ns); assert!( tier.tail_ns * 10 <= m * 11 + 9, "{what}: tail {} more than 10% above {m}", tier.tail_ns ); assert_eq!(tier.tail_ns, two_figures_up(m), "{what}"); } } #[test] fn rounding_up_to_two_figures() { for (x, want) in [ (0, 0), (7, 7), (99, 99), (100, 100), (101, 110), (4_321, 4_400), ] { assert_eq!(two_figures_up(x), want, "{x}"); } } /// Host-side nodes publishing into ROS, a call edge beside it, and a loop on the host. fn graph(period_ms: u64) -> Graph { let mut g = Graph::new(); let set = |g: &mut Graph, n, k: &str, v: &str| { g.get_node_mut(n) .unwrap() .properties .insert(k.into(), v.into()); }; let value = g.add_node("Control", 0.0, 0.0); set(&mut g, value, "value", "12"); let print = g.add_node("PrintLn", 0.0, 0.0); g.add_wire(value, "value", print, "input", WireMode::Copy) .unwrap(); let publish = g.add_node("ros.publish", 0.0, 0.0); for (k, v) in [ ("topic", "ratio"), ("type", "std_msgs/msg/Float64"), ("field", "data"), (KEY, "ros(/robot)"), ] { set(&mut g, publish, k, v); } g.add_wire(value, "value", publish, "value", WireMode::Copy) .unwrap(); let timer = g.add_node("embedded.timer.interval", 0.0, 0.0); set(&mut g, timer, "period_ms", &period_ms.to_string()); g } /// End to end: the CSV's measurement is the tail `analyze` gives a call edge and a DDS /// publication, and whether a loop on the host is refused follows from it. #[test] fn the_partitioner_costs_its_edges_with_the_measured_tiers() { let (_, t) = newest(); let call = two_figures_up(measured(&t, CALL.row)); let dds = two_figures_up(measured(&t, DDS_PUBLISH.row)); assert_eq!(CALL_TAIL_NS, call); assert_eq!(DDS_PUBLISH_NS, dds); let a = analyze(&graph(1), &Options::default()).unwrap(); assert!(a.problems.is_empty(), "{:?}", a.problems); let tail = |class: LatencyClass| { a.edges .iter() .find(|e| e.class == class) .unwrap_or_else(|| panic!("no {class} edge in {:?}", a.edges)) .tail_ns() }; assert_eq!(tail(LatencyClass::Call), call); assert_eq!(tail(LatencyClass::DdsPublish), dds); assert_eq!(LatencyClass::Call.wire_ns(1 << 20), call); // The host's loop crosses both: it is refused exactly when 1 ms is under 10x the larger // measured tail, and a loop slow enough for it passes. let worst = call.max(dds); assert_eq!( !a.violations.is_empty(), 1_000_000 < LOOP_MARGIN * worst, "{:?}", a.violations ); let enough = (LOOP_MARGIN * worst).div_ceil(1_000_000).max(1); let a = analyze(&graph(enough), &Options::default()).unwrap(); assert!(a.violations.is_empty(), "{:?}", a.violations); } /// The table's own shape: the rows it names are the harness's. #[test] fn the_tiers_name_the_harness_rows() { let rows: [Tier; 2] = [CALL, DDS_PUBLISH]; assert_eq!(rows[0].row, "chain|chain16|msg64|flowrs-static-call|pass"); assert_eq!( rows[1].row, "ros|one-way|sensor_msgs/Imu|flowrs-ros2edge-rustdds|send-call" ); }