diff --git a/crates/doc/src/id_index.rs b/crates/doc/src/id_index.rs index da36e952..cdb32c32 100644 --- a/crates/doc/src/id_index.rs +++ b/crates/doc/src/id_index.rs @@ -15,16 +15,72 @@ //! duplicated ids come from damaged files, which are cached afresh when opened. use std::collections::HashMap; +use std::hash::{BuildHasherDefault, Hasher}; use std::sync::{Arc, RwLock}; use crate::{Node, NodeId}; -/// Each id's index path; `None` for an id that appears more than once. -pub(crate) type Paths = HashMap>>; +/// Hasher for node ids: one multiply. Ids are small counters, not attacker-chosen keys, and the +/// default SipHash dominated the cost of building the cache. +#[derive(Default)] +pub struct IdHasher(u64); -/// Ids found by walking because the cache didn't have them, before the cache is rebuilt: each -/// rebuild costs a few walks, so rebuilding only after a few keeps a lookup's cost within a small -/// factor of a walk whatever the edit pattern (say, add an object, look it up, again and again). +impl Hasher for IdHasher { + fn write(&mut self, bytes: &[u8]) { + for &b in bytes { + self.0 = (self.0 ^ u64::from(b)).wrapping_mul(0x100_0000_01b3); + } + } + fn write_u64(&mut self, v: u64) { + self.0 = (self.0 ^ v).wrapping_mul(0x9e37_79b9_7f4a_7c15); + } + fn finish(&self) -> u64 { + self.0 ^ (self.0 >> 29) + } +} + +/// A map keyed by node id with [`IdHasher`]. +pub type IdMap = HashMap>; +/// Where each node is: its parent's entry (`u32::MAX` for a top-level layer) and its index among +/// its siblings. One flat table, so building it allocates nothing per node. +#[derive(Default)] +pub(crate) struct Paths { + /// Each id's entry in `at`; `None` for an id that appears more than once. + ids: IdMap>, + at: Vec<(u32, u32)>, +} + +impl Paths { + /// The index path of `id`: `Some(None)` when the id appears more than once, `None` when it + /// isn't in the table. + pub(crate) fn get(&self, id: &NodeId) -> Option>> { + let e = (*self.ids.get(id)?)?; + let mut path = Vec::new(); + let mut cur = e; + // Each step goes to a parent entry, made before its children: at most `at.len()` steps. + for _ in 0..=self.at.len() { + let Some(&(parent, i)) = self.at.get(cur as usize) else { return Some(None) }; + path.push(i as usize); + if parent == u32::MAX { + path.reverse(); + return Some(Some(path)); + } + cur = parent; + } + Some(None) + } + pub(crate) fn contains_key(&self, id: &NodeId) -> bool { + self.ids.contains_key(id) + } + pub(crate) fn len(&self) -> usize { + self.ids.len() + } +} + +/// Lookups that had to walk (the cache didn't have the id, or its path no longer checks out) +/// before the cache is rebuilt: a rebuild costs a few walks, so rebuilding only after a few keeps +/// a lookup's cost within a small factor of a walk whatever the edit pattern (say, delete an +/// object and look the next one up, again and again). const MISSES_PER_REBUILD: u32 = 4; /// The cache, shared by clones of a document (each checks every path it uses against its own tree). @@ -39,18 +95,18 @@ impl IdIndex { /// Rebuild the cache from `layers` (one walk) and return it. pub(crate) fn rebuild(&self, layers: &[Arc]) -> Arc { - fn walk(nodes: &[Arc], path: &mut Vec, out: &mut Paths) { + fn walk(nodes: &[Arc], parent: u32, out: &mut Paths) { for (i, n) in nodes.iter().enumerate() { - path.push(i); - out.entry(n.id).and_modify(|e| *e = None).or_insert_with(|| Some(path.clone())); + let e = out.at.len() as u32; + out.at.push((parent, i as u32)); + out.ids.entry(n.id).and_modify(|x| *x = None).or_insert(Some(e)); if let Some(ch) = n.children() { - walk(ch, path, out); + walk(ch, e, out); } - path.pop(); } } - let mut out = Paths::new(); - walk(layers, &mut Vec::new(), &mut out); + let mut out = Paths::default(); + walk(layers, u32::MAX, &mut out); let out = Arc::new(out); if let Ok(mut g) = self.0.write() { *g = Some(out.clone()); @@ -59,7 +115,8 @@ impl IdIndex { out } - /// Note an id found by walking that the cache didn't have; true when it's time to rebuild. + /// Note a lookup that had to walk (an id the cache didn't have, or a path that no longer + /// checks out); true when it's time to rebuild. pub(crate) fn missed(&self) -> bool { self.1.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1 >= MISSES_PER_REBUILD } diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 6e529106..831d67ea 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -772,21 +772,15 @@ impl Document { // The cached path, while the node there still has the id ([`id_index`]). let cached = self.index.get(); match cached.as_ref().and_then(|m| m.get(&id)) { - Some(Some(p)) if self.node_at(p).is_some_and(|n| n.id == id) => return Some(p.clone()), + Some(Some(p)) if self.node_at(&p).is_some_and(|n| n.id == id) => return Some(p), Some(None) => return self.walk_index_path(id), - Some(Some(_)) => { - // Stale: the tree changed. Rebuild, then answer from it (what a walk gives now). - return match self.index.rebuild(&self.layers).get(&id) { - Some(Some(p)) => Some(p.clone()), - Some(None) => self.walk_index_path(id), - None => None, - }; - } - None => {} + _ => {} } - // Not cached: deleted, added since, or no cache yet. + // A stale path (the tree changed), an id the cache doesn't have (deleted or added since), + // or no cache yet: walk, and rebuild now and then. let found = self.walk_index_path(id); - if cached.is_none() || (found.is_some() && self.index.missed()) { + let stale = cached.as_ref().is_some_and(|m| m.contains_key(&id)); + if cached.is_none() || ((stale || found.is_some()) && self.index.missed()) { self.index.rebuild(&self.layers); } found @@ -842,8 +836,8 @@ impl Document { // share of the document, one walk is quicker than checking each cached path. if let Some(cache) = self.index.get().filter(|c| want.len().saturating_mul(8) < c.len()) { want.retain(|id| match cache.get(id) { - Some(Some(p)) if self.node_at(p).is_some_and(|n| n.id == *id) => { - out.insert(*id, p.clone()); + Some(Some(p)) if self.node_at(&p).is_some_and(|n| n.id == *id) => { + out.insert(*id, p); false } _ => true,