Skip to content

Commit 77a67de

Browse files
committed
fix(ui): Fix performance of ReadReceiptTimelineUpdate::apply.
This patch improves the performance of `ReadReceiptTimelineUpdate::apply`, which does 2 things: it calls `remove_old_receipt` and `add_new_receipt`. Both of them need an timeline item position. Until this patch, `rfind_event_by_id` was used and was the bottleneck. The improvement is twofold as is as follows. First off, when collecting data to create `ReadReceiptTimelineUpdate`, the timeline item position can be known ahead of time by using `EventMeta::timeline_item_index`. This data is not always available, for example if the timeline item isn't created yet. But let's try to collect these data if there are some. Second, inside `ReadReceiptTimelineUpdate::remove_old_receipt`, we use the timeline item position collected from `EventMeta` if it exists. Otherwise, let's fallback to a similar `rfind_event_by_id` pattern, without using intermediate types. It's more straightforward here: we don't need an `EventTimelineItemWithId`, we only need the position. Once the position is known, it is stored in `Self` (!), this is the biggest improvement here. Le't see why. Finally, inside `ReadReceiptTimelineUpdate::add_new_receipt`, we use the timeline item position collected from `EventMeta` if it exists, similarly to what `remove_old_receipt` does. Otherwise, let's fallback to an iterator to find the position. However, instead of iterating over **all** items, we can skip the first ones, up to the position of the timeline item holding the old receipt, so up to the position found by `remove_old_receipt`. I'm testing this patch with the `test_lazy_back_pagination` test in matrix-org#4594. With 10_000 events in the sync, the `ReadReceipts::maybe_update_read_receipt` method was taking 52% of the whole execution time. With this patch, it takes 8.1%.
1 parent f27eb4d commit 77a67de

File tree

1 file changed

+73
-14
lines changed

1 file changed

+73
-14
lines changed

crates/matrix-sdk-ui/src/timeline/controller/read_receipts.rs

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,34 +101,43 @@ impl ReadReceipts {
101101

102102
// Get old receipt.
103103
let old_receipt = self.get_latest(new_receipt.user_id, &new_receipt.receipt_type);
104+
104105
if old_receipt
105106
.as_ref()
106107
.is_some_and(|(old_receipt_event_id, _)| old_receipt_event_id == new_receipt.event_id)
107108
{
108109
// The receipt has not changed so there is nothing to do.
109110
return;
110111
}
112+
111113
let old_event_id = old_receipt.map(|(event_id, _)| event_id);
112114

113115
// Find receipts positions.
114116
let mut old_receipt_pos = None;
117+
let mut old_item_pos = None;
115118
let mut old_item_event_id = None;
116119
let mut new_receipt_pos = None;
120+
let mut new_item_pos = None;
117121
let mut new_item_event_id = None;
122+
118123
for (pos, event) in all_events.iter().rev().enumerate() {
119-
if old_event_id == Some(&event.event_id) {
124+
if old_receipt_pos.is_none() && old_event_id == Some(&event.event_id) {
120125
old_receipt_pos = Some(pos);
121126
}
127+
122128
// The receipt should appear on the first event that is visible.
123129
if old_receipt_pos.is_some() && old_item_event_id.is_none() && event.visible {
130+
old_item_pos = event.timeline_item_index;
124131
old_item_event_id = Some(event.event_id.clone());
125132
}
126133

127-
if new_receipt.event_id == event.event_id {
134+
if new_receipt_pos.is_none() && new_receipt.event_id == event.event_id {
128135
new_receipt_pos = Some(pos);
129136
}
137+
130138
// The receipt should appear on the first event that is visible.
131139
if new_receipt_pos.is_some() && new_item_event_id.is_none() && event.visible {
140+
new_item_pos = event.timeline_item_index;
132141
new_item_event_id = Some(event.event_id.clone());
133142
}
134143

@@ -166,6 +175,7 @@ impl ReadReceipts {
166175
if let Some(old_event_id) = old_event_id.cloned() {
167176
self.remove_event_receipt_for_user(&old_event_id, new_receipt.user_id);
168177
}
178+
169179
// Add the new receipt to the new event.
170180
self.add_event_receipt_for_user(
171181
new_receipt.event_id.to_owned(),
@@ -193,9 +203,12 @@ impl ReadReceipts {
193203
}
194204

195205
let timeline_update = ReadReceiptTimelineUpdate {
206+
old_item_pos,
196207
old_event_id: old_item_event_id,
208+
new_item_pos,
197209
new_event_id: new_item_event_id,
198210
};
211+
199212
timeline_update.apply(
200213
timeline_items,
201214
new_receipt.user_id.to_owned(),
@@ -273,27 +286,51 @@ struct FullReceipt<'a> {
273286
/// A read receipt update in the timeline.
274287
#[derive(Clone, Debug, Default)]
275288
struct ReadReceiptTimelineUpdate {
289+
/// The position of the timeline item that had the old receipt of the user,
290+
/// if any.
291+
old_item_pos: Option<usize>,
276292
/// The old event that had the receipt of the user, if any.
277293
old_event_id: Option<OwnedEventId>,
294+
/// The position of the timeline item that has the new receipt of the user,
295+
/// if any.
296+
new_item_pos: Option<usize>,
278297
/// The new event that has the receipt of the user, if any.
279298
new_event_id: Option<OwnedEventId>,
280299
}
281300

282301
impl ReadReceiptTimelineUpdate {
283302
/// Remove the old receipt from the corresponding timeline item.
284-
fn remove_old_receipt(&self, items: &mut ObservableItemsTransaction<'_>, user_id: &UserId) {
303+
fn remove_old_receipt(&mut self, items: &mut ObservableItemsTransaction<'_>, user_id: &UserId) {
285304
let Some(event_id) = &self.old_event_id else {
286305
// Nothing to do.
287306
return;
288307
};
289308

290-
let Some((receipt_pos, event_item)) = rfind_event_by_id(items, event_id) else {
309+
let item_pos = self.old_item_pos.or_else(|| {
310+
items
311+
.iter()
312+
.enumerate()
313+
.rev()
314+
.filter_map(|(nth, item)| Some((nth, item.as_event()?)))
315+
.find_map(|(nth, event_item)| {
316+
(event_item.event_id() == Some(event_id)).then_some(nth)
317+
})
318+
});
319+
320+
let Some(item_pos) = item_pos else {
291321
debug!(%event_id, %user_id, "inconsistent state: old event item for read receipt was not found");
292322
return;
293323
};
294324

295-
let event_item_id = event_item.internal_id.to_owned();
296-
let mut event_item = event_item.clone();
325+
self.old_item_pos = Some(item_pos);
326+
327+
let event_item = &items[item_pos];
328+
let event_item_id = event_item.unique_id().to_owned();
329+
330+
let Some(mut event_item) = event_item.as_event().cloned() else {
331+
warn!("received a read receipt for a virtual item, this should not be possible");
332+
return;
333+
};
297334

298335
if let Some(remote_event_item) = event_item.as_remote_mut() {
299336
if remote_event_item.read_receipts.swap_remove(user_id).is_none() {
@@ -303,7 +340,7 @@ impl ReadReceiptTimelineUpdate {
303340
receipt doesn't have a receipt for the user"
304341
);
305342
}
306-
items.replace(receipt_pos, TimelineItem::new(event_item, event_item_id));
343+
items.replace(item_pos, TimelineItem::new(event_item, event_item_id));
307344
} else {
308345
warn!("received a read receipt for a local item, this should not be possible");
309346
}
@@ -321,26 +358,48 @@ impl ReadReceiptTimelineUpdate {
321358
return;
322359
};
323360

324-
let Some((receipt_pos, event_item)) = rfind_event_by_id(items, &event_id) else {
325-
// This can happen for new timeline items, the receipts will be loaded directly
326-
// during construction of the item.
361+
let item_pos = self.new_item_pos.or_else(|| {
362+
items
363+
.iter()
364+
.enumerate()
365+
// Don't iterate over all items if the `old_item_pos` is known: the `item_pos`
366+
// for the new item is necessarily _after_ the old item.
367+
.skip(self.old_item_pos.unwrap_or(0))
368+
.rev()
369+
.filter_map(|(nth, item)| Some((nth, item.as_event()?)))
370+
.find_map(|(nth, event_item)| {
371+
(event_item.event_id() == Some(&event_id)).then_some(nth)
372+
})
373+
});
374+
375+
let Some(item_pos) = item_pos else {
376+
debug!(%event_id, %user_id, "inconsistent state: new event item for read receipt was not found");
327377
return;
328378
};
329379

330-
let event_item_id = event_item.internal_id.to_owned();
331-
let mut event_item = event_item.clone();
380+
debug_assert!(
381+
item_pos >= self.old_item_pos.unwrap_or(0),
382+
"The new receipt must be added on a timeline item that is _after_ the timeline item that was holding the old receipt");
383+
384+
let event_item = &items[item_pos];
385+
let event_item_id = event_item.unique_id().to_owned();
386+
387+
let Some(mut event_item) = event_item.as_event().cloned() else {
388+
warn!("received a read receipt for a virtual item, this should not be possible");
389+
return;
390+
};
332391

333392
if let Some(remote_event_item) = event_item.as_remote_mut() {
334393
remote_event_item.read_receipts.insert(user_id, receipt);
335-
items.replace(receipt_pos, TimelineItem::new(event_item, event_item_id));
394+
items.replace(item_pos, TimelineItem::new(event_item, event_item_id));
336395
} else {
337396
warn!("received a read receipt for a local item, this should not be possible");
338397
}
339398
}
340399

341400
/// Apply this update to the timeline.
342401
fn apply(
343-
self,
402+
mut self,
344403
items: &mut ObservableItemsTransaction<'_>,
345404
user_id: OwnedUserId,
346405
receipt: Receipt,

0 commit comments

Comments
 (0)