From 63af4be1e20e4fa0d06cab37255399d62bf75bbc Mon Sep 17 00:00:00 2001 From: miruka Date: Wed, 20 May 2020 03:42:40 -0400 Subject: [PATCH] Defer fetching user profiles for events Previously, events for which the sender, target (state_key) or remover was missing from the room members would have their profile fetched from network when registering the event into models. This could cause very slow past events loading times for rooms, since the event registering function (which contained the profile retrieval directives) is run sequentially event-by-event. Missing profiles are now lazy-loaded when events come into the user's view in the QML timeline. --- TODO.md | 5 +- src/backend/backend.py | 6 +- src/backend/matrix_client.py | 86 ++++++++++++++----- src/backend/models/items.py | 1 + src/backend/nio_callbacks.py | 12 +-- src/gui/Pages/Chat/Timeline/EventDelegate.qml | 9 ++ 6 files changed, 87 insertions(+), 32 deletions(-) diff --git a/TODO.md b/TODO.md index 06c094a9..67e4ff28 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,12 @@ # TODO -- Defer retrieving profiles for events from members not anymore in the room +- add room members loading indicator - fix lag when clicking accounts in the AccountBar with a very long room list - - fix: on startup, if a room's last event is a membership change, it won't be visible in timeline no matter what the user config is - fix: there are rooms without messages on first sync -- avatar loading performance problem? +- fix binding loops? - update docstrings - update flatpak nio required version diff --git a/src/backend/backend.py b/src/backend/backend.py index 57814418..fd81c71d 100644 --- a/src/backend/backend.py +++ b/src/backend/backend.py @@ -221,10 +221,10 @@ class Backend: async def get_profile(self, user_id: str) -> nio.ProfileGetResponse: """Cache and return the matrix profile of `user_id`.""" - if user_id in self.profile_cache: - return self.profile_cache[user_id] - async with self.get_profile_locks[user_id]: + if user_id in self.profile_cache: + return self.profile_cache[user_id] + client = await self.get_any_client() response = await client.get_profile(user_id) diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index e2e44bde..bf457c80 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -1283,49 +1283,89 @@ class MatrixClient(nio.AsyncClient): HTML.rooms_user_id_names[room.room_id].pop(user_id, None) - async def get_member_name_avatar( - self, room_id: str, user_id: str, - ) -> Tuple[str, str]: - """Return a room member's display name and avatar. + async def get_event_profiles(self, room_id: str, event_id: str) -> None: + """Fetch from network an event's sender, target and remover's profile. - If the member isn't found in the room (e.g. they left), their + This should be called from QML, see `MatrixClient.get_member_profile`'s + docstring. + """ + + ev: Event = self.models[self.user_id, room_id, "events"][event_id] + + if not ev.fetch_profile: + return + + get_profile = partial( + self.get_member_profile, room_id, can_fetch_from_network=True, + ) + + if not ev.sender_name and not ev.sender_avatar: + sender_name, sender_avatar, _ = await get_profile(ev.sender_id) + ev.sender_name = sender_name + ev.sender_avatar = sender_avatar + + if ev.target_id and not ev.target_name and not ev.target_avatar: + target_name, target_avatar, _ = await get_profile(ev.target_id) + ev.target_name = target_name + ev.target_avatar = target_avatar + + if ev.redacter_id and not ev.redacter_name: + redacter_name, _, _ = await get_profile(ev.target_id) + ev.redacter_name = redacter_name + + ev.fetch_profile = False + + + async def get_member_profile( + self, room_id: str, user_id: str, can_fetch_from_network: bool = False, + ) -> Tuple[str, str, bool]: + """Return a room member's (display_name, avatar, should_lazy_fetch) + + The returned tuple's last element tells whether + `MatrixClient.get_event_profiles()` should be called by QML + with `can_fetch_from_network = True` when appropriate, + e.g. when this message comes in the user's view. + + If the member isn't found in the room (e.g. they left) and + `can_fetch_from_network` is `True`, their profile is retrieved using `MatrixClient.backend.get_profile()`. """ try: - item = self.models[self.user_id, room_id, "members"][user_id] + member = self.models[self.user_id, room_id, "members"][user_id] + return (member.display_name, member.avatar_url, False) + + except KeyError: # e.g. member is not in the room anymore + if not can_fetch_from_network: + return ("", "", True) - except KeyError: # e.g. user is not anymore in the room try: info = await self.backend.get_profile(user_id) - return (info.displayname or "", info.avatar_url or "") - + return (info.displayname or "", info.avatar_url or "", False) except MatrixError: - return ("", "") - - else: - return (item.display_name, item.avatar_url) + return ("", "", False) async def register_nio_event( self, - room: nio.MatrixRoom, - ev: nio.Event, - event_id: str = "", + room: nio.MatrixRoom, + ev: nio.Event, + event_id: str = "", + override_fetch_profile: Optional[bool] = None, **fields, ) -> None: """Register a `nio.Event` as a `Event` object in our model.""" await self.register_nio_room(room) - sender_name, sender_avatar = \ - await self.get_member_name_avatar(room.room_id, ev.sender) + sender_name, sender_avatar, must_fetch_sender = \ + await self.get_member_profile(room.room_id, ev.sender) target_id = getattr(ev, "state_key", "") or "" - target_name, target_avatar = \ - await self.get_member_name_avatar(room.room_id, target_id) \ - if target_id else ("", "") + target_name, target_avatar, must_fetch_target = \ + await self.get_member_profile(room.room_id, target_id) \ + if target_id else ("", "", False) content = fields.get("content", "").strip() @@ -1349,6 +1389,10 @@ class MatrixClient(nio.AsyncClient): target_name = target_name, target_avatar = target_avatar, links = Event.parse_links(content), + fetch_profile = + (must_fetch_sender or must_fetch_target) + if override_fetch_profile is None else + override_fetch_profile, **fields, ) diff --git a/src/backend/models/items.py b/src/backend/models/items.py index 6bd16c62..665454dd 100644 --- a/src/backend/models/items.py +++ b/src/backend/models/items.py @@ -217,6 +217,7 @@ class Event(ModelItem): sender_id: str = field() sender_name: str = field() sender_avatar: str = field() + fetch_profile: bool = False content: str = "" inline_content: str = "" diff --git a/src/backend/nio_callbacks.py b/src/backend/nio_callbacks.py index 0bb838a9..d2808383 100644 --- a/src/backend/nio_callbacks.py +++ b/src/backend/nio_callbacks.py @@ -196,6 +196,10 @@ class NioCallbacks: async def onRedactedEvent(self, room, ev, event_id: str = "") -> None: + redacter_name, _, must_fetch_redacter = \ + await self.client.get_member_profile(room.room_id, ev.redacter) \ + if ev.redacter else ("", "", False) + await self.client.register_nio_event( room, ev, @@ -206,11 +210,9 @@ class NioCallbacks: type(ev), ev.redacter, ev.sender, ev.reason, ), - redacter_id = ev.redacter or "", - redacter_name = - (await self.client.get_member_name_avatar( - room.room_id, ev.redacter, - ))[0] if ev.redacter else "", + redacter_id = ev.redacter or "", + redacter_name = redacter_name, + override_fetch_profile = True, ) diff --git a/src/gui/Pages/Chat/Timeline/EventDelegate.qml b/src/gui/Pages/Chat/Timeline/EventDelegate.qml index e50e1b5f..17c6ee78 100644 --- a/src/gui/Pages/Chat/Timeline/EventDelegate.qml +++ b/src/gui/Pages/Chat/Timeline/EventDelegate.qml @@ -17,6 +17,8 @@ HColumnLayout { property var hoveredMediaTypeUrl: [] + property var fetchProfilesFuture: null + // Remember timeline goes from newest message at index 0 to oldest readonly property var previousModel: eventList.model.get(model.index + 1) readonly property var nextModel: eventList.model.get(model.index - 1) @@ -64,6 +66,13 @@ HColumnLayout { // HSelectableLabel's MouseArea hover events onCursorShapeChanged: eventList.cursorShape = cursorShape + Component.onCompleted: if (model.fetch_profile) py.callClientCoro( + chat.userId, "get_event_profiles", [chat.roomId, model.id], + ) + + Component.onDestruction: + if (fetchProfilesFuture) fetchProfilesFuture.cancel() + function json() { let event = ModelStore.get(chat.userId, chat.roomId, "events")