diff options
| -rw-r--r-- | justfile | 4 | ||||
| -rw-r--r-- | src/server/client_shell_graphics.rs | 5 | ||||
| -rw-r--r-- | src/server/headless/retained_surface.rs | 13 | ||||
| -rw-r--r-- | src/server/headless/tests/pane_graphics.rs | 78 |
4 files changed, 82 insertions, 18 deletions
@@ -95,6 +95,10 @@ bench-terminal-targets: bench-bsp-layout: cargo test --release --locked --bin herdr bsp_layout_profile -- --ignored --nocapture --test-threads=1 +# Profile full and retained text, static-image, and unchanged-image updates. +bench-retained-graphics: + cargo test --release --locked --bin herdr render_scale_profile_retained_graphics -- --ignored --nocapture --test-threads=1 + # ~3-5 minute CPU comparison; downloads stable unless HERDR_PERF_BASELINE_BIN is set bench-release-smoke: cargo build --release --locked diff --git a/src/server/client_shell_graphics.rs b/src/server/client_shell_graphics.rs index d0af03a9..8499b964 100644 --- a/src/server/client_shell_graphics.rs +++ b/src/server/client_shell_graphics.rs @@ -3,7 +3,7 @@ use crate::protocol::{ClientShellPopupSurface, SurfaceGraphicsScene}; pub(crate) fn collect_retained( app: &crate::app::App, - surface: &crate::protocol::PaneSurfaceFrame, + panes: &[crate::protocol::PaneSurfacePane], target: crate::ui::TabSurfaceTarget, cell_size: crate::kitty_graphics::HostCellSize, delivered: &DeliveryCache, @@ -12,8 +12,7 @@ pub(crate) fn collect_retained( let rect = |rect: crate::protocol::SurfaceRect| { ratatui::layout::Rect::new(rect.x, rect.y, rect.width, rect.height) }; - let pane_infos = surface - .panes + let pane_infos = panes .iter() .map(|pane| { let (workspace_index, id) = app.parse_pane_id(&pane.pane_id)?; diff --git a/src/server/headless/retained_surface.rs b/src/server/headless/retained_surface.rs index bf98bc3e..2a98536f 100644 --- a/src/server/headless/retained_surface.rs +++ b/src/server/headless/retained_surface.rs @@ -445,12 +445,10 @@ impl HeadlessServer { fallback!("graphics_target"); }; let client = &self.clients[&client_id]; - let mut next_surface = surface.clone(); - crate::server::render_stream::apply_pane_surface_patch(&mut next_surface, &patch); let Some((graphics, delivery, sources)) = crate::server::client_shell_graphics::collect_retained( &self.app, - &next_surface, + &panes, target, client.cell_size, &client.shell_graphics_delivery, @@ -460,14 +458,19 @@ impl HeadlessServer { fallback!("graphics_geometry"); }; graphics_changed = graphics != surface.graphics; - next_surface.graphics = graphics; - Some((next_surface, delivery, sources)) + Some((graphics, delivery, sources)) } else { None }; if patch.rows.is_empty() && !cursor_changed && !metadata_changed && !graphics_changed { continue; } + let graphics = graphics.map(|(graphics, delivery, sources)| { + let mut next_surface = surface.clone(); + crate::server::render_stream::apply_pane_surface_patch(&mut next_surface, &patch); + next_surface.graphics = graphics; + (next_surface, delivery, sources) + }); updates.push(RetainedRecipientUpdate { client_id, patch, diff --git a/src/server/headless/tests/pane_graphics.rs b/src/server/headless/tests/pane_graphics.rs index 1255f4eb..70904fd5 100644 --- a/src/server/headless/tests/pane_graphics.rs +++ b/src/server/headless/tests/pane_graphics.rs @@ -5,6 +5,62 @@ fn receive_render(receiver: &std::sync::mpsc::Receiver<Vec<u8>>, timeout: Durati } #[tokio::test] +async fn unchanged_retained_graphics_leave_the_committed_surface_and_delivery_untouched() { + let (mut server, _control_rx, client_rx, pane_id) = retained_test_server_with_control( + b"populated terminal\r\n\x1b_Ga=T,f=32,t=d,i=7,p=3,s=1,v=1,c=1,r=1,q=2;/wAA/w==\x1b\\", + ); + let client = server.clients.get_mut(&1).unwrap(); + client.mode = ClientConnectionMode::ClientShell; + client.cell_size = crate::kitty_graphics::HostCellSize { + width_px: 10, + height_px: 20, + }; + server.render_and_stream(); + let _ = receive_render(&client_rx, Duration::from_millis(100)); + + let before = server.clients[&1] + .render_state + .last_pane_surface() + .unwrap() + .clone(); + assert_eq!(before.graphics.placements.len(), 1); + assert!(before.graphics.assets.is_empty()); + let cells = server.clients[&1] + .render_state + .last_pane_surface() + .unwrap() + .frame + .cells + .as_ptr(); + let mut hidden = crate::workspace::Workspace::test_new("hidden"); + let hidden_pane = hidden.tabs[0].root_pane; + hidden.insert_test_runtime( + hidden_pane, + crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b"hidden output"), + ); + server.app.state.workspaces.push(hidden); + for source in [pane_id, hidden_pane] { + assert!(server.render_retained_pane_surface_and_stream(&HashSet::from([source]))); + assert!(client_rx.try_recv().is_err()); + let after = server.clients[&1].render_state.last_pane_surface().unwrap(); + assert_eq!(after, &before); + assert_eq!(after.frame.cells.as_ptr(), cells); + } + + // Skipping the no-op must not discard the upload cache for the next real update. + write_shared_test_pane(&mut server, pane_id, b"\x1b[Hchanged"); + assert!(server.render_retained_pane_surface_and_stream(&HashSet::from([pane_id]))); + let ServerMessage::PaneSurface(updated) = + read_server_message(receive_render(&client_rx, Duration::from_millis(100))) + else { + panic!("expected retained image with changed text"); + }; + assert!(updated.graphics.assets.is_empty()); + assert_eq!(updated.graphics.placements, before.graphics.placements); + assert!(frame_text(&updated.frame).contains("changed")); +} + +#[tokio::test] async fn first_kitty_image_updates_retained_surface_without_full_redraw() { let (mut server, _control_rx, client_rx, pane_id) = retained_test_server_with_control(b"text before image"); @@ -241,7 +297,7 @@ async fn retained_unicode_image_arrives_after_fragmented_upload_without_reupload async fn render_scale_profile_retained_graphics() { use ratatui::layout::Direction; for retained in [false, true] { - for image in ["none", "visible", "offscreen"] { + for image in ["none", "visible", "offscreen", "unchanged-image"] { for count in [1, 15] { let (mut server, _control_rx, client_rx, root) = retained_test_server_with_control(b"populated terminal\r\n"); @@ -289,12 +345,14 @@ async fn render_scale_profile_retained_graphics() { let sources = pane_ids.iter().copied().collect(); let mut samples = Vec::new(); for sample in 0..110 { - for id in &pane_ids { - write_shared_test_pane( - &mut server, - *id, - format!("\x1b[H{sample:03}").as_bytes(), - ); + if image != "unchanged-image" { + for id in &pane_ids { + write_shared_test_pane( + &mut server, + *id, + format!("\x1b[H{sample:03}").as_bytes(), + ); + } } let started = Instant::now(); if retained { @@ -314,9 +372,9 @@ async fn render_scale_profile_retained_graphics() { } samples.sort_unstable(); println!( - "retained={retained} 80x24 panes={count} image={image} median_us={} p95_us={}", - samples[50].as_micros(), - samples[94].as_micros() + "retained={retained} 80x24 panes={count} image={image} median_us={:.3} p95_us={:.3}", + samples[50].as_secs_f64() * 1e6, + samples[94].as_secs_f64() * 1e6 ); } } |
