From 0c7aadb92b0d3b7042ff8658c33df5b0b697cefe Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 1 Nov 2018 16:28:13 -0600 Subject: [PATCH] Improve room list sort performance by caching common variables This won't help much if the user is in a ton of highly active rooms, but for the most part this will help those in thousands of rooms, many of which are likely to be quiet. Fixes https://github.com/vector-im/riot-web/issues/7646 Fixes https://github.com/vector-im/riot-web/issues/7645 (due to timestamp ordering) --- src/stores/RoomListStore.js | 135 ++++++++++++++++++++++++++++++++---- 1 file changed, 120 insertions(+), 15 deletions(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 65148ec8c4..e77debd2f2 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -54,6 +54,7 @@ class RoomListStore extends Store { "im.vector.fake.archived": [], }, ready: false, + roomCache: {}, // roomId => { cacheType => value } }; } @@ -85,6 +86,8 @@ class RoomListStore extends Store { !payload.isLiveUnfilteredRoomTimelineEvent || !this._eventTriggersRecentReorder(payload.event) ) break; + + this._clearCachedRoomState(payload.event.getRoomId()); this._generateRoomLists(); } break; @@ -112,6 +115,8 @@ class RoomListStore extends Store { if (liveTimeline !== eventTimeline || !this._eventTriggersRecentReorder(payload.event) ) break; + + this._clearCachedRoomState(payload.event.getRoomId()); this._generateRoomLists(); } break; @@ -222,12 +227,20 @@ class RoomListStore extends Store { // thousand times. const pinUnread = SettingsStore.getValue("pinUnreadRooms"); const pinMentioned = SettingsStore.getValue("pinMentionedRooms"); + this._timings = {}; Object.keys(lists).forEach((listKey) => { let comparator; switch (RoomListStore._listOrders[listKey]) { case "recent": comparator = (roomA, roomB) => { - return this._recentsComparator(roomA, roomB, pinUnread, pinMentioned); + this._timings["overall_" + roomA.roomId + "_" + roomB.roomId] = { + type: "overall", + start: performance.now(), + end: 0, + }; + const ret = this._recentsComparator(roomA, roomB, pinUnread, pinMentioned); + this._timings["overall_" + roomA.roomId + "_" + roomB.roomId].end = performance.now(); + return ret; }; break; case "manual": @@ -238,12 +251,76 @@ class RoomListStore extends Store { lists[listKey].sort(comparator); }); + // Combine the samples for performance metrics + const samplesByType = {}; + for (const sampleName of Object.keys(this._timings)) { + const sample = this._timings[sampleName]; + if (!samplesByType[sample.type]) samplesByType[sample.type] = { + min: 999999999, + max: 0, + count: 0, + total: 0, + }; + + const record = samplesByType[sample.type]; + const duration = sample.end - sample.start; + if (duration < record.min) record.min = duration; + if (duration > record.max) record.max = duration; + record.count++; + record.total += duration; + } + + for (const category of Object.keys(samplesByType)) { + const {min, max, count, total} = samplesByType[category]; + const average = total / count; + + console.log(`RoomListSortPerf : type=${category} min=${min} max=${max} total=${total} samples=${count} average=${average}`); + } + this._setState({ lists, ready: true, // Ready to receive updates via Room.tags events }); } + _updateCachedRoomState(roomId, type, value) { + const roomCache = this._state.roomCache; + if (!roomCache[roomId]) roomCache[roomId] = {}; + + if (value) roomCache[roomId][type] = value; + else delete roomCache[roomId][type]; + + this._setState({roomCache}); + } + + _clearCachedRoomState(roomId) { + const roomCache = this._state.roomCache; + delete roomCache[roomId]; + this._setState({roomCache}); + } + + _getRoomState(room, type) { + const roomId = room.roomId; + const roomCache = this._state.roomCache; + if (roomCache[roomId] && typeof roomCache[roomId][type] !== 'undefined') { + return roomCache[roomId][type]; + } + + if (type === "timestamp") { + const ts = this._tsOfNewestEvent(room); + this._updateCachedRoomState(roomId, "timestamp", ts); + return ts; + } else if (type === "unread") { + const unread = room.getUnreadNotificationCount() > 0; + this._updateCachedRoomState(roomId, "unread", unread); + return unread; + } else if (type === "notifications") { + const notifs = room.getUnreadNotificationCount("highlight") > 0; + this._updateCachedRoomState(roomId, "notifications", notifs); + return notifs; + } else throw new Error("Unrecognized room cache type: " + type); + } + _eventTriggersRecentReorder(ev) { return ev.getTs() && ( Unread.eventTriggersUnreadCount(ev) || @@ -270,30 +347,58 @@ class RoomListStore extends Store { } _recentsComparator(roomA, roomB, pinUnread, pinMentioned) { + //console.log("Comparing " + roomA.roomId + " with " + roomB.roomId +" || pinUnread=" + pinUnread +" pinMentioned="+pinMentioned); // We try and set the ordering to be Mentioned > Unread > Recent - // assuming the user has the right settings, of course + // assuming the user has the right settings, of course. + + this._timings["timestamp_" + roomA.roomId + "_" + roomB.roomId] = { + type: "timestamp", + start: performance.now(), + end: 0, + }; + const timestampA = this._getRoomState(roomA, "timestamp"); + const timestampB = this._getRoomState(roomB, "timestamp"); + const timestampDiff = timestampB - timestampA; + this._timings["timestamp_" + roomA.roomId + "_" + roomB.roomId].end = performance.now(); if (pinMentioned) { - const mentionsA = roomA.getUnreadNotificationCount("highlight") > 0; - const mentionsB = roomB.getUnreadNotificationCount("highlight") > 0; - if (mentionsA && !mentionsB) return -1; - if (!mentionsA && mentionsB) return 1; - if (mentionsA && mentionsB) return 0; - // If neither have mentions, fall through to remaining checks + this._timings["mentioned_" + roomA.roomId + "_" + roomB.roomId] = { + type: "mentioned", + start: performance.now(), + end: 0, + }; + const mentionsA = this._getRoomState(roomA, "notifications"); + const mentionsB = this._getRoomState(roomB, "notifications"); + this._timings["mentioned_" + roomA.roomId + "_" + roomB.roomId].end = performance.now(); + if (mentionsA && !mentionsB) return -1; + if (!mentionsA && mentionsB) return 1; + + // If they both have notifications, sort by timestamp. + // If neither have notifications (the fourth check not shown + // here), then try and sort by unread messages and finally by + // timestamp. + if (mentionsA && mentionsB) return timestampDiff; } if (pinUnread) { - const unreadA = Unread.doesRoomHaveUnreadMessages(roomA); - const unreadB = Unread.doesRoomHaveUnreadMessages(roomB); + this._timings["unread_" + roomA.roomId + "_" + roomB.roomId] = { + type: "unread", + start: performance.now(), + end: 0, + }; + const unreadA = this._getRoomState(roomA, "unread"); + const unreadB = this._getRoomState(roomB, "notifications"); + this._timings["unread_" + roomA.roomId + "_" + roomB.roomId].end = performance.now(); if (unreadA && !unreadB) return -1; if (!unreadA && unreadB) return 1; - if (unreadA && unreadB) return 0; - // If neither have unread messages, fall through to remaining checks + + // If they both have unread messages, sort by timestamp + // If nether have unread message (the fourth check not shown + // here), then just sort by timestamp anyways. + if (unreadA && unreadB) return timestampDiff; } - // XXX: We could use a cache here and update it when we see new - // events that trigger a reorder - return this._tsOfNewestEvent(roomB) - this._tsOfNewestEvent(roomA); + return timestampDiff; } _lexicographicalComparator(roomA, roomB) {