Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): Timeline consumes updates as VectorDiffs from the EventCache #4408

Merged
merged 21 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1e977d9
feat(ui): Add `TimelineBuilder::with_vectordiffs_as_inputs`.
Hywan Dec 13, 2024
8a5fe2d
refactor(sdk): Add `RoomEventCacheUpdate::UpdateTimelineEvents`.
Hywan Dec 9, 2024
c48255d
feat(ui): Add blank `handle_remote_events_with_diffs`.
Hywan Dec 9, 2024
07b5313
task(ui): Support `VectorDiff::Append` in `TimelineStateTransaction::…
Hywan Dec 9, 2024
5a5ad77
task(ui): Support `VectorDiff::PushFront` in `TimelineStateTransactio…
Hywan Dec 9, 2024
49d8a39
task(ui): Support `VectorDiff::PushBack` in `TimelineStateTransaction…
Hywan Dec 9, 2024
07ed0b8
task(ui): Support `VectorDiff::Clear` in `TimelineStateTransaction::h…
Hywan Dec 9, 2024
c4da2eb
task(ui): Add `ObservableItems::insert_remote_event`.
Hywan Dec 9, 2024
f8f7443
task(ui): Add `AllRemoteEvents::range`.
Hywan Dec 10, 2024
e2a8441
task(ui): Support `VectorDiff::Insert` in `TimelineStateTransaction::…
Hywan Dec 10, 2024
6ef9f73
task(ui): Support `VectorDiff::Remove,` in `TimelineStateTransaction:…
Hywan Dec 10, 2024
d928ef8
refactor(ui): Deduplicate remote events conditionnally.
Hywan Dec 13, 2024
54877e9
task(ui): `DayDivider` has been renamed `DateDivider`.
Hywan Dec 13, 2024
7385415
test(ui): Increase the `recursion_limit`.
Hywan Dec 13, 2024
14597ed
fix(common): Use a trick to avoid hitting the `recursion_limit` too q…
Hywan Dec 18, 2024
a33efb8
refactor(ui): Deduplicate timeline items conditionnally.
Hywan Dec 16, 2024
c4becc2
refactor(ui): `Timeline` receives pagination events as `VectorDiff`s!
Hywan Dec 17, 2024
1390a50
chore(ui): Make Clippy happy.
Hywan Dec 18, 2024
0466742
feat(ui): Enable `TimelineSettings::vectordiffs_as_inputs` if event c…
Hywan Dec 20, 2024
c3094bb
refactor(sdk): Rename two variables.
Hywan Dec 20, 2024
60fefdd
test: Increase timeout for codecoverage.
Hywan Dec 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion crates/matrix-sdk-base/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
# Private feature, see
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
# details.
test-send-sync = ["matrix-sdk-crypto?/test-send-sync"]
test-send-sync = [
"matrix-sdk-common/test-send-sync",
"matrix-sdk-crypto?/test-send-sync",
]

# "message-ids" feature doesn't do anything and is deprecated.
message-ids = []
Expand Down
4 changes: 4 additions & 0 deletions crates/matrix-sdk-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
[features]
js = ["wasm-bindgen-futures"]
uniffi = ["dep:uniffi"]
# Private feature, see
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
# details.
test-send-sync = []

[dependencies]
async-trait = { workspace = true }
Expand Down
33 changes: 33 additions & 0 deletions crates/matrix-sdk-common/src/deserialized_responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,22 @@ pub struct EncryptionInfo {
/// Previously, this differed from [`TimelineEvent`] by wrapping an
/// [`AnySyncTimelineEvent`] instead of an [`AnyTimelineEvent`], but nowadays
/// they are essentially identical, and one of them should probably be removed.
//
// 🚨 Note about this type, please read! 🚨
//
// `SyncTimelineEvent` is heavily used across the SDK crates. In some cases, we
// are reaching a [`recursion_limit`] when the compiler is trying to figure out
// if `SyncTimelineEvent` implements `Sync` when it's embedded in other types.
//
// We want to help the compiler so that one doesn't need to increase the
// `recursion_limit`. We stop the recursive check by (un)safely implement `Sync`
// and `Send` on `SyncTimelineEvent` directly.
//
// See
// https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823
// which has addressed this issue first
//
// [`recursion_limit`]: https://doc.rust-lang.org/reference/attributes/limits.html#the-recursion_limit-attribute
#[derive(Clone, Debug, Serialize)]
pub struct SyncTimelineEvent {
/// The event itself, together with any information on decryption.
Expand All @@ -318,6 +334,23 @@ pub struct SyncTimelineEvent {
pub push_actions: Vec<Action>,
}

// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
#[cfg(not(feature = "test-send-sync"))]
unsafe impl Send for SyncTimelineEvent {}

// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
#[cfg(not(feature = "test-send-sync"))]
unsafe impl Sync for SyncTimelineEvent {}

#[cfg(feature = "test-send-sync")]
#[test]
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
fn test_send_sync_for_sync_timeline_event() {
fn assert_send_sync<T: Send + Sync>() {}

assert_send_sync::<SyncTimelineEvent>();
}

impl SyncTimelineEvent {
/// Create a new `SyncTimelineEvent` from the given raw event.
///
Expand Down
44 changes: 37 additions & 7 deletions crates/matrix-sdk-ui/src/timeline/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ impl TimelineBuilder {
self
}

/// Use `VectorDiff`s as the new “input mechanism” for the `Timeline`.
///
/// Read `TimelineSettings::vectordiffs_as_inputs` to learn more.
pub fn with_vectordiffs_as_inputs(mut self) -> Self {
self.settings.vectordiffs_as_inputs = true;
self
}
Comment on lines +148 to +154
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Would it make sense that it's a feature flag on the EventCache, and that the EC would choose to emit AddTimelineItems or UpdateTimelineItems based on the feature flag? Otherwise, any other user of the event cache, that's not the timeline, has to implement the same filtering mechanism.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about this quite a lot too.

First off, this setting is temporary. It's gonna be removed as soon as we consider RoomEventCacheUpdate::UpdateTimelineEvents stable and we want to switch to it. The other variant, AddTimelineEvents will be removed at the same time.

Second, we have validated this approach during our team meeting :-p.

Third, while I share your concerns, putting the setting on the EventCache makes it so the Timeline can receive both inputs at the same time (AddTimelineEvents and UpdateTimelineEvents), which can create a lot of confusion and inconsistent state.

Fourth, this setting is used to decide if the Timeline has to apply its own deduplication mechanism or not:

  • with AddTimelineEvents: yes, it has to,
  • with UpdateTimelineEvents: no, because it's expected to be handle by the VectorDiff. Putting the settings on the EventCachemakes it way more difficult for theTimelineto decide. Actually, it requires a setting in theTimeline` anyway.

Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third, while I share your concerns, putting the setting on the EventCache makes it so the Timeline can receive both inputs at the same time (AddTimelineEvents and UpdateTimelineEvents), which can create a lot of confusion and inconsistent state.

No: the idea is that, the EventCache would emit AddTimelineEvents if !feature_flag, and emit UpdateTimelineEvents if feature_flag, not both at the same time.

Re: fourth, you're right the timeline would need to get this information; it's relatively trivial to add a getter on the feature flag in the event cache, and get the feature flag value from the TimelineBuilder.

(Re: second, sometimes people change their mind when they realize they were wrong, and I definitely was when I validated it 😇)

Overall, I would even call for not having a feature flag at all, and remove the previous code entirely, but I understand that it's a big change, and it would be nice to test in isolation (especially before the EOY holidays). So my preference would be to have a feature flag on the EventCache, for consistency (it would also simplify code at the FFI layer, I suppose), but we can probably live with the feature flag on the Timeline, if you strongly prefer it.


/// Create a [`Timeline`] with the options set on this builder.
#[tracing::instrument(
skip(self),
Expand All @@ -154,11 +162,17 @@ impl TimelineBuilder {
)
)]
pub async fn build(self) -> Result<Timeline, Error> {
let Self { room, settings, unable_to_decrypt_hook, focus, internal_id_prefix } = self;
let Self { room, mut settings, unable_to_decrypt_hook, focus, internal_id_prefix } = self;

let client = room.client();
let event_cache = client.event_cache();

// Enable `TimelineSettings::vectordiffs_as_inputs` if and only if the event
// cache storage is enabled.
settings.vectordiffs_as_inputs = event_cache.has_storage();

let settings_vectordiffs_as_inputs = settings.vectordiffs_as_inputs;

// Subscribe the event cache to sync responses, in case we hadn't done it yet.
event_cache.subscribe()?;

Expand Down Expand Up @@ -276,16 +290,32 @@ impl TimelineBuilder {
inner.clear().await;
}

// TODO: remove once `UpdateTimelineEvents` is stabilized.
RoomEventCacheUpdate::AddTimelineEvents { events, origin } => {
trace!("Received new timeline events.");
if !settings_vectordiffs_as_inputs {
trace!("Received new timeline events.");

inner.add_events_at(
events.into_iter(),
TimelineNewItemPosition::End { origin: match origin {
EventsOrigin::Sync => RemoteEventOrigin::Sync,
}
}
).await;
}
}

inner.add_events_at(
events.into_iter(),
TimelineNewItemPosition::End { origin: match origin {
RoomEventCacheUpdate::UpdateTimelineEvents { diffs, origin } => {
if settings_vectordiffs_as_inputs {
trace!("Received new timeline events diffs");

inner.handle_remote_events_with_diffs(
diffs,
match origin {
EventsOrigin::Sync => RemoteEventOrigin::Sync,
}
}
).await;
).await;
}
}

RoomEventCacheUpdate::AddEphemeralEvents { events } => {
Expand Down
35 changes: 34 additions & 1 deletion crates/matrix-sdk-ui/src/timeline/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,20 +124,29 @@ pub(super) struct TimelineController<P: RoomDataProvider = Room> {
pub(crate) room_data_provider: P,

/// Settings applied to this timeline.
settings: TimelineSettings,
pub(super) settings: TimelineSettings,
}

#[derive(Clone)]
pub(super) struct TimelineSettings {
/// Should the read receipts and read markers be handled?
pub(super) track_read_receipts: bool,

/// Event filter that controls what's rendered as a timeline item (and thus
/// what can carry read receipts).
pub(super) event_filter: Arc<TimelineEventFilterFn>,

/// Are unparsable events added as timeline items of their own kind?
pub(super) add_failed_to_parse: bool,

/// Should the timeline items be grouped by day or month?
pub(super) date_divider_mode: DateDividerMode,

/// Whether `VectorDiff` is the “input mechanism” to use.
///
/// This mechanism will replace the existing one, but this runtime feature
/// flag is necessary for the transition and the testing phase.
pub(super) vectordiffs_as_inputs: bool,
}

#[cfg(not(tarpaulin_include))]
Expand All @@ -146,6 +155,7 @@ impl fmt::Debug for TimelineSettings {
f.debug_struct("TimelineSettings")
.field("track_read_receipts", &self.track_read_receipts)
.field("add_failed_to_parse", &self.add_failed_to_parse)
.field("vectordiffs_as_inputs", &self.vectordiffs_as_inputs)
.finish_non_exhaustive()
}
}
Expand All @@ -157,6 +167,7 @@ impl Default for TimelineSettings {
event_filter: Arc::new(default_event_filter),
add_failed_to_parse: true,
date_divider_mode: DateDividerMode::Daily,
vectordiffs_as_inputs: false,
}
}
}
Expand Down Expand Up @@ -665,6 +676,27 @@ impl<P: RoomDataProvider> TimelineController<P> {
state.add_remote_events_at(events, position, &self.room_data_provider, &self.settings).await
}

/// Handle updates on events as [`VectorDiff`]s.
pub(super) async fn handle_remote_events_with_diffs(
&self,
diffs: Vec<VectorDiff<SyncTimelineEvent>>,
origin: RemoteEventOrigin,
) {
if diffs.is_empty() {
return;
}

let mut state = self.state.write().await;
state
.handle_remote_events_with_diffs(
diffs,
origin,
&self.room_data_provider,
&self.settings,
)
.await
}

pub(super) async fn clear(&self) {
self.state.write().await.clear();
}
Expand Down Expand Up @@ -757,6 +789,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
txn_id,
send_handle,
content,
&self.settings,
)
.await;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use std::{
cmp::Ordering,
collections::{vec_deque::Iter, VecDeque},
ops::Deref,
ops::{Deref, RangeBounds},
sync::Arc,
};

Expand Down Expand Up @@ -220,6 +220,13 @@ impl<'observable_items> ObservableItemsTransaction<'observable_items> {
self.all_remote_events.push_back(event_meta);
}

/// Insert a new remote event at a specific index.
///
/// Not to be confused with inserting a timeline item!
pub fn insert_remote_event(&mut self, event_index: usize, event_meta: EventMeta) {
self.all_remote_events.insert(event_index, event_meta);
}

/// Get a remote event by using an event ID.
pub fn get_remote_event_by_event_id_mut(
&mut self,
Expand Down Expand Up @@ -1155,11 +1162,25 @@ mod observable_items_tests {
pub struct AllRemoteEvents(VecDeque<EventMeta>);

impl AllRemoteEvents {
/// Return a reference to a remote event.
pub fn get(&self, event_index: usize) -> Option<&EventMeta> {
self.0.get(event_index)
}

/// Return a front-to-back iterator over all remote events.
pub fn iter(&self) -> Iter<'_, EventMeta> {
self.0.iter()
}

/// Return a front-to-back iterator covering ranges of all remote events
/// described by `range`.
pub fn range<R>(&self, range: R) -> Iter<'_, EventMeta>
where
R: RangeBounds<usize>,
{
self.0.range(range)
}

/// Remove all remote events.
fn clear(&mut self) {
self.0.clear();
Expand Down Expand Up @@ -1189,6 +1210,18 @@ impl AllRemoteEvents {
self.0.push_back(event_meta)
}

/// Insert a new remote event at a specific index.
fn insert(&mut self, event_index: usize, event_meta: EventMeta) {
// If there is an associated `timeline_item_index`, shift all the
// `timeline_item_index` that come after this one.
if let Some(new_timeline_item_index) = event_meta.timeline_item_index {
self.increment_all_timeline_item_index_after(new_timeline_item_index);
}

// Insert the event.
self.0.insert(event_index, event_meta)
}

/// Remove one remote event at a specific index, and return it if it exists.
fn remove(&mut self, event_index: usize) -> Option<EventMeta> {
// Remove the event.
Expand Down Expand Up @@ -1261,8 +1294,7 @@ impl AllRemoteEvents {
}

/// Notify that a timeline item has been removed at
/// `new_timeline_item_index`. If `event_index` is `Some(_)`, it means the
/// remote event at `event_index` must be unmapped.
/// `new_timeline_item_index`.
fn timeline_item_has_been_removed_at(&mut self, timeline_item_index_to_remove: usize) {
for event_meta in self.0.iter_mut() {
let mut remove_timeline_item_index = false;
Expand Down Expand Up @@ -1318,6 +1350,34 @@ mod all_remote_events_tests {
}
}

#[test]
fn test_range() {
let mut events = AllRemoteEvents::default();

// Push some events.
events.push_back(event_meta("$ev0", None));
events.push_back(event_meta("$ev1", None));
events.push_back(event_meta("$ev2", None));

assert_eq!(events.iter().count(), 3);
Hywan marked this conversation as resolved.
Show resolved Hide resolved

// Test a few combinations.
assert_eq!(events.range(..).count(), 3);
assert_eq!(events.range(1..).count(), 2);
assert_eq!(events.range(0..=1).count(), 2);

// Iterate on some of them.
let mut some_events = events.range(1..);

assert_matches!(some_events.next(), Some(EventMeta { event_id, .. }) => {
assert_eq!(event_id.as_str(), "$ev1");
});
assert_matches!(some_events.next(), Some(EventMeta { event_id, .. }) => {
assert_eq!(event_id.as_str(), "$ev2");
});
assert!(some_events.next().is_none());
}

#[test]
fn test_clear() {
let mut events = AllRemoteEvents::default();
Expand Down Expand Up @@ -1399,6 +1459,37 @@ mod all_remote_events_tests {
);
}

#[test]
fn test_insert() {
let mut events = AllRemoteEvents::default();

// Insert on an empty set, nothing particular.
events.insert(0, event_meta("$ev0", Some(0)));

// Insert at the end with no `timeline_item_index`.
events.insert(1, event_meta("$ev1", None));

// Insert at the end with a `timeline_item_index`.
events.insert(2, event_meta("$ev2", Some(1)));

// Insert at the start, with a `timeline_item_index`.
events.insert(0, event_meta("$ev3", Some(0)));

assert_events!(
events,
[
// `timeline_item_index` is untouched
("$ev3", Some(0)),
// `timeline_item_index` has been shifted once
("$ev0", Some(1)),
// no `timeline_item_index`
("$ev1", None),
// `timeline_item_index` has been shifted once
("$ev2", Some(2)),
]
);
}

#[test]
fn test_remove() {
let mut events = AllRemoteEvents::default();
Expand Down
Loading
Loading