//! Performance benchmark fixtures with S/M/L tiered SLOs (bd-wnuo). //! //! Measures TUI update+render cycle time with synthetic data at three scales: //! - **S-tier** (small): ~100 issues, 50 MRs — CI gate, strict SLOs //! - **M-tier** (medium): ~1,000 issues, 500 MRs — CI gate, relaxed SLOs //! - **L-tier** (large): ~5,000 issues, 2,000 MRs — advisory, no CI gate //! //! SLOs are measured in wall-clock time per operation (update or render). //! Tests run 20 iterations and assert on the median to avoid flaky p95. //! //! These test the TUI state/render performance, NOT database query time. //! DB benchmarks belong in the root `lore` crate. use std::time::{Duration, Instant}; use chrono::{TimeZone, Utc}; use ftui::Model; use ftui::render::frame::Frame; use ftui::render::grapheme_pool::GraphemePool; use lore_tui::app::LoreApp; use lore_tui::clock::FakeClock; use lore_tui::message::{Msg, Screen}; use lore_tui::state::dashboard::{DashboardData, EntityCounts, LastSyncInfo, ProjectSyncInfo}; use lore_tui::state::issue_list::{IssueListPage, IssueListRow}; use lore_tui::state::mr_list::{MrListPage, MrListRow}; use lore_tui::task_supervisor::TaskKey; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const RENDER_WIDTH: u16 = 120; const RENDER_HEIGHT: u16 = 40; const ITERATIONS: usize = 20; // SLOs (median per operation). // These are generous to avoid CI flakiness. const SLO_UPDATE_S: Duration = Duration::from_millis(10); const SLO_UPDATE_M: Duration = Duration::from_millis(50); const SLO_RENDER_S: Duration = Duration::from_millis(20); const SLO_RENDER_M: Duration = Duration::from_millis(50); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- fn frozen_clock() -> FakeClock { FakeClock::new(Utc.with_ymd_and_hms(2026, 1, 15, 12, 0, 0).unwrap()) } fn test_app() -> LoreApp { let mut app = LoreApp::new(); app.clock = Box::new(frozen_clock()); app } fn render_app(app: &LoreApp) { let mut pool = GraphemePool::new(); let mut frame = Frame::new(RENDER_WIDTH, RENDER_HEIGHT, &mut pool); app.view(&mut frame); } fn median(durations: &mut [Duration]) -> Duration { durations.sort(); durations[durations.len() / 2] } // --------------------------------------------------------------------------- // Seeded fixture generators // --------------------------------------------------------------------------- /// Simple xorshift64 PRNG for deterministic fixtures. struct Rng(u64); impl Rng { fn new(seed: u64) -> Self { Self(seed.wrapping_add(1)) } fn next(&mut self) -> u64 { let mut x = self.0; x ^= x << 13; x ^= x >> 7; x ^= x << 17; self.0 = x; x } fn range(&mut self, max: u64) -> u64 { self.next() % max } } const AUTHORS: &[&str] = &[ "alice", "bob", "carol", "dave", "eve", "frank", "grace", "heidi", "ivan", "judy", "karl", "lucy", "mike", "nancy", "oscar", "peggy", "quinn", "ruth", "steve", "tina", ]; const LABELS: &[&str] = &[ "backend", "frontend", "infra", "bug", "feature", "refactor", "docs", "ci", "security", "performance", "ui", "api", "testing", "devops", "database", ]; const PROJECTS: &[&str] = &[ "infra/platform", "web/frontend", "api/backend", "tools/scripts", "data/pipeline", ]; fn random_author(rng: &mut Rng) -> String { AUTHORS[rng.range(AUTHORS.len() as u64) as usize].to_string() } fn random_labels(rng: &mut Rng, max: usize) -> Vec { let count = rng.range(max as u64 + 1) as usize; (0..count) .map(|_| LABELS[rng.range(LABELS.len() as u64) as usize].to_string()) .collect() } fn random_project(rng: &mut Rng) -> String { PROJECTS[rng.range(PROJECTS.len() as u64) as usize].to_string() } fn random_state(rng: &mut Rng) -> String { match rng.range(10) { 0..=5 => "closed".to_string(), 6..=8 => "opened".to_string(), _ => "merged".to_string(), } } fn generate_issue_list(count: usize, seed: u64) -> IssueListPage { let mut rng = Rng::new(seed); let rows = (0..count) .map(|i| IssueListRow { project_path: random_project(&mut rng), iid: (i + 1) as i64, title: format!( "{} {} for {} component", if rng.range(2) == 0 { "Fix" } else { "Add" }, [ "retry logic", "caching", "validation", "error handling", "rate limiting" ][rng.range(5) as usize], ["auth", "payments", "search", "notifications", "dashboard"][rng.range(5) as usize] ), state: random_state(&mut rng), author: random_author(&mut rng), labels: random_labels(&mut rng, 3), updated_at: 1_736_900_000_000 + rng.range(100_000_000) as i64, }) .collect(); IssueListPage { rows, next_cursor: None, total_count: count as u64, } } fn generate_mr_list(count: usize, seed: u64) -> MrListPage { let mut rng = Rng::new(seed); let rows = (0..count) .map(|i| MrListRow { project_path: random_project(&mut rng), iid: (i + 1) as i64, title: format!( "{}: {} {} implementation", if rng.range(3) == 0 { "WIP" } else { "feat" }, ["Implement", "Refactor", "Update", "Fix", "Add"][rng.range(5) as usize], ["middleware", "service", "handler", "model", "view"][rng.range(5) as usize] ), state: random_state(&mut rng), author: random_author(&mut rng), labels: random_labels(&mut rng, 2), updated_at: 1_736_900_000_000 + rng.range(100_000_000) as i64, draft: rng.range(5) == 0, target_branch: "main".to_string(), }) .collect(); MrListPage { rows, next_cursor: None, total_count: count as u64, } } fn generate_dashboard_data( issues_total: u64, mrs_total: u64, project_count: usize, ) -> DashboardData { let mut rng = Rng::new(42); DashboardData { counts: EntityCounts { issues_total, issues_open: issues_total * 3 / 10, mrs_total, mrs_open: mrs_total / 5, discussions: issues_total * 3, notes_total: issues_total * 8, notes_system_pct: 18, documents: issues_total * 2, embeddings: issues_total, }, projects: (0..project_count) .map(|_| ProjectSyncInfo { path: random_project(&mut rng), minutes_since_sync: rng.range(60), }) .collect(), recent: vec![], last_sync: Some(LastSyncInfo { status: "succeeded".into(), finished_at: Some(1_736_942_100_000), command: "sync".into(), error: None, }), } } // --------------------------------------------------------------------------- // Benchmark runner // --------------------------------------------------------------------------- fn bench_update(app: &mut LoreApp, msg_factory: impl Fn() -> Msg) -> Duration { let mut durations = Vec::with_capacity(ITERATIONS); for _ in 0..ITERATIONS { let msg = msg_factory(); let start = Instant::now(); app.update(msg); durations.push(start.elapsed()); } median(&mut durations) } fn bench_render(app: &LoreApp) -> Duration { let mut durations = Vec::with_capacity(ITERATIONS); for _ in 0..ITERATIONS { let start = Instant::now(); render_app(app); durations.push(start.elapsed()); } median(&mut durations) } // --------------------------------------------------------------------------- // S-Tier Benchmarks (100 issues, 50 MRs) // --------------------------------------------------------------------------- #[test] fn bench_s_tier_dashboard_update() { let mut app = test_app(); let data = generate_dashboard_data(100, 50, 5); let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::Dashboard)) .generation; let med = bench_update(&mut app, || Msg::DashboardLoaded { generation, data: Box::new(data.clone()), }); eprintln!("S-tier dashboard update median: {med:?}"); assert!( med < SLO_UPDATE_S, "S-tier dashboard update {med:?} exceeds SLO {SLO_UPDATE_S:?}" ); } #[test] fn bench_s_tier_issue_list_update() { let mut app = test_app(); let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::IssueList)) .generation; let med = bench_update(&mut app, || Msg::IssueListLoaded { generation, page: generate_issue_list(100, 1), }); eprintln!("S-tier issue list update median: {med:?}"); assert!( med < SLO_UPDATE_S, "S-tier issue list update {med:?} exceeds SLO {SLO_UPDATE_S:?}" ); } #[test] fn bench_s_tier_mr_list_update() { let mut app = test_app(); let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::MrList)) .generation; let med = bench_update(&mut app, || Msg::MrListLoaded { generation, page: generate_mr_list(50, 2), }); eprintln!("S-tier MR list update median: {med:?}"); assert!( med < SLO_UPDATE_S, "S-tier MR list update {med:?} exceeds SLO {SLO_UPDATE_S:?}" ); } #[test] fn bench_s_tier_dashboard_render() { let mut app = test_app(); let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::Dashboard)) .generation; app.update(Msg::DashboardLoaded { generation, data: Box::new(generate_dashboard_data(100, 50, 5)), }); let med = bench_render(&app); eprintln!("S-tier dashboard render median: {med:?}"); assert!( med < SLO_RENDER_S, "S-tier dashboard render {med:?} exceeds SLO {SLO_RENDER_S:?}" ); } #[test] fn bench_s_tier_issue_list_render() { let mut app = test_app(); let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::IssueList)) .generation; app.update(Msg::NavigateTo(Screen::IssueList)); app.update(Msg::IssueListLoaded { generation, page: generate_issue_list(100, 1), }); let med = bench_render(&app); eprintln!("S-tier issue list render median: {med:?}"); assert!( med < SLO_RENDER_S, "S-tier issue list render {med:?} exceeds SLO {SLO_RENDER_S:?}" ); } // --------------------------------------------------------------------------- // M-Tier Benchmarks (1,000 issues, 500 MRs) // --------------------------------------------------------------------------- #[test] fn bench_m_tier_dashboard_update() { let mut app = test_app(); let data = generate_dashboard_data(1_000, 500, 10); let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::Dashboard)) .generation; let med = bench_update(&mut app, || Msg::DashboardLoaded { generation, data: Box::new(data.clone()), }); eprintln!("M-tier dashboard update median: {med:?}"); assert!( med < SLO_UPDATE_M, "M-tier dashboard update {med:?} exceeds SLO {SLO_UPDATE_M:?}" ); } #[test] fn bench_m_tier_issue_list_update() { let mut app = test_app(); let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::IssueList)) .generation; let med = bench_update(&mut app, || Msg::IssueListLoaded { generation, page: generate_issue_list(1_000, 10), }); eprintln!("M-tier issue list update median: {med:?}"); assert!( med < SLO_UPDATE_M, "M-tier issue list update {med:?} exceeds SLO {SLO_UPDATE_M:?}" ); } #[test] fn bench_m_tier_mr_list_update() { let mut app = test_app(); let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::MrList)) .generation; let med = bench_update(&mut app, || Msg::MrListLoaded { generation, page: generate_mr_list(500, 20), }); eprintln!("M-tier MR list update median: {med:?}"); assert!( med < SLO_UPDATE_M, "M-tier MR list update {med:?} exceeds SLO {SLO_UPDATE_M:?}" ); } #[test] fn bench_m_tier_dashboard_render() { let mut app = test_app(); let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::Dashboard)) .generation; app.update(Msg::DashboardLoaded { generation, data: Box::new(generate_dashboard_data(1_000, 500, 10)), }); let med = bench_render(&app); eprintln!("M-tier dashboard render median: {med:?}"); assert!( med < SLO_RENDER_M, "M-tier dashboard render {med:?} exceeds SLO {SLO_RENDER_M:?}" ); } #[test] fn bench_m_tier_issue_list_render() { let mut app = test_app(); let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::IssueList)) .generation; app.update(Msg::NavigateTo(Screen::IssueList)); app.update(Msg::IssueListLoaded { generation, page: generate_issue_list(1_000, 10), }); let med = bench_render(&app); eprintln!("M-tier issue list render median: {med:?}"); assert!( med < SLO_RENDER_M, "M-tier issue list render {med:?} exceeds SLO {SLO_RENDER_M:?}" ); } // --------------------------------------------------------------------------- // L-Tier Benchmarks (5,000 issues, 2,000 MRs) — advisory, not CI gate // --------------------------------------------------------------------------- #[test] fn bench_l_tier_issue_list_update() { let mut app = test_app(); let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::IssueList)) .generation; let med = bench_update(&mut app, || Msg::IssueListLoaded { generation, page: generate_issue_list(5_000, 100), }); // Advisory — log but don't fail CI. eprintln!("L-tier issue list update median: {med:?} (advisory, no SLO gate)"); } #[test] fn bench_l_tier_issue_list_render() { let mut app = test_app(); let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::IssueList)) .generation; app.update(Msg::NavigateTo(Screen::IssueList)); app.update(Msg::IssueListLoaded { generation, page: generate_issue_list(5_000, 100), }); let med = bench_render(&app); eprintln!("L-tier issue list render median: {med:?} (advisory, no SLO gate)"); } // --------------------------------------------------------------------------- // Combined update+render cycle benchmarks // --------------------------------------------------------------------------- #[test] fn bench_full_cycle_s_tier() { let mut app = test_app(); let mut durations = Vec::with_capacity(ITERATIONS); for i in 0..ITERATIONS { let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::IssueList)) .generation; let page = generate_issue_list(100, i as u64 + 500); let start = Instant::now(); app.update(Msg::IssueListLoaded { generation, page }); render_app(&app); durations.push(start.elapsed()); } let med = median(&mut durations); eprintln!("S-tier full cycle (update+render) median: {med:?}"); assert!( med < SLO_UPDATE_S + SLO_RENDER_S, "S-tier full cycle {med:?} exceeds combined SLO {:?}", SLO_UPDATE_S + SLO_RENDER_S ); } #[test] fn bench_full_cycle_m_tier() { let mut app = test_app(); let mut durations = Vec::with_capacity(ITERATIONS); for i in 0..ITERATIONS { let generation = app .supervisor .submit(TaskKey::LoadScreen(Screen::IssueList)) .generation; let page = generate_issue_list(1_000, i as u64 + 500); let start = Instant::now(); app.update(Msg::IssueListLoaded { generation, page }); render_app(&app); durations.push(start.elapsed()); } let med = median(&mut durations); eprintln!("M-tier full cycle (update+render) median: {med:?}"); assert!( med < SLO_UPDATE_M + SLO_RENDER_M, "M-tier full cycle {med:?} exceeds combined SLO {:?}", SLO_UPDATE_M + SLO_RENDER_M ); }