Browse Source

timeline: Refactor code to minimize diff and add tests

pipelines/786320
Kévin Commaille 1 year ago committed by Kévin Commaille
parent
commit
d1b1f4ad3f
  1. 7
      Cargo.lock
  2. 3
      Cargo.toml
  3. 2
      src/prelude.rs
  4. 285
      src/session/model/room/timeline/mod.rs
  5. 343
      src/session/model/room/timeline/timeline_item_diff_minimizer/mod.rs
  6. 600
      src/session/model/room/timeline/timeline_item_diff_minimizer/tests.rs

7
Cargo.lock generated

@ -168,6 +168,12 @@ dependencies = [
"zbus 5.2.0",
]
[[package]]
name = "assert_matches2"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15832d94c458da98cac0ffa6eca52cc19c2a3c6c951058500a5ae8f01f0fdf56"
[[package]]
name = "assign"
version = "1.1.1"
@ -1355,6 +1361,7 @@ name = "fractal"
version = "10.0.0-beta"
dependencies = [
"ashpd",
"assert_matches2",
"async-once-cell",
"diff",
"djb_hash",

3
Cargo.toml

@ -112,6 +112,9 @@ oo7 = { version = "0.3", default-features = false, features = [
"tracing",
] }
[dev-dependencies]
assert_matches2 = "0.1"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
cast_possible_truncation = "allow"

2
src/prelude.rs

@ -4,7 +4,7 @@ pub(crate) use crate::{
ToastableDialogImpl,
},
contrib::CameraExt,
session::model::UserExt,
session::model::{TimelineItemExt, UserExt},
session_list::SessionInfoExt,
user_facing_error::UserFacingError,
utils::{

285
src/session/model/room/timeline/mod.rs

@ -1,11 +1,4 @@
mod timeline_item;
mod virtual_item;
use std::{
collections::{HashMap, VecDeque},
ops::ControlFlow,
sync::Arc,
};
use std::{collections::HashMap, ops::ControlFlow, sync::Arc};
use futures_util::StreamExt;
use gtk::{
@ -31,8 +24,13 @@ use ruma::{
use tokio::task::AbortHandle;
use tracing::error;
mod timeline_item;
mod timeline_item_diff_minimizer;
mod virtual_item;
use self::timeline_item_diff_minimizer::{TimelineItemDiff, TimelineItemStore};
pub(crate) use self::{
timeline_item::{TimelineItem, TimelineItemImpl},
timeline_item::{TimelineItem, TimelineItemExt, TimelineItemImpl},
virtual_item::{VirtualItem, VirtualItemKind},
};
use super::{Event, Room};
@ -280,7 +278,7 @@ mod imp {
fn update_with_diff_list(&self, diff_list: Vec<VectorDiff<Arc<SdkTimelineItem>>>) {
let was_empty = self.is_empty();
if let Some(diff_list) = self.minimize_diff_list(diff_list) {
if let Some(diff_list) = self.try_minimize_diff_list(diff_list) {
// The diff could not be minimized, handle it manually.
for diff in diff_list {
self.update_with_single_diff(diff);
@ -302,150 +300,15 @@ mod imp {
/// room history.
///
/// Returns the list of diffs if it could not be minimized.
#[allow(clippy::too_many_lines)]
fn minimize_diff_list(
fn try_minimize_diff_list(
&self,
diff_list: Vec<VectorDiff<Arc<SdkTimelineItem>>>,
) -> Option<Vec<VectorDiff<Arc<SdkTimelineItem>>>> {
if diff_list.len() == 1
|| diff_list.iter().any(|diff| {
matches!(
diff,
VectorDiff::Clear | VectorDiff::Truncate { .. } | VectorDiff::Reset { .. }
)
})
{
// Minimizing this will probably make a worse diff, so we return early.
if !self.can_minimize_diff_list(&diff_list) {
return Some(diff_list);
}
// Get the current state.
let old_items = self
.sdk_items
.snapshot()
.into_iter()
.map(|obj| {
obj.downcast::<TimelineItem>()
.expect("SDK items are TimelineItems")
})
.collect::<Vec<_>>();
let mut item_map = old_items
.iter()
.cloned()
.map(|item| (item.timeline_id(), item))
.collect::<HashMap<_, _>>();
let mut new_items = VecDeque::from(old_items.clone());
let mut updated_item_ids = Vec::new();
let mut update_or_create_item = |value: Arc<SdkTimelineItem>| {
let timeline_id = value.unique_id().0.clone();
item_map
.entry(timeline_id)
.and_modify(|item| {
self.update_item(item, &value);
updated_item_ids.push(item.timeline_id());
})
.or_insert_with(|| self.create_item(&value))
.clone()
};
// Get the new state by applying the diffs.
for diff in diff_list {
match diff {
VectorDiff::Append { values } => {
let items = values.into_iter().map(&mut update_or_create_item);
new_items.extend(items);
}
VectorDiff::PushFront { value } => {
let item = update_or_create_item(value);
new_items.push_front(item);
}
VectorDiff::PushBack { value } => {
let item = update_or_create_item(value);
new_items.push_back(item);
}
VectorDiff::PopFront => {
new_items.pop_front();
}
VectorDiff::PopBack => {
new_items.pop_back();
}
VectorDiff::Insert { index, value } => {
let item = update_or_create_item(value);
new_items.insert(index, item);
}
VectorDiff::Set { index, value } => {
let item = update_or_create_item(value);
*new_items
.get_mut(index)
.expect("an item should already exist at the given index") = item;
}
VectorDiff::Remove { index } => {
new_items.remove(index);
}
VectorDiff::Clear | VectorDiff::Truncate { .. } | VectorDiff::Reset { .. } => {
unreachable!()
}
}
}
// Use a diff algorithm to minimize the removals and additions.
let old_item_ids = old_items
.iter()
.map(TimelineItem::timeline_id)
.collect::<Vec<_>>();
let new_item_ids = new_items
.iter()
.map(TimelineItem::timeline_id)
.collect::<Vec<_>>();
let mut pos = 0;
// Remove and insert items in batch.
let mut n_removals = 0;
let mut additions = Vec::<TimelineItem>::new();
for result in diff::slice(&old_item_ids, &new_item_ids) {
match result {
diff::Result::Left(_) => {
if !additions.is_empty() {
self.update_items(pos, 0, &additions);
pos += additions.len() as u32;
additions.clear();
}
n_removals += 1;
}
diff::Result::Both(timeline_id, _) => {
if !additions.is_empty() || n_removals > 0 {
self.update_items(pos, n_removals, &additions);
pos += additions.len() as u32;
additions.clear();
}
if updated_item_ids.contains(timeline_id) {
// The header visibility might have changed.
self.update_items_headers(pos, 1);
}
pos += 1;
}
diff::Result::Right(timeline_id) => {
let item = item_map
.get(timeline_id)
.expect("item should exist in map")
.clone();
additions.push(item);
}
}
}
// Process the remaining items.
if !additions.is_empty() || n_removals > 0 {
self.update_items(pos, n_removals, &additions);
}
self.minimize_diff_list(diff_list);
None
}
@ -543,14 +406,9 @@ mod imp {
self.sdk_items.splice(pos, n_removals, additions);
// Update the header visibility of all the new additions, or only the first item
// after a removal.
let update_nb = if additions.is_empty() {
n_removals.min(1)
} else {
additions.len() as u32
};
self.update_items_headers(pos, update_nb);
// Update the header visibility of all the new additions, and the first item
// after this batch.
self.update_items_headers(pos, additions.len() as u32);
// Try to update the latest unread message.
if !additions.is_empty() {
@ -560,7 +418,8 @@ mod imp {
}
}
/// Update `nb` items' headers starting at `pos`.
/// Update the headers of the item at the given position and the given
/// number of items after it.
fn update_items_headers(&self, pos: u32, nb: u32) {
let sdk_items = &self.sdk_items;
@ -594,49 +453,6 @@ mod imp {
}
}
/// Update the given item with the given value.
fn update_item(&self, item: &TimelineItem, value: &SdkTimelineItem) {
item.update_with(value);
if let Some(event) = item.downcast_ref::<Event>() {
// Update the identifier in the event map, in case we switched from a
// transaction ID to an event ID.
self.event_map
.borrow_mut()
.insert(event.identifier(), event.clone());
// Try to update the latest unread message.
self.room().update_latest_activity(iter::once(event));
}
}
/// Create a `TimelineItem` in this `Timeline` from the given SDK
/// timeline item.
fn create_item(&self, item: &SdkTimelineItem) -> TimelineItem {
let room = self.room();
let item = TimelineItem::new(item, room);
if let Some(event) = item.downcast_ref::<Event>() {
self.event_map
.borrow_mut()
.insert(event.identifier(), event.clone());
// Keep track of the activity of the sender.
if event.counts_as_unread() {
if let Some(members) = room.members() {
let member = members.get_or_create(event.sender_id());
member.set_latest_activity(u64::from(event.origin_server_ts().get()));
}
}
if event.is_room_create_event() {
self.set_has_room_create(true);
}
}
item
}
/// Remove the given item from this `Timeline`.
fn remove_item(&self, item: &TimelineItem) {
if let Some(event) = item.downcast_ref::<Event>() {
@ -765,6 +581,75 @@ mod imp {
.expect("handle is uninitialized");
}
}
impl TimelineItemStore for Timeline {
type Item = TimelineItem;
type Data = Arc<SdkTimelineItem>;
fn items(&self) -> Vec<TimelineItem> {
self.sdk_items
.snapshot()
.into_iter()
.map(|obj| {
obj.downcast::<TimelineItem>()
.expect("SDK items are TimelineItems")
})
.collect()
}
fn create_item(&self, data: &Arc<SdkTimelineItem>) -> TimelineItem {
let room = self.room();
let item = TimelineItem::new(data, room);
if let Some(event) = item.downcast_ref::<Event>() {
self.event_map
.borrow_mut()
.insert(event.identifier(), event.clone());
// Keep track of the activity of the sender.
if event.counts_as_unread() {
if let Some(members) = room.members() {
let member = members.get_or_create(event.sender_id());
member.set_latest_activity(u64::from(event.origin_server_ts().get()));
}
}
if event.is_room_create_event() {
self.set_has_room_create(true);
}
}
item
}
fn update_item(&self, item: &TimelineItem, data: &Arc<SdkTimelineItem>) {
item.update_with(data);
if let Some(event) = item.downcast_ref::<Event>() {
// Update the identifier in the event map, in case we switched from a
// transaction ID to an event ID.
self.event_map
.borrow_mut()
.insert(event.identifier(), event.clone());
// Try to update the latest unread message.
self.room().update_latest_activity(iter::once(event));
}
}
fn apply_item_diff_list(&self, item_diff_list: Vec<TimelineItemDiff<TimelineItem>>) {
for item_diff in item_diff_list {
match item_diff {
TimelineItemDiff::Splice(splice) => {
self.update_items(splice.pos, splice.n_removals, &splice.additions);
}
TimelineItemDiff::Update(update) => {
self.update_items_headers(update.pos, update.n_items);
}
}
}
}
}
}
glib::wrapper! {

343
src/session/model/room/timeline/timeline_item_diff_minimizer/mod.rs

@ -0,0 +1,343 @@
use std::{
collections::{HashMap, VecDeque},
sync::Arc,
};
use gtk::prelude::*;
use matrix_sdk_ui::{eyeball_im::VectorDiff, timeline::TimelineItem as SdkTimelineItem};
mod tests;
use super::TimelineItem;
use crate::prelude::*;
/// Trait to access data from a type that store `TimelineItem`s.
pub(super) trait TimelineItemStore: Sized {
type Item: IsA<TimelineItem>;
type Data: TimelineItemData;
/// The current list of items.
fn items(&self) -> Vec<Self::Item>;
/// Create a `TimelineItem` with the given `TimelineItemData`.
fn create_item(&self, data: &Self::Data) -> Self::Item;
/// Update the given item with the given timeline ID.
fn update_item(&self, item: &Self::Item, data: &Self::Data);
/// Apply the given list of item diffs to this store.
fn apply_item_diff_list(&self, item_diff_list: Vec<TimelineItemDiff<Self::Item>>);
/// Whether the given diff list can be minimized by calling
/// `minimize_diff_list`.
///
/// It can be minimized if there is more than 1 item in the list and if the
/// list only includes supported `VectorDiff` variants.
fn can_minimize_diff_list(&self, diff_list: &[VectorDiff<Self::Data>]) -> bool {
diff_list.len() > 1
&& !diff_list.iter().any(|diff| {
matches!(
diff,
VectorDiff::Clear | VectorDiff::Truncate { .. } | VectorDiff::Reset { .. }
)
})
}
/// Minimize the given diff list and apply it to this store.
///
/// Panics if the diff list contains unsupported `VectorDiff` variants. This
/// will never panic if `can_minimize_diff_list` returns `true`.
fn minimize_diff_list(&self, diff_list: Vec<VectorDiff<Self::Data>>) {
TimelineItemDiffMinimizer::new(self).apply(diff_list);
}
}
/// Trait implemented by types that provide data for `TimelineItem`s.
pub(super) trait TimelineItemData {
/// The unique timeline ID of the data.
fn timeline_id(&self) -> &str;
}
impl TimelineItemData for SdkTimelineItem {
fn timeline_id(&self) -> &str {
&self.unique_id().0
}
}
impl<T> TimelineItemData for Arc<T>
where
T: TimelineItemData,
{
fn timeline_id(&self) -> &str {
(**self).timeline_id()
}
}
/// A helper struct to minimize a list of `VectorDiff`.
///
/// This does not support `VectorDiff::Clear`, `VectorDiff::Truncate` and
/// `VectorDiff::Reset` as we assume that lists including those cannot be
/// minimized in an optimal way.
struct TimelineItemDiffMinimizer<'a, S, I> {
store: &'a S,
item_map: HashMap<String, I>,
updated_item_ids: Vec<String>,
}
impl<'a, S, I> TimelineItemDiffMinimizer<'a, S, I> {
/// Construct a `TimelineItemDiffMinimizer` with the given store.
fn new(store: &'a S) -> Self {
Self {
store,
item_map: HashMap::new(),
updated_item_ids: Vec::new(),
}
}
}
impl<S, I> TimelineItemDiffMinimizer<'_, S, I>
where
S: TimelineItemStore<Item = I>,
I: IsA<TimelineItem>,
{
/// Load the items from the store.
///
/// Returns the list of timeline IDs of the items.
fn load_items(&mut self) -> Vec<String> {
let items = self.store.items();
let item_ids = items.iter().map(S::Item::timeline_id).collect();
self.item_map
.extend(items.into_iter().map(|item| (item.timeline_id(), item)));
item_ids
}
/// Update or create an item in the store using the given data.
///
/// Returns the timeline ID of the item.
fn update_or_create_item(&mut self, data: &S::Data) -> String {
let timeline_id = data.timeline_id().to_owned();
self.item_map
.entry(timeline_id)
.and_modify(|item| {
self.store.update_item(item, data);
self.updated_item_ids.push(item.timeline_id());
})
.or_insert_with(|| self.store.create_item(data))
.timeline_id()
}
/// Apply the given diff to the given items.
fn apply_diff_to_items(
&mut self,
item_ids: &[String],
diff_list: Vec<VectorDiff<S::Data>>,
) -> Vec<String> {
let mut new_item_ids = VecDeque::from(item_ids.to_owned());
// Get the new state by applying the diffs.
for diff in diff_list {
match diff {
VectorDiff::Append { values } => {
let items = values
.into_iter()
.map(|data| self.update_or_create_item(data));
new_item_ids.extend(items);
}
VectorDiff::PushFront { value } => {
let item = self.update_or_create_item(&value);
new_item_ids.push_front(item);
}
VectorDiff::PushBack { value } => {
let item = self.update_or_create_item(&value);
new_item_ids.push_back(item);
}
VectorDiff::PopFront => {
new_item_ids.pop_front();
}
VectorDiff::PopBack => {
new_item_ids.pop_back();
}
VectorDiff::Insert { index, value } => {
let item = self.update_or_create_item(&value);
new_item_ids.insert(index, item);
}
VectorDiff::Set { index, value } => {
let item_id = self.update_or_create_item(&value);
*new_item_ids
.get_mut(index)
.expect("an item should already exist at the given index") = item_id;
}
VectorDiff::Remove { index } => {
new_item_ids.remove(index);
}
VectorDiff::Clear | VectorDiff::Truncate { .. } | VectorDiff::Reset { .. } => {
unreachable!()
}
}
}
new_item_ids.into()
}
/// Compute the list of item diffs between the two given lists.
///
/// Uses a diff algorithm to minimize the removals and additions.
fn item_diff_list(
&self,
old_item_ids: &[String],
new_item_ids: &[String],
) -> Vec<TimelineItemDiff<S::Item>> {
let mut item_diff_list = Vec::new();
let mut pos = 0;
// Group diffs in batch.
let mut n_removals = 0;
let mut additions = None;
let mut n_updates = 0;
for result in diff::slice(old_item_ids, new_item_ids) {
match result {
diff::Result::Left(_) => {
if let Some(additions) = additions.take() {
let item_diff = SpliceDiff {
pos,
n_removals: 0,
additions,
};
pos += item_diff.additions.len() as u32;
item_diff_list.push(item_diff.into());
} else if n_updates > 0 {
let item_diff = UpdateDiff {
pos,
n_items: n_updates,
};
item_diff_list.push(item_diff.into());
pos += n_updates;
n_updates = 0;
}
n_removals += 1;
}
diff::Result::Both(timeline_id, _) => {
if additions.is_some() || n_removals > 0 {
let item_diff = SpliceDiff {
pos,
n_removals,
additions: additions.take().unwrap_or_default(),
};
pos += item_diff.additions.len() as u32;
item_diff_list.push(item_diff.into());
n_removals = 0;
}
if self.updated_item_ids.contains(timeline_id) {
n_updates += 1;
} else {
if n_updates > 0 {
let item_diff = UpdateDiff {
pos,
n_items: n_updates,
};
item_diff_list.push(item_diff.into());
pos += n_updates;
n_updates = 0;
}
pos += 1;
}
}
diff::Result::Right(timeline_id) => {
if n_updates > 0 {
let item_diff = UpdateDiff {
pos,
n_items: n_updates,
};
item_diff_list.push(item_diff.into());
pos += n_updates;
n_updates = 0;
}
let item = self
.item_map
.get(timeline_id)
.expect("item should exist in map")
.clone();
additions.get_or_insert_with(Vec::new).push(item);
}
}
}
// Process the remaining batches.
if additions.is_some() || n_removals > 0 {
let item_diff = SpliceDiff {
pos,
n_removals,
additions: additions.take().unwrap_or_default(),
};
item_diff_list.push(item_diff.into());
} else if n_updates > 0 {
let item_diff = UpdateDiff {
pos,
n_items: n_updates,
};
item_diff_list.push(item_diff.into());
}
item_diff_list
}
/// Minimize the given diff and apply it to the store.
fn apply(mut self, diff_list: Vec<VectorDiff<S::Data>>) {
let old_item_ids = self.load_items();
let new_item_ids = self.apply_diff_to_items(&old_item_ids, diff_list);
let item_diff_list = self.item_diff_list(&old_item_ids, &new_item_ids);
self.store.apply_item_diff_list(item_diff_list);
}
}
/// A minimized diff for timeline items.
#[derive(Debug, Clone)]
pub(super) enum TimelineItemDiff<T> {
/// Remove then add items.
Splice(SpliceDiff<T>),
/// Update items.
Update(UpdateDiff),
}
impl<T> From<SpliceDiff<T>> for TimelineItemDiff<T> {
fn from(value: SpliceDiff<T>) -> Self {
Self::Splice(value)
}
}
impl<T> From<UpdateDiff> for TimelineItemDiff<T> {
fn from(value: UpdateDiff) -> Self {
Self::Update(value)
}
}
/// A diff to remove then add items.
#[derive(Debug, Clone)]
pub(super) struct SpliceDiff<T> {
/// The position where the change happens
pub(super) pos: u32,
/// The number of items to remove.
pub(super) n_removals: u32,
/// The items to add.
pub(super) additions: Vec<T>,
}
/// A diff to update items.
#[derive(Debug, Clone)]
pub(super) struct UpdateDiff {
/// The position from where to start updating items.
pub(super) pos: u32,
/// The number of items to update.
pub(super) n_items: u32,
}

600
src/session/model/room/timeline/timeline_item_diff_minimizer/tests.rs

@ -0,0 +1,600 @@
#![cfg(test)]
#![allow(clippy::too_many_lines)]
use std::cell::RefCell;
use assert_matches2::assert_matches;
use gtk::{glib, prelude::*, subclass::prelude::*};
use matrix_sdk_ui::eyeball_im::Vector;
use super::*;
use crate::session::model::TimelineItemImpl;
/// Timeline item store to test `TimelineItemDiffMinimizer`.
#[derive(Debug, Clone, Default)]
struct TestTimelineItemStore {
/// The items in the store.
items: RefCell<Vec<TestTimelineItem>>,
}
impl TestTimelineItemStore {
/// Set `processed` to false for all items.
fn reset_processed_items(&self) {
for item in &*self.items.borrow() {
item.downcast_ref::<TestTimelineItem>()
.expect("TestTimelineItemStore only receives TestTimelineItem")
.set_processed(false);
}
}
}
impl TimelineItemStore for TestTimelineItemStore {
type Item = TestTimelineItem;
type Data = TestTimelineItemData;
fn items(&self) -> Vec<TestTimelineItem> {
self.items.borrow().clone()
}
fn create_item(&self, data: &Self::Data) -> TestTimelineItem {
println!("create_item: {data:?}");
TestTimelineItem::new(data)
}
fn update_item(&self, item: &TestTimelineItem, data: &Self::Data) {
println!("update_item: {item:?} {data:?}");
item.set_version(data.version);
}
fn apply_item_diff_list(&self, item_diff_list: Vec<TimelineItemDiff<TestTimelineItem>>) {
for item_diff in item_diff_list {
match item_diff {
TimelineItemDiff::Splice(splice_diff) => {
let mut items = self.items.borrow_mut();
let pos = splice_diff.pos as usize;
let n_removals = splice_diff.n_removals as usize;
let n_additions = splice_diff.additions.len();
items.splice(pos..pos + n_removals, splice_diff.additions);
// Set all the new additions and the first one after the current batch as
// processed.
for item in items.iter().skip(pos).take(n_additions + 1) {
item.set_processed(true);
}
}
TimelineItemDiff::Update(update_diff) => {
let pos = update_diff.pos as usize;
let n_items = update_diff.n_items as usize;
let items = &*self.items.borrow();
let len = items.len();
assert!(
len >= pos + n_items,
"len = {len}; pos = {pos}; n_items = {n_items}"
);
// Mark them all and the first one after the current batch as processed.
for item in items.iter().skip(pos).take(n_items + 1) {
item.set_processed(true);
}
}
}
}
}
}
/// Timeline item data to test `TimelineItemDiffMinimizer`.
#[derive(Debug, Clone, Copy)]
struct TestTimelineItemData {
timeline_id: &'static str,
version: u8,
}
impl TimelineItemData for TestTimelineItemData {
fn timeline_id(&self) -> &str {
self.timeline_id
}
}
mod imp {
use std::cell::Cell;
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::TestTimelineItem)]
pub struct TestTimelineItem {
/// The version of the item.
#[property(get, set, construct)]
version: Cell<u8>,
/// Whether the item was processed in `apply_item_diff_list`.
#[property(get, set)]
processed: Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for TestTimelineItem {
const NAME: &'static str = "TestTimelineItem";
type Type = super::TestTimelineItem;
type ParentType = TimelineItem;
}
#[glib::derived_properties]
impl ObjectImpl for TestTimelineItem {}
impl TimelineItemImpl for TestTimelineItem {}
}
glib::wrapper! {
/// Timeline item to test `TimelineItemDiffMinimizer`.
pub struct TestTimelineItem(ObjectSubclass<imp::TestTimelineItem>) @extends TimelineItem;
}
impl TestTimelineItem {
fn new(data: &TestTimelineItemData) -> Self {
glib::Object::builder()
.property("timeline-id", data.timeline_id)
.property("version", data.version)
.build()
}
}
/// Test diff lists for each `VectorDiff` variant.
///
/// Although we will not use the minimizer for a single `VectorDiff`, this tests
/// at least the correctness of the code.
#[test]
fn process_single_vector_diff() {
let store = TestTimelineItemStore::default();
// Append.
let diff = VectorDiff::Append {
values: Vector::from([
TestTimelineItemData {
timeline_id: "a",
version: 0,
},
TestTimelineItemData {
timeline_id: "b",
version: 0,
},
TestTimelineItemData {
timeline_id: "c",
version: 0,
},
]),
};
assert!(store.can_minimize_diff_list(&[diff.clone(), diff.clone()]));
store.minimize_diff_list(vec![diff]);
let items = store.items();
assert_eq!(items.len(), 3);
assert_eq!(items[0].timeline_id(), "a");
assert_eq!(items[0].version(), 0);
assert!(items[0].processed());
assert_eq!(items[1].timeline_id(), "b");
assert_eq!(items[1].version(), 0);
assert!(items[1].processed());
assert_eq!(items[2].timeline_id(), "c");
assert_eq!(items[2].version(), 0);
assert!(items[2].processed());
store.reset_processed_items();
// Pop front.
let diff = VectorDiff::PopFront;
assert!(store.can_minimize_diff_list(&[diff.clone(), diff.clone()]));
store.minimize_diff_list(vec![diff]);
let items = store.items();
assert_eq!(items.len(), 2);
assert_eq!(items[0].timeline_id(), "b");
assert_eq!(items[0].version(), 0);
assert!(items[0].processed());
assert_eq!(items[1].timeline_id(), "c");
assert_eq!(items[1].version(), 0);
assert!(!items[1].processed());
store.reset_processed_items();
// Pop back.
let diff = VectorDiff::PopBack;
assert!(store.can_minimize_diff_list(&[diff.clone(), diff.clone()]));
store.minimize_diff_list(vec![diff]);
let items = store.items();
assert_eq!(items.len(), 1);
assert_eq!(items[0].timeline_id(), "b");
assert_eq!(items[0].version(), 0);
assert!(!items[0].processed());
store.reset_processed_items();
// Push front.
let diff = VectorDiff::PushFront {
value: TestTimelineItemData {
timeline_id: "a",
version: 1,
},
};
assert!(store.can_minimize_diff_list(&[diff.clone(), diff.clone()]));
store.minimize_diff_list(vec![diff]);
let items = store.items();
assert_eq!(items.len(), 2);
assert_eq!(items[0].timeline_id(), "a");
assert_eq!(items[0].version(), 1);
assert!(items[0].processed());
assert_eq!(items[1].timeline_id(), "b");
assert_eq!(items[1].version(), 0);
assert!(items[1].processed());
store.reset_processed_items();
// Push back.
let diff = VectorDiff::PushBack {
value: TestTimelineItemData {
timeline_id: "d",
version: 0,
},
};
assert!(store.can_minimize_diff_list(&[diff.clone(), diff.clone()]));
store.minimize_diff_list(vec![diff]);
let items = store.items();
assert_eq!(items.len(), 3);
assert_eq!(items[0].timeline_id(), "a");
assert_eq!(items[0].version(), 1);
assert!(!items[0].processed());
assert_eq!(items[1].timeline_id(), "b");
assert_eq!(items[1].version(), 0);
assert!(!items[1].processed());
assert_eq!(items[2].timeline_id(), "d");
assert_eq!(items[2].version(), 0);
assert!(items[2].processed());
store.reset_processed_items();
// Insert.
let diff = VectorDiff::Insert {
index: 2,
value: TestTimelineItemData {
timeline_id: "c",
version: 1,
},
};
assert!(store.can_minimize_diff_list(&[diff.clone(), diff.clone()]));
store.minimize_diff_list(vec![diff]);
let items = store.items();
assert_eq!(items.len(), 4);
assert_eq!(items[0].timeline_id(), "a");
assert_eq!(items[0].version(), 1);
assert!(!items[0].processed());
assert_eq!(items[1].timeline_id(), "b");
assert_eq!(items[1].version(), 0);
assert!(!items[1].processed());
assert_eq!(items[2].timeline_id(), "c");
assert_eq!(items[2].version(), 1);
assert!(items[2].processed());
assert_eq!(items[3].timeline_id(), "d");
assert_eq!(items[3].version(), 0);
assert!(items[3].processed());
store.reset_processed_items();
// Set same item (update).
let diff = VectorDiff::Set {
index: 1,
value: TestTimelineItemData {
timeline_id: "b",
version: 1,
},
};
assert!(store.can_minimize_diff_list(&[diff.clone(), diff.clone()]));
store.minimize_diff_list(vec![diff]);
let items = store.items();
assert_eq!(items.len(), 4);
assert_eq!(items[0].timeline_id(), "a");
assert_eq!(items[0].version(), 1);
assert!(!items[0].processed());
assert_eq!(items[1].timeline_id(), "b");
assert_eq!(items[1].version(), 1);
assert!(items[1].processed());
assert_eq!(items[2].timeline_id(), "c");
assert_eq!(items[2].version(), 1);
assert!(items[2].processed());
assert_eq!(items[3].timeline_id(), "d");
assert_eq!(items[3].version(), 0);
assert!(!items[3].processed());
store.reset_processed_items();
// Set new item (replace).
let diff = VectorDiff::Set {
index: 1,
value: TestTimelineItemData {
timeline_id: "b1",
version: 0,
},
};
assert!(store.can_minimize_diff_list(&[diff.clone(), diff.clone()]));
store.minimize_diff_list(vec![diff]);
let items = store.items();
assert_eq!(items.len(), 4);
assert_eq!(items[0].timeline_id(), "a");
assert_eq!(items[0].version(), 1);
assert!(!items[0].processed());
assert_eq!(items[1].timeline_id(), "b1");
assert_eq!(items[1].version(), 0);
assert!(items[1].processed());
assert_eq!(items[2].timeline_id(), "c");
assert_eq!(items[2].version(), 1);
assert!(items[2].processed());
assert_eq!(items[3].timeline_id(), "d");
assert_eq!(items[3].version(), 0);
assert!(!items[3].processed());
store.reset_processed_items();
// The following variants are not supported.
let diff = VectorDiff::Clear;
assert!(!store.can_minimize_diff_list(&[diff.clone(), diff.clone()]));
let diff = VectorDiff::Truncate { length: 2 };
assert!(!store.can_minimize_diff_list(&[diff.clone(), diff.clone()]));
let diff = VectorDiff::Reset {
values: Vector::new(),
};
assert!(!store.can_minimize_diff_list(&[diff.clone(), diff.clone()]));
// And empty list or with a single item cannot be minimized.
assert!(!store.can_minimize_diff_list(&[]));
assert!(!store.can_minimize_diff_list(&[VectorDiff::PopBack]));
}
/// Minimize only insertions or only removals.
#[test]
fn minimize_simple_diff() {
let store = TestTimelineItemStore::default();
// Minimize out of order insertions.
let diff_list = vec![
VectorDiff::PushBack {
value: TestTimelineItemData {
timeline_id: "b",
version: 0,
},
},
VectorDiff::PushBack {
value: TestTimelineItemData {
timeline_id: "d",
version: 0,
},
},
VectorDiff::PushFront {
value: TestTimelineItemData {
timeline_id: "a",
version: 0,
},
},
VectorDiff::Insert {
index: 2,
value: TestTimelineItemData {
timeline_id: "c",
version: 0,
},
},
];
assert!(store.can_minimize_diff_list(&diff_list));
let mut minimizer = TimelineItemDiffMinimizer::new(&store);
assert_eq!(store.items().len(), 0);
let old_item_ids = minimizer.load_items();
assert_eq!(old_item_ids.len(), 0);
let new_item_ids = minimizer.apply_diff_to_items(&old_item_ids, diff_list);
assert_eq!(new_item_ids.len(), 4);
assert_eq!(new_item_ids[0], "a");
assert_eq!(new_item_ids[1], "b");
assert_eq!(new_item_ids[2], "c");
assert_eq!(new_item_ids[3], "d");
let item_diff_list = minimizer.item_diff_list(&old_item_ids, &new_item_ids);
assert_eq!(item_diff_list.len(), 1);
assert_matches!(&item_diff_list[0], TimelineItemDiff::Splice(splice_diff));
assert_eq!(splice_diff.pos, 0);
assert_eq!(splice_diff.n_removals, 0);
assert_eq!(splice_diff.additions.len(), 4);
store.apply_item_diff_list(item_diff_list);
let items = store.items();
assert_eq!(items.len(), 4);
assert_eq!(items[0].timeline_id(), "a");
assert_eq!(items[0].version(), 0);
assert!(items[0].processed());
assert_eq!(items[1].timeline_id(), "b");
assert_eq!(items[1].version(), 0);
assert!(items[1].processed());
assert_eq!(items[2].timeline_id(), "c");
assert_eq!(items[2].version(), 0);
assert!(items[2].processed());
assert_eq!(items[3].timeline_id(), "d");
assert_eq!(items[3].version(), 0);
assert!(items[3].processed());
// Minimize out of order removals.
let diff_list = vec![
VectorDiff::PopBack,
VectorDiff::Remove { index: 1 },
VectorDiff::PopBack,
VectorDiff::PopFront,
];
assert!(store.can_minimize_diff_list(&diff_list));
let mut minimizer = TimelineItemDiffMinimizer::new(&store);
assert_eq!(store.items().len(), 4);
let old_item_ids = minimizer.load_items();
assert_eq!(old_item_ids.len(), 4);
let new_item_ids = minimizer.apply_diff_to_items(&old_item_ids, diff_list);
assert_eq!(new_item_ids.len(), 0);
let item_diff_list = minimizer.item_diff_list(&old_item_ids, &new_item_ids);
assert_eq!(item_diff_list.len(), 1);
assert_matches!(&item_diff_list[0], TimelineItemDiff::Splice(splice_diff));
assert_eq!(splice_diff.pos, 0);
assert_eq!(splice_diff.n_removals, 4);
assert_eq!(splice_diff.additions.len(), 0);
store.apply_item_diff_list(item_diff_list);
let items = store.items();
assert_eq!(items.len(), 0);
}
/// Minimize mix of insertions and removals.
#[test]
fn minimize_complex_diff() {
let store = TestTimelineItemStore::default();
// Populate the store first.
store.minimize_diff_list(vec![VectorDiff::Append {
values: Vector::from([
TestTimelineItemData {
timeline_id: "a",
version: 0,
},
TestTimelineItemData {
timeline_id: "c",
version: 0,
},
TestTimelineItemData {
timeline_id: "d",
version: 0,
},
TestTimelineItemData {
timeline_id: "e",
version: 0,
},
TestTimelineItemData {
timeline_id: "f",
version: 0,
},
TestTimelineItemData {
timeline_id: "g",
version: 0,
},
TestTimelineItemData {
timeline_id: "h",
version: 0,
},
]),
}]);
store.reset_processed_items();
let diff_list = vec![
VectorDiff::Remove { index: 1 },
VectorDiff::Insert {
index: 1,
value: TestTimelineItemData {
timeline_id: "b",
version: 0,
},
},
VectorDiff::Insert {
index: 2,
value: TestTimelineItemData {
timeline_id: "c",
version: 1,
},
},
VectorDiff::PopBack,
VectorDiff::Set {
index: 3,
value: TestTimelineItemData {
timeline_id: "d1",
version: 0,
},
},
VectorDiff::Set {
index: 4,
value: TestTimelineItemData {
timeline_id: "e",
version: 1,
},
},
];
let mut minimizer = TimelineItemDiffMinimizer::new(&store);
assert_eq!(store.items().len(), 7);
let old_item_ids = minimizer.load_items();
assert_eq!(old_item_ids.len(), 7);
assert_eq!(old_item_ids[0], "a");
assert_eq!(old_item_ids[1], "c");
assert_eq!(old_item_ids[2], "d");
assert_eq!(old_item_ids[3], "e");
assert_eq!(old_item_ids[4], "f");
assert_eq!(old_item_ids[5], "g");
assert_eq!(old_item_ids[6], "h");
let new_item_ids = minimizer.apply_diff_to_items(&old_item_ids, diff_list);
assert_eq!(new_item_ids.len(), 7);
assert_eq!(new_item_ids[0], "a");
assert_eq!(new_item_ids[1], "b");
assert_eq!(new_item_ids[2], "c");
assert_eq!(new_item_ids[3], "d1");
assert_eq!(new_item_ids[4], "e");
assert_eq!(new_item_ids[5], "f");
assert_eq!(new_item_ids[6], "g");
let item_diff_list = minimizer.item_diff_list(&old_item_ids, &new_item_ids);
assert_eq!(item_diff_list.len(), 5);
assert_matches!(&item_diff_list[0], TimelineItemDiff::Splice(splice_diff));
assert_eq!(splice_diff.pos, 1);
assert_eq!(splice_diff.n_removals, 0);
assert_eq!(splice_diff.additions.len(), 1);
assert_matches!(&item_diff_list[1], TimelineItemDiff::Update(update_diff));
assert_eq!(update_diff.pos, 2);
assert_eq!(update_diff.n_items, 1);
assert_matches!(&item_diff_list[2], TimelineItemDiff::Splice(splice_diff));
assert_eq!(splice_diff.pos, 3);
assert_eq!(splice_diff.n_removals, 1);
assert_eq!(splice_diff.additions.len(), 1);
assert_matches!(&item_diff_list[3], TimelineItemDiff::Update(update_diff));
assert_eq!(update_diff.pos, 4);
assert_eq!(update_diff.n_items, 1);
assert_matches!(&item_diff_list[4], TimelineItemDiff::Splice(splice_diff));
assert_eq!(splice_diff.pos, 7);
assert_eq!(splice_diff.n_removals, 1);
assert_eq!(splice_diff.additions.len(), 0);
store.apply_item_diff_list(item_diff_list);
let items = store.items();
assert_eq!(items.len(), 7);
assert_eq!(items[0].timeline_id(), "a");
assert_eq!(items[0].version(), 0);
assert!(!items[0].processed());
assert_eq!(items[1].timeline_id(), "b");
assert_eq!(items[1].version(), 0);
assert!(items[1].processed());
assert_eq!(items[2].timeline_id(), "c");
assert_eq!(items[2].version(), 1);
assert!(items[2].processed());
assert_eq!(items[3].timeline_id(), "d1");
assert_eq!(items[3].version(), 0);
assert!(items[3].processed());
assert_eq!(items[4].timeline_id(), "e");
assert_eq!(items[4].version(), 1);
assert!(items[4].processed());
assert_eq!(items[5].timeline_id(), "f");
assert_eq!(items[5].version(), 0);
assert!(items[5].processed());
assert_eq!(items[6].timeline_id(), "g");
assert_eq!(items[6].version(), 0);
assert!(!items[6].processed());
}
Loading…
Cancel
Save