diff --git a/.changeset/strange-mirrors-visit.md b/.changeset/strange-mirrors-visit.md new file mode 100644 index 00000000..a80cf927 --- /dev/null +++ b/.changeset/strange-mirrors-visit.md @@ -0,0 +1,6 @@ +--- +"loro-wasm": patch +"loro-crdt": patch +--- + +Perf(wasm) cache text.toDelta diff --git a/crates/loro-internal/src/event.rs b/crates/loro-internal/src/event.rs index aff62e83..991a98c1 100644 --- a/crates/loro-internal/src/event.rs +++ b/crates/loro-internal/src/event.rs @@ -4,7 +4,6 @@ use itertools::Itertools; use loro_delta::{array_vec::ArrayVec, delta_trait::DeltaAttr, DeltaItem, DeltaRope}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; -use tracing::trace; use crate::{ container::richtext::richtext_state::RichtextStateChunk, @@ -457,12 +456,11 @@ impl Diff { /// Transform the cursor based on this diff pub(crate) fn transform_cursor(&self, pos: usize, left_prior: bool) -> usize { - let ans = match self { + match self { Diff::List(list) => list.transform_pos(pos, left_prior), Diff::Text(text) => text.transform_pos(pos, left_prior), _ => pos, - }; - ans + } } } diff --git a/crates/loro-internal/src/handler.rs b/crates/loro-internal/src/handler.rs index 348a766a..5e3f7e99 100644 --- a/crates/loro-internal/src/handler.rs +++ b/crates/loro-internal/src/handler.rs @@ -1186,6 +1186,20 @@ impl TextHandler { } } + /// Get the version id of the richtext + /// + /// This can be used to detect whether the richtext is changed + pub fn version_id(&self) -> usize { + match &self.inner { + MaybeDetached::Detached(_) => { + unimplemented!("Detached text container does not have version id") + } + MaybeDetached::Attached(a) => { + a.with_state(|state| state.as_richtext_state_mut().unwrap().get_version_id()) + } + } + } + pub fn get_richtext_value(&self) -> LoroValue { match &self.inner { MaybeDetached::Detached(t) => { diff --git a/crates/loro-internal/src/state/richtext_state.rs b/crates/loro-internal/src/state/richtext_state.rs index 8ae22d43..200a0cee 100644 --- a/crates/loro-internal/src/state/richtext_state.rs +++ b/crates/loro-internal/src/state/richtext_state.rs @@ -37,7 +37,10 @@ use super::ContainerState; pub struct RichtextState { idx: ContainerIdx, config: Arc>, - pub(crate) state: LazyLoad, + state: LazyLoad, + /// This is used to indicate whether the richtext state is changed, so the downstream has an easy way to cache + /// NOTE: We need to ensure the invariance that the version id is always increased when the richtext state is changed + version_id: usize, } struct Pos { @@ -52,9 +55,23 @@ impl RichtextState { idx, config, state: LazyLoad::Src(Default::default()), + version_id: 0, } } + #[inline] + fn update_version(&mut self) { + self.version_id = self.version_id.wrapping_add(1); + } + + /// Get the version id of the richtext + /// + /// This can be used to detect whether the richtext is changed + #[inline] + pub fn get_version_id(&self) -> usize { + self.version_id + } + /// Get the text content of the richtext /// /// This uses `mut` because we may need to build the state from snapshot @@ -84,6 +101,7 @@ impl RichtextState { style_starts: &mut FxHashMap, Pos>, style: &Arc, ) -> Pos { + self.update_version(); match style_starts.remove(style) { Some(x) => x, None => { @@ -213,6 +231,7 @@ impl Clone for RichtextState { idx: self.idx, config: self.config.clone(), state: self.state.clone(), + version_id: self.version_id, } } } @@ -244,6 +263,7 @@ impl ContainerState for RichtextState { _txn: &Weak>>, _state: &Weak>, ) -> Diff { + self.update_version(); let InternalDiff::RichtextRaw(richtext) = diff else { unreachable!() }; @@ -425,6 +445,7 @@ impl ContainerState for RichtextState { _txn: &Weak>>, _state: &Weak>, ) { + self.update_version(); let InternalDiff::RichtextRaw(richtext) = diff else { unreachable!() }; @@ -511,6 +532,7 @@ impl ContainerState for RichtextState { } fn apply_local_op(&mut self, r_op: &RawOp, op: &Op) -> LoroResult<()> { + self.update_version(); match &op.content { crate::op::InnerContent::List(l) => match l { list_op::InnerListOp::Insert { slice: _, pos: _ } => { @@ -634,6 +656,7 @@ impl ContainerState for RichtextState { #[doc = " Restore the state to the state represented by the ops that exported by `get_snapshot_ops`"] fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) { + self.update_version(); assert_eq!(ctx.mode, EncodeMode::Snapshot); let mut loader = RichtextStateLoader::default(); let mut id_to_style = FxHashMap::default(); diff --git a/crates/loro-internal/src/undo.rs b/crates/loro-internal/src/undo.rs index 886d09d1..26b80194 100644 --- a/crates/loro-internal/src/undo.rs +++ b/crates/loro-internal/src/undo.rs @@ -735,7 +735,7 @@ pub(crate) fn undo( let next = if i + 1 < spans.len() { spans[i + 1].0.id_last().into() } else { - match last_frontiers_or_last_bi.clone() { + match last_frontiers_or_last_bi { Either::Left(last_frontiers) => last_frontiers.clone(), Either::Right(right) => break 'block right, } diff --git a/crates/loro-wasm/src/convert.rs b/crates/loro-wasm/src/convert.rs index b044ddbb..65b157ea 100644 --- a/crates/loro-wasm/src/convert.rs +++ b/crates/loro-wasm/src/convert.rs @@ -316,7 +316,12 @@ fn map_delta_to_js(value: &ResolvedMapDelta, doc: &Arc) -> JsValue { pub(crate) fn handler_to_js_value(handler: Handler, doc: Option>) -> JsValue { match handler { - Handler::Text(t) => LoroText { handler: t, doc }.into(), + Handler::Text(t) => LoroText { + handler: t, + doc, + delta_cache: None, + } + .into(), Handler::Map(m) => LoroMap { handler: m, doc }.into(), Handler::List(l) => LoroList { handler: l, doc }.into(), Handler::Tree(t) => LoroTree { handler: t, doc }.into(), diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index 9b628daa..4855fa85 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -588,6 +588,7 @@ impl Loro { Ok(LoroText { handler: text, doc: Some(self.0.clone()), + delta_cache: None, }) } @@ -721,6 +722,7 @@ impl Loro { LoroText { handler: richtext, doc: Some(self.0.clone()), + delta_cache: None, } .into() } @@ -1348,6 +1350,7 @@ fn convert_container_path_to_js_value(path: &[(ContainerID, Index)]) -> JsValue pub struct LoroText { handler: TextHandler, doc: Option>, + delta_cache: Option<(usize, JsValue)>, } #[derive(Serialize, Deserialize)] @@ -1367,6 +1370,7 @@ impl LoroText { Self { handler: TextHandler::new_detached(), doc: None, + delta_cache: None, } } @@ -1480,10 +1484,19 @@ impl LoroText { /// console.log(text.toDelta()); // [ { insert: 'Hello', attributes: { bold: true } } ] /// ``` #[wasm_bindgen(js_name = "toDelta")] - pub fn to_delta(&self) -> JsStringDelta { + pub fn to_delta(&mut self) -> JsStringDelta { + let version = self.handler.version_id(); + if let Some((v, delta)) = self.delta_cache.as_ref() { + if *v == version { + return delta.clone().into(); + } + } + let delta = self.handler.get_richtext_value(); let value: JsValue = delta.into(); - value.into() + let ans: JsStringDelta = value.clone().into(); + self.delta_cache = Some((version, value)); + ans } /// Get the container id of the text.