atagen

matrix-rust-sdk

public
Star0
Watch0

Commit 9ef2e9aaf533

tarball

atagen <boss@atagen.co>2026-10-25 10:44 UTC

3 files changed+174-29
event-cache: commit history against the window memory holds

commit_history_candidate only diffed incrementally when memory held every stored event, so after a restart, lazy load or shrink each fill and recent load replaced memory with the full candidate and broadcast a Reset of the whole room. It now reloads only the loaded window, extended by chunks the operation created next to it, and sends inserts against what subscribers already hold, falling back to a Reset only when that no longer extends. load_retained_history keeps loading everything through HistoryCommitScope::All.
Modified file: crates/matrix-sdk/src/event_cache/caches/room/history.rs+5 additions−9 deletions
@@ -11,7 +11,7 @@ use matrix_sdk_base::{
1111 use ruma::{OwnedEventId, OwnedTransactionId, TransactionId};
1212 use tokio::sync::{mpsc, oneshot, watch};
1313
14-use super::{RoomEventCache, RoomEventCacheInner};
14+use super::{HistoryCommitScope, RoomEventCache, RoomEventCacheInner};
1515 use crate::{
1616 event_cache::{EventCacheError, Result, caches::event_linked_chunk::EventLinkedChunk},
1717 room::MessagesOptions,
@@ -277,7 +277,6 @@ impl RoomEventCache {
277277 {
278278 let mut state = self.inner.state.write().await?;
279279 let mut candidate = state.history_candidate().await?;
280- let baseline_count = candidate.events().count();
281280 let mut known = HashSet::new();
282281 let mut arrivals = Vec::new();
283282 let mut leading = Vec::new();
@@ -378,7 +377,7 @@ impl RoomEventCache {
378377 };
379378
380379 inserted.reverse();
381- state.commit_history_candidate(candidate, inserted, baseline_count).await?;
380+ state.commit_history_candidate(candidate, inserted, HistoryCommitScope::Loaded).await?;
382381 Ok(RecentHistoryOutcome { pages, events: raw_count, continuity })
383382 }
384383 }
@@ -397,8 +396,7 @@ impl RoomEventCache {
397396 if !gaps(&candidate).is_empty()
398397 && candidate.chunks().count() != state.room_linked_chunk().chunks().count()
399398 {
400- let baseline_count = candidate.events().count();
401- state.commit_history_candidate(candidate, Vec::new(), baseline_count).await?;
399+ state.commit_history_candidate(candidate, Vec::new(), HistoryCommitScope::All).await?;
402400 }
403401 Ok(())
404402 }
@@ -436,7 +434,6 @@ impl RoomEventCache {
436434 {
437435 let mut state = self.inner.state.write().await?;
438436 let mut candidate = state.history_candidate().await?;
439- let baseline_count = candidate.events().count();
440437 let older_boundary = candidate.revents().find_map(|(_, event)| event.event_id());
441438 let mut known: HashSet<_> =
442439 candidate.events().filter_map(|(_, event)| event.event_id()).collect();
@@ -464,7 +461,7 @@ impl RoomEventCache {
464461 None
465462 };
466463 candidate.push_live_events(gap, &events);
467- state.commit_history_candidate(candidate, events, baseline_count).await?;
464+ state.commit_history_candidate(candidate, events, HistoryCommitScope::Loaded).await?;
468465 Ok(history)
469466 }
470467 }
@@ -513,7 +510,6 @@ impl RoomEventCache {
513510 {
514511 let mut state = self.inner.state.write().await?;
515512 let mut candidate = state.history_candidate().await?;
516- let baseline_count = candidate.events().count();
517513 let Some(current) = find_gap(&candidate, &gap_id) else {
518514 return Ok(HistoryGapFillOutcome::NotFound);
519515 };
@@ -537,7 +533,7 @@ impl RoomEventCache {
537533 failure.as_ref().map(ToString::to_string),
538534 );
539535 let count = events.len();
540- state.commit_history_candidate(candidate, events, baseline_count).await?;
536+ state.commit_history_candidate(candidate, events, HistoryCommitScope::Loaded).await?;
541537 if let Some(error) = failure {
542538 return Err(EventCacheError::PaginationError(std::sync::Arc::new(error)));
543539 }
Modified file: crates/matrix-sdk/src/event_cache/caches/room/mod.rs+71 additions
@@ -41,6 +41,7 @@ use ruma::{
4141 events::{AnyRoomAccountDataEvent, AnySyncEphemeralRoomEvent, relation::RelationType},
4242 serde::Raw,
4343 };
44+use state::HistoryCommitScope;
4445 pub(super) use state::{LockedRoomEventCacheState, RoomEventCacheStateLockWriteGuard};
4546 pub use subscriber::RoomEventCacheSubscriber;
4647 use tokio::sync::{Notify, broadcast::Receiver, mpsc};
@@ -1970,6 +1971,76 @@ mod timed_tests {
19701971 }
19711972
19721973 #[async_test]
1974+ async fn test_history_commit_after_shrink_stays_within_the_loaded_window() {
1975+ let room_id = room_id!("!shrunk-window:saucisse.bzh");
1976+ let client = MockClientBuilder::new(None).build().await;
1977+ let event_cache = client.event_cache();
1978+ event_cache.subscribe().unwrap();
1979+ client.base_client().get_or_create_room(room_id, RoomState::Joined);
1980+ let room = client.get_room(room_id).unwrap();
1981+ let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap();
1982+ let f = EventFactory::new().room(room_id).sender(*ALICE);
1983+ let old_id = event_id!("$window-old");
1984+ let new_id = event_id!("$window-new");
1985+ let newer_id = event_id!("$window-newer");
1986+
1987+ room_event_cache
1988+ .handle_joined_room_update(JoinedRoomUpdate {
1989+ timeline: Timeline {
1990+ events: vec![f.text_msg("old").event_id(old_id).into_event()],
1991+ ..Default::default()
1992+ },
1993+ ..Default::default()
1994+ })
1995+ .await
1996+ .unwrap();
1997+ room_event_cache
1998+ .handle_joined_room_update(JoinedRoomUpdate {
1999+ timeline: Timeline {
2000+ limited: true,
2001+ prev_batch: Some("new".to_owned()),
2002+ events: vec![f.text_msg("new").event_id(new_id).into_event()],
2003+ },
2004+ ..Default::default()
2005+ })
2006+ .await
2007+ .unwrap();
2008+ {
2009+ let mut state = room_event_cache.inner.state.write().await.unwrap();
2010+ state.shrink_to_last_chunk().await.unwrap();
2011+ let _ = state.room_linked_chunk_mut().updates_as_vector_diffs();
2012+ }
2013+
2014+ let (events, mut subscriber) = room_event_cache.subscribe().await.unwrap();
2015+ let ids: Vec<_> = events.iter().filter_map(|event| event.event_id()).collect();
2016+ assert_eq!(ids, [new_id]);
2017+
2018+ room_event_cache
2019+ .append_history_section(
2020+ vec![f.text_msg("newer").event_id(newer_id).into_event()],
2021+ Some("newer".to_owned()),
2022+ )
2023+ .await
2024+ .unwrap();
2025+
2026+ assert_let_timeout!(
2027+ Ok(RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. })) =
2028+ subscriber.recv()
2029+ );
2030+ assert_matches!(diffs.as_slice(), [VectorDiff::Insert { index: 1, value }] => {
2031+ assert_eq!(value.event_id().as_deref(), Some(newer_id));
2032+ });
2033+ let ids: Vec<_> = room_event_cache
2034+ .events()
2035+ .await
2036+ .unwrap()
2037+ .iter()
2038+ .filter_map(|event| event.event_id())
2039+ .collect();
2040+ assert_eq!(ids, [new_id, newer_id]);
2041+ }
2042+
2043+ #[async_test]
19732044 async fn test_limited_sync_after_shrink_to_a_gap_keeps_its_discontinuity() {
19742045 let room_id = room_id!("!shrunk-history:saucisse.bzh");
19752046 let client = MockClientBuilder::new(None).build().await;
Modified file: crates/matrix-sdk/src/event_cache/caches/room/state.rs+98 additions−20 deletions
@@ -30,8 +30,8 @@ use matrix_sdk_base::{
3030 store::{EventCacheStoreLock, EventCacheStoreLockGuard, EventCacheStoreLockState},
3131 },
3232 linked_chunk::{
33- ChunkContent, ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Position, Update,
34- lazy_loader,
33+ ChunkContent, ChunkIdentifier, ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Position,
34+ Update, lazy_loader,
3535 },
3636 serde_helpers::{extract_edit_target, extract_thread_root},
3737 sync::Timeline,
@@ -775,25 +775,36 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
775775 }
776776
777777 pub(super) async fn history_candidate(&self) -> Result<EventLinkedChunk, EventCacheError> {
778+ self.load_from_store(None).await
779+ }
780+
781+ /// Loads stored chunks from the newest back to `first`, or all of them.
782+ async fn load_from_store(
783+ &self,
784+ first: Option<ChunkIdentifier>,
785+ ) -> Result<EventLinkedChunk, EventCacheError> {
778786 let linked_chunk_id = LinkedChunkId::Room(&self.state.room_id);
779787 let (last, generator) = self.store.load_last_chunk(linked_chunk_id).await?;
780788 let metadata = self.store.load_all_chunks_metadata(linked_chunk_id).await?;
781- let mut candidate = EventLinkedChunk::with_initial_linked_chunk(
789+ let mut loaded = EventLinkedChunk::with_initial_linked_chunk(
782790 lazy_loader::from_last_chunk(last, generator)?,
783791 (!metadata.is_empty()).then_some(metadata),
784792 );
785793 loop {
786- let first =
787- candidate.chunks().next().expect("a linked chunk is never empty").identifier();
788- let Some(previous) = self.store.load_previous_chunk(linked_chunk_id, first).await?
794+ let current =
795+ loaded.chunks().next().expect("a linked chunk is never empty").identifier();
796+ if Some(current) == first {
797+ break;
798+ }
799+ let Some(previous) = self.store.load_previous_chunk(linked_chunk_id, current).await?
789800 else {
790801 break;
791802 };
792- candidate.insert_new_chunk_as_first(previous)?;
803+ loaded.insert_new_chunk_as_first(previous)?;
793804 }
794- let _ = candidate.store_updates().take();
795- let _ = candidate.updates_as_vector_diffs();
796- Ok(candidate)
805+ let _ = loaded.store_updates().take();
806+ let _ = loaded.updates_as_vector_diffs();
807+ Ok(loaded)
797808 }
798809
799810 pub(crate) fn history_gap_positions(&self) -> Vec<super::HistoryGapPosition> {
@@ -825,20 +836,33 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
825836 &mut self,
826837 mut candidate: EventLinkedChunk,
827838 events: Vec<Event>,
828- baseline_count: usize,
839+ scope: HistoryCommitScope,
829840 ) -> Result<(), EventCacheError> {
830- let use_incremental = self.state.room_linked_chunk.events().count() == baseline_count;
831- self.send_updates_to_store(candidate.store_updates().take()).await?;
832- self.state.room_linked_chunk = candidate;
841+ let previous: Vec<_> =
842+ self.state.room_linked_chunk.events().map(|(_, event)| event.event_id()).collect();
843+ let mut diffs = match scope {
844+ HistoryCommitScope::Loaded => {
845+ let start = self.loaded_window_start(&candidate).await?;
846+ self.send_updates_to_store(candidate.store_updates().take()).await?;
847+ self.state.room_linked_chunk = self.load_from_store(start).await?;
848+ insertions_from(&previous, &self.state.room_linked_chunk)
849+ }
850+ HistoryCommitScope::All => {
851+ self.send_updates_to_store(candidate.store_updates().take()).await?;
852+ let _ = candidate.updates_as_vector_diffs();
853+ self.state.room_linked_chunk = candidate;
854+ None
855+ }
856+ }
857+ .unwrap_or_else(|| {
858+ let values =
859+ self.state.room_linked_chunk.events().map(|(_, event)| event.clone()).collect();
860+ vec![VectorDiff::Reset { values }]
861+ });
833862 self.reset_pagination_status(false);
834863 let result =
835864 self.post_process_new_events(events, PostProcessingOrigin::Backpagination, None).await;
836- let mut diffs = self.state.room_linked_chunk.updates_as_vector_diffs();
837- if !use_incremental {
838- let values =
839- self.state.room_linked_chunk.events().map(|(_, event)| event.clone()).collect();
840- diffs = vec![VectorDiff::Reset { values }];
841- }
865+ diffs.extend(self.state.room_linked_chunk.updates_as_vector_diffs());
842866 self.state.update_sender.send(
843867 RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
844868 diffs,
@@ -850,6 +874,34 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
850874 result
851875 }
852876
877+ /// The first chunk of `candidate` that belongs to the window memory already
878+ /// loaded. A chunk created by the operation joins the window of the stored
879+ /// chunk after it, so a page filled into a gap right before the window shows,
880+ /// while history further back stays unloaded.
881+ async fn loaded_window_start(
882+ &self,
883+ candidate: &EventLinkedChunk,
884+ ) -> Result<Option<ChunkIdentifier>, EventCacheError> {
885+ let stored: HashSet<_> = self
886+ .store
887+ .load_all_chunks_metadata(LinkedChunkId::Room(&self.state.room_id))
888+ .await?
889+ .into_iter()
890+ .map(|metadata| metadata.identifier)
891+ .collect();
892+ let loaded: HashSet<_> =
893+ self.state.room_linked_chunk.chunks().map(|chunk| chunk.identifier()).collect();
894+ let mut start = None;
895+ for chunk in candidate.rchunks() {
896+ let identifier = chunk.identifier();
897+ if stored.contains(&identifier) && !loaded.contains(&identifier) {
898+ break;
899+ }
900+ start = Some(identifier);
901+ }
902+ Ok(start)
903+ }
904+
853905 fn reset_pagination_status(&self, hit_timeline_start: bool) {
854906 let mut status = self.state.pagination_status.write();
855907 self.state.generation.fetch_add(1, Ordering::SeqCst);
@@ -1523,6 +1575,32 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
15231575 }
15241576 }
15251577
1578+/// How much of the committed history ends up in memory.
1579+pub(super) enum HistoryCommitScope {
1580+ /// Keep the window memory already held, plus pages added right next to it.
1581+ Loaded,
1582+ /// Load every stored section, as before attaching a fresh subscriber.
1583+ All,
1584+}
1585+
1586+/// Insert diffs turning `previous` into the events now loaded, or `None` when
1587+/// the loaded events no longer extend it, which calls for a reset.
1588+fn insertions_from(
1589+ previous: &[Option<OwnedEventId>],
1590+ loaded: &EventLinkedChunk,
1591+) -> Option<Vec<VectorDiff<Event>>> {
1592+ let mut remaining = previous.iter().peekable();
1593+ let mut diffs = Vec::new();
1594+ for (index, (_, event)) in loaded.events().enumerate() {
1595+ if remaining.peek().is_some_and(|id| **id == event.event_id()) {
1596+ remaining.next();
1597+ } else {
1598+ diffs.push(VectorDiff::Insert { index, value: event.clone() });
1599+ }
1600+ }
1601+ remaining.next().is_none().then_some(diffs)
1602+}
1603+
15261604 /// Extract a valid read receipt event from the ephemeral events, if
15271605 /// available.
15281606 fn extract_read_receipt(