Skip to content

Commit c120da7

Browse files
authored
feat(sdk): Optimise how LinkedChunk::insert_gap_at works when inserting at first position
feat(sdk): Optimise how `LinkedChunk::insert_gap_at` works when inserting at first position
2 parents 8eafaa5 + 962c0bf commit c120da7

File tree

1 file changed

+112
-53
lines changed

1 file changed

+112
-53
lines changed

crates/matrix-sdk/src/event_cache/linked_chunk.rs

Lines changed: 112 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,30 @@ impl<Item, Gap, const CAP: usize> LinkedChunk<Item, Gap, CAP> {
197197
.chunk_mut(chunk_identifier)
198198
.ok_or(LinkedChunkError::InvalidChunkIdentifier { identifier: chunk_identifier })?;
199199

200+
// If `item_index` is 0, we don't want to split the current items chunk to
201+
// insert a new gap chunk, otherwise it would create an empty current items
202+
// chunk. Let's handle this case in particular.
203+
//
204+
// Of course this optimisation applies if there is a previous chunk. Remember
205+
// the invariant: a `Gap` cannot be the first chunk.
206+
if item_index == 0 && chunk.is_items() && chunk.previous.is_some() {
207+
let previous_chunk = chunk
208+
.previous_mut()
209+
// SAFETY: The `previous` chunk exists because we have tested
210+
// `chunk.previous.is_some()` in the `if` statement.
211+
.expect("Previous chunk must be present");
212+
213+
previous_chunk.insert_next(Chunk::new_gap_leaked(
214+
chunk_identifier_generator.generate_next().unwrap(),
215+
content,
216+
));
217+
218+
// We don't need to update `self.last` because we have inserted a new chunk
219+
// before `chunk`.
220+
221+
return Ok(());
222+
}
223+
200224
let chunk = match &mut chunk.content {
201225
ChunkContent::Gap(..) => {
202226
return Err(LinkedChunkError::ChunkIsAGap { identifier: chunk_identifier });
@@ -241,17 +265,21 @@ impl<Item, Gap, const CAP: usize> LinkedChunk<Item, Gap, CAP> {
241265
///
242266
/// Because the `chunk_identifier` can represent non-gap chunk, this method
243267
/// returns a `Result`.
268+
///
269+
/// This method returns a reference to the (first if many) newly created
270+
/// `Chunk` that contains the `items`.
244271
pub fn replace_gap_at<I>(
245272
&mut self,
246273
items: I,
247274
chunk_identifier: ChunkIdentifier,
248-
) -> Result<(), LinkedChunkError>
275+
) -> Result<&Chunk<Item, Gap, CAP>, LinkedChunkError>
249276
where
250277
I: IntoIterator<Item = Item>,
251278
I::IntoIter: ExactSizeIterator,
252279
{
253280
let chunk_identifier_generator = self.chunk_identifier_generator.clone();
254281
let chunk_ptr;
282+
let new_chunk_ptr;
255283

256284
{
257285
let chunk = self
@@ -269,8 +297,7 @@ impl<Item, Gap, const CAP: usize> LinkedChunk<Item, Gap, CAP> {
269297
// Insert a new items chunk…
270298
.insert_next(Chunk::new_items_leaked(
271299
chunk_identifier_generator.generate_next().unwrap(),
272-
))
273-
// … and insert the items.
300+
)) // … and insert the items.
274301
.push_items(items, &chunk_identifier_generator);
275302

276303
(
@@ -283,6 +310,11 @@ impl<Item, Gap, const CAP: usize> LinkedChunk<Item, Gap, CAP> {
283310
}
284311
};
285312

313+
new_chunk_ptr = chunk
314+
.next
315+
// SAFETY: A new `Chunk` has just been inserted, so it exists.
316+
.unwrap();
317+
286318
// Now that new items have been pushed, we can unlink the gap chunk.
287319
chunk.unlink();
288320

@@ -301,11 +333,16 @@ impl<Item, Gap, const CAP: usize> LinkedChunk<Item, Gap, CAP> {
301333

302334
// Re-box the chunk, and let Rust does its job.
303335
//
304-
// SAFETY: `chunk` is unlinked but it still exists in memory! We have its
305-
// pointer, which is valid and well aligned.
336+
// SAFETY: `chunk` is unlinked and not borrowed anymore. `LinkedChunk` doesn't
337+
// use it anymore, it's a leak. It is time to re-`Box` it and drop it.
306338
let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) };
307339

308-
Ok(())
340+
Ok(
341+
// SAFETY: `new_chunk_ptr` is valid, non-null and well-aligned. It's taken from
342+
// `chunk`, and that's how the entire `LinkedChunk` type works. Pointer construction
343+
// safety is guaranteed by `Chunk::new_items_leaked` and `Chunk::new_gap_leaked`.
344+
unsafe { new_chunk_ptr.as_ref() },
345+
)
309346
}
310347

311348
/// Get the chunk as a reference, from its identifier, if it exists.
@@ -334,23 +371,23 @@ impl<Item, Gap, const CAP: usize> LinkedChunk<Item, Gap, CAP> {
334371
}
335372
}
336373

337-
/// Search for a chunk, and return its identifier.
374+
/// Search backwards for a chunk, and return its identifier.
338375
pub fn chunk_identifier<'a, P>(&'a self, mut predicate: P) -> Option<ChunkIdentifier>
339376
where
340377
P: FnMut(&'a Chunk<Item, Gap, CAP>) -> bool,
341378
{
342379
self.rchunks().find_map(|chunk| predicate(chunk).then(|| chunk.identifier()))
343380
}
344381

345-
/// Search for an item, and return its position.
382+
/// Search backwards for an item, and return its position.
346383
pub fn item_position<'a, P>(&'a self, mut predicate: P) -> Option<Position>
347384
where
348385
P: FnMut(&'a Item) -> bool,
349386
{
350387
self.ritems().find_map(|(item_position, item)| predicate(item).then_some(item_position))
351388
}
352389

353-
/// Iterate over the chunks, backward.
390+
/// Iterate over the chunks, backwards.
354391
///
355392
/// It iterates from the last to the first chunk.
356393
pub fn rchunks(&self) -> LinkedChunkIterBackward<'_, Item, Gap, CAP> {
@@ -941,75 +978,60 @@ mod tests {
941978
};
942979

943980
macro_rules! assert_items_eq {
944-
( @_ [ $iterator:ident, $chunk_index:ident, $item_index:ident ] { [-] $( $rest:tt )* } { $( $accumulator:tt )* } ) => {
981+
( @_ [ $iterator:ident ] { [-] $( $rest:tt )* } { $( $accumulator:tt )* } ) => {
945982
assert_items_eq!(
946983
@_
947-
[ $iterator, $chunk_index, $item_index ]
984+
[ $iterator ]
948985
{ $( $rest )* }
949986
{
950987
$( $accumulator )*
951-
$chunk_index += 1;
988+
{
989+
let chunk = $iterator .next().expect("next chunk (expect gap)");
990+
assert!(chunk.is_gap(), "chunk ");
991+
}
952992
}
953993
)
954994
};
955995

956-
( @_ [ $iterator:ident, $chunk_index:ident, $item_index:ident ] { [ $( $item:expr ),* ] $( $rest:tt )* } { $( $accumulator:tt )* } ) => {
996+
( @_ [ $iterator:ident ] { [ $( $item:expr ),* ] $( $rest:tt )* } { $( $accumulator:tt )* } ) => {
957997
assert_items_eq!(
958998
@_
959-
[ $iterator, $chunk_index, $item_index ]
999+
[ $iterator ]
9601000
{ $( $rest )* }
9611001
{
9621002
$( $accumulator )*
963-
let _expected_chunk_identifier = $iterator .peek().unwrap().1.chunk_identifier();
964-
$(
965-
assert_matches!(
966-
$iterator .next(),
967-
Some((chunk_index, Position(chunk_identifier, item_index), & $item )) => {
968-
// Ensure the chunk index (from the enumeration) is correct.
969-
assert_eq!(chunk_index, $chunk_index);
970-
// Ensure the chunk identifier is the same for all items in this chunk.
971-
assert_eq!(chunk_identifier, _expected_chunk_identifier);
972-
// Ensure the item has the expected position.
973-
assert_eq!(item_index, $item_index);
974-
}
975-
);
976-
$item_index += 1;
977-
)*
978-
$item_index = 0;
979-
$chunk_index += 1;
1003+
{
1004+
let chunk = $iterator .next().expect("next chunk (expect items)");
1005+
assert!(chunk.is_items());
1006+
1007+
let ChunkContent::Items(items) = chunk.content() else { unreachable!() };
1008+
1009+
let mut items_iterator = items.iter();
1010+
1011+
$(
1012+
assert_eq!(items_iterator.next(), Some(& $item ));
1013+
)*
1014+
1015+
assert!(items_iterator.next().is_none(), "no more items");
1016+
}
9801017
}
9811018
)
9821019
};
9831020

984-
( @_ [ $iterator:ident, $chunk_index:ident, $item_index:ident ] {} { $( $accumulator:tt )* } ) => {
1021+
( @_ [ $iterator:ident ] {} { $( $accumulator:tt )* } ) => {
9851022
{
986-
let mut $chunk_index = 0;
987-
let mut $item_index = 0;
9881023
$( $accumulator )*
1024+
assert!( $iterator .next().is_none(), "no more chunks");
9891025
}
9901026
};
9911027

9921028
( $linked_chunk:expr, $( $all:tt )* ) => {
9931029
assert_items_eq!(
9941030
@_
995-
[ iterator, _chunk_index, _item_index ]
1031+
[ iterator ]
9961032
{ $( $all )* }
9971033
{
998-
let mut iterator = $linked_chunk
999-
.chunks()
1000-
.enumerate()
1001-
.filter_map(|(chunk_index, chunk)| match &chunk.content {
1002-
ChunkContent::Gap(..) => None,
1003-
ChunkContent::Items(items) => {
1004-
let identifier = chunk.identifier();
1005-
1006-
Some(items.iter().enumerate().map(move |(item_index, item)| {
1007-
(chunk_index, Position(identifier, item_index), item)
1008-
}))
1009-
}
1010-
})
1011-
.flatten()
1012-
.peekable();
1034+
let mut iterator = $linked_chunk.chunks();
10131035
}
10141036
)
10151037
}
@@ -1392,14 +1414,48 @@ mod tests {
13921414
assert_items_eq!(linked_chunk, ['a'] [-] ['b', 'c'] ['d', 'e', 'f']);
13931415
}
13941416

1395-
// Insert at the beginning of a chunk.
1417+
// Insert at the beginning of a chunk + it's the first chunk.
13961418
{
13971419
let position_of_a = linked_chunk.item_position(|item| *item == 'a').unwrap();
13981420
linked_chunk.insert_gap_at((), position_of_a)?;
13991421

1422+
// A new empty chunk is created as the first chunk.
14001423
assert_items_eq!(linked_chunk, [] [-] ['a'] [-] ['b', 'c'] ['d', 'e', 'f']);
14011424
}
14021425

1426+
// Insert at the beginning of a chunk.
1427+
{
1428+
let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
1429+
linked_chunk.insert_gap_at((), position_of_d)?;
1430+
1431+
// A new empty chunk is NOT created, i.e. `['d', 'e', 'f']` is not
1432+
// split into `[]` + `['d', 'e', 'f']` because it's a waste of
1433+
// space.
1434+
assert_items_eq!(linked_chunk, [] [-] ['a'] [-] ['b', 'c'] [-] ['d', 'e', 'f']);
1435+
}
1436+
1437+
// Insert in an empty chunk + it's the first chunk.
1438+
{
1439+
let position_of_first_empty_chunk = Position(ChunkIdentifier(0), 0);
1440+
assert_matches!(
1441+
linked_chunk.insert_gap_at((), position_of_first_empty_chunk),
1442+
Err(LinkedChunkError::InvalidItemIndex { index: 0 })
1443+
);
1444+
}
1445+
1446+
// Insert in an empty chunk.
1447+
{
1448+
// Replace a gap by empty items.
1449+
let gap_identifier = linked_chunk.chunk_identifier(Chunk::is_gap).unwrap();
1450+
let position = linked_chunk.replace_gap_at([], gap_identifier)?.first_position();
1451+
1452+
assert_items_eq!(linked_chunk, [] [-] ['a'] [-] ['b', 'c'] [] ['d', 'e', 'f']);
1453+
1454+
linked_chunk.insert_gap_at((), position)?;
1455+
1456+
assert_items_eq!(linked_chunk, [] [-] ['a'] [-] ['b', 'c'] [-] [] ['d', 'e', 'f']);
1457+
}
1458+
14031459
// Insert in a chunk that does not exist.
14041460
{
14051461
assert_matches!(
@@ -1445,7 +1501,9 @@ mod tests {
14451501
let gap_identifier = linked_chunk.chunk_identifier(Chunk::is_gap).unwrap();
14461502
assert_eq!(gap_identifier, ChunkIdentifier(1));
14471503

1448-
linked_chunk.replace_gap_at(['d', 'e', 'f', 'g', 'h'], gap_identifier)?;
1504+
let new_chunk =
1505+
linked_chunk.replace_gap_at(['d', 'e', 'f', 'g', 'h'], gap_identifier)?;
1506+
assert_eq!(new_chunk.identifier(), ChunkIdentifier(3));
14491507
assert_items_eq!(
14501508
linked_chunk,
14511509
['a', 'b'] ['d', 'e', 'f'] ['g', 'h'] ['l', 'm']
@@ -1463,7 +1521,8 @@ mod tests {
14631521
let gap_identifier = linked_chunk.chunk_identifier(Chunk::is_gap).unwrap();
14641522
assert_eq!(gap_identifier, ChunkIdentifier(5));
14651523

1466-
linked_chunk.replace_gap_at(['w', 'x', 'y', 'z'], gap_identifier)?;
1524+
let new_chunk = linked_chunk.replace_gap_at(['w', 'x', 'y', 'z'], gap_identifier)?;
1525+
assert_eq!(new_chunk.identifier(), ChunkIdentifier(6));
14671526
assert_items_eq!(
14681527
linked_chunk,
14691528
['a', 'b'] ['d', 'e', 'f'] ['g', 'h'] ['l', 'm'] ['w', 'x', 'y'] ['z']

0 commit comments

Comments
 (0)