aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/render_stream.rs170
1 files changed, 170 insertions, 0 deletions
diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs
index f1f6c428..f14deb5e 100644
--- a/src/server/render_stream.rs
+++ b/src/server/render_stream.rs
@@ -467,3 +467,173 @@ fn focused_terminal_suppresses_host_cursor(
.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id)
.is_some_and(crate::terminal::TerminalRuntime::synchronized_output_active)
}
+
+#[cfg(test)]
+mod render_scale_benchmark {
+ use std::hint::black_box;
+ use std::time::Instant;
+
+ use ratatui::layout::Direction;
+
+ use super::*;
+ use crate::app::Mode;
+ use crate::terminal::TerminalRuntime;
+ use crate::workspace::Workspace;
+
+ const AREA: Rect = Rect::new(0, 0, 120, 40);
+ const SAMPLE_COUNT: usize = 40;
+ const WARMUP_COUNT: usize = 5;
+
+ #[derive(Clone, Copy)]
+ struct RenderStats {
+ median_us: u128,
+ p95_us: u128,
+ max_us: u128,
+ }
+
+ fn history() -> String {
+ (0..2_000).map(|line| format!("line-{line}\r\n")).collect()
+ }
+
+ fn runtime(history: &str) -> TerminalRuntime {
+ TerminalRuntime::test_with_scrollback_bytes(
+ AREA.width,
+ AREA.height,
+ 1024 * 1024,
+ history.as_bytes(),
+ )
+ }
+
+ fn app_with_workspaces(workspace_count: usize) -> AppState {
+ let history = history();
+ let workspaces = (0..workspace_count)
+ .map(|index| {
+ let mut workspace = Workspace::test_new(&format!("bench-{}", index + 1));
+ let root_pane = workspace.tabs[0].root_pane;
+ workspace.tabs[0]
+ .runtimes
+ .insert(root_pane, runtime(&history));
+ workspace
+ })
+ .collect();
+ app_with(workspaces)
+ }
+
+ fn app_with_active_panes(pane_count: usize) -> AppState {
+ let history = history();
+ let mut workspace = Workspace::test_new("bench");
+ let root_pane = workspace.tabs[0].root_pane;
+ workspace.tabs[0]
+ .runtimes
+ .insert(root_pane, runtime(&history));
+ let mut pane_ids = vec![root_pane];
+
+ for index in 1..pane_count {
+ let target = pane_ids[(index - 1) / 2];
+ workspace.tabs[0].layout.focus_pane(target);
+ let direction = if index % 2 == 0 {
+ Direction::Vertical
+ } else {
+ Direction::Horizontal
+ };
+ let pane_id = workspace.test_split(direction);
+ workspace.tabs[0]
+ .runtimes
+ .insert(pane_id, runtime(&history));
+ pane_ids.push(pane_id);
+ }
+
+ app_with(vec![workspace])
+ }
+
+ fn app_with(workspaces: Vec<Workspace>) -> AppState {
+ let mut app = AppState::test_new();
+ app.mode = Mode::Terminal;
+ app.pane_scrollbars = true;
+ app.workspaces = workspaces;
+ app.active = Some(0);
+ app.selected = 0;
+ app
+ }
+
+ fn profile(mut app: AppState) -> RenderStats {
+ for _ in 0..WARMUP_COUNT {
+ black_box(render_virtual(&mut app, AREA, true));
+ }
+
+ let mut samples = Vec::with_capacity(SAMPLE_COUNT);
+ for _ in 0..SAMPLE_COUNT {
+ let started = Instant::now();
+ black_box(render_virtual(&mut app, AREA, true));
+ samples.push(started.elapsed().as_micros());
+ }
+ samples.sort_unstable();
+
+ RenderStats {
+ median_us: samples[SAMPLE_COUNT / 2],
+ p95_us: samples[(SAMPLE_COUNT - 1) * 95 / 100],
+ max_us: samples[SAMPLE_COUNT - 1],
+ }
+ }
+
+ fn profile_cardinalities(build: fn(usize) -> AppState) -> [(usize, RenderStats); 3] {
+ [1, 15, 50].map(|count| (count, profile(build(count))))
+ }
+
+ fn print_profiles(label: &str, profiles: [(usize, RenderStats); 3]) {
+ let baseline_median_us = profiles[0].1.median_us as f64;
+ let baseline_p95_us = profiles[0].1.p95_us as f64;
+ println!("{label}");
+ println!(" count median_us p95_us max_us median_vs_1x p95_vs_1x");
+ for (count, stats) in profiles {
+ println!(
+ "{count:>10} {:>9} {:>6} {:>6} {:>12.2} {:>9.2}",
+ stats.median_us,
+ stats.p95_us,
+ stats.max_us,
+ stats.median_us as f64 / baseline_median_us,
+ stats.p95_us as f64 / baseline_p95_us,
+ );
+ }
+ }
+
+ fn assert_full_render_avoids_aggregate_input_state(mut app: AppState, scenario: &str) {
+ crate::pane::reset_aggregate_input_state_reads();
+ black_box(render_virtual(&mut app, AREA, true));
+ assert_eq!(
+ crate::pane::aggregate_input_state_reads(),
+ 0,
+ "full render collected aggregate input state for {scenario}",
+ );
+ }
+
+ #[tokio::test(flavor = "current_thread")]
+ async fn aggregate_input_state_counter_records_reads() {
+ let runtime = TerminalRuntime::test_with_screen_bytes(80, 24, b"");
+ crate::pane::reset_aggregate_input_state_reads();
+ black_box(runtime.input_state());
+ assert_eq!(crate::pane::aggregate_input_state_reads(), 1);
+ }
+
+ #[tokio::test(flavor = "current_thread")]
+ async fn full_render_avoids_aggregate_input_state_reads() {
+ assert_full_render_avoids_aggregate_input_state(
+ app_with_workspaces(15),
+ "background workspaces",
+ );
+ assert_full_render_avoids_aggregate_input_state(app_with_active_panes(15), "active panes");
+ }
+
+ #[tokio::test(flavor = "current_thread")]
+ #[ignore = "manual full-render scaling profile"]
+ async fn render_scale_profile() {
+ print_profiles(
+ "background-workspace resize/layout (one pane each)",
+ profile_cardinalities(app_with_workspaces),
+ );
+ print_profiles(
+ "active panes (one workspace)",
+ profile_cardinalities(app_with_active_panes),
+ );
+ }
+}