use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
enum RenderBackendMode {
Test,
CrosstermSink,
CrosstermReal,
}
impl RenderBackendMode {
fn from_env() -> Self {
match std::env::var("PROFILE_TUI_RENDER_BACKEND")
.unwrap_or_else(|_| "test".to_string())
.trim()
{
"test" | "" => Self::Test,
"crossterm-sink" => Self::CrosstermSink,
"crossterm-real" => Self::CrosstermReal,
value => {
eprintln!(
"tui_render_pipeline warning=unsupported_backend value={value:?} using=test"
);
Self::Test
}
}
}
fn as_str(self) -> &'static str {
match self {
Self::Test => "test",
Self::CrosstermSink => "crossterm-sink",
Self::CrosstermReal => "crossterm-real",
}
}
}
#[derive(Debug, Default, Clone, Copy, Serialize)]
struct DurationMetricStats {
min: f64,
p50: f64,
p95: f64,
max: f64,
avg: f64,
}
#[derive(Debug, Default, Clone, Copy, Serialize)]
struct U64MetricStats {
min: u64,
p50: u64,
p95: u64,
max: u64,
avg: f64,
}
#[derive(Debug, Default, Clone, Copy, Serialize)]
struct CountingWriterStats {
stdout_bytes: u64,
write_calls: u64,
write_duration: Duration,
flush_calls: u64,
flush_duration: Duration,
}
#[derive(Debug, Clone, Copy, Default, Serialize)]
struct RenderFrameStats {
apply: Duration,
render_draw: Duration,
diff: Duration,
diff_cells: u64,
backend_draw: Duration,
stdout_bytes: u64,
write_calls: u64,
write_duration: Duration,
flush_calls: u64,
flush_duration: Duration,
terminal_draw: Option<Duration>,
controlled_draw: Duration,
}
#[derive(Debug, Serialize)]
pub(super) struct RenderScenarioSummary {
schema: u64,
scenario: &'static str,
backend_mode: &'static str,
area: (u16, u16),
frames: usize,
warmup_frames: usize,
elapsed_ms: f64,
frames_per_sec: f64,
apply_ms: DurationMetricStats,
terminal_draw_ms: Option<DurationMetricStats>,
controlled_draw_ms: DurationMetricStats,
render_draw_ms: DurationMetricStats,
diff_ms: DurationMetricStats,
diff_cells: U64MetricStats,
changed_cell_ratio: f64,
backend_draw_ms: DurationMetricStats,
stdout_bytes: U64MetricStats,
write_calls: U64MetricStats,
write_ms: DurationMetricStats,
flush_calls: U64MetricStats,
flush_ms: DurationMetricStats,
}
fn render_duration_stats(values: impl Iterator<Item = Duration>) -> DurationMetricStats {
let values = values.collect::<Vec<_>>();
if values.is_empty() {
return DurationMetricStats::default();
}
let avg = values.iter().sum::<Duration>().as_secs_f64() * 1000.0 / values.len() as f64;
let mut millis = values
.iter()
.map(|duration| duration.as_secs_f64() * 1000.0)
.collect::<Vec<_>>();
millis.sort_by(f64::total_cmp);
DurationMetricStats {
min: round_millis(millis[0]),
p50: round_millis(percentile(&millis, 0.50)),
p95: round_millis(percentile(&millis, 0.95)),
max: round_millis(*millis.last().unwrap()),
avg: round_millis(avg),
}
}
fn render_u64_stats(values: impl Iterator<Item = u64>) -> U64MetricStats {
let mut values = values.collect::<Vec<_>>();
if values.is_empty() {
return U64MetricStats::default();
}
values.sort_unstable();
let avg = values.iter().sum::<u64>() as f64 / values.len() as f64;
U64MetricStats {
min: values[0],
p50: percentile_u64(&values, 0.50),
p95: percentile_u64(&values, 0.95),
max: *values.last().unwrap(),
avg: round_millis(avg),
}
}
fn percentile_u64(sorted: &[u64], percentile: f64) -> u64 {
if sorted.is_empty() {
return 0;
}
let rank = (percentile * sorted.len() as f64).ceil() as usize;
sorted[rank.saturating_sub(1).min(sorted.len() - 1)]
}
#[derive(Debug)]
struct CountingWriter<W: Write> {
inner: W,
stats: Rc<RefCell<CountingWriterStats>>,
}
impl<W: Write> CountingWriter<W> {
fn new(inner: W, stats: Rc<RefCell<CountingWriterStats>>) -> Self {
Self { inner, stats }
}
}
impl<W: Write> Write for CountingWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let started = Instant::now();
let written = self.inner.write(buf)?;
let elapsed = started.elapsed();
let mut stats = self.stats.borrow_mut();
stats.stdout_bytes = stats.stdout_bytes.saturating_add(written as u64);
stats.write_calls = stats.write_calls.saturating_add(1);
stats.write_duration += elapsed;
Ok(written)
}
fn flush(&mut self) -> io::Result<()> {
let started = Instant::now();
self.inner.flush()?;
let elapsed = started.elapsed();
let mut stats = self.stats.borrow_mut();
stats.flush_calls = stats.flush_calls.saturating_add(1);
stats.flush_duration += elapsed;
Ok(())
}
}
#[derive(Debug)]
struct CountingBackend<B> {
inner: B,
writer_stats: Option<Rc<RefCell<CountingWriterStats>>>,
last_backend_draw: Duration,
last_writer_delta: CountingWriterStats,
}
impl<B> CountingBackend<B> {
fn new(inner: B, writer_stats: Option<Rc<RefCell<CountingWriterStats>>>) -> Self {
Self {
inner,
writer_stats,
last_backend_draw: Duration::ZERO,
last_writer_delta: CountingWriterStats::default(),
}
}
fn take_frame_io_stats(&mut self) -> (Duration, CountingWriterStats) {
let draw = self.last_backend_draw;
let writer = self.last_writer_delta;
self.last_backend_draw = Duration::ZERO;
self.last_writer_delta = CountingWriterStats::default();
(draw, writer)
}
}
impl<B: Backend> Backend for CountingBackend<B> {
type Error = B::Error;
fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
where
I: Iterator<Item = (u16, u16, &'a ratatui::buffer::Cell)>,
{
let before = self
.writer_stats
.as_ref()
.map(|stats| *stats.borrow())
.unwrap_or_default();
let started = Instant::now();
let result = self.inner.draw(content);
self.last_backend_draw = started.elapsed();
let after = self
.writer_stats
.as_ref()
.map(|stats| *stats.borrow())
.unwrap_or_default();
self.last_writer_delta.stdout_bytes =
after.stdout_bytes.saturating_sub(before.stdout_bytes);
self.last_writer_delta.write_calls = after.write_calls.saturating_sub(before.write_calls);
self.last_writer_delta.write_duration = after
.write_duration
.checked_sub(before.write_duration)
.unwrap_or_default();
result
}
fn append_lines(&mut self, n: u16) -> Result<(), Self::Error> {
self.inner.append_lines(n)
}
fn hide_cursor(&mut self) -> Result<(), Self::Error> {
self.inner.hide_cursor()
}
fn show_cursor(&mut self) -> Result<(), Self::Error> {
self.inner.show_cursor()
}
fn get_cursor_position(&mut self) -> Result<Position, Self::Error> {
self.inner.get_cursor_position()
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), Self::Error> {
self.inner.set_cursor_position(position)
}
fn clear(&mut self) -> Result<(), Self::Error> {
self.inner.clear()
}
fn clear_region(&mut self, clear_type: ratatui::backend::ClearType) -> Result<(), Self::Error> {
self.inner.clear_region(clear_type)
}
fn size(&self) -> Result<Size, Self::Error> {
self.inner.size()
}
fn window_size(&mut self) -> Result<ratatui::backend::WindowSize, Self::Error> {
self.inner.window_size()
}
fn flush(&mut self) -> Result<(), Self::Error> {
let before = self
.writer_stats
.as_ref()
.map(|stats| *stats.borrow())
.unwrap_or_default();
let result = self.inner.flush();
let after = self
.writer_stats
.as_ref()
.map(|stats| *stats.borrow())
.unwrap_or_default();
self.last_writer_delta.flush_calls = after.flush_calls.saturating_sub(before.flush_calls);
self.last_writer_delta.flush_duration = after
.flush_duration
.checked_sub(before.flush_duration)
.unwrap_or_default();
result
}
}
fn controlled_render_frame<B: Backend>(
terminal: &mut Terminal<CountingBackend<B>>,
previous: &mut Buffer,
state: &MissionControlState,
) -> Result<RenderFrameStats, B::Error> {
let total_started = Instant::now();
terminal.current_buffer_mut().reset();
let render_started = Instant::now();
{
let mut frame = terminal.get_frame();
crate::tui::render::draw(&mut frame, state);
}
let render_draw = render_started.elapsed();
let current = terminal.current_buffer_mut().clone();
let diff_started = Instant::now();
let updates = previous.diff(¤t);
let diff = diff_started.elapsed();
let diff_cells = updates.len() as u64;
terminal.backend_mut().draw(updates.into_iter())?;
terminal.backend_mut().flush()?;
let (backend_draw, writer) = terminal.backend_mut().take_frame_io_stats();
*previous = current;
terminal.swap_buffers();
Ok(RenderFrameStats {
apply: Duration::ZERO,
render_draw,
diff,
diff_cells,
backend_draw,
stdout_bytes: writer.stdout_bytes,
write_calls: writer.write_calls,
write_duration: writer.write_duration,
flush_calls: writer.flush_calls,
flush_duration: writer.flush_duration,
terminal_draw: None,
controlled_draw: total_started.elapsed(),
})
}
fn render_scenario_summary(
scenario: &'static str,
mode: RenderBackendMode,
area: Rect,
warmup_frames: usize,
frames: Vec<RenderFrameStats>,
elapsed: Duration,
) -> RenderScenarioSummary {
let measured = frames
.iter()
.skip(warmup_frames)
.copied()
.collect::<Vec<_>>();
let measured = if measured.is_empty() {
&frames
} else {
&measured
};
let terminal_values = measured
.iter()
.filter_map(|frame| frame.terminal_draw)
.collect::<Vec<_>>();
let area_cells = u64::from(area.width)
.saturating_mul(u64::from(area.height))
.max(1);
RenderScenarioSummary {
schema: 1,
scenario,
backend_mode: mode.as_str(),
area: (area.width, area.height),
frames: measured.len(),
warmup_frames,
elapsed_ms: round_millis(elapsed.as_secs_f64() * 1000.0),
frames_per_sec: round_millis(measured.len() as f64 / elapsed.as_secs_f64().max(0.001)),
apply_ms: render_duration_stats(measured.iter().map(|frame| frame.apply)),
terminal_draw_ms: (!terminal_values.is_empty())
.then(|| render_duration_stats(terminal_values.into_iter())),
controlled_draw_ms: render_duration_stats(
measured.iter().map(|frame| frame.controlled_draw),
),
render_draw_ms: render_duration_stats(measured.iter().map(|frame| frame.render_draw)),
diff_ms: render_duration_stats(measured.iter().map(|frame| frame.diff)),
diff_cells: render_u64_stats(measured.iter().map(|frame| frame.diff_cells)),
changed_cell_ratio: round_millis(
measured.iter().map(|frame| frame.diff_cells).sum::<u64>() as f64
/ measured.len().max(1) as f64
/ area_cells as f64,
),
backend_draw_ms: render_duration_stats(measured.iter().map(|frame| frame.backend_draw)),
stdout_bytes: render_u64_stats(measured.iter().map(|frame| frame.stdout_bytes)),
write_calls: render_u64_stats(measured.iter().map(|frame| frame.write_calls)),
write_ms: render_duration_stats(measured.iter().map(|frame| frame.write_duration)),
flush_calls: render_u64_stats(measured.iter().map(|frame| frame.flush_calls)),
flush_ms: render_duration_stats(measured.iter().map(|frame| frame.flush_duration)),
}
}
fn print_render_summary(summary: &RenderScenarioSummary) {
EMIT_RENDER_SUMMARIES.with(|emit| {
if emit.get() {
println!(
"tui_render_pipeline {}",
serde_json::to_string(summary).unwrap()
);
}
});
}
fn render_profile_line(summaries: &[RenderScenarioSummary], elapsed: Duration) -> ProfileResult {
let frames = summaries
.iter()
.map(|summary| summary.frames as u64)
.sum::<u64>();
let avg_frames_per_sec = if summaries.is_empty() {
0
} else {
(summaries
.iter()
.map(|summary| summary.frames_per_sec)
.sum::<f64>()
/ summaries.len() as f64) as u64
};
let avg_render_us = average_stat_us(summaries, |summary| summary.render_draw_ms.avg);
let avg_diff_us = average_stat_us(summaries, |summary| summary.diff_ms.avg);
let avg_diff_cells = (summaries
.iter()
.map(|summary| summary.diff_cells.avg)
.sum::<f64>()
/ summaries.len().max(1) as f64) as u64;
let avg_stdout_bytes = (summaries
.iter()
.map(|summary| summary.stdout_bytes.avg)
.sum::<f64>()
/ summaries.len().max(1) as f64) as u64;
let avg_flush_us = average_stat_us(summaries, |summary| summary.flush_ms.avg);
ProfileResult {
scenario: "tui_render_pipeline",
iterations: summaries.len() as u64,
elapsed,
primary_metric: "frames",
metric_value: frames,
artifact: "target/profiling/issue-48/tui-render-pipeline.json",
counters: vec![
("frames_per_sec", avg_frames_per_sec),
("avg_render_us", avg_render_us),
("avg_diff_us", avg_diff_us),
("avg_diff_cells", avg_diff_cells),
("avg_stdout_bytes", avg_stdout_bytes),
("avg_flush_us", avg_flush_us),
],
}
}
fn average_stat_us(
summaries: &[RenderScenarioSummary],
value: impl Fn(&RenderScenarioSummary) -> f64,
) -> u64 {
if summaries.is_empty() {
return 0;
}
(summaries.iter().map(value).sum::<f64>() * 1000.0 / summaries.len() as f64) as u64
}
pub(super) fn seed_render_heavy_transcript_state(turns: usize) -> MissionControlState {
let mut state = MissionControlState::default();
for index in 0..turns {
state.apply_output_event(&OutputEvent::UserPrompt {
text: format!("inspect synthetic turn {index}"),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: RENDERING_TRANSCRIPT.to_string(),
});
}
state
}
pub(super) fn seed_render_streaming_state() -> MissionControlState {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "stream synthetic assistant response".to_string(),
});
state
}
fn apply_streaming_frame(state: &mut MissionControlState, frame: usize) {
for index in 0..5 {
let sequence = frame * 5 + index;
state.apply_output_event(&OutputEvent::AssistantDelta {
text: format!("delta-{sequence} "),
});
let id = ActivityId::new(format!("render-tool-{sequence}"));
state.apply_activity_event(ActivityEvent::Started {
id: id.clone(),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new(format!("synthetic tool {sequence}")),
});
state.apply_activity_event(ActivityEvent::Delta {
id: id.clone(),
preview: format!("preview line {sequence}"),
});
state.apply_activity_event(ActivityEvent::Finished {
id,
status: ActivityStatus::Success,
metadata: None,
});
}
}
fn seed_closed_modal_state() -> MissionControlState {
seed_render_heavy_transcript_state(8)
}
fn open_synthetic_skills_modal(state: &mut MissionControlState) {
state.open_skills_modal(
vec![
crate::tui::state::SkillToggleRow {
name: "synthetic-render-skill".to_string(),
source: "test".to_string(),
enabled: true,
},
crate::tui::state::SkillToggleRow {
name: "disabled-render-skill".to_string(),
source: "test".to_string(),
enabled: false,
},
],
8,
);
}
fn run_test_backend_render_scenario(
scenario: &'static str,
area: Rect,
frames: usize,
mut state: MissionControlState,
mut mutate: impl FnMut(&mut MissionControlState, usize, Rect),
) -> RenderScenarioSummary {
let warmup_frames = 1;
let mut terminal_draw_terminal = Terminal::with_options(
TestBackend::new(area.width, area.height),
TerminalOptions {
viewport: Viewport::Fixed(area),
},
)
.expect("test backend terminal draw terminal");
let mut controlled_terminal = Terminal::with_options(
CountingBackend::new(TestBackend::new(area.width, area.height), None),
TerminalOptions {
viewport: Viewport::Fixed(area),
},
)
.expect("test backend controlled terminal");
let mut previous = Buffer::empty(area);
let mut stats = Vec::with_capacity(frames + warmup_frames);
let started = Instant::now();
for frame_index in 0..frames + warmup_frames {
let apply_started = Instant::now();
mutate(&mut state, frame_index, area);
let apply = apply_started.elapsed();
let mut frame_stats =
controlled_render_frame(&mut controlled_terminal, &mut previous, &state)
.expect("controlled test backend frame");
frame_stats.apply = apply;
let terminal_started = Instant::now();
terminal_draw_terminal
.draw(|frame| crate::tui::render::draw(frame, &state))
.expect("terminal draw");
frame_stats.terminal_draw = Some(terminal_started.elapsed());
stats.push(frame_stats);
}
let summary = render_scenario_summary(
scenario,
RenderBackendMode::Test,
area,
warmup_frames,
stats,
started.elapsed(),
);
print_render_summary(&summary);
summary
}
fn run_crossterm_render_scenario(
scenario: &'static str,
mode: RenderBackendMode,
area: Rect,
frames: usize,
mut state: MissionControlState,
mut mutate: impl FnMut(&mut MissionControlState, usize, Rect),
) -> RenderScenarioSummary {
let warmup_frames = 1;
let writer_stats = Rc::new(RefCell::new(CountingWriterStats::default()));
let writer: Box<dyn Write> = match mode {
RenderBackendMode::CrosstermSink => Box::new(io::sink()),
RenderBackendMode::CrosstermReal => Box::new(io::stdout()),
RenderBackendMode::Test => unreachable!("test mode uses TestBackend"),
};
let writer = CountingWriter::new(writer, Rc::clone(&writer_stats));
let backend = CountingBackend::new(CrosstermBackend::new(writer), Some(writer_stats));
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fixed(area),
},
)
.expect("crossterm controlled terminal");
let mut previous = Buffer::empty(area);
let mut stats = Vec::with_capacity(frames + warmup_frames);
let started = Instant::now();
for frame_index in 0..frames + warmup_frames {
let apply_started = Instant::now();
mutate(&mut state, frame_index, area);
let apply = apply_started.elapsed();
let mut frame_stats = controlled_render_frame(&mut terminal, &mut previous, &state)
.expect("controlled crossterm frame");
frame_stats.apply = apply;
stats.push(frame_stats);
}
let summary = render_scenario_summary(
scenario,
mode,
area,
warmup_frames,
stats,
started.elapsed(),
);
print_render_summary(&summary);
summary
}
pub(super) fn run_render_scenario(
scenario: &'static str,
area: Rect,
frames: usize,
state: MissionControlState,
mutate: impl FnMut(&mut MissionControlState, usize, Rect),
) -> RenderScenarioSummary {
match RenderBackendMode::from_env() {
RenderBackendMode::Test => {
run_test_backend_render_scenario(scenario, area, frames, state, mutate)
}
RenderBackendMode::CrosstermSink => run_crossterm_render_scenario(
scenario,
RenderBackendMode::CrosstermSink,
area,
frames,
state,
mutate,
),
RenderBackendMode::CrosstermReal => run_crossterm_render_scenario(
scenario,
RenderBackendMode::CrosstermReal,
area,
frames,
state,
mutate,
),
}
}
fn run_resize_transition_scenario(
scenario: &'static str,
previous_area: Rect,
current_area: Rect,
state: MissionControlState,
) -> RenderScenarioSummary {
let mode = RenderBackendMode::from_env();
if mode != RenderBackendMode::Test {
return run_render_scenario(scenario, current_area, 1, state, |_, _, _| {});
}
let mut terminal = Terminal::with_options(
CountingBackend::new(
TestBackend::new(previous_area.width, previous_area.height),
None,
),
TerminalOptions {
viewport: Viewport::Fixed(previous_area),
},
)
.expect("resize transition terminal");
controlled_render_frame(&mut terminal, &mut Buffer::empty(previous_area), &state)
.expect("resize transition baseline frame");
let mut terminal = Terminal::with_options(
CountingBackend::new(
TestBackend::new(current_area.width, current_area.height),
None,
),
TerminalOptions {
viewport: Viewport::Fixed(current_area),
},
)
.expect("resize transition resized terminal");
let started = Instant::now();
let stats = vec![
controlled_render_frame(&mut terminal, &mut Buffer::empty(current_area), &state)
.expect("resize transition first frame"),
];
let summary = render_scenario_summary(
scenario,
RenderBackendMode::Test,
current_area,
0,
stats,
started.elapsed(),
);
print_render_summary(&summary);
summary
}
fn render_scenario_idle() -> RenderScenarioSummary {
run_render_scenario(
"idle",
Rect::new(0, 0, 120, 36),
20,
MissionControlState::default(),
|_, _, _| {},
)
}
fn render_scenario_streaming() -> RenderScenarioSummary {
run_render_scenario(
"streaming",
Rect::new(0, 0, 120, 36),
20,
seed_render_streaming_state(),
|state, frame, _| apply_streaming_frame(state, frame),
)
}
fn render_scenario_heavy_transcript() -> RenderScenarioSummary {
run_render_scenario(
"heavy_transcript",
Rect::new(0, 0, 120, 36),
12,
seed_render_heavy_transcript_state(24),
|_, _, _| {},
)
}
fn render_scenario_scroll() -> RenderScenarioSummary {
run_render_scenario(
"scroll",
Rect::new(0, 0, 120, 36),
18,
seed_render_heavy_transcript_state(24),
|state, frame, _| state.set_scroll_offset(&state.scroll_views.transcript, frame % 16),
)
}
fn render_scenario_resize() -> Vec<RenderScenarioSummary> {
let sizes = [(80, 24), (120, 40), (160, 50), (50, 12)];
let mut summaries = Vec::with_capacity(sizes.len());
for (index, (width, height)) in sizes.into_iter().enumerate() {
let previous = if index == 0 {
Rect::new(0, 0, 120, 36)
} else {
let (previous_width, previous_height) = sizes[index - 1];
Rect::new(0, 0, previous_width, previous_height)
};
let current = Rect::new(0, 0, width, height);
summaries.push(run_resize_transition_scenario(
match (width, height) {
(80, 24) => "resize_first_frame_80x24",
(120, 40) => "resize_first_frame_120x40",
(160, 50) => "resize_first_frame_160x50",
(50, 12) => "resize_first_frame_50x12",
_ => "resize_first_frame",
},
previous,
current,
seed_render_heavy_transcript_state(10),
));
}
summaries
}
fn render_scenario_modal() -> RenderScenarioSummary {
run_render_scenario(
"modal_open",
Rect::new(0, 0, 120, 36),
12,
seed_closed_modal_state(),
|state, frame, _| {
if frame == 1 {
open_synthetic_skills_modal(state);
}
},
)
}
fn run_tui_render_pipeline(filter: Option<&str>) -> Vec<RenderScenarioSummary> {
let mut summaries = Vec::new();
match filter {
Some("idle") => summaries.push(render_scenario_idle()),
Some("streaming") => summaries.push(render_scenario_streaming()),
Some("heavy_transcript") => summaries.push(render_scenario_heavy_transcript()),
Some("scroll") => summaries.push(render_scenario_scroll()),
Some("resize") => summaries.extend(render_scenario_resize()),
Some("modal") => summaries.push(render_scenario_modal()),
None => {
summaries.push(render_scenario_idle());
summaries.push(render_scenario_streaming());
summaries.push(render_scenario_heavy_transcript());
summaries.push(render_scenario_scroll());
summaries.extend(render_scenario_resize());
summaries.push(render_scenario_modal());
}
Some(other) => panic!("unknown render scenario {other}"),
}
summaries
}
fn run_tui_render_pipeline_profile(
filter: Option<&str>,
started: Instant,
) -> Vec<RenderScenarioSummary> {
let min_iterations = profile_iterations();
let min_duration = profile_min_duration();
let mut summaries = Vec::new();
let mut iterations = 0;
while iterations < min_iterations || started.elapsed() < min_duration {
EMIT_RENDER_SUMMARIES.with(|emit| emit.set(iterations + 1 >= min_iterations));
summaries.extend(run_tui_render_pipeline(filter));
iterations += 1;
}
EMIT_RENDER_SUMMARIES.with(|emit| emit.set(true));
summaries
}
pub(super) fn assert_render_summaries(summaries: &[RenderScenarioSummary]) {
assert!(!summaries.is_empty());
for summary in summaries {
assert!(summary.frames > 0, "{summary:?}");
assert!(summary.frames_per_sec > 0.0, "{summary:?}");
assert!(
summary.render_draw_ms.max >= summary.render_draw_ms.min,
"{summary:?}"
);
assert!(
summary.diff_cells.max >= summary.diff_cells.min,
"{summary:?}"
);
}
}
#[test]
#[ignore = "diagnostic-only TUI render profiling harness; run explicitly with --ignored --nocapture"]
fn profile_tui_render_pipeline() {
let started = Instant::now();
let summaries = run_tui_render_pipeline_profile(None, started);
assert_render_summaries(&summaries);
render_profile_line(&summaries, started.elapsed()).print();
}
#[test]
#[ignore = "diagnostic-only TUI render profiling harness; run explicitly with --ignored --nocapture"]
fn profile_tui_render_pipeline_idle() {
let summaries = run_tui_render_pipeline(Some("idle"));
assert_render_summaries(&summaries);
}
#[test]
#[ignore = "diagnostic-only TUI render profiling harness; run explicitly with --ignored --nocapture"]
fn profile_tui_render_pipeline_streaming() {
let summaries = run_tui_render_pipeline(Some("streaming"));
assert_render_summaries(&summaries);
}
#[test]
#[ignore = "diagnostic-only TUI render profiling harness; run explicitly with --ignored --nocapture"]
fn profile_tui_render_pipeline_heavy_transcript() {
let summaries = run_tui_render_pipeline(Some("heavy_transcript"));
assert_render_summaries(&summaries);
}
#[test]
#[ignore = "diagnostic-only TUI render profiling harness; run explicitly with --ignored --nocapture"]
fn profile_tui_render_pipeline_scroll() {
let summaries = run_tui_render_pipeline(Some("scroll"));
assert_render_summaries(&summaries);
}
#[test]
#[ignore = "diagnostic-only TUI render profiling harness; run explicitly with --ignored --nocapture"]
fn profile_tui_render_pipeline_resize() {
let summaries = run_tui_render_pipeline(Some("resize"));
assert_render_summaries(&summaries);
}
#[test]
#[ignore = "diagnostic-only TUI render profiling harness; run explicitly with --ignored --nocapture"]
fn profile_tui_render_pipeline_modal() {
let summaries = run_tui_render_pipeline(Some("modal"));
assert_render_summaries(&summaries);
}