mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 18:41:56 +03:00
assistant: Refesh message headers only for dirty messages (#16881)
We've noticed performance issues in long conversations with assistants; the profiles pointed to slowiness in WrapMap (and indeed there were some low hanging fruits that we picked up in https://github.com/zed-industries/zed/pull/16761). That however did not fully resolve the issue, as WrapMap still cracked through in profiles; basically, the speedup I've landed has just moved the post elsewhere. The higher level issue is that we were trying to refresh message headers for all messages, irrespective of whether they've actually needed to be updated. This PR fixes that by using `replace_blocks` API where possible. Release Notes: - Improved performance of Assistant Panel with long conversations.
This commit is contained in:
parent
2c541aee24
commit
aaddb73b28
@ -13,9 +13,10 @@ use crate::{
|
|||||||
terminal_inline_assistant::TerminalInlineAssistant,
|
terminal_inline_assistant::TerminalInlineAssistant,
|
||||||
Assist, CacheStatus, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore,
|
Assist, CacheStatus, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore,
|
||||||
CycleMessageRole, DeployHistory, DeployPromptLibrary, InlineAssistId, InlineAssistant,
|
CycleMessageRole, DeployHistory, DeployPromptLibrary, InlineAssistId, InlineAssistant,
|
||||||
InsertIntoEditor, MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus,
|
InsertIntoEditor, Message, MessageId, MessageMetadata, MessageStatus, ModelSelector,
|
||||||
QuoteSelection, RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus,
|
PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
|
||||||
ToggleModelSelector, WorkflowStepResolution, WorkflowStepView,
|
SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, WorkflowStepResolution,
|
||||||
|
WorkflowStepView,
|
||||||
};
|
};
|
||||||
use crate::{ContextStoreEvent, ModelPickerDelegate};
|
use crate::{ContextStoreEvent, ModelPickerDelegate};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
@ -1702,6 +1703,8 @@ struct WorkflowAssist {
|
|||||||
assist_ids: Vec<InlineAssistId>,
|
assist_ids: Vec<InlineAssistId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MessageHeader = MessageMetadata;
|
||||||
|
|
||||||
pub struct ContextEditor {
|
pub struct ContextEditor {
|
||||||
context: Model<Context>,
|
context: Model<Context>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
@ -1709,7 +1712,7 @@ pub struct ContextEditor {
|
|||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
editor: View<Editor>,
|
editor: View<Editor>,
|
||||||
blocks: HashSet<CustomBlockId>,
|
blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
|
||||||
image_blocks: HashSet<CustomBlockId>,
|
image_blocks: HashSet<CustomBlockId>,
|
||||||
scroll_position: Option<ScrollPosition>,
|
scroll_position: Option<ScrollPosition>,
|
||||||
remote_id: Option<workspace::ViewId>,
|
remote_id: Option<workspace::ViewId>,
|
||||||
@ -3036,176 +3039,209 @@ impl ContextEditor {
|
|||||||
fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
|
fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
|
||||||
let excerpt_id = *buffer.as_singleton().unwrap().0;
|
let excerpt_id = *buffer.as_singleton().unwrap().0;
|
||||||
let old_blocks = std::mem::take(&mut self.blocks);
|
let mut old_blocks = std::mem::take(&mut self.blocks);
|
||||||
let new_blocks = self
|
let mut blocks_to_remove: HashMap<_, _> = old_blocks
|
||||||
.context
|
.iter()
|
||||||
.read(cx)
|
.map(|(message_id, (_, block_id))| (*message_id, *block_id))
|
||||||
.messages(cx)
|
.collect();
|
||||||
.map(|message| BlockProperties {
|
let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
|
||||||
position: buffer
|
|
||||||
.anchor_in_excerpt(excerpt_id, message.anchor)
|
|
||||||
.unwrap(),
|
|
||||||
height: 2,
|
|
||||||
style: BlockStyle::Sticky,
|
|
||||||
render: Box::new({
|
|
||||||
let context = self.context.clone();
|
|
||||||
move |cx| {
|
|
||||||
let message_id = message.id;
|
|
||||||
let show_spinner = message.role == Role::Assistant
|
|
||||||
&& message.status == MessageStatus::Pending;
|
|
||||||
|
|
||||||
let label = match message.role {
|
let render_block = |message: MessageMetadata| -> RenderBlock {
|
||||||
Role::User => {
|
Box::new({
|
||||||
Label::new("You").color(Color::Default).into_any_element()
|
let context = self.context.clone();
|
||||||
|
move |cx| {
|
||||||
|
let message_id = MessageId(message.timestamp);
|
||||||
|
let show_spinner = message.role == Role::Assistant
|
||||||
|
&& message.status == MessageStatus::Pending;
|
||||||
|
|
||||||
|
let label = match message.role {
|
||||||
|
Role::User => {
|
||||||
|
Label::new("You").color(Color::Default).into_any_element()
|
||||||
|
}
|
||||||
|
Role::Assistant => {
|
||||||
|
let label = Label::new("Assistant").color(Color::Info);
|
||||||
|
if show_spinner {
|
||||||
|
label
|
||||||
|
.with_animation(
|
||||||
|
"pulsating-label",
|
||||||
|
Animation::new(Duration::from_secs(2))
|
||||||
|
.repeat()
|
||||||
|
.with_easing(pulsating_between(0.4, 0.8)),
|
||||||
|
|label, delta| label.alpha(delta),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
} else {
|
||||||
|
label.into_any_element()
|
||||||
}
|
}
|
||||||
Role::Assistant => {
|
}
|
||||||
let label = Label::new("Assistant").color(Color::Info);
|
|
||||||
if show_spinner {
|
Role::System => Label::new("System")
|
||||||
label
|
.color(Color::Warning)
|
||||||
.with_animation(
|
.into_any_element(),
|
||||||
"pulsating-label",
|
};
|
||||||
Animation::new(Duration::from_secs(2))
|
|
||||||
.repeat()
|
let sender = ButtonLike::new("role")
|
||||||
.with_easing(pulsating_between(0.4, 0.8)),
|
.style(ButtonStyle::Filled)
|
||||||
|label, delta| label.alpha(delta),
|
.child(label)
|
||||||
|
.tooltip(|cx| {
|
||||||
|
Tooltip::with_meta(
|
||||||
|
"Toggle message role",
|
||||||
|
None,
|
||||||
|
"Available roles: You (User), Assistant, System",
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_click({
|
||||||
|
let context = context.clone();
|
||||||
|
move |_, cx| {
|
||||||
|
context.update(cx, |context, cx| {
|
||||||
|
context.cycle_message_roles(
|
||||||
|
HashSet::from_iter(Some(message_id)),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id(("message_header", message_id.as_u64()))
|
||||||
|
.pl(cx.gutter_dimensions.full_width())
|
||||||
|
.h_11()
|
||||||
|
.w_full()
|
||||||
|
.relative()
|
||||||
|
.gap_1()
|
||||||
|
.child(sender)
|
||||||
|
.children(match &message.cache {
|
||||||
|
Some(cache) if cache.is_final_anchor => match cache.status {
|
||||||
|
CacheStatus::Cached => Some(
|
||||||
|
div()
|
||||||
|
.id("cached")
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::DatabaseZap)
|
||||||
|
.size(IconSize::XSmall)
|
||||||
|
.color(Color::Hint),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.tooltip(|cx| {
|
||||||
} else {
|
Tooltip::with_meta(
|
||||||
label.into_any_element()
|
"Context cached",
|
||||||
}
|
None,
|
||||||
}
|
"Large messages cached to optimize performance",
|
||||||
|
cx,
|
||||||
Role::System => Label::new("System")
|
)
|
||||||
.color(Color::Warning)
|
})
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
};
|
),
|
||||||
|
CacheStatus::Pending => Some(
|
||||||
let sender = ButtonLike::new("role")
|
div()
|
||||||
.style(ButtonStyle::Filled)
|
.child(
|
||||||
.child(label)
|
Icon::new(IconName::Ellipsis)
|
||||||
.tooltip(|cx| {
|
.size(IconSize::XSmall)
|
||||||
Tooltip::with_meta(
|
.color(Color::Hint),
|
||||||
"Toggle message role",
|
)
|
||||||
None,
|
.into_any_element(),
|
||||||
"Available roles: You (User), Assistant, System",
|
),
|
||||||
cx,
|
},
|
||||||
)
|
_ => None,
|
||||||
})
|
})
|
||||||
.on_click({
|
.children(match &message.status {
|
||||||
let context = context.clone();
|
MessageStatus::Error(error) => Some(
|
||||||
move |_, cx| {
|
Button::new("show-error", "Error")
|
||||||
context.update(cx, |context, cx| {
|
.color(Color::Error)
|
||||||
context.cycle_message_roles(
|
.selected_label_color(Color::Error)
|
||||||
HashSet::from_iter(Some(message_id)),
|
.selected_icon_color(Color::Error)
|
||||||
|
.icon(IconName::XCircle)
|
||||||
|
.icon_color(Color::Error)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.icon_position(IconPosition::Start)
|
||||||
|
.tooltip(move |cx| {
|
||||||
|
Tooltip::with_meta(
|
||||||
|
"Error interacting with language model",
|
||||||
|
None,
|
||||||
|
"Click for more details",
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
.on_click({
|
||||||
});
|
let context = context.clone();
|
||||||
|
let error = error.clone();
|
||||||
h_flex()
|
move |_, cx| {
|
||||||
.id(("message_header", message_id.as_u64()))
|
context.update(cx, |_, cx| {
|
||||||
.pl(cx.gutter_dimensions.full_width())
|
cx.emit(ContextEvent::ShowAssistError(
|
||||||
.h_11()
|
error.clone(),
|
||||||
.w_full()
|
));
|
||||||
.relative()
|
});
|
||||||
.gap_1()
|
}
|
||||||
.child(sender)
|
})
|
||||||
.children(match &message.cache {
|
.into_any_element(),
|
||||||
Some(cache) if cache.is_final_anchor => match cache.status {
|
),
|
||||||
CacheStatus::Cached => Some(
|
MessageStatus::Canceled => Some(
|
||||||
div()
|
ButtonLike::new("canceled")
|
||||||
.id("cached")
|
.child(Icon::new(IconName::XCircle).color(Color::Disabled))
|
||||||
.child(
|
.child(
|
||||||
Icon::new(IconName::DatabaseZap)
|
Label::new("Canceled")
|
||||||
.size(IconSize::XSmall)
|
.size(LabelSize::Small)
|
||||||
.color(Color::Hint),
|
.color(Color::Disabled),
|
||||||
)
|
)
|
||||||
.tooltip(|cx| {
|
.tooltip(move |cx| {
|
||||||
Tooltip::with_meta(
|
Tooltip::with_meta(
|
||||||
"Context cached",
|
"Canceled",
|
||||||
None,
|
None,
|
||||||
"Large messages cached to optimize performance",
|
"Interaction with the assistant was canceled",
|
||||||
cx,
|
cx,
|
||||||
)
|
|
||||||
}).into_any_element()
|
|
||||||
),
|
|
||||||
CacheStatus::Pending => Some(
|
|
||||||
div()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Ellipsis)
|
|
||||||
.size(IconSize::XSmall)
|
|
||||||
.color(Color::Hint),
|
|
||||||
).into_any_element()
|
|
||||||
),
|
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.children(match &message.status {
|
|
||||||
MessageStatus::Error(error) => Some(
|
|
||||||
Button::new("show-error", "Error")
|
|
||||||
.color(Color::Error)
|
|
||||||
.selected_label_color(Color::Error)
|
|
||||||
.selected_icon_color(Color::Error)
|
|
||||||
.icon(IconName::XCircle)
|
|
||||||
.icon_color(Color::Error)
|
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.icon_position(IconPosition::Start)
|
|
||||||
.tooltip(move |cx| {
|
|
||||||
Tooltip::with_meta(
|
|
||||||
"Error interacting with language model",
|
|
||||||
None,
|
|
||||||
"Click for more details",
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on_click({
|
|
||||||
let context = context.clone();
|
|
||||||
let error = error.clone();
|
|
||||||
move |_, cx| {
|
|
||||||
context.update(cx, |_, cx| {
|
|
||||||
cx.emit(ContextEvent::ShowAssistError(
|
|
||||||
error.clone(),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into_any_element(),
|
|
||||||
),
|
|
||||||
MessageStatus::Canceled => Some(
|
|
||||||
ButtonLike::new("canceled")
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::XCircle).color(Color::Disabled),
|
|
||||||
)
|
)
|
||||||
.child(
|
})
|
||||||
Label::new("Canceled")
|
.into_any_element(),
|
||||||
.size(LabelSize::Small)
|
),
|
||||||
.color(Color::Disabled),
|
_ => None,
|
||||||
)
|
})
|
||||||
.tooltip(move |cx| {
|
.into_any_element()
|
||||||
Tooltip::with_meta(
|
}
|
||||||
"Canceled",
|
|
||||||
None,
|
|
||||||
"Interaction with the assistant was canceled",
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.into_any_element(),
|
|
||||||
),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
disposition: BlockDisposition::Above,
|
|
||||||
priority: usize::MAX,
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
};
|
||||||
|
let create_block_properties = |message: &Message| BlockProperties {
|
||||||
|
position: buffer
|
||||||
|
.anchor_in_excerpt(excerpt_id, message.anchor)
|
||||||
|
.unwrap(),
|
||||||
|
height: 2,
|
||||||
|
style: BlockStyle::Sticky,
|
||||||
|
disposition: BlockDisposition::Above,
|
||||||
|
priority: usize::MAX,
|
||||||
|
render: render_block(MessageMetadata::from(message)),
|
||||||
|
};
|
||||||
|
let mut new_blocks = vec![];
|
||||||
|
let mut block_index_to_message = vec![];
|
||||||
|
for message in self.context.read(cx).messages(cx) {
|
||||||
|
if let Some(_) = blocks_to_remove.remove(&message.id) {
|
||||||
|
// This is an old message that we might modify.
|
||||||
|
let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
|
||||||
|
debug_assert!(
|
||||||
|
false,
|
||||||
|
"old_blocks should contain a message_id we've just removed."
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// Should we modify it?
|
||||||
|
let message_meta = MessageMetadata::from(&message);
|
||||||
|
if meta != &message_meta {
|
||||||
|
blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
|
||||||
|
*meta = message_meta;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is a new message.
|
||||||
|
new_blocks.push(create_block_properties(&message));
|
||||||
|
block_index_to_message.push((message.id, MessageMetadata::from(&message)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor.replace_blocks(blocks_to_replace, None, cx);
|
||||||
|
editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx);
|
||||||
|
|
||||||
editor.remove_blocks(old_blocks, None, cx);
|
|
||||||
let ids = editor.insert_blocks(new_blocks, None, cx);
|
let ids = editor.insert_blocks(new_blocks, None, cx);
|
||||||
self.blocks = HashSet::from_iter(ids);
|
old_blocks.extend(ids.into_iter().zip(block_index_to_message).map(
|
||||||
|
|(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)),
|
||||||
|
));
|
||||||
|
self.blocks = old_blocks;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,11 +330,22 @@ pub struct MessageCacheMetadata {
|
|||||||
pub struct MessageMetadata {
|
pub struct MessageMetadata {
|
||||||
pub role: Role,
|
pub role: Role,
|
||||||
pub status: MessageStatus,
|
pub status: MessageStatus,
|
||||||
timestamp: clock::Lamport,
|
pub(crate) timestamp: clock::Lamport,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub cache: Option<MessageCacheMetadata>,
|
pub cache: Option<MessageCacheMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&Message> for MessageMetadata {
|
||||||
|
fn from(message: &Message) -> Self {
|
||||||
|
Self {
|
||||||
|
role: message.role,
|
||||||
|
status: message.status.clone(),
|
||||||
|
timestamp: message.id.0,
|
||||||
|
cache: message.cache.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl MessageMetadata {
|
impl MessageMetadata {
|
||||||
pub fn is_cache_valid(&self, buffer: &BufferSnapshot, range: &Range<usize>) -> bool {
|
pub fn is_cache_valid(&self, buffer: &BufferSnapshot, range: &Range<usize>) -> bool {
|
||||||
let result = match &self.cache {
|
let result = match &self.cache {
|
||||||
|
Loading…
Reference in New Issue
Block a user