From 87c08c6b7a1dbd250fa3a8e9424a47b3176f99eb Mon Sep 17 00:00:00 2001 From: Eric Yue Date: Mon, 28 Sep 2026 03:58:55 +0800 Subject: perf: skip frame clones for unchanged retained graphics (#4672) --- src/server/client_shell_graphics.rs | 5 +- src/server/headless/retained_surface.rs | 13 +++-- src/server/headless/tests/pane_graphics.rs | 78 ++++++++++++++++++++++++++---- 3 files changed, 78 insertions(+), 18 deletions(-) (limited to 'src') 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 @@ -4,6 +4,62 @@ fn receive_render(receiver: &std::sync::mpsc::Receiver>, timeout: Durati receiver.recv_timeout(timeout).unwrap() } +#[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) = @@ -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 ); } } -- cgit v1.2.3