OXYGENHUB

A state index for the working copy's base f9575918477f

Each workspace keeps its working copy's base as a local state index
(oxygen#41): the change metadata, tree and tree view, replace records,
and every file's deferred history with its graph key, in a flat binary
layout whose tree, view and histories decode side by side. A snapshot
reads it instead of replaying history when every dependency it asks for
is in it and their heads are its heads; otherwise it loads history and
writes the index for next time. It is derived only: the same state
whatever order history was read in, and a test build checks every read
against a full load.

The tree's own maps use the fast hasher, and a node's placements and
modes (nearly always one) are held without an allocation. Deferred
entries keep their graph key once worked out.

On the Linux tip the base now loads in 0.22 to 0.24 s rather than 1.0 s
(history 0.70 s, tree view 0.31 s); a one-file status is about 1.9 s.

refs #41
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 10:15 UTC
Signature
✔ signed by Andy McCall
Hash
f9575918477f3f804d14bddb1e3d4ef61067c4697531d5238942a87c4b227f3a
Depends on
0bdc3f91a39f 2306a4d24779 252e8cb23fbc 75167672ebcd 86e1824f2d0b 982864068a59 a4fad5b085c7 cc7a00565161 cdb5beba7f05 e83354cdac22 e8d38e47e9d7 f6451fbbae3f ff0af1ee299d
Depended on by
c2e1d09047db 74b4a3c62ea4 bf0247ee3590 ac03130a2b95 5552e69c7325 79b5a90ffd43 d45dab0484ea fddf64baa3d2 fb5b6181618a 638fb21420d7 f9466021286a ce14f0d68b15 4cce912a0953 36e51aa806c6 3f1b8fe25ec9 05059d434aca
In bookmarks
agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
In tags
v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
24 atoms, 1 tree operation, 6 files

Browse the tree at this change

added crates/oxy-core/src/flat.rs +137 −0
1 +
//! A flat binary layout for local indexes (oxygen#41): fixed-width
2 +
//! little-endian numbers and length-prefixed bytes, read in one pass
3 +
//! without building a generic value tree. Only for caches this repository
4 +
//! derives and can derive again: never exchanged, never trusted beyond the
5 +
//! key they are stored under, and a reader that finds anything amiss fails
6 +
//! so the caller rebuilds.
7 +
8 +
use crate::hash::ObjectRef;
9 +
use crate::objects::ObjectError;
10 +
11 +
#[derive(Default)]
12 +
pub struct Writer(pub Vec<u8>);
13 +
14 +
impl Writer {
15 +
    pub fn u8(&mut self, v: u8) {
16 +
        self.0.push(v);
17 +
    }
18 +
    pub fn u32(&mut self, v: u32) {
19 +
        self.0.extend_from_slice(&v.to_le_bytes());
20 +
    }
21 +
    pub fn u64(&mut self, v: u64) {
22 +
        self.0.extend_from_slice(&v.to_le_bytes());
23 +
    }
24 +
    /// A count, which must fit in 32 bits.
25 +
    pub fn count(&mut self, n: usize) {
26 +
        self.u32(u32::try_from(n).expect("an index count fits in 32 bits"));
27 +
    }
28 +
    pub fn fixed(&mut self, b: &[u8]) {
29 +
        self.0.extend_from_slice(b);
30 +
    }
31 +
    pub fn bytes(&mut self, b: &[u8]) {
32 +
        self.count(b.len());
33 +
        self.0.extend_from_slice(b);
34 +
    }
35 +
    pub fn object(&mut self, r: &ObjectRef) {
36 +
        self.fixed(&r.0);
37 +
    }
38 +
    pub fn opt_object(&mut self, r: &Option<ObjectRef>) {
39 +
        match r {
40 +
            None => self.u8(0),
41 +
            Some(r) => {
42 +
                self.u8(1);
43 +
                self.object(r);
44 +
            }
45 +
        }
46 +
    }
47 +
}
48 +
49 +
pub struct Reader<'a> {
50 +
    buf: &'a [u8],
51 +
    pos: usize,
52 +
}
53 +
54 +
fn short() -> ObjectError {
55 +
    ObjectError::Schema("index: truncated".into())
56 +
}
57 +
58 +
impl<'a> Reader<'a> {
59 +
    pub fn new(buf: &'a [u8]) -> Reader<'a> {
60 +
        Reader { buf, pos: 0 }
61 +
    }
62 +
    pub fn at_end(&self) -> bool {
63 +
        self.pos == self.buf.len()
64 +
    }
65 +
    fn take(&mut self, n: usize) -> Result<&'a [u8], ObjectError> {
66 +
        let end = self.pos.checked_add(n).ok_or_else(short)?;
67 +
        let s = self.buf.get(self.pos..end).ok_or_else(short)?;
68 +
        self.pos = end;
69 +
        Ok(s)
70 +
    }
71 +
    pub fn u8(&mut self) -> Result<u8, ObjectError> {
72 +
        Ok(self.take(1)?[0])
73 +
    }
74 +
    pub fn u32(&mut self) -> Result<u32, ObjectError> {
75 +
        Ok(u32::from_le_bytes(self.fixed::<4>()?))
76 +
    }
77 +
    pub fn u64(&mut self) -> Result<u64, ObjectError> {
78 +
        Ok(u64::from_le_bytes(self.fixed::<8>()?))
79 +
    }
80 +
    /// A count; checked against what is left, so a corrupt one cannot ask
81 +
    /// for more room than the index could hold.
82 +
    pub fn count(&mut self) -> Result<usize, ObjectError> {
83 +
        let n = self.u32()? as usize;
84 +
        if n > self.buf.len() - self.pos {
85 +
            return Err(short());
86 +
        }
87 +
        Ok(n)
88 +
    }
89 +
    pub fn fixed<const N: usize>(&mut self) -> Result<[u8; N], ObjectError> {
90 +
        Ok(self.take(N)?.try_into().expect("N bytes"))
91 +
    }
92 +
    pub fn bytes(&mut self) -> Result<&'a [u8], ObjectError> {
93 +
        let n = self.u32()? as usize;
94 +
        self.take(n)
95 +
    }
96 +
    pub fn text(&mut self) -> Result<String, ObjectError> {
97 +
        String::from_utf8(self.bytes()?.to_vec())
98 +
            .map_err(|_| ObjectError::Schema("index: text is not UTF-8".into()))
99 +
    }
100 +
    pub fn object(&mut self) -> Result<ObjectRef, ObjectError> {
101 +
        Ok(ObjectRef(self.fixed::<32>()?))
102 +
    }
103 +
    pub fn opt_object(&mut self) -> Result<Option<ObjectRef>, ObjectError> {
104 +
        Ok(match self.u8()? {
105 +
            0 => None,
106 +
            _ => Some(self.object()?),
107 +
        })
108 +
    }
109 +
}
110 +
111 +
#[cfg(test)]
112 +
mod tests {
113 +
    use super::*;
114 +
115 +
    #[test]
116 +
    fn numbers_and_bytes_round_trip_and_truncation_fails() {
117 +
        let mut w = Writer::default();
118 +
        w.u8(7);
119 +
        w.u32(70_000);
120 +
        w.u64(1 << 40);
121 +
        w.bytes(b"path/name");
122 +
        w.opt_object(&Some(ObjectRef([3; 32])));
123 +
        w.opt_object(&None);
124 +
        let mut r = Reader::new(&w.0);
125 +
        assert_eq!(r.u8().unwrap(), 7);
126 +
        assert_eq!(r.u32().unwrap(), 70_000);
127 +
        assert_eq!(r.u64().unwrap(), 1 << 40);
128 +
        assert_eq!(r.text().unwrap(), "path/name");
129 +
        assert_eq!(r.opt_object().unwrap(), Some(ObjectRef([3; 32])));
130 +
        assert_eq!(r.opt_object().unwrap(), None);
131 +
        assert!(r.at_end());
132 +
        let mut r = Reader::new(&w.0[..w.0.len() - 3]);
133 +
        r.fixed::<13>().unwrap();
134 +
        assert!(r.text().is_ok());
135 +
        assert!(r.opt_object().is_err());
136 +
    }
137 +
}
modified crates/oxy-core/src/lib.rs +1 −0
…
8 unchanged lines
9 9
pub mod diff;
10 10
pub mod durable;
11 11
pub mod fasthash;
12 +
pub mod flat;
12 13
pub mod graph;
13 14
pub mod hash;
14 15
pub mod identity;
…
16 unchanged lines
modified crates/oxy-core/src/repo/mod.rs +67 −0
…
1102 unchanged lines
1103 1103
            .put(ObjectType::Blob, &Self::blob_for(content).encode())?)
1104 1104
    }
1105 1105
1106 +
    /// This workspace's state index (oxygen#41): its working copy's base,
1107 +
    /// saved so a snapshot reads it rather than replaying history.
1108 +
    fn state_index_path(&self) -> PathBuf {
1109 +
        self.oxy
1110 +
            .join("index")
1111 +
            .join(format!("state-{}", self.workspace.to_hex()))
1112 +
    }
1113 +
1114 +
    /// The history-only state of `deps` (as [`Repo::load_state_history`]),
1115 +
    /// from this workspace's state index when it holds that history: every
1116 +
    /// one of `deps` is in it and their heads are its heads. Otherwise it
1117 +
    /// is loaded from history and indexed for next time.
1118 +
    pub fn load_state_indexed(&self, deps: &[ChangeHash]) -> Result<State> {
1119 +
        if let Some(st) = self.read_state_index(deps) {
1120 +
            #[cfg(test)]
1121 +
            assert_eq!(
1122 +
                st.write_index(),
1123 +
                self.load_state_history(deps)?.write_index(),
1124 +
                "a state index holds the state history gives"
1125 +
            );
1126 +
            return Ok(st);
1127 +
        }
1128 +
        let st = self.load_state_history(deps)?;
1129 +
        self.write_state_index(deps, &st);
1130 +
        Ok(st)
1131 +
    }
1132 +
1133 +
    fn read_state_index(&self, deps: &[ChangeHash]) -> Option<State> {
1134 +
        let _t = crate::timing::phase("load: state index");
1135 +
        let bytes = fs::read(self.state_index_path()).ok()?;
1136 +
        let mut r = crate::flat::Reader::new(&bytes);
1137 +
        let mut heads = Vec::new();
1138 +
        for _ in 0..r.count().ok()? {
1139 +
            heads.push(r.object().ok()?);
1140 +
        }
1141 +
        let n = 4 + 32 * heads.len();
1142 +
        let st = State::read_index(&bytes[n..]).ok()?;
1143 +
        if !deps.iter().all(|d| st.contains(d)) {
1144 +
            return None;
1145 +
        }
1146 +
        let mut have = st.maximal(deps);
1147 +
        have.sort();
1148 +
        (have == heads).then_some(st)
1149 +
    }
1150 +
1151 +
    /// Save `st`, the history-only state of `deps`, as this workspace's
1152 +
    /// state index; quietly nothing when it cannot be (it is only a cache).
1153 +
    pub(crate) fn write_state_index(&self, deps: &[ChangeHash], st: &State) {
1154 +
        let _t = crate::timing::phase("write: state index");
1155 +
        let Some(body) = st.write_index() else {
1156 +
            return;
1157 +
        };
1158 +
        let mut heads = st.maximal(deps);
1159 +
        heads.sort();
1160 +
        let mut w = crate::flat::Writer::default();
1161 +
        w.count(heads.len());
1162 +
        for h in &heads {
1163 +
            w.object(h);
1164 +
        }
1165 +
        w.fixed(&body);
1166 +
        let path = self.state_index_path();
1167 +
        let tmp = path.with_extension("tmp");
1168 +
        let _ = fs::create_dir_all(self.oxy.join("index"))
1169 +
            .and_then(|_| fs::write(&tmp, &w.0))
1170 +
            .and_then(|_| fs::rename(&tmp, &path));
1171 +
    }
1172 +
1106 1173
    fn locks_dir(&self) -> PathBuf {
1107 1174
        self.oxy.join("locks")
1108 1175
    }
…
650 unchanged lines
modified crates/oxy-core/src/repo/snapshot.rs +1 −1
…
305 unchanged lines
306 306
        // manifest already knows equal to it.
307 307
        let mut base = {
308 308
            let _t = crate::timing::phase("snapshot: load base history");
309 -
            self.load_state_history(&old_wc.deps)?
309 +
            self.load_state_indexed(&old_wc.deps)?
310 310
        };
311 311
        let view = {
312 312
            let _t = crate::timing::phase("snapshot: tree view");
…
2208 unchanged lines
modified crates/oxy-core/src/state.rs +330 −4
…
85 unchanged lines
86 86
    /// The text blob and line count of each insert in `edits` left without
87 87
    /// its lines, in order: read when the graph is built.
88 88
    texts: Vec<TextRef>,
89 +
    /// Its graph key, once worked out (or read from an index); reset by
90 +
    /// any edit added.
91 +
    key: std::sync::OnceLock<[u8; 32]>,
89 92
}
90 93
91 94
#[derive(Clone)]
92 95
enum DeferredBase {
93 96
    Inline(Value),
94 97
    Blob(ObjectRef),
98 +
}
99 +
100 +
/// A local index of a history-only state (oxygen#41), in the flat layout
101 +
/// of [`crate::flat`]: its change metadata, tree (with its view), replace
102 +
/// records and every file's deferred history with its graph key, so a
103 +
/// command reads it rather than replaying history. Derived only: written
104 +
/// from a state loaded the long way, and read back as the same state.
105 +
impl State {
106 +
    const INDEX_MAGIC: &'static [u8] = b"oxy state index 1\n";
107 +
108 +
    /// The index of this state, or `None` when it has anything an index
109 +
    /// does not hold: a built graph, or text in memory (a change added
110 +
    /// whole rather than as its skeleton).
111 +
    pub fn write_index(&self) -> Option<Vec<u8>> {
112 +
        use crate::flat::Writer;
113 +
        let deferred = self.deferred.as_ref()?;
114 +
        if !self.files.is_empty() {
115 +
            return None;
116 +
        }
117 +
        let atom = |w: &mut Writer, a: &AtomRef| match a {
118 +
            AtomRef::Start => w.u8(0),
119 +
            AtomRef::End => w.u8(1),
120 +
            AtomRef::Atom { change, index } => {
121 +
                w.u8(2);
122 +
                w.object(change);
123 +
                w.u32(*index);
124 +
            }
125 +
        };
126 +
        let mut w = Writer::default();
127 +
        w.fixed(Self::INDEX_MAGIC);
128 +
        let mut changes: Vec<(&ChangeHash, &Arc<ChangeMeta>)> = self.changes.iter().collect();
129 +
        changes.sort_by_key(|(h, _)| **h);
130 +
        w.count(changes.len());
131 +
        for (h, m) in changes {
132 +
            w.object(h);
133 +
            w.fixed(&m.change_id.0);
134 +
            w.count(m.deps.len());
135 +
            for d in &m.deps {
136 +
                w.object(d);
137 +
            }
138 +
            w.object(&m.author);
139 +
            w.u64(m.hlc.wall_ms);
140 +
            w.u32(m.hlc.logical);
141 +
            w.bytes(m.message.as_bytes());
142 +
            w.u8(m.kind.tag() as u8);
143 +
        }
144 +
        // The tree and the files' histories as blocks of their own, so a
145 +
        // reader decodes them side by side.
146 +
        let mut tw = Writer::default();
147 +
        self.tree.write_flat(&mut tw);
148 +
        w.bytes(&tw.0);
149 +
        let mut replaces: Vec<(&NodeId, &Vec<ReplaceRecord>)> = self.replaces.iter().collect();
150 +
        replaces.sort_by_key(|(n, _)| **n);
151 +
        w.count(replaces.len());
152 +
        for (n, recs) in replaces {
153 +
            w.fixed(&n.0);
154 +
            w.count(recs.len());
155 +
            // In any order: which record is live does not depend on it.
156 +
            let mut recs: Vec<&ReplaceRecord> = recs.iter().collect();
157 +
            recs.sort();
158 +
            for (c, old, new) in recs {
159 +
                w.object(c);
160 +
                w.opt_object(old);
161 +
                w.opt_object(new);
162 +
            }
163 +
        }
164 +
        let outer = w;
165 +
        let mut w = Writer::default();
166 +
        let mut nodes: Vec<(&NodeId, &Arc<Deferred>)> = deferred.iter().collect();
167 +
        nodes.sort_by_key(|(n, _)| **n);
168 +
        w.count(nodes.len());
169 +
        for (n, d) in nodes {
170 +
            w.fixed(&n.0);
171 +
            match &d.base {
172 +
                None => w.u8(0),
173 +
                Some(DeferredBase::Blob(r)) => {
174 +
                    w.u8(1);
175 +
                    w.object(r);
176 +
                }
177 +
                Some(DeferredBase::Inline(v)) => {
178 +
                    w.u8(2);
179 +
                    w.bytes(&cbor::encode(v));
180 +
                }
181 +
            }
182 +
            // Each change's edits with the texts of its inserts, sorted by
183 +
            // change: a graph does not depend on the order changes applied
184 +
            // in, and the index is then the same however history was read.
185 +
            let mut texts = d.texts.iter();
186 +
            let mut per_change: Vec<(&ChangeHash, &Vec<Edit>, Vec<&TextRef>)> = Vec::new();
187 +
            for (c, edits) in &d.edits {
188 +
                let n = edits
189 +
                    .iter()
190 +
                    .filter(|e| matches!(e, Edit::Insert { lines, .. } if lines.is_empty()))
191 +
                    .count();
192 +
                per_change.push((c, edits, texts.by_ref().take(n).collect()));
193 +
            }
194 +
            per_change.sort_by_key(|(c, _, _)| **c);
195 +
            w.count(per_change.len());
196 +
            for (c, edits, _) in &per_change {
197 +
                w.object(c);
198 +
                w.count(edits.len());
199 +
                for e in edits.iter() {
200 +
                    match e {
201 +
                        Edit::Insert {
202 +
                            node,
203 +
                            after,
204 +
                            before,
205 +
                            lines,
206 +
                        } => {
207 +
                            if !lines.is_empty() {
208 +
                                return None;
209 +
                            }
210 +
                            w.u8(0);
211 +
                            w.fixed(&node.0);
212 +
                            atom(&mut w, after);
213 +
                            atom(&mut w, before);
214 +
                        }
215 +
                        Edit::Delete { node, atoms } => {
216 +
                            w.u8(1);
217 +
                            w.fixed(&node.0);
218 +
                            w.count(atoms.len());
219 +
                            for r in atoms {
220 +
                                w.object(&r.change);
221 +
                                w.u32(r.from);
222 +
                                w.u32(r.to);
223 +
                            }
224 +
                        }
225 +
                        Edit::Replace { .. } => return None,
226 +
                    }
227 +
                }
228 +
            }
229 +
            let texts: Vec<&TextRef> = per_change
230 +
                .iter()
231 +
                .flat_map(|(_, _, t)| t.iter().copied())
232 +
                .collect();
233 +
            w.count(texts.len());
234 +
            for t in texts {
235 +
                match t {
236 +
                    TextRef::Blob(r, count, form) => {
237 +
                        w.u8(0);
238 +
                        w.object(r);
239 +
                        w.u32(*count);
240 +
                        w.u8(match form {
241 +
                            crate::objects::TextForm::Cbor => 0,
242 +
                            crate::objects::TextForm::Raw => 1,
243 +
                            crate::objects::TextForm::CborFallback => 2,
244 +
                        });
245 +
                    }
246 +
                    TextRef::Inline { change, start, end } => {
247 +
                        w.u8(1);
248 +
                        w.object(change);
249 +
                        w.u32(*start);
250 +
                        w.u32(*end);
251 +
                    }
252 +
                }
253 +
            }
254 +
            let mut h = blake3::Hasher::new();
255 +
            h.update(b"oxy graph key 1\0");
256 +
            h.update(&n.0);
257 +
            w.fixed(d.key.get_or_init(|| d.graph_key(h)));
258 +
        }
259 +
        let mut outer = outer;
260 +
        outer.bytes(&w.0);
261 +
        Some(outer.0)
262 +
    }
263 +
264 +
    /// The state an index holds (see [`State::write_index`]).
265 +
    pub fn read_index(bytes: &[u8]) -> Result<State, ObjectError> {
266 +
        use crate::flat::Reader;
267 +
        let bad = |what: &str| ObjectError::Schema(format!("state index: {what}"));
268 +
        let mut r = Reader::new(bytes);
269 +
        if r.fixed::<18>()?.as_slice() != Self::INDEX_MAGIC {
270 +
            return Err(bad("not a state index"));
271 +
        }
272 +
        let node = |r: &mut Reader| -> Result<NodeId, ObjectError> { Ok(NodeId(r.fixed::<16>()?)) };
273 +
        let mut st = State::new_history_only();
274 +
        let t = crate::timing::phase("index: changes");
275 +
        let n = r.count()?;
276 +
        st.changes.reserve(n);
277 +
        for _ in 0..n {
278 +
            let h = r.object()?;
279 +
            let change_id = crate::ids::ChangeId(r.fixed::<16>()?);
280 +
            let mut deps = Vec::new();
281 +
            for _ in 0..r.count()? {
282 +
                deps.push(r.object()?);
283 +
            }
284 +
            let meta = ChangeMeta {
285 +
                change_id,
286 +
                deps,
287 +
                author: r.object()?,
288 +
                hlc: Hlc {
289 +
                    wall_ms: r.u64()?,
290 +
                    logical: r.u32()?,
291 +
                },
292 +
                message: r.text()?,
293 +
                kind: ChangeKind::from_tag(r.u8()? as u64)?,
294 +
            };
295 +
            st.changes.insert(h, Arc::new(meta));
296 +
        }
297 +
        drop(t);
298 +
        let tree_bytes = r.bytes()?;
299 +
        for _ in 0..r.count()? {
300 +
            let n = node(&mut r)?;
301 +
            let mut recs = Vec::new();
302 +
            for _ in 0..r.count()? {
303 +
                recs.push((r.object()?, r.opt_object()?, r.opt_object()?));
304 +
            }
305 +
            st.replaces.insert(n, recs);
306 +
        }
307 +
        let deferred_bytes = r.bytes()?;
308 +
        if !r.at_end() {
309 +
            return Err(bad("trailing bytes"));
310 +
        }
311 +
        let (tree, deferred) = rayon::join(
312 +
            || TreeGraph::read_flat(&mut Reader::new(tree_bytes)),
313 +
            || Self::read_deferred(deferred_bytes),
314 +
        );
315 +
        st.tree = tree?;
316 +
        st.deferred = Some(deferred?);
317 +
        Ok(st)
318 +
    }
319 +
320 +
    fn read_deferred(bytes: &[u8]) -> Result<HashMap<NodeId, Arc<Deferred>>, ObjectError> {
321 +
        use crate::flat::Reader;
322 +
        let _t = crate::timing::phase("index: deferred");
323 +
        let bad = |what: &str| ObjectError::Schema(format!("state index: {what}"));
324 +
        let node = |r: &mut Reader| -> Result<NodeId, ObjectError> { Ok(NodeId(r.fixed::<16>()?)) };
325 +
        let atom = |r: &mut Reader| -> Result<AtomRef, ObjectError> {
326 +
            Ok(match r.u8()? {
327 +
                0 => AtomRef::Start,
328 +
                1 => AtomRef::End,
329 +
                2 => AtomRef::Atom {
330 +
                    change: r.object()?,
331 +
                    index: r.u32()?,
332 +
                },
333 +
                _ => return Err(bad("atom")),
334 +
            })
335 +
        };
336 +
        let mut r = Reader::new(bytes);
337 +
        let mut deferred: HashMap<NodeId, Arc<Deferred>> = HashMap::new();
338 +
        let n = r.count()?;
339 +
        deferred.reserve(n);
340 +
        for _ in 0..n {
341 +
            let id = node(&mut r)?;
342 +
            let base = match r.u8()? {
343 +
                0 => None,
344 +
                1 => Some(DeferredBase::Blob(r.object()?)),
345 +
                _ => Some(DeferredBase::Inline(cbor::decode(r.bytes()?)?)),
346 +
            };
347 +
            let mut edits = Vec::new();
348 +
            for _ in 0..r.count()? {
349 +
                let c = r.object()?;
350 +
                let mut es = Vec::new();
351 +
                for _ in 0..r.count()? {
352 +
                    es.push(match r.u8()? {
353 +
                        0 => Edit::Insert {
354 +
                            node: node(&mut r)?,
355 +
                            after: atom(&mut r)?,
356 +
                            before: atom(&mut r)?,
357 +
                            lines: crate::lines::Lines::default(),
358 +
                        },
359 +
                        1 => {
360 +
                            let node = node(&mut r)?;
361 +
                            let mut atoms = Vec::new();
362 +
                            for _ in 0..r.count()? {
363 +
                                atoms.push(crate::objects::AtomRange {
364 +
                                    change: r.object()?,
365 +
                                    from: r.u32()?,
366 +
                                    to: r.u32()?,
367 +
                                });
368 +
                            }
369 +
                            Edit::Delete { node, atoms }
370 +
                        }
371 +
                        _ => return Err(bad("edit")),
372 +
                    });
373 +
                }
374 +
                edits.push((c, es));
375 +
            }
376 +
            let mut texts = Vec::new();
377 +
            for _ in 0..r.count()? {
378 +
                texts.push(match r.u8()? {
379 +
                    0 => TextRef::Blob(
380 +
                        r.object()?,
381 +
                        r.u32()?,
382 +
                        match r.u8()? {
383 +
                            0 => crate::objects::TextForm::Cbor,
384 +
                            1 => crate::objects::TextForm::Raw,
385 +
                            _ => crate::objects::TextForm::CborFallback,
386 +
                        },
387 +
                    ),
388 +
                    _ => TextRef::Inline {
389 +
                        change: r.object()?,
390 +
                        start: r.u32()?,
391 +
                        end: r.u32()?,
392 +
                    },
393 +
                });
394 +
            }
395 +
            let d = Deferred {
396 +
                base,
397 +
                edits,
398 +
                texts,
399 +
                key: std::sync::OnceLock::from(r.fixed::<32>()?),
400 +
            };
401 +
            deferred.insert(id, Arc::new(d));
402 +
        }
403 +
        if !r.at_end() {
404 +
            return Err(bad("trailing bytes"));
405 +
        }
406 +
        Ok(deferred)
407 +
    }
95 408
}
96 409
97 410
impl Clone for State {
…
405 unchanged lines
503 816
            h.update(&[0]);
504 817
            return Some(*h.finalize().as_bytes());
505 818
        };
819 +
        Some(*d.key.get_or_init(|| d.graph_key(h)))
820 +
    }
821 +
822 +
    pub fn len(&self) -> usize {
823 +
        self.changes.len()
824 +
    }
825 +
}
826 +
827 +
impl Deferred {
828 +
    /// See [`State::graph_key`]: `h` has taken the node already.
829 +
    fn graph_key(&self, mut h: blake3::Hasher) -> [u8; 32] {
830 +
        let d = self;
506 831
        match &d.base {
507 832
            None => h.update(&[0]),
508 833
            Some(DeferredBase::Blob(r)) => h.update(&[1]).update(&r.0),
…
6 unchanged lines
515 840
        for c in changes {
516 841
            h.update(&c.0);
517 842
        }
518 -
        Some(*h.finalize().as_bytes())
843 +
        *h.finalize().as_bytes()
519 844
    }
845 +
}
520 846
521 -
    pub fn len(&self) -> usize {
522 -
        self.changes.len()
523 -
    }
847 +
impl State {
524 848
    pub fn is_empty(&self) -> bool {
525 849
        self.changes.is_empty()
526 850
    }
…
343 unchanged lines
870 1194
                    base: Some(base),
871 1195
                    edits: Vec::new(),
872 1196
                    texts: Vec::new(),
1197 +
                    key: Default::default(),
873 1198
                }),
874 1199
            );
875 1200
        }
…
104 unchanged lines
980 1305
                        let d = Arc::make_mut(deferred.entry(node).or_default());
981 1306
                        d.edits.push((h, edits));
982 1307
                        d.texts.extend(node_texts);
1308 +
                        d.key = Default::default();
983 1309
                    }
984 1310
                }
985 1311
            }
…
1067 unchanged lines
modified crates/oxy-core/src/tree.rs +275 −13
…
8 unchanged lines
9 9
/// `(change, index into that change's tree_ops)`.
10 10
pub type OpKey = (ChangeHash, u32);
11 11
12 +
/// The graph's own maps: keyed by hashes and random ids, which need no
13 +
/// SipHash (see [`crate::fasthash`]).
14 +
type Map<K, V> = crate::fasthash::FastMap<K, V>;
15 +
16 +
/// A node's placement or mode atoms, sorted: nearly always one, held
17 +
/// without an allocation.
18 +
#[derive(Clone, Debug, Default, PartialEq, Eq)]
19 +
enum KeySet {
20 +
    #[default]
21 +
    Empty,
22 +
    One(OpKey),
23 +
    Many(Vec<OpKey>),
24 +
}
25 +
26 +
impl KeySet {
27 +
    fn insert(&mut self, k: OpKey) {
28 +
        match self {
29 +
            KeySet::Empty => *self = KeySet::One(k),
30 +
            KeySet::One(a) if *a == k => {}
31 +
            KeySet::One(a) => {
32 +
                let a = *a;
33 +
                *self = KeySet::Many(if a < k { vec![a, k] } else { vec![k, a] });
34 +
            }
35 +
            KeySet::Many(v) => {
36 +
                if let Err(i) = v.binary_search(&k) {
37 +
                    v.insert(i, k);
38 +
                }
39 +
            }
40 +
        }
41 +
    }
42 +
    fn remove(&mut self, k: &OpKey) {
43 +
        match self {
44 +
            KeySet::One(a) if a == k => *self = KeySet::Empty,
45 +
            KeySet::Many(v) => {
46 +
                if let Ok(i) = v.binary_search(k) {
47 +
                    v.remove(i);
48 +
                }
49 +
                if v.len() == 1 {
50 +
                    *self = KeySet::One(v[0]);
51 +
                } else if v.is_empty() {
52 +
                    *self = KeySet::Empty;
53 +
                }
54 +
            }
55 +
            _ => {}
56 +
        }
57 +
    }
58 +
    fn as_slice(&self) -> &[OpKey] {
59 +
        match self {
60 +
            KeySet::Empty => &[],
61 +
            KeySet::One(a) => std::slice::from_ref(a),
62 +
            KeySet::Many(v) => v,
63 +
        }
64 +
    }
65 +
    fn iter(&self) -> std::slice::Iter<'_, OpKey> {
66 +
        self.as_slice().iter()
67 +
    }
68 +
    fn len(&self) -> usize {
69 +
        self.as_slice().len()
70 +
    }
71 +
    fn is_empty(&self) -> bool {
72 +
        matches!(self, KeySet::Empty)
73 +
    }
74 +
}
75 +
76 +
impl<'a> IntoIterator for &'a KeySet {
77 +
    type Item = &'a OpKey;
78 +
    type IntoIter = std::slice::Iter<'a, OpKey>;
79 +
    fn into_iter(self) -> Self::IntoIter {
80 +
        self.iter()
81 +
    }
82 +
}
83 +
12 84
#[derive(Clone, Debug, PartialEq, Eq)]
13 85
pub struct Placement {
14 86
    pub node: NodeId,
…
3 unchanged lines
18 90
19 91
#[derive(Clone, Debug, Default)]
20 92
pub struct TreeGraph {
21 -
    kinds: HashMap<NodeId, NodeKind>,
22 -
    introduced: HashMap<NodeId, ChangeHash>,
23 -
    placements: HashMap<OpKey, Placement>,
24 -
    modes: HashMap<OpKey, (NodeId, Mode)>,
93 +
    kinds: Map<NodeId, NodeKind>,
94 +
    introduced: Map<NodeId, ChangeHash>,
95 +
    placements: Map<OpKey, Placement>,
96 +
    modes: Map<OpKey, (NodeId, Mode)>,
25 97
    /// Placement atoms retired by `RemoveNode`/`Move`, and mode atoms retired
26 98
    /// by `SetMode`. Kept apart because `AddNode` introduces one of each
27 99
    /// under the same key.
28 -
    deleted_placements: HashMap<OpKey, BTreeSet<ChangeHash>>,
29 -
    deleted_modes: HashMap<OpKey, BTreeSet<ChangeHash>>,
30 -
    placements_of: HashMap<NodeId, BTreeSet<OpKey>>,
31 -
    modes_of: HashMap<NodeId, BTreeSet<OpKey>>,
100 +
    deleted_placements: Map<OpKey, BTreeSet<ChangeHash>>,
101 +
    deleted_modes: Map<OpKey, BTreeSet<ChangeHash>>,
102 +
    placements_of: Map<NodeId, KeySet>,
103 +
    modes_of: Map<NodeId, KeySet>,
32 104
    /// The live subsets of `placements_of` and `modes_of`, kept as changes
33 105
    /// apply, so the view walks what is live rather than all history.
34 -
    live_placements_of: HashMap<NodeId, BTreeSet<OpKey>>,
35 -
    live_modes_of: HashMap<NodeId, BTreeSet<OpKey>>,
106 +
    live_placements_of: Map<NodeId, KeySet>,
107 +
    live_modes_of: Map<NodeId, KeySet>,
36 108
    /// The view, once computed, with the path of each directory in it;
37 109
    /// kept up to date as ops apply when that is simple (see
38 110
    /// `update_view`), cleared otherwise.
…
605 unchanged lines
644 716
    pub fn to_value(&self) -> crate::cbor::Value {
645 717
        use crate::cbor::Value;
646 718
        let opkey = |k: &OpKey| vec![Value::Bytes(k.0 .0.to_vec()), Value::Uint(k.1 as u64)];
647 -
        let deleters = |m: &HashMap<OpKey, BTreeSet<ChangeHash>>| {
719 +
        let deleters = |m: &Map<OpKey, BTreeSet<ChangeHash>>| {
648 720
            let mut ks: Vec<&OpKey> = m.keys().collect();
649 721
            ks.sort();
650 722
            Value::Array(
…
107 unchanged lines
758 830
        }
759 831
        let deleters = |v: &crate::cbor::Value,
760 832
                        what: &str|
761 -
         -> Result<HashMap<OpKey, BTreeSet<ChangeHash>>, ObjectError> {
762 -
            let mut out = HashMap::new();
833 +
         -> Result<Map<OpKey, BTreeSet<ChangeHash>>, ObjectError> {
834 +
            let mut out = Map::default();
763 835
            for d in array(v, what)? {
764 836
                let e = array(d, what)?;
765 837
                if e.len() != 3 {
…
29 unchanged lines
795 867
        let keys: Vec<OpKey> = g.modes.keys().copied().collect();
796 868
        for k in keys {
797 869
            g.index_mode(k);
870 +
        }
871 +
        Ok(g)
872 +
    }
873 +
}
874 +
875 +
/// The tree in the flat layout of a local index (oxygen#41), with its view
876 +
/// when that has no conflicts, so a loaded tree need not work it out again.
877 +
impl TreeGraph {
878 +
    pub(crate) fn write_flat(&self, w: &mut crate::flat::Writer) {
879 +
        let key = |w: &mut crate::flat::Writer, k: &OpKey| {
880 +
            w.object(&k.0);
881 +
            w.u32(k.1);
882 +
        };
883 +
        let mut kinds: Vec<(&NodeId, &NodeKind)> = self.kinds.iter().collect();
884 +
        kinds.sort_by_key(|(n, _)| **n);
885 +
        w.count(kinds.len());
886 +
        for (n, k) in kinds {
887 +
            w.fixed(&n.0);
888 +
            w.u8(k.tag() as u8);
889 +
        }
890 +
        let mut intro: Vec<(&NodeId, &ChangeHash)> = self.introduced.iter().collect();
891 +
        intro.sort();
892 +
        w.count(intro.len());
893 +
        for (n, c) in intro {
894 +
            w.fixed(&n.0);
895 +
            w.object(c);
896 +
        }
897 +
        let mut pl: Vec<(&OpKey, &Placement)> = self.placements.iter().collect();
898 +
        pl.sort_by_key(|(k, _)| **k);
899 +
        w.count(pl.len());
900 +
        for (k, p) in pl {
901 +
            key(w, k);
902 +
            w.fixed(&p.node.0);
903 +
            w.fixed(&p.parent.0);
904 +
            w.bytes(p.name.as_bytes());
905 +
        }
906 +
        let mut modes: Vec<(&OpKey, &(NodeId, Mode))> = self.modes.iter().collect();
907 +
        modes.sort_by_key(|(k, _)| **k);
908 +
        w.count(modes.len());
909 +
        for (k, (n, m)) in modes {
910 +
            key(w, k);
911 +
            w.fixed(&n.0);
912 +
            w.u8(m.bits() as u8);
913 +
        }
914 +
        for m in [&self.deleted_placements, &self.deleted_modes] {
915 +
            let mut ks: Vec<(&OpKey, &BTreeSet<ChangeHash>)> = m.iter().collect();
916 +
            ks.sort_by_key(|(k, _)| **k);
917 +
            w.count(ks.len());
918 +
            for (k, by) in ks {
919 +
                key(w, k);
920 +
                w.count(by.len());
921 +
                for c in by {
922 +
                    w.object(c);
923 +
                }
924 +
            }
925 +
        }
926 +
        // The view as a block of its own, decoded beside the maps.
927 +
        let outer = std::mem::take(w);
928 +
        let view = self.view();
929 +
        if view.conflicts.is_empty() {
930 +
            w.u8(1);
931 +
            w.count(view.entries.len());
932 +
            for (path, e) in view.entries.iter() {
933 +
                w.bytes(path.as_bytes());
934 +
                w.fixed(&e.node.0);
935 +
                w.u8(e.kind.tag() as u8);
936 +
                w.u8(e.mode.bits() as u8);
937 +
                key(w, &e.placement);
938 +
                match &e.mode_atom {
939 +
                    None => w.u8(0),
940 +
                    Some(k) => {
941 +
                        w.u8(1);
942 +
                        key(w, k);
943 +
                    }
944 +
                }
945 +
            }
946 +
        } else {
947 +
            w.u8(0);
948 +
        }
949 +
        let view_bytes = std::mem::replace(w, outer);
950 +
        w.bytes(&view_bytes.0);
951 +
    }
952 +
953 +
    pub(crate) fn read_flat(
954 +
        r: &mut crate::flat::Reader,
955 +
    ) -> Result<TreeGraph, crate::objects::ObjectError> {
956 +
        use crate::objects::ObjectError;
957 +
        let node = |r: &mut crate::flat::Reader| -> Result<NodeId, ObjectError> {
958 +
            Ok(NodeId(r.fixed::<16>()?))
959 +
        };
960 +
        let key = |r: &mut crate::flat::Reader| -> Result<OpKey, ObjectError> {
961 +
            Ok((r.object()?, r.u32()?))
962 +
        };
963 +
        let mut g = TreeGraph::new();
964 +
        let t = crate::timing::phase("index: tree maps");
965 +
        for _ in 0..r.count()? {
966 +
            let n = node(r)?;
967 +
            g.kinds.insert(n, NodeKind::from_tag(r.u8()? as u64)?);
968 +
        }
969 +
        for _ in 0..r.count()? {
970 +
            let n = node(r)?;
971 +
            g.introduced.insert(n, r.object()?);
972 +
        }
973 +
        for _ in 0..r.count()? {
974 +
            let k = key(r)?;
975 +
            let p = Placement {
976 +
                node: node(r)?,
977 +
                parent: node(r)?,
978 +
                name: r.text()?,
979 +
            };
980 +
            g.placements_of.entry(p.node).or_default().insert(k);
981 +
            g.placements.insert(k, p);
982 +
        }
983 +
        for _ in 0..r.count()? {
984 +
            let k = key(r)?;
985 +
            let n = node(r)?;
986 +
            g.modes.insert(k, (n, Mode::from_bits(r.u8()? as u64)?));
987 +
            g.modes_of.entry(n).or_default().insert(k);
798 988
        }
989 +
        for i in 0..2 {
990 +
            let mut m: Map<OpKey, BTreeSet<ChangeHash>> = Map::default();
991 +
            for _ in 0..r.count()? {
992 +
                let k = key(r)?;
993 +
                let mut by = BTreeSet::new();
994 +
                for _ in 0..r.count()? {
995 +
                    by.insert(r.object()?);
996 +
                }
997 +
                m.insert(k, by);
998 +
            }
999 +
            if i == 0 {
1000 +
                g.deleted_placements = m;
1001 +
            } else {
1002 +
                g.deleted_modes = m;
1003 +
            }
1004 +
        }
1005 +
        drop(t);
1006 +
        let view_bytes = r.bytes()?;
1007 +
        let (view, ()) = rayon::join(
1008 +
            || Self::read_view(view_bytes),
1009 +
            || {
1010 +
                let _t = crate::timing::phase("index: tree live");
1011 +
                let keys: Vec<OpKey> = g.placements.keys().copied().collect();
1012 +
                for k in keys {
1013 +
                    g.index_placement(k);
1014 +
                }
1015 +
                let keys: Vec<OpKey> = g.modes.keys().copied().collect();
1016 +
                for k in keys {
1017 +
                    g.index_mode(k);
1018 +
                }
1019 +
            },
1020 +
        );
1021 +
        if let Some(view) = view? {
1022 +
            #[cfg(test)]
1023 +
            assert_eq!(view, g.compute_view(), "an index's view is the tree's");
1024 +
            g.view_cache = std::sync::OnceLock::from(CachedView::new(view));
1025 +
        }
799 1026
        Ok(g)
1027 +
    }
1028 +
}
1029 +
1030 +
impl TreeGraph {
1031 +
    /// The view block of a tree's flat form, when it holds one.
1032 +
    fn read_view(bytes: &[u8]) -> Result<Option<TreeView>, crate::objects::ObjectError> {
1033 +
        let _t = crate::timing::phase("index: view");
1034 +
        let mut r = crate::flat::Reader::new(bytes);
1035 +
        if r.u8()? != 1 {
1036 +
            return Ok(None);
1037 +
        }
1038 +
        let key = |r: &mut crate::flat::Reader| -> Result<OpKey, crate::objects::ObjectError> {
1039 +
            Ok((r.object()?, r.u32()?))
1040 +
        };
1041 +
        let n = r.count()?;
1042 +
        let mut entries = BTreeMap::new();
1043 +
        for _ in 0..n {
1044 +
            let path = r.text()?;
1045 +
            let e = TreeEntry {
1046 +
                path: path.clone(),
1047 +
                node: NodeId(r.fixed::<16>()?),
1048 +
                kind: NodeKind::from_tag(r.u8()? as u64)?,
1049 +
                mode: Mode::from_bits(r.u8()? as u64)?,
1050 +
                placement: key(&mut r)?,
1051 +
                mode_atom: match r.u8()? {
1052 +
                    0 => None,
1053 +
                    _ => Some(key(&mut r)?),
1054 +
                },
1055 +
            };
1056 +
            entries.insert(path, e);
1057 +
        }
1058 +
        Ok(Some(TreeView {
1059 +
            entries: std::sync::Arc::new(entries),
1060 +
            conflicts: Vec::new(),
1061 +
        }))
800 1062
    }
801 1063
}
802 1064
…
401 unchanged lines