atagen

matrix-rust-sdk

public
Star0
Watch0

Commit b9c68844ca91

tarball

atagen <boss@atagen.co>2026-10-28 07:47 UTC

3 files changed+102-15
event-cache: hold off rooms nobody views while a room restores or fills

A room opened during a big catch-up waited 2.9s on its restore, its IndexedDB reads queued behind four other rooms' cache creation and sync handling at a time, 470 rooms of it. A restore, recent load or gap fill now keeps sync updates for unviewed rooms waiting until it finishes, for 3s at most per stretch, while rooms on screen still go first.
Modified file: crates/matrix-sdk/src/event_cache/caches/room/history.rs+7 additions
@@ -150,6 +150,13 @@ async fn coordinate_history(
150150 };
151151 let cache = RoomEventCache { inner };
152152 let recent = matches!(operation, HistoryOperation::Recent);
153+ let _background = matches!(
154+ operation,
155+ HistoryOperation::Restore | HistoryOperation::Recent | HistoryOperation::Fill(_)
156+ )
157+ .then(|| cache.inner.weak_room.get())
158+ .flatten()
159+ .map(|room| room.client().event_cache().hold_background());
153160 let name = operation.name();
154161 tracing::debug!(room_id = %cache.room_id(), operation = name, "history operation started");
155162 let mut waiters = vec![reply];
Modified file: crates/matrix-sdk/src/event_cache/mod.rs+56 additions−13 deletions
@@ -48,7 +48,7 @@ use matrix_sdk_base::{
4848 timer,
4949 };
5050 use matrix_sdk_common::timeout::timeout;
51-use ruma::{OwnedRoomId, RoomId};
51+use ruma::{OwnedRoomId, RoomId, time::Instant};
5252 use tokio::sync::{
5353 Mutex, RwLock,
5454 broadcast::{Receiver, Sender, channel},
@@ -248,6 +248,7 @@ impl EventCache {
248248 clears: Default::default(),
249249 dropped_room_updates: Default::default(),
250250 room_updates_handled: watch::Sender::new(0),
251+ background_holds: Default::default(),
251252 by_room: Default::default(),
252253 drop_handles: Default::default(),
253254 auto_shrink_sender: Default::default(),
@@ -395,6 +396,17 @@ impl EventCache {
395396 self.inner.dropped_room_updates.load(Ordering::SeqCst)
396397 }
397398
399+ /// Holds off sync updates for rooms nobody views until dropped, for
400+ /// [`BACKGROUND_HOLD`] at most, so a room's history work isn't queued
401+ /// behind a catch-up's storage traffic.
402+ pub(crate) fn hold_background(&self) -> BackgroundHold {
403+ self.inner.background_holds.send_modify(|holds| {
404+ holds.count += 1;
405+ holds.since.get_or_insert_with(Instant::now);
406+ });
407+ BackgroundHold { inner: self.inner.clone() }
408+ }
409+
398410 /// Check whether [`EventCache::subscribe`] has been called.
399411 pub fn has_subscribed(&self) -> bool {
400412 self.inner.drop_handles.get().is_some()
@@ -539,6 +551,9 @@ struct EventCacheInner {
539551 /// the client's `room_updates_sent` terms.
540552 room_updates_handled: watch::Sender<u64>,
541553
554+ /// History work in progress, holding off rooms nobody views.
555+ background_holds: watch::Sender<BackgroundHolds>,
556+
542557 /// Lazily-filled cache of live [`RoomEventCache`], once per room.
543558 //
544559 // It's behind an `Arc` to get owned locks.
@@ -604,6 +619,34 @@ const ROOM_UPDATES_AT_ONCE: usize = 1;
604619 #[cfg(target_family = "wasm")]
605620 const ROOM_UPDATES_AT_ONCE: usize = 4;
606621
622+/// How long a room's history work can hold off sync updates for rooms nobody
623+/// views. Opening a room during a 470 room catch-up otherwise queued its
624+/// restore's IndexedDB reads behind every other room's, for 2.9s.
625+const BACKGROUND_HOLD: Duration = Duration::from_secs(3);
626+
627+#[derive(Debug, Default)]
628+struct BackgroundHolds {
629+ count: usize,
630+ since: Option<Instant>,
631+}
632+
633+/// Holds off sync updates for rooms nobody views while alive, see
634+/// [`EventCache::hold_background`].
635+pub(crate) struct BackgroundHold {
636+ inner: Arc<EventCacheInner>,
637+}
638+
639+impl Drop for BackgroundHold {
640+ fn drop(&mut self) {
641+ self.inner.background_holds.send_modify(|holds| {
642+ holds.count -= 1;
643+ if holds.count == 0 {
644+ holds.since = None;
645+ }
646+ });
647+ }
648+}
649+
607650 /// One room's part of a [`RoomUpdates`].
608651 enum RoomUpdate {
609652 Left(LeftRoomUpdate),
@@ -736,19 +779,19 @@ impl EventCacheInner {
736779
737780 /// Handle the next few rooms of `pending`, those on screen first, with the
738781 /// `multiple_room_updates_lock` held.
782+ async fn viewed_pending(&self, pending: &PendingRoomUpdates) -> Vec<OwnedRoomId> {
783+ let by_room = self.by_room.read().await;
784+ pending
785+ .rooms
786+ .keys()
787+ .filter(|room_id| by_room.get(*room_id).is_some_and(|caches| caches.room.is_viewed()))
788+ .take(ROOM_UPDATES_AT_ONCE)
789+ .cloned()
790+ .collect()
791+ }
792+
739793 async fn handle_pending_room_updates(&self, pending: &mut PendingRoomUpdates) {
740- let viewed: Vec<OwnedRoomId> = {
741- let by_room = self.by_room.read().await;
742- pending
743- .rooms
744- .keys()
745- .filter(|room_id| {
746- by_room.get(*room_id).is_some_and(|caches| caches.room.is_viewed())
747- })
748- .take(ROOM_UPDATES_AT_ONCE)
749- .cloned()
750- .collect()
751- };
794+ let viewed = self.viewed_pending(pending).await;
752795 // rooms on screen get a round of their own, so a slow cold room can't hold them back
753796 let round: Vec<OwnedRoomId> = if viewed.is_empty() {
754797 pending.rooms.keys().take(ROOM_UPDATES_AT_ONCE).cloned().collect()
Modified file: crates/matrix-sdk/src/event_cache/tasks.rs+39 additions−2 deletions
@@ -20,7 +20,7 @@ use std::{
2020 use eyeball::Subscriber;
2121 use matrix_sdk_base::{
2222 linked_chunk::OwnedLinkedChunkId, serde_helpers::extract_thread_root_from_content,
23- sync::RoomUpdates,
23+ sleep::sleep, sync::RoomUpdates,
2424 };
2525 use ruma::{OwnedEventId, OwnedTransactionId};
2626 use tokio::{
@@ -36,7 +36,7 @@ use tokio::{
3636 use tracing::{Instrument as _, Span, debug, error, info, info_span, instrument, trace, warn};
3737
3838 use super::{
39- AutoShrinkChannelPayload, EventCacheInner, EventsOrigin, PendingRoomUpdates,
39+ AutoShrinkChannelPayload, BACKGROUND_HOLD, EventCacheInner, EventsOrigin, PendingRoomUpdates,
4040 RoomEventCacheLinkedChunkUpdate, RoomEventCacheUpdate, TimelineVectorDiffs,
4141 };
4242 use crate::{
@@ -78,6 +78,10 @@ pub(super) async fn room_updates_task(
7878 }
7979 }
8080
81+ if inner.viewed_pending(&pending).await.is_empty() {
82+ wait_out_background_hold(&inner, &mut room_updates_feed, &mut pending).await;
83+ }
84+
8185 {
8286 let _lock = inner.multiple_room_updates_lock.lock().await;
8387 inner.handle_pending_room_updates(&mut pending).await;
@@ -102,6 +106,39 @@ pub(super) async fn room_updates_task(
102106 }
103107 }
104108
109+/// Wait while a room's history work holds off rooms nobody views, until it
110+/// ends, [`BACKGROUND_HOLD`] passes, or an update for a viewed room arrives.
111+async fn wait_out_background_hold(
112+ inner: &EventCacheInner,
113+ feed: &mut Receiver<RoomUpdates>,
114+ pending: &mut PendingRoomUpdates,
115+) {
116+ let mut holds = inner.background_holds.subscribe();
117+ loop {
118+ let since = holds.borrow_and_update().since;
119+ let Some(left) = since.and_then(|since| BACKGROUND_HOLD.checked_sub(since.elapsed()))
120+ else {
121+ return;
122+ };
123+ select! {
124+ changed = holds.changed() => if changed.is_err() {
125+ return;
126+ },
127+ () = sleep(left) => return,
128+ update = feed.recv() => {
129+ match update {
130+ Ok(updates) => pending.fold(updates),
131+ Err(RecvError::Lagged(num_skipped)) => pending.lagged += num_skipped,
132+ Err(RecvError::Closed) => return,
133+ }
134+ if !inner.viewed_pending(pending).await.is_empty() {
135+ return;
136+ }
137+ }
138+ }
139+ }
140+}
141+
105142 /// Listen to _ignore user list update changes_ to clear the rooms when a user
106143 /// is ignored or unignored.
107144 #[instrument(skip_all)]