Reduce segment cloning when rendering messages (#33340)

While working on retries, I discovered some opportunities to reduce
cloning of message segments. These segments have full `String`s (not
`SharedString`s), so cloning them means copying cloning all the bytes of
all the strings in the message, which would be nice to avoid!

Release Notes:

- N/A
This commit is contained in:
Richard Feldman 2025-06-25 14:10:48 -04:00 committed by GitHub
parent 8e831ced5b
commit 4516b099e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 98 additions and 86 deletions

View file

@ -198,6 +198,13 @@ impl MessageSegment {
Self::RedactedThinking(_) => false, Self::RedactedThinking(_) => false,
} }
} }
pub fn text(&self) -> Option<&str> {
match self {
MessageSegment::Text(text) => Some(text),
_ => None,
}
}
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]

View file

@ -809,7 +809,12 @@ impl ActiveThread {
}; };
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() { for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
this.push_message(&message.id, &message.segments, window, cx); let rendered_message = RenderedMessage::from_segments(
&message.segments,
this.language_registry.clone(),
cx,
);
this.push_rendered_message(message.id, rendered_message);
for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) { for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
this.render_tool_use_markdown( this.render_tool_use_markdown(
@ -875,36 +880,11 @@ impl ActiveThread {
&self.text_thread_store &self.text_thread_store
} }
fn push_message( fn push_rendered_message(&mut self, id: MessageId, rendered_message: RenderedMessage) {
&mut self,
id: &MessageId,
segments: &[MessageSegment],
_window: &mut Window,
cx: &mut Context<Self>,
) {
let old_len = self.messages.len(); let old_len = self.messages.len();
self.messages.push(*id); self.messages.push(id);
self.list_state.splice(old_len..old_len, 1); self.list_state.splice(old_len..old_len, 1);
self.rendered_messages_by_id.insert(id, rendered_message);
let rendered_message =
RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
self.rendered_messages_by_id.insert(*id, rendered_message);
}
fn edited_message(
&mut self,
id: &MessageId,
segments: &[MessageSegment],
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
return;
};
self.list_state.splice(index..index + 1, 1);
let rendered_message =
RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
self.rendered_messages_by_id.insert(*id, rendered_message);
} }
fn deleted_message(&mut self, id: &MessageId) { fn deleted_message(&mut self, id: &MessageId) {
@ -1037,31 +1017,43 @@ impl ActiveThread {
} }
} }
ThreadEvent::MessageAdded(message_id) => { ThreadEvent::MessageAdded(message_id) => {
if let Some(message_segments) = self if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
.thread thread.message(*message_id).map(|message| {
.read(cx) RenderedMessage::from_segments(
.message(*message_id) &message.segments,
.map(|message| message.segments.clone()) self.language_registry.clone(),
{ cx,
self.push_message(message_id, &message_segments, window, cx); )
})
}) {
self.push_rendered_message(*message_id, rendered_message);
} }
self.save_thread(cx); self.save_thread(cx);
cx.notify(); cx.notify();
} }
ThreadEvent::MessageEdited(message_id) => { ThreadEvent::MessageEdited(message_id) => {
if let Some(message_segments) = self if let Some(index) = self.messages.iter().position(|id| id == message_id) {
.thread if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
.read(cx) thread.message(*message_id).map(|message| {
.message(*message_id) let mut rendered_message = RenderedMessage {
.map(|message| message.segments.clone()) language_registry: self.language_registry.clone(),
{ segments: Vec::with_capacity(message.segments.len()),
self.edited_message(message_id, &message_segments, window, cx); };
for segment in &message.segments {
rendered_message.push_segment(segment, cx);
}
rendered_message
})
}) {
self.list_state.splice(index..index + 1, 1);
self.rendered_messages_by_id
.insert(*message_id, rendered_message);
self.scroll_to_bottom(cx);
self.save_thread(cx);
cx.notify();
}
} }
self.scroll_to_bottom(cx);
self.save_thread(cx);
cx.notify();
} }
ThreadEvent::MessageDeleted(message_id) => { ThreadEvent::MessageDeleted(message_id) => {
self.deleted_message(message_id); self.deleted_message(message_id);
@ -1311,17 +1303,11 @@ impl ActiveThread {
fn start_editing_message( fn start_editing_message(
&mut self, &mut self,
message_id: MessageId, message_id: MessageId,
message_segments: &[MessageSegment], message_text: impl Into<Arc<str>>,
message_creases: &[MessageCrease], message_creases: &[MessageCrease],
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
// User message should always consist of a single text segment,
// therefore we can skip returning early if it's not a text segment.
let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
return;
};
let editor = crate::message_editor::create_editor( let editor = crate::message_editor::create_editor(
self.workspace.clone(), self.workspace.clone(),
self.context_store.downgrade(), self.context_store.downgrade(),
@ -1333,7 +1319,7 @@ impl ActiveThread {
cx, cx,
); );
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.set_text(message_text.clone(), window, cx); editor.set_text(message_text, window, cx);
insert_message_creases(editor, message_creases, &self.context_store, window, cx); insert_message_creases(editor, message_creases, &self.context_store, window, cx);
editor.focus_handle(cx).focus(window); editor.focus_handle(cx).focus(window);
editor.move_to_end(&editor::actions::MoveToEnd, window, cx); editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
@ -1828,8 +1814,6 @@ impl ActiveThread {
return div().children(loading_dots).into_any(); return div().children(loading_dots).into_any();
} }
let message_creases = message.creases.clone();
let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else { let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
return Empty.into_any(); return Empty.into_any();
}; };
@ -2144,15 +2128,30 @@ impl ActiveThread {
}), }),
) )
.on_click(cx.listener({ .on_click(cx.listener({
let message_segments = message.segments.clone(); let message_creases = message.creases.clone();
move |this, _, window, cx| { move |this, _, window, cx| {
this.start_editing_message( if let Some(message_text) =
message_id, this.thread.read(cx).message(message_id).and_then(|message| {
&message_segments, message.segments.first().and_then(|segment| {
&message_creases, match segment {
window, MessageSegment::Text(message_text) => {
cx, Some(Into::<Arc<str>>::into(message_text.as_str()))
); }
_ => {
None
}
}
})
})
{
this.start_editing_message(
message_id,
message_text,
&message_creases,
window,
cx,
);
}
} }
})), })),
), ),
@ -3826,13 +3825,15 @@ mod tests {
}); });
active_thread.update_in(cx, |active_thread, window, cx| { active_thread.update_in(cx, |active_thread, window, cx| {
active_thread.start_editing_message( if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
message.id, active_thread.start_editing_message(
message.segments.as_slice(), message.id,
message.creases.as_slice(), message_text,
window, message.creases.as_slice(),
cx, window,
); cx,
);
}
let editor = active_thread let editor = active_thread
.editing_message .editing_message
.as_ref() .as_ref()
@ -3847,13 +3848,15 @@ mod tests {
let message = thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap()); let message = thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap());
active_thread.update_in(cx, |active_thread, window, cx| { active_thread.update_in(cx, |active_thread, window, cx| {
active_thread.start_editing_message( if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
message.id, active_thread.start_editing_message(
message.segments.as_slice(), message.id,
message.creases.as_slice(), message_text,
window, message.creases.as_slice(),
cx, window,
); cx,
);
}
let editor = active_thread let editor = active_thread
.editing_message .editing_message
.as_ref() .as_ref()
@ -3935,13 +3938,15 @@ mod tests {
// Edit the message while the completion is still running // Edit the message while the completion is still running
active_thread.update_in(cx, |active_thread, window, cx| { active_thread.update_in(cx, |active_thread, window, cx| {
active_thread.start_editing_message( if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
message.id, active_thread.start_editing_message(
message.segments.as_slice(), message.id,
message.creases.as_slice(), message_text,
window, message.creases.as_slice(),
cx, window,
); cx,
);
}
let editor = active_thread let editor = active_thread
.editing_message .editing_message
.as_ref() .as_ref()