aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-08 19:48:51 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-08 19:49:06 +0300
commita7cd7806f2fa0251e2dca41ffec61ceafb6f0e18 (patch)
tree37ea48c24e145e5514e44d78eb8807c661537948 /src/server
parent07222706c78943da814e39abece8b5ba5a10dfc3 (diff)
fix: reduce scroll CPU in headless server
refs #512
Diffstat (limited to 'src/server')
-rw-r--r--src/server/headless.rs88
-rw-r--r--src/server/render_stream.rs62
2 files changed, 114 insertions, 36 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs
index 0f9f3bb6..edd1a388 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -2795,7 +2795,7 @@ impl HeadlessServer {
return false;
};
let prepare_started = crate::render_prof::timer();
- let Some(prepared) = client.render_state.prepare_frame(&frame) else {
+ let Some(prepared) = client.render_state.prepare_frame(frame) else {
client.render_pending = false;
crate::render_prof::event("retained_send.skip_identical");
crate::render_prof::duration_since("retained_send.prepare_frame", prepare_started);
@@ -2831,7 +2831,7 @@ impl HeadlessServer {
match writer.render.try_send(serialized) {
Ok(()) => {
client.render_pending = false;
- client.render_state.commit_sent_frame(frame, prepared);
+ client.render_state.commit_sent_frame(prepared);
crate::render_prof::event("retained_send.sent");
crate::render_prof::duration_since("retained_send.try_send", send_started);
true
@@ -3007,21 +3007,21 @@ impl HeadlessServer {
commit_graphics_cache = false;
}
+ let max_frame_size = if frame.graphics.is_empty() {
+ MAX_FRAME_SIZE
+ } else {
+ MAX_GRAPHICS_FRAME_SIZE
+ };
+ let has_graphics = !frame.graphics.is_empty();
let prepare_started = crate::render_prof::timer();
- let Some(mut prepared) = client.render_state.prepare_frame(&frame) else {
+ let Some(mut prepared) = client.render_state.prepare_frame(frame) else {
client.render_pending = false;
crate::render_prof::event("full_render.skip_identical");
crate::render_prof::duration_since("full_render.prepare_frame", prepare_started);
continue;
};
crate::render_prof::duration_since("full_render.prepare_frame", prepare_started);
- let mut frame_to_commit = frame.clone();
- let max_frame_size = if frame.graphics.is_empty() {
- MAX_FRAME_SIZE
- } else {
- MAX_GRAPHICS_FRAME_SIZE
- };
let serialize_started = crate::render_prof::timer();
let serialized = match Self::frame_server_message_with_max(
prepared.message(),
@@ -3031,17 +3031,22 @@ impl HeadlessServer {
crate::render_prof::duration_since("full_render.serialize", serialize_started);
framed
}
- Err(protocol::FramingError::Oversized { claimed, max })
- if !frame.graphics.is_empty() =>
- {
+ Err(protocol::FramingError::Oversized { claimed, max }) if has_graphics => {
warn!(
client_id,
claimed, max, "dropping graphics from oversized frame for client"
);
- let mut text_only_frame = frame.clone();
+ let Some(mut text_only_frame) = prepared.into_frame() else {
+ crate::render_prof::event("full_render.serialize_error");
+ crate::render_prof::duration_since(
+ "full_render.serialize",
+ serialize_started,
+ );
+ continue;
+ };
text_only_frame.graphics.clear();
let Some(text_only_prepared) =
- client.render_state.prepare_frame(&text_only_frame)
+ client.render_state.prepare_frame(text_only_frame)
else {
client.render_pending = false;
crate::render_prof::event("full_render.skip_identical_text_only");
@@ -3065,7 +3070,6 @@ impl HeadlessServer {
}
};
prepared = text_only_prepared;
- frame_to_commit = text_only_frame;
commit_graphics_cache = false;
crate::render_prof::duration_since("full_render.serialize", serialize_started);
framed
@@ -3097,9 +3101,7 @@ impl HeadlessServer {
client.graphics_cache = next_graphics_cache;
client.graphics_surface_reset_pending = false;
}
- client
- .render_state
- .commit_sent_frame(frame_to_commit, prepared);
+ client.render_state.commit_sent_frame(prepared);
crate::render_prof::event("full_render.sent");
crate::render_prof::duration_since("full_render.try_send", send_started);
}
@@ -4505,6 +4507,52 @@ next_tab = ""
}
#[test]
+ fn terminal_attach_page_key_forwards_in_alternate_screen_without_mouse_reporting() {
+ let rt = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .expect("test runtime");
+ let _runtime_guard = rt.enter();
+ let mut bytes = b"\x1b[?1049h".to_vec();
+ for line in 0..80 {
+ bytes.extend_from_slice(format!("line {line:02}\r\n").as_bytes());
+ }
+ let (runtime, mut input_rx) =
+ crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes(
+ 20, 5, 4096, &bytes, 4,
+ );
+ runtime.scroll_up(3);
+
+ apply_terminal_attach_scroll(
+ &runtime,
+ AttachScrollSource::PageKey {
+ input: b"\x1b[5~".to_vec(),
+ },
+ AttachScrollDirection::Up,
+ 4,
+ None,
+ None,
+ 0,
+ )
+ .expect("page key forward");
+
+ assert_eq!(
+ runtime
+ .scroll_metrics()
+ .expect("scroll metrics")
+ .offset_from_bottom,
+ 0
+ );
+ assert_eq!(
+ input_rx.try_recv().expect("forwarded page key"),
+ Bytes::from_static(b"\x1b[5~")
+ );
+ drop(runtime);
+ drop(_runtime_guard);
+ rt.shutdown_timeout(Duration::from_millis(100));
+ }
+
+ #[test]
fn headless_scheduled_tasks_expire_agent_metadata() {
let mut server = test_headless_server();
let workspace = crate::workspace::Workspace::test_new("metadata");
@@ -6384,9 +6432,9 @@ next_tab = ""
frame.cells[hyperlink_idx].hyperlink = Some(0);
let prepared = client
.render_state
- .prepare_frame(&frame)
+ .prepare_frame(frame)
.expect("hyperlink frame differs");
- client.render_state.commit_sent_frame(frame, prepared);
+ client.render_state.commit_sent_frame(prepared);
let runtime = server
.app
diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs
index 6699298d..58f7a456 100644
--- a/src/server/render_stream.rs
+++ b/src/server/render_stream.rs
@@ -41,25 +41,24 @@ impl ClientRenderState {
}
}
- pub(crate) fn prepare_frame(&mut self, frame: &FrameData) -> Option<PreparedRender> {
+ pub(crate) fn prepare_frame(&mut self, frame: FrameData) -> Option<PreparedRender> {
match self {
Self::Semantic { last_frame } => {
- if last_frame.as_ref() == Some(frame) {
+ if last_frame.as_ref() == Some(&frame) {
crate::render_prof::event("prepare_frame.semantic.skip_current");
return None;
}
crate::render_prof::event("prepare_frame.semantic.changed");
- Some(PreparedRender {
- message: ServerMessage::Frame(frame.clone()),
- encoded: None,
+ Some(PreparedRender::Semantic {
+ message: ServerMessage::Frame(frame),
})
}
Self::TerminalAnsi { blit_encoder, seq } => {
- if blit_encoder.is_current(frame) {
+ if blit_encoder.is_current(&frame) {
crate::render_prof::event("prepare_frame.ansi.skip_current");
return None;
}
- let mut encoded = blit_encoder.encode(frame, false);
+ let mut encoded = blit_encoder.encode(&frame, false);
crate::render_prof::event("prepare_frame.ansi.changed");
crate::render_prof::counter("prepare_frame.ansi.bytes", encoded.bytes.len() as u64);
if encoded.full {
@@ -72,7 +71,7 @@ impl ClientRenderState {
"prepare_frame.graphics.bytes",
frame.graphics.len() as u64,
);
- Some(PreparedRender {
+ Some(PreparedRender::TerminalAnsi {
message: ServerMessage::Terminal(TerminalFrame {
seq: *seq + 1,
width: frame.width,
@@ -80,6 +79,7 @@ impl ClientRenderState {
full: encoded.full,
bytes: encoded.bytes.clone(),
}),
+ frame,
encoded: Some(encoded),
})
}
@@ -93,10 +93,22 @@ impl ClientRenderState {
}
}
- pub(crate) fn commit_sent_frame(&mut self, frame: FrameData, prepared: PreparedRender) {
- match (self, prepared.encoded) {
- (Self::Semantic { last_frame }, None) => *last_frame = Some(frame),
- (Self::TerminalAnsi { blit_encoder, seq }, Some(encoded)) => {
+ pub(crate) fn commit_sent_frame(&mut self, prepared: PreparedRender) {
+ match (self, prepared) {
+ (
+ Self::Semantic { last_frame },
+ PreparedRender::Semantic {
+ message: ServerMessage::Frame(frame),
+ },
+ ) => *last_frame = Some(frame),
+ (
+ Self::TerminalAnsi { blit_encoder, seq },
+ PreparedRender::TerminalAnsi {
+ frame,
+ encoded: Some(encoded),
+ ..
+ },
+ ) => {
blit_encoder.commit(frame, encoded);
*seq += 1;
}
@@ -138,14 +150,32 @@ fn rfind_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
}
/// A prepared client render message plus any baseline state needed after send.
-pub(crate) struct PreparedRender {
- message: ServerMessage,
- encoded: Option<EncodedBlit>,
+pub(crate) enum PreparedRender {
+ Semantic {
+ message: ServerMessage,
+ },
+ TerminalAnsi {
+ message: ServerMessage,
+ frame: FrameData,
+ encoded: Option<EncodedBlit>,
+ },
}
impl PreparedRender {
pub(crate) fn message(&self) -> &ServerMessage {
- &self.message
+ match self {
+ Self::Semantic { message } | Self::TerminalAnsi { message, .. } => message,
+ }
+ }
+
+ pub(crate) fn into_frame(self) -> Option<FrameData> {
+ match self {
+ Self::Semantic {
+ message: ServerMessage::Frame(frame),
+ } => Some(frame),
+ Self::TerminalAnsi { frame, .. } => Some(frame),
+ _ => None,
+ }
}
}