From 1d57249e5dab7cc41695084d554eac71fd9aa41f Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 3 Jun 2026 11:51:31 +0200 Subject: [PATCH] agent_ui: Cache cross-channel thread import check off the render loop channels_with_threads opened a fresh SQLite connection per release-channel database on every call, and the sidebar called it twice per frame from its render path, blocking the render loop on disk I/O. Run the per-channel checks on a background thread and cache the resulting channel names once at sidebar construction, since the per-channel databases cannot change while this process runs. Release Notes: - Improved agent panel sidebar rendering performance by no longer querying release-channel databases on every frame --- crates/agent_ui/src/thread_import.rs | 54 ++++++++++++++-------------- crates/sidebar/src/sidebar.rs | 25 ++++++++++--- crates/zed/src/zed/open_listener.rs | 1 + 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/crates/agent_ui/src/thread_import.rs b/crates/agent_ui/src/thread_import.rs index c2f521641288de..838949d4335eaa 100644 --- a/crates/agent_ui/src/thread_import.rs +++ b/crates/agent_ui/src/thread_import.rs @@ -60,24 +60,37 @@ impl Dismissable for CrossChannelImportOnboarding { const KEY: &'static str = "dismissed-cross-channel-thread-import"; } -/// Returns the list of non-Dev, non-current release channels that have -/// at least one thread in their database. The result is suitable for -/// building a user-facing message ("from Zed Preview and Nightly"). -pub fn channels_with_threads(cx: &App) -> Vec { +pub fn channels_with_threads(cx: &App) -> Task> { let Some(current_channel) = ReleaseChannel::try_global(cx) else { - return Vec::new(); + return Task::ready(Vec::new()); }; let database_dir = paths::database_dir(); - ReleaseChannel::ALL - .iter() - .copied() - .filter(|channel| { - *channel != current_channel - && *channel != ReleaseChannel::Dev - && channel_has_threads(database_dir, *channel) - }) - .collect() + let channel_has_threads = |database_dir: &std::path::Path, channel: ReleaseChannel| { + let db_path = db::db_path(database_dir, channel); + if !db_path.exists() { + return false; + } + let connection = sqlez::connection::Connection::open_file(&db_path.to_string_lossy()); + connection + .select_row::("SELECT 1 FROM sidebar_threads LIMIT 1") + .ok() + .and_then(|mut query| query().ok().flatten()) + .unwrap_or(false) + }; + + cx.background_spawn(async move { + ReleaseChannel::ALL + .iter() + .copied() + .filter(|channel| { + *channel != current_channel + && *channel != ReleaseChannel::Dev + && channel_has_threads(database_dir, *channel) + }) + .map(|channel| SharedString::new_static(channel.display_name())) + .collect() + }) } #[derive(Clone)] @@ -658,19 +671,6 @@ fn import_threads_from_other_channels_in( .detach(); } -fn channel_has_threads(database_dir: &std::path::Path, channel: ReleaseChannel) -> bool { - let db_path = db::db_path(database_dir, channel); - if !db_path.exists() { - return false; - } - let connection = sqlez::connection::Connection::open_file(&db_path.to_string_lossy()); - connection - .select_row::("SELECT 1 FROM sidebar_threads LIMIT 1") - .ok() - .and_then(|mut query| query().ok().flatten()) - .unwrap_or(false) -} - fn read_threads_from_channel( database_dir: &std::path::Path, channel: ReleaseChannel, diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 9bb20212cc40d5..e4359dc30d9286 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -780,6 +780,9 @@ pub struct Sidebar { /// buttons. This field tracks whether we were using verbose labels so they /// can stay stable after dismissing one of the banners. import_banners_use_verbose_labels: Option, + /// Display names of other release channels that have threads available to + /// import. + cross_channel_import_channels: Vec, } impl Sidebar { @@ -858,6 +861,17 @@ impl Sidebar { ) .detach(); + let channels_with_threads = channels_with_threads(cx); + cx.spawn(async move |this, cx| { + let channels = channels_with_threads.await; + this.update(cx, |this, cx| { + this.cross_channel_import_channels = channels; + cx.notify(); + }) + .ok(); + }) + .detach(); + let deferred_multi_workspace = multi_workspace.downgrade(); cx.defer_in(window, move |this, window, cx| { if let Some(multi_workspace) = deferred_multi_workspace.upgrade() { @@ -898,6 +912,7 @@ impl Sidebar { _subscriptions: Vec::new(), _draft_editor_observations: Vec::new(), import_banners_use_verbose_labels: None, + cross_channel_import_channels: Vec::new(), } } @@ -7199,7 +7214,8 @@ impl Sidebar { } fn should_render_cross_channel_import_onboarding(&self, cx: &App) -> bool { - !CrossChannelImportOnboarding::dismissed(cx) && !channels_with_threads(cx).is_empty() + !CrossChannelImportOnboarding::dismissed(cx) + && !self.cross_channel_import_channels.is_empty() } fn render_cross_channel_import_onboarding( @@ -7207,11 +7223,10 @@ impl Sidebar { verbose_labels: bool, cx: &mut Context, ) -> impl IntoElement { - let channels = channels_with_threads(cx); - let channel_names = channels + let channel_names = self + .cross_channel_import_channels .iter() - .map(|channel| channel.display_name()) - .collect::>() + .map(SharedString::as_str) .join(" and "); let description = format!( diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index a68d827373e5a7..2b33614f29c076 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -2198,6 +2198,7 @@ mod tests { .await; assert!(!errored); + cx.run_until_parked(); let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); multi_workspace