Skip to content

Commit 2800a4c

Browse files
authored
perf: skip useless unmark op (#878)
* perf: skip useless unmark op if the target range doesn't contain given style key * chore: changeset
1 parent 3c8c76a commit 2800a4c

File tree

6 files changed

+210
-44
lines changed

6 files changed

+210
-44
lines changed

.changeset/gorgeous-pets-rhyme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"loro-crdt": patch
3+
"loro-crdt-map": patch
4+
---
5+
6+
perf: skip useless unmark op #878

crates/loro-internal/src/container/richtext/richtext_state.rs

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use self::query::{
3131

3232
use super::{
3333
style_range_map::{IterAnchorItem, StyleRangeMap, Styles},
34-
AnchorType, RichtextSpan, StyleOp,
34+
AnchorType, RichtextSpan, StyleKey, StyleOp,
3535
};
3636

3737
pub(crate) use crate::cursor::PosType;
@@ -1295,13 +1295,29 @@ impl RichtextState {
12951295
result
12961296
}
12971297

1298-
fn has_styles(&self) -> bool {
1298+
pub(crate) fn has_styles(&self) -> bool {
12991299
self.style_ranges
13001300
.as_ref()
13011301
.map(|x| x.has_style())
13021302
.unwrap_or(false)
13031303
}
13041304

1305+
pub(crate) fn range_has_style_key(
1306+
&mut self,
1307+
range: Range<usize>,
1308+
key: &StyleKey,
1309+
) -> bool {
1310+
self.check_cache();
1311+
let result = match self.style_ranges.as_ref() {
1312+
Some(s) => s.range_contains_key(range, key),
1313+
None => false,
1314+
};
1315+
self.check_cache();
1316+
result
1317+
}
1318+
1319+
/// Return the entity range and text styles at the given range.
1320+
/// If in the target range the leaves are not in the same span, the returned styles would be None
13051321
pub(crate) fn get_entity_range_and_text_styles_at_range(
13061322
&mut self,
13071323
range: Range<usize>,
@@ -1960,16 +1976,15 @@ impl RichtextState {
19601976
match &self.style_ranges {
19611977
Some(s) => {
19621978
let mut idx = current_entity_index;
1963-
Box::new(
1964-
s.iter_range(current_entity_index..end_entity_index)
1965-
.map(move |elem_slice| {
1966-
let len = elem_slice.end.unwrap_or(elem_slice.elem.len)
1967-
- elem_slice.start.unwrap_or(0);
1968-
let range = idx..idx + len;
1969-
idx += len;
1970-
(range, &elem_slice.elem.styles)
1971-
}),
1972-
)
1979+
Box::new(s.iter_range(current_entity_index..end_entity_index).map(
1980+
move |elem_slice| {
1981+
let len = elem_slice.end.unwrap_or(elem_slice.elem.len)
1982+
- elem_slice.start.unwrap_or(0);
1983+
let range = idx..idx + len;
1984+
idx += len;
1985+
(range, &elem_slice.elem.styles)
1986+
},
1987+
))
19731988
}
19741989
None => Box::new(Some((0..usize::MAX / 2, &*EMPTY_STYLES)).into_iter()),
19751990
};
@@ -1984,8 +1999,8 @@ impl RichtextState {
19841999
for span in self.tree.iter_range(start_cursor..end_cursor) {
19852000
match &span.elem {
19862001
RichtextStateChunk::Text(t) => {
1987-
let chunk_len = span.end.unwrap_or(span.elem.rle_len())
1988-
- span.start.unwrap_or(0); // length in rle_len (unicode_len)
2002+
let chunk_len =
2003+
span.end.unwrap_or(span.elem.rle_len()) - span.start.unwrap_or(0); // length in rle_len (unicode_len)
19892004
let mut processed_len = 0;
19902005

19912006
while processed_len < chunk_len {
@@ -2014,13 +2029,12 @@ impl RichtextState {
20142029
let slice_start = span.start.unwrap_or(0) + processed_len;
20152030
let slice_end = slice_start + take_len;
20162031

2017-
let text_content =
2018-
unicode_slice(t.as_str(), slice_start, slice_end)
2019-
.map_err(|_| LoroError::OutOfBound {
2020-
pos: slice_end,
2021-
len: t.unicode_len() as usize,
2022-
info: "Slice delta out of bound".into(),
2023-
})?;
2032+
let text_content = unicode_slice(t.as_str(), slice_start, slice_end)
2033+
.map_err(|_| LoroError::OutOfBound {
2034+
pos: slice_end,
2035+
len: t.unicode_len() as usize,
2036+
info: "Slice delta out of bound".into(),
2037+
})?;
20242038

20252039
let styles = cur_styles.as_ref().unwrap();
20262040
if let Some(last) = ans.last_mut() {

crates/loro-internal/src/container/richtext/style_range_map.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,37 @@ impl StyleRangeMap {
188188
}
189189
}
190190

191+
pub(crate) fn range_contains_key(&self, range: Range<usize>, key: &StyleKey) -> bool {
192+
if range.is_empty() || !self.has_style {
193+
return false;
194+
}
195+
196+
let mut query = self.tree.query::<LengthFinder>(&range.start).unwrap();
197+
let mut pos = range.start;
198+
loop {
199+
let elem = self.tree.get_elem(query.cursor.leaf).unwrap();
200+
if elem.styles.contains_key(key) {
201+
return true;
202+
}
203+
204+
let remaining_in_elem = elem.len - query.cursor.offset;
205+
let next_pos = pos + remaining_in_elem;
206+
if next_pos >= range.end {
207+
break;
208+
}
209+
210+
match self.tree.next_elem(query.cursor) {
211+
Some(next_cursor) => {
212+
pos = next_pos;
213+
query.cursor = next_cursor;
214+
}
215+
None => break,
216+
}
217+
}
218+
219+
false
220+
}
221+
191222
/// Insert entities at `pos` with length of `len`
192223
///
193224
/// # Internal

crates/loro-internal/src/handler.rs

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::{
44
container::{
55
idx::ContainerIdx,
66
list::list_op::{DeleteSpan, DeleteSpanWithId, ListOp},
7-
richtext::{richtext_state::PosType, RichtextState, StyleOp, TextStyleInfoFlag},
7+
richtext::{richtext_state::PosType, RichtextState, StyleKey, StyleOp, TextStyleInfoFlag},
88
},
99
cursor::{Cursor, Side},
1010
delta::{DeltaItem, Meta, StyleMeta, TreeExternalDiff},
@@ -1968,10 +1968,10 @@ impl TextHandler {
19681968
match &self.inner {
19691969
MaybeDetached::Detached(t) => {
19701970
let mut g = t.lock().unwrap();
1971-
self.mark_for_detached(&mut g.value, key, &value, start, end, pos_type, false)
1971+
self.mark_for_detached(&mut g.value, key, &value, start, end, pos_type)
19721972
}
19731973
MaybeDetached::Attached(a) => {
1974-
a.with_txn(|txn| self.mark_with_txn(txn, start, end, key, value, pos_type, false))
1974+
a.with_txn(|txn| self.mark_with_txn(txn, start, end, key, value, pos_type))
19751975
}
19761976
}
19771977
}
@@ -1984,9 +1984,9 @@ impl TextHandler {
19841984
start: usize,
19851985
end: usize,
19861986
pos_type: PosType,
1987-
is_delete: bool,
19881987
) -> Result<(), LoroError> {
19891988
let key: InternalString = key.into();
1989+
let is_delete = matches!(value, &LoroValue::Null);
19901990
if start >= end {
19911991
return Err(loro_common::LoroError::ArgErr(
19921992
"Start must be less than end".to_string().into_boxed_str(),
@@ -2005,11 +2005,18 @@ impl TextHandler {
20052005
state.get_entity_range_and_text_styles_at_range(start..end, pos_type);
20062006
if let Some(styles) = styles {
20072007
if styles.has_key_value(&key, value) {
2008-
// already has the same style, skip
20092008
return Ok(());
20102009
}
20112010
}
20122011

2012+
let has_target_style =
2013+
state.range_has_style_key(entity_range.clone(), &StyleKey::Key(key.clone()));
2014+
let missing_style_key = is_delete && !has_target_style;
2015+
2016+
if missing_style_key {
2017+
return Ok(());
2018+
}
2019+
20132020
let style_op = Arc::new(StyleOp {
20142021
lamport: 0,
20152022
peer: 0,
@@ -2043,10 +2050,9 @@ impl TextHandler {
20432050
start,
20442051
end,
20452052
pos_type,
2046-
true,
20472053
),
20482054
MaybeDetached::Attached(a) => a.with_txn(|txn| {
2049-
self.mark_with_txn(txn, start, end, key, LoroValue::Null, pos_type, true)
2055+
self.mark_with_txn(txn, start, end, key, LoroValue::Null, pos_type)
20502056
}),
20512057
}
20522058
}
@@ -2060,7 +2066,6 @@ impl TextHandler {
20602066
key: impl Into<InternalString>,
20612067
value: LoroValue,
20622068
pos_type: PosType,
2063-
is_delete: bool,
20642069
) -> LoroResult<()> {
20652070
if start >= end {
20662071
return Err(loro_common::LoroError::ArgErr(
@@ -2070,6 +2075,7 @@ impl TextHandler {
20702075

20712076
let inner = self.inner.try_attached_state()?;
20722077
let key: InternalString = key.into();
2078+
let is_delete = matches!(&value, &LoroValue::Null);
20732079

20742080
let mut doc_state = inner.doc.state.lock().unwrap();
20752081
let len = doc_state.with_state_mut(inner.container_idx, |state| {
@@ -2084,26 +2090,34 @@ impl TextHandler {
20842090
});
20852091
}
20862092

2087-
let (entity_range, skip, event_start, event_end) =
2088-
doc_state.with_state_mut(inner.container_idx, |state| {
2093+
let (entity_range, skip, missing_style_key, event_start, event_end) = doc_state
2094+
.with_state_mut(inner.container_idx, |state| {
20892095
let state = state.as_richtext_state_mut().unwrap();
20902096
let event_start = state.index_to_event_index(start, pos_type);
20912097
let event_end = state.index_to_event_index(end, pos_type);
20922098
let (entity_range, styles) =
20932099
state.get_entity_range_and_styles_at_range(start..end, pos_type);
20942100

2095-
let skip = match styles {
2096-
Some(styles) if styles.has_key_value(&key, &value) => {
2097-
// already has the same style, skip
2098-
true
2099-
}
2100-
_ => false,
2101-
};
2102-
2103-
(entity_range, skip, event_start, event_end)
2101+
let skip = styles
2102+
.as_ref()
2103+
.map(|styles| styles.has_key_value(&key, &value))
2104+
.unwrap_or(false);
2105+
let has_target_style = state.has_style_key_in_entity_range(
2106+
entity_range.clone(),
2107+
&StyleKey::Key(key.clone()),
2108+
);
2109+
let missing_style_key = is_delete && !has_target_style;
2110+
2111+
(
2112+
entity_range,
2113+
skip,
2114+
missing_style_key,
2115+
event_start,
2116+
event_end,
2117+
)
21042118
});
21052119

2106-
if skip {
2120+
if skip || missing_style_key {
21072121
return Ok(());
21082122
}
21092123

@@ -2269,7 +2283,6 @@ impl TextHandler {
22692283
key.deref(),
22702284
value,
22712285
PosType::Event,
2272-
false,
22732286
)?;
22742287
}
22752288
}
@@ -4449,7 +4462,7 @@ mod test {
44494462
.insert_with_txn(&mut txn, 0, "hello world", PosType::Unicode)
44504463
.unwrap();
44514464
handler
4452-
.mark_with_txn(&mut txn, 0, 5, "bold", true.into(), PosType::Event, false)
4465+
.mark_with_txn(&mut txn, 0, 5, "bold", true.into(), PosType::Event)
44534466
.unwrap();
44544467
txn.commit().unwrap();
44554468

crates/loro-internal/src/state/richtext_state.rs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{
1515
richtext_state::{
1616
DrainInfo, EntityRangeInfo, IterRangeItem, PosType, RichtextStateChunk,
1717
},
18-
AnchorType, RichtextState as InnerState, StyleOp, Styles,
18+
AnchorType, RichtextState as InnerState, StyleKey, StyleOp, Styles,
1919
},
2020
},
2121
delta::{StyleMeta, StyleMetaItem},
@@ -283,6 +283,74 @@ impl RichtextState {
283283
}
284284
}
285285

286+
#[cfg(test)]
287+
mod tests {
288+
use crate::{container::richtext::StyleKey, cursor::PosType, handler::HandlerTrait, LoroDoc};
289+
290+
#[test]
291+
fn has_style_key_in_entity_range_basic() {
292+
let loro = LoroDoc::new_auto_commit();
293+
let text = loro.get_text("text");
294+
text.insert(0, "abcdef", PosType::Unicode).unwrap();
295+
text.mark(1, 3, "bold", true.into(), PosType::Unicode)
296+
.unwrap();
297+
298+
let bold_key = StyleKey::Key("bold".into());
299+
let has_style = text
300+
.with_state(|state| {
301+
let st = state.as_richtext_state_mut().unwrap();
302+
let (entity_range, _) =
303+
st.get_entity_range_and_styles_at_range(1..3, PosType::Unicode);
304+
Ok(st.has_style_key_in_entity_range(entity_range, &bold_key))
305+
})
306+
.unwrap();
307+
assert!(has_style);
308+
309+
let missing = text
310+
.with_state(|state| {
311+
let st = state.as_richtext_state_mut().unwrap();
312+
let (entity_range, _) =
313+
st.get_entity_range_and_styles_at_range(4..5, PosType::Unicode);
314+
Ok(st.has_style_key_in_entity_range(entity_range, &bold_key))
315+
})
316+
.unwrap();
317+
assert!(!missing);
318+
}
319+
320+
#[test]
321+
fn has_style_key_in_entity_range_spans_elements() {
322+
let loro = LoroDoc::new_auto_commit();
323+
let text = loro.get_text("text");
324+
text.insert(0, "abcdefgh", PosType::Unicode).unwrap();
325+
text.mark(0, 2, "bold", true.into(), PosType::Unicode)
326+
.unwrap();
327+
text.mark(3, 5, "bold", true.into(), PosType::Unicode)
328+
.unwrap();
329+
330+
let bold_key = StyleKey::Key("bold".into());
331+
332+
let has_style_across_segments = text
333+
.with_state(|state| {
334+
let st = state.as_richtext_state_mut().unwrap();
335+
let (entity_range, _) =
336+
st.get_entity_range_and_styles_at_range(0..5, PosType::Unicode);
337+
Ok(st.has_style_key_in_entity_range(entity_range, &bold_key))
338+
})
339+
.unwrap();
340+
assert!(has_style_across_segments);
341+
342+
let gap_has_style = text
343+
.with_state(|state| {
344+
let st = state.as_richtext_state_mut().unwrap();
345+
let (entity_range, _) =
346+
st.get_entity_range_and_styles_at_range(6..7, PosType::Unicode);
347+
Ok(st.has_style_key_in_entity_range(entity_range, &bold_key))
348+
})
349+
.unwrap();
350+
assert!(!gap_has_style);
351+
}
352+
}
353+
286354
impl Clone for RichtextState {
287355
fn clone(&self) -> Self {
288356
Self {
@@ -725,6 +793,19 @@ impl RichtextState {
725793
}
726794
}
727795

796+
#[inline]
797+
pub(crate) fn has_styles(&mut self) -> bool {
798+
self.state.get_mut().has_styles()
799+
}
800+
801+
pub(crate) fn has_style_key_in_entity_range(
802+
&mut self,
803+
range: Range<usize>,
804+
key: &StyleKey,
805+
) -> bool {
806+
self.state.get_mut().range_has_style_key(range, key)
807+
}
808+
728809
/// Check if the content and style ranges are consistent.
729810
///
730811
/// Panic if inconsistent.

0 commit comments

Comments
 (0)