From 0213720d803ce514b5139fbce228a9a14c95fa00 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 16:11:05 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[performance=20improvement]?= =?UTF-8?q?=20Optimize=20space=20sync=20N+1=20requests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract avatar fetching into `get_joined_space_info` and use `futures_util::future::join_all` to concurrently fetch space avatars during initial sync, append, and reset. This resolves an N+1 fetching bottleneck while preserving correct UI rendering order. Co-authored-by: kevinaboos <1139460+kevinaboos@users.noreply.github.com> --- .jules/bolt.md | 3 +++ src/space_service_sync.rs | 33 ++++++++++++++++++++++++--------- src/utils.rs | 5 ++--- 3 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 000000000..c81e60cfa --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-18 - Optimized space sync async N+1 fetching +**Learning:** In space service synchronization, sequential async execution (awaiting each avatar fetch in a loop for `VectorDiff::Append`, `VectorDiff::Reset`, or initial synchronization) caused a significant N+1 query problem that blocked rendering the list until all were complete. +**Action:** Use `futures_util::future::join_all` on an iterator of futures to run asynchronous fetches (like avatar requests) concurrently while resolving sequentially to preserve the correct UI order. diff --git a/src/space_service_sync.rs b/src/space_service_sync.rs index c02bbc8a1..657bc725e 100644 --- a/src/space_service_sync.rs +++ b/src/space_service_sync.rs @@ -3,7 +3,7 @@ use std::{collections::{HashMap, HashSet, hash_map::Entry}, iter::Peekable, sync::Arc}; use eyeball_im::VectorDiff; -use futures_util::StreamExt; +use futures_util::{future::join_all, StreamExt}; use imbl::Vector; use makepad_widgets::*; use matrix_sdk::{Client, RoomState, media::MediaRequestParameters}; @@ -130,9 +130,12 @@ pub async fn space_service_loop(client: Client) -> anyhow::Result<()> { // Get the set of top-level (root) spaces that the user has joined. let (initial_spaces, mut spaces_diff_stream) = space_service.subscribe_to_top_level_joined_spaces().await; - for space in &initial_spaces { - add_new_space(space, &client).await; + + let info_futures = initial_spaces.iter().map(|space| get_joined_space_info(space, &client)); + for jsi in join_all(info_futures).await { + enqueue_spaces_list_update(SpacesListUpdate::AddJoinedSpace(jsi)); } + let mut all_joined_spaces: Vector = initial_spaces; if LOG_SPACE_SERVICE_DIFFS { log!("space_service: initial set: {all_joined_spaces:?}"); } @@ -224,8 +227,13 @@ pub async fn space_service_loop(client: Client) -> anyhow::Result<()> { match diff { VectorDiff::Append { values: new_spaces } => { if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Append {}", new_spaces.len()); } + + let info_futures = new_spaces.iter().map(|space| get_joined_space_info(space, &client)); + for jsi in join_all(info_futures).await { + enqueue_spaces_list_update(SpacesListUpdate::AddJoinedSpace(jsi)); + } + for new_space in new_spaces { - add_new_space(&new_space, &client).await; all_joined_spaces.push_back(new_space); } } @@ -315,9 +323,12 @@ pub async fn space_service_loop(client: Client) -> anyhow::Result<()> { remove_space(&space); } enqueue_spaces_list_update(SpacesListUpdate::ClearSpaces); - for new_space in &new_spaces { - add_new_space(new_space, &client).await; + + let info_futures = new_spaces.iter().map(|space| get_joined_space_info(space, &client)); + for jsi in join_all(info_futures).await { + enqueue_spaces_list_update(SpacesListUpdate::AddJoinedSpace(jsi)); } + all_joined_spaces = new_spaces; } } @@ -334,7 +345,7 @@ pub async fn space_service_loop(client: Client) -> anyhow::Result<()> { } -async fn add_new_space(space: &SpaceRoom, client: &Client) { +async fn get_joined_space_info(space: &SpaceRoom, client: &Client) -> JoinedSpaceInfo { let space_avatar_opt = if let Some(url) = &space.avatar_url { fetch_space_avatar(url.clone(), client) .await @@ -345,7 +356,7 @@ async fn add_new_space(space: &SpaceRoom, client: &Client) { || utils::avatar_from_room_name(Some(&space.display_name)) ); - let jsi = JoinedSpaceInfo { + JoinedSpaceInfo { space_name_id: RoomNameId::new( matrix_sdk::RoomDisplayName::Named(space.display_name.clone()), space.room_id.clone(), @@ -358,7 +369,11 @@ async fn add_new_space(space: &SpaceRoom, client: &Client) { world_readable: space.world_readable, guest_can_join: space.guest_can_join, children_count: space.children_count, - }; + } +} + +async fn add_new_space(space: &SpaceRoom, client: &Client) { + let jsi = get_joined_space_info(space, client).await; enqueue_spaces_list_update(SpacesListUpdate::AddJoinedSpace(jsi)); } diff --git a/src/utils.rs b/src/utils.rs index e4ed4a126..74032767b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -106,7 +106,7 @@ pub fn load_png_or_jpg(img: &ImageRef, cx: &mut Cx, data: &[u8]) -> Result<(), I .or_else(|_| img.load_jpg_from_data(cx, data)) } - let res = match imghdr::from_bytes(data) { + match imghdr::from_bytes(data) { Some(imghdr::Type::Png) => img.load_png_from_data(cx, data), Some(imghdr::Type::Jpeg) => img.load_jpg_from_data(cx, data), Some(unsupported) => { @@ -123,7 +123,7 @@ pub fn load_png_or_jpg(img: &ImageRef, cx: &mut Cx, data: &[u8]) -> Result<(), I ImageError::UnsupportedFormat }) } - }; + } // Disabled: dumping invalid user-selected image bytes can duplicate private or large files. // if let Err(err) = res.as_ref() { // // debugging: dump out the bad image to disk @@ -141,7 +141,6 @@ pub fn load_png_or_jpg(img: &ImageRef, cx: &mut Cx, data: &[u8]) -> Result<(), I // let _ = std::fs::write(path, data) // .inspect_err(|e| error!("Failed to write bad image to disk: {e}")); // } - res }