Skip to content

Conversation

ZhangHanDong
Copy link
Contributor

@ZhangHanDong ZhangHanDong commented Jul 11, 2025

Fixed issue #452.

The @mention search feature was iterating over the entire members list on the main UI thread, causing significant performance issues and UI freezes, especially in rooms with many members.

Changes

  1. Moved search to background thread
  • Implemented async search using tokio::task::spawn_blocking
  • Added streaming/partial results support for better responsiveness
  • First match is sent immediately for instant feedback
  1. Refactored search logic into separate module
  • Created new src/room/member_search.rs module
  • Moved search functions from sliding_sync.rs for better code organization
  • Improved separation of concerns between protocol handling and business logic

Performance Improvements

  • Search no longer blocks the UI thread
  • Results stream in batches for smoother experience
  • Loading animations remain responsive during search

@ZhangHanDong ZhangHanDong requested a review from kevinaboos July 11, 2025 16:06
@kevinaboos kevinaboos added the waiting-on-review This issue is waiting to be reviewed label Jul 11, 2025
Copy link
Member

@kevinaboos kevinaboos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Alex, the code looks quite good in general!

I have only one major suggestion, which addresses the problem that there is no way to cancel a member search that's ongoing in the background.

Instead of using Makepad actions to send the results from the background task to the UI thread (which requires a search_id), we should just use a direct channel to communicate between them. This design has a few benefits:

  1. No need for a search_id: just create the channel in the MentionableTextInput code and include the sender side of the channel in the MatrixRequest:: SearchRoomMembers request.
    • The MentionableTextInput will hold on to the receiver side of the channel and check it for updates. You could certainly use a dedicated action to tell the MentionableTextInput widget to check its channel for updates; that would be better than using Event::Signal like we typically do.
  2. This allows a SearchRoomMembers request to be aborted/canceled very easily. We only have to drop the receiver side in the MentionableTextInput, which will then cause any sender.send() operation in the background thread to fail.
    • When send() fails, we can stop searching and return from the search_room_members_streaming function, as that would indicate that the UI side is no longer listening for updates.
  3. Reduces the quantity and the size of actions that are sent through Makepad's event system, which itself reduces the amount of memory allocations needed to store those larger action types.

@kevinaboos kevinaboos added waiting-on-author This issue is waiting on the original author for a response and removed waiting-on-review This issue is waiting to be reviewed labels Jul 22, 2025
@ZhangHanDong
Copy link
Contributor Author

Further optimizations have been made:

  1. Used channel communication instead of makepad actions.
  2. Selected the appropriate data structure, BinaryHeap, to prioritize the best matching results for user mentions.
  3. Utilized indices to avoid copying RoomMember objects in the search results.

@ZhangHanDong ZhangHanDong requested a review from kevinaboos August 1, 2025 05:53
@ZhangHanDong ZhangHanDong added waiting-on-review This issue is waiting to be reviewed and removed waiting-on-author This issue is waiting on the original author for a response labels Aug 1, 2025
Copy link
Contributor

@alanpoon alanpoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZhangHanDong#1

I have created a PR request after some amendments.

Comment on lines 1001 to 1007
submit_async_request(MatrixRequest::SearchRoomMembers {
room_id: room_props.room_id.clone(),
search_text: search_text.to_string(),
sender,
max_results: max_visible_items * SEARCH_BUFFER_MULTIPLIER,
cached_members: cached_members.clone(),
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use cx.spawn, instead of using submit_async_request. Also there are two such calls duplicated in check_search_channel, user_update_list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s strange—it seems like you’re looking at the old code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use cx.spawn? Does this method exist?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"cx.spawn" seems to be changed to cx.spawn_thread.

  1. There is no matrix Request to be made for submit_async_request(MatrixRequest::SearchRoomMembers).
  2. It is also not using room's timelineUpdate sender.
  3. "search_room_members_streaming" is not an async function anyway. Hence there is no need for tokio.
  4. It is a just a thread that does data processing. " cx.spawn_thread" should work.

Copy link
Contributor Author

@ZhangHanDong ZhangHanDong Aug 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. you're right. Fixed it.

However, directly using cx.spawn_local here is not the best solution; relying on tokio is necessary.

Directly using cx.spawn_local is not the optimal approach in this context. It’s still necessary to rely on tokio, because tokio manages spawn_blocking threads and task cancellation more effectively than Makepad’s built-in threading. Of course, I won’t continue using submit_async_request(MatrixRequest::SearchRoomMembers) here, to avoid introducing logical ambiguity.

@ZhangHanDong
Copy link
Contributor Author

ZhangHanDong commented Aug 1, 2025

ZhangHanDong#1

I have created a PR request after some amendments.

The code in your PR seems to be outdated.None of them are based on my latest code.The issue you mentioned does not exist in the current code.

@ZhangHanDong
Copy link
Contributor Author

May I ask what issue you encountered during testing? Please comment here directly instead of submitting a PR, as the code will conflict. @alanpoon

Comment on lines 1001 to 1007
submit_async_request(MatrixRequest::SearchRoomMembers {
room_id: room_props.room_id.clone(),
search_text: search_text.to_string(),
sender,
max_results: max_visible_items * SEARCH_BUFFER_MULTIPLIER,
cached_members: cached_members.clone(),
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"cx.spawn" seems to be changed to cx.spawn_thread.

  1. There is no matrix Request to be made for submit_async_request(MatrixRequest::SearchRoomMembers).
  2. It is also not using room's timelineUpdate sender.
  3. "search_room_members_streaming" is not an async function anyway. Hence there is no need for tokio.
  4. It is a just a thread that does data processing. " cx.spawn_thread" should work.

@ZhangHanDong
Copy link
Contributor Author

ZhangHanDong commented Aug 2, 2025

Further optimizations:

  1. Changed ‎SearchResult to ‎Vec<usize> to reduce ‎String cloning.
  2. Added a ‎BackgroundRequest enum to unify ‎MatrixRequest and ‎AsyncJob. This is because there are currently local async tasks that don’t need to interact with the matrix server, but we still want to reuse the tokio runtime for better thread management instead of using ‎cx.spawn_thread. The new ‎AsyncJob variant handles these local async tasks.

@ZhangHanDong ZhangHanDong requested a review from alanpoon August 2, 2025 08:02
@alanpoon
Copy link
Contributor

alanpoon commented Aug 4, 2025

Further optimizations:

  1. Changed ‎SearchResult to ‎Vec<usize> to reduce ‎String cloning.
  2. Added a ‎BackgroundRequest enum to unify ‎MatrixRequest and ‎AsyncJob. This is because there are currently local async tasks that don’t need to interact with the matrix server, but we still want to reuse the tokio runtime for better thread management instead of using ‎cx.spawn_thread. The new ‎AsyncJob variant handles these local async tasks.

Given that the Alvin's logout PR will do a Tokio::runtime::shutdown_background. So if there is a possible panic when there is any tokio thread running, searching for member and when the user logouts at the same time.
What is the better thread management in tokio?

Copy link
Contributor

@alanpoon alanpoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check with Kevin on Element before making big changes to submit_async_request.

if self.is_searching {
let search_text = self.cmd_text_input.search_text();
self.update_user_list(cx, &search_text, scope);
}
} else if self.cmd_text_input.view(id!(popup)).visible() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Should handle the actions that hide or show the popup instead of checking the visibility of the popup on any actions.
  2. Need to CommandTextInput's public functions, rather than querying its inner widgets. Open a discussion on why the existing CommandTextInput's public functions are insufficient.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, the CommandTextInput component doesn’t have any relevant public methods. I can add a TODO comment here, but I don’t want this PR to be blocked by Makepad.

if should_update_ui && !self.accumulated_results.is_empty() {
// Received search results, updating UI...
// Hide loading and show results immediately
self.members_loading = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is weird to set self.members_loading to be false, based on the non-empty accumulated_results. self.show_loading_indicator(cx), never seems to be executed. I would imagine comparing room_props.room_members.len with room.active_members_count() to determine self.members_loading to be true / false.

Comment on lines 905 to 907
if self.accumulated_results.is_empty() {
// Clear results before showing empty state
self.accumulated_results.clear();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you want to clear an empty array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it looks like some redundant code wasn’t completely removed.

self.cmd_text_input.text_input_ref().set_key_focus(cx);
if self.members_loading {
// Show loading indicator while fetching members
self.show_loading_indicator(cx);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"self.members_loading" seems to be always false. self.show_loading_indicator(cx), does not seem to be executed.

let mut all_results = Vec::new();
let mut sent_count = 0;

for (index, member) in members.iter().enumerate() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"members" is Arc<Vec>. This means that length of room member list is dynamic, mutated by matrix sdk. This function search_room_members_streaming is called once. Which means that the mentionable user list only contain the room members existed at that point of time.

I am expecting a while loop with perhaps a time interval / room member sync event to access the Arc<Vec> until the accumulated length == room.active_member_count. Call CommandTextInput.add_item when room members are ready and fit the search criteria.

Also use slice member[i..j] instead of enumerate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current approach uses a member cache fetched from the Matrix server during room screen initialization, which is sufficient for most use cases and avoids the complexity of real-time synchronization. This PR focuses on delivering MVP functionality; further optimizations can be addressed later to avoid premature optimization. For now, when a user reopens a room, the latest member list is fetched from the server.

Comment on lines 678 to 685
popup.set_visible(cx, true);
self.cmd_text_input.text_input_ref().set_key_focus(cx);
} else {
// Show "no matches" if we're searching and not loading
self.show_no_matches_indicator(cx);
popup.set_visible(cx, true);
self.cmd_text_input.text_input_ref().set_key_focus(cx);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use CommandTextInput's public functions instead of manually accessing child widgets.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s leave it for a future PR—I’ve already added a TODO.

self.search_receiver = None;

// If we still have loading indicator, clear it
if self.members_loading {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confusing checks before state reset. Move the following code to separate functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored.

Comment on lines 940 to 942
// Clone the indices to avoid borrowing issues
let member_indices = self.accumulated_results.clone();
let user_items_added = self.add_user_mention_items(cx, &member_indices, user_items_limit, is_desktop, room_props);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just access self.accumulated_results inside add_user_mention_items? It is not multiple threaded. It is also not Arc<Mutex<Vec>>, where you need lock to avoid data race. If that is what you mean by borrowing issue.

Copy link
Contributor Author

@ZhangHanDong ZhangHanDong Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because there was a compilation error before.This section of the code has now been refactored.

self.accumulated_results.clear();

// Check if we have cached members
let has_members = matches!(&room_props.room_members, Some(members) if !members.is_empty());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, has_members may not be enough, room_members is a Arc<Vec>. May not be fully synced at this point of time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refer to the previous explanation.

Comment on lines 989 to 995
self.members_loading = false;

let popup = self.cmd_text_input.view(id!(popup));
let header_view = self.cmd_text_input.view(id!(popup.header_view));
header_view.set_visible(cx, true);
popup.set_visible(cx, true);
self.cmd_text_input.text_input_ref().set_key_focus(cx);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use CommandTextInput's public functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refer to the previous explanation.

@ZhangHanDong ZhangHanDong added waiting-on-author This issue is waiting on the original author for a response and removed waiting-on-review This issue is waiting to be reviewed labels Aug 5, 2025
@ZhangHanDong
Copy link
Contributor Author

ZhangHanDong commented Aug 5, 2025

Please check with Kevin on Element before making big changes to submit_async_request.

This isn’t a major change—it just reuses submit_async_request under a new name to handle background tasks, avoiding any semantic confusion that it’s sending messages to Matrix. There are no changes to submit_async_request itself.

@kevinaboos kevinaboos removed their request for review August 5, 2025 20:04
@kevinaboos
Copy link
Member

Please check with Kevin on Element before making big changes to submit_async_request.

This isn’t a major change—it just reuses submit_async_request under a new name to handle background tasks, avoiding any semantic confusion that it’s sending messages to Matrix. There are no changes to submit_async_request itself.

I don't disagree with the name change, but let's do it in a separate PR. There are various considerations here, and I don't think there's much of a compelling reason to differentiate between Matrix vs. non-Matrix background requests, especially if they're handled by the same worker.

For now, you can just move the SearchRoomMembers variant into the existing MatrixRequest enum, which is especially convenient since there is only one type of AsyncJob right now. We can refactor the large MatrixRequest enum later when it is actually required and/or beneficial to do so.

In my TSP support PR (#557), I've created a completely separate worker handler and request sender channel for this purpose, so it's probably best for us to consolidate these later, such that we have one Matrix-specific async worker, and one general async worker for everything else. But doing it in a separate PR will make everything easier to review — there's no need for 1500 lines of unrelated minor changes to sliding_sync in this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
waiting-on-author This issue is waiting on the original author for a response
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants