| 1 | //! Bounded, dependency-free measurements of actual terminal-buffer rendering. |
| 2 | use codewhale_ratatui::{ |
| 3 | Caps, Message, MotionMode, NativeComposer, OceanColumn, Paint, PostureBar, SessionList, |
| 4 | SessionRow, TerminalShell, Theme, Workbar, WorkbarPanel, WorkbarRow, color::ColorDepth, |
| 5 | detect::Appearance, gallery, |
| 6 | }; |
| 7 | use ratatui::{buffer::Buffer, layout::Rect}; |
| 8 | use std::{ |
| 9 | hint::black_box, |
| 10 | time::{Duration, Instant}, |
| 11 | }; |
| 12 | |
| 13 | fn measure(name: &str, samples: usize, batch: usize, mut render: impl FnMut()) { |
| 14 | for _ in 0..batch { |
| 15 | render(); |
| 16 | } |
| 17 | let mut timings = Vec::with_capacity(samples); |
| 18 | for _ in 0..samples { |
| 19 | let start = Instant::now(); |
| 20 | for _ in 0..batch { |
| 21 | render(); |
| 22 | } |
| 23 | timings.push(start.elapsed().as_secs_f64() * 1_000_000.0 / batch as f64); |
| 24 | } |
| 25 | timings.sort_by(f64::total_cmp); |
| 26 | println!( |
| 27 | "{name:32} {:12.3} {:12.3}", |
| 28 | timings[samples / 2], |
| 29 | timings[(samples * 95).div_ceil(100).saturating_sub(1)] |
| 30 | ); |
| 31 | } |
| 32 | |
| 33 | fn prepared( |
| 34 | name: &str, |
| 35 | component: &dyn Paint, |
| 36 | area: Rect, |
| 37 | theme: &Theme, |
| 38 | samples: usize, |
| 39 | batch: usize, |
| 40 | ) { |
| 41 | let mut buf = Buffer::empty(area); |
| 42 | measure(name, samples, batch, || { |
| 43 | buf.reset(); |
| 44 | component.paint(area, &mut buf, theme); |
| 45 | black_box(&buf); |
| 46 | }); |
| 47 | } |
| 48 | |
| 49 | fn main() { |
| 50 | let quick = std::env::args().any(|arg| arg == "--quick"); |
| 51 | let (samples, batch) = if quick { (5, 2) } else { (31, 20) }; |
| 52 | let theme = Theme::new(Caps { |
| 53 | depth: ColorDepth::TrueColor, |
| 54 | ascii: false, |
| 55 | appearance: Appearance::Dark, |
| 56 | }) |
| 57 | .tui(); |
| 58 | println!("Codewhale rendering · native Underwater · {samples} samples × {batch} paints"); |
| 59 | println!("{:32} {:>12} {:>12}", "scenario", "median µs", "p95 µs"); |
| 60 | let composer = NativeComposer::new("Review the Unicode text: 海洋 e\u{301} 👩💻").focused(true); |
| 61 | prepared( |
| 62 | "composer / 112×5", |
| 63 | &composer, |
| 64 | Rect::new(0, 0, 112, 5), |
| 65 | &theme, |
| 66 | samples, |
| 67 | batch, |
| 68 | ); |
| 69 | let dock = Workbar::new( |
| 70 | WorkbarPanel::Tasks, |
| 71 | (0..10) |
| 72 | .map(|i| WorkbarRow::new(i.to_string(), "Review the native component")) |
| 73 | .collect(), |
| 74 | ); |
| 75 | prepared( |
| 76 | "workbar / 112×5", |
| 77 | &dock, |
| 78 | Rect::new(0, 0, 112, 5), |
| 79 | &theme, |
| 80 | samples, |
| 81 | batch, |
| 82 | ); |
| 83 | for count in [10, 100, 10_000] { |
| 84 | let sessions = SessionList::new( |
| 85 | (0..count) |
| 86 | .map(|i| SessionRow::new(format!("{i:08x}"), "A saved Codewhale session")) |
| 87 | .collect(), |
| 88 | ); |
| 89 | prepared( |
| 90 | &format!("sessions {count} / 112×38"), |
| 91 | &sessions, |
| 92 | Rect::new(0, 0, 112, 38), |
| 93 | &theme, |
| 94 | samples, |
| 95 | batch, |
| 96 | ); |
| 97 | } |
| 98 | let entries = gallery::entries(); |
| 99 | let work = entries |
| 100 | .iter() |
| 101 | .find(|entry| entry.name == "showcase-work") |
| 102 | .expect("native showcase-work fixture"); |
| 103 | let area = Rect::new(0, 0, 112, 38); |
| 104 | let mut buf = Buffer::empty(area); |
| 105 | let native_theme = gallery::theme_for(work, &theme); |
| 106 | // Build foregrounds before the ocean pass: the full gallery fixture |
| 107 | // already finishes its grounds, which would skip most work on reapply. |
| 108 | let shell = TerminalShell::new(5).workbar_rows(5); |
| 109 | let regions = shell.areas(area); |
| 110 | shell.paint(area, &mut buf, &theme); |
| 111 | let body = (0..28) |
| 112 | .map(|i| format!("Review {i}: Keep the native composer, dock and 海洋 text in view.")) |
| 113 | .collect::<Vec<_>>() |
| 114 | .join("\n"); |
| 115 | Message::native(&body).paint(regions.conversation, &mut buf, &theme); |
| 116 | composer.paint(regions.composer, &mut buf, &theme); |
| 117 | PostureBar::new("ask").paint(regions.posture, &mut buf, &theme); |
| 118 | dock.paint(regions.workbar, &mut buf, &theme); |
| 119 | let foreground = buf.clone(); |
| 120 | let ocean = OceanColumn::new(Duration::from_millis(22_500), MotionMode::Full); |
| 121 | measure("ocean + fixture clone / 112×38", samples, batch, || { |
| 122 | let mut buf = foreground.clone(); |
| 123 | ocean.apply(area, &mut buf, &theme); |
| 124 | black_box(&buf); |
| 125 | }); |
| 126 | measure("native work / construct + paint", samples, batch, || { |
| 127 | buf.reset(); |
| 128 | (work.draw)(area, &mut buf, &native_theme); |
| 129 | black_box(&buf); |
| 130 | }); |
| 131 | } |
| 132 |