OXYGENHUB

State index: each file's deferred history decoded only when used (refs #41) 3f1b8fe25ec9

Index version 2 stores each node's graph key beside its encoded history,
which stays encoded until the file is built or changed; an unchanged entry
is copied as it is when the index is written again. Test builds now check
that every index written reads back to the same bytes. One-file status on
the Linux tree: 0.81 s (from 0.9 to 1.07 s), 260 MB (from 336 MB).
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 21:27 UTC
Signature
✔ signed by Andy McCall
Hash
3f1b8fe25ec99cbdc6d5bbac60f00651ad88b2fc7a139ebd29eb0734f9bb8789
Depends on
57d8dc05c9d4 86e1824f2d0b 982864068a59 a4fad5b085c7 cc7a00565161 e8d38e47e9d7 f6451fbbae3f f9575918477f ff0af1ee299d
Depended on by
5552e69c7325 79b5a90ffd43 d45dab0484ea acadb5ed28a1 f9466021286a 4cce912a0953 36e51aa806c6
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.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
23 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified crates/oxy-core/src/repo/mod.rs +6 −0
…
1154 unchanged lines
1155 1155
        let Some(body) = st.write_index() else {
1156 1156
            return;
1157 1157
        };
1158 +
        #[cfg(test)]
1159 +
        assert_eq!(
1160 +
            State::read_index(&body).ok().and_then(|s| s.write_index()),
1161 +
            Some(body.clone()),
1162 +
            "a state index reads back as written"
1163 +
        );
1158 1164
        let mut heads = st.maximal(deps);
1159 1165
        heads.sort();
1160 1166
        let mut w = crate::flat::Writer::default();
…
665 unchanged lines
modified crates/oxy-core/src/state.rs +146 −81
…
78 unchanged lines
79 79
/// version 2 checkpoint, if any, and the edits of later changes in the
80 80
/// order they were applied (spec §11). Building it later gives the same
81 81
/// graph, since graph construction does not depend on order (§8).
82 +
///
83 +
/// Read from a state index, it stays encoded until first used: a command
84 +
/// that builds a few files decodes only their histories.
82 85
#[derive(Clone, Default)]
83 86
struct Deferred {
87 +
    body: std::sync::OnceLock<DeferredBody>,
88 +
    /// The body as the index encodes it, decoded into `body` on first use.
89 +
    raw: Option<(Arc<[u8]>, std::ops::Range<usize>)>,
90 +
    /// Its graph key, once worked out (or read from an index); reset by
91 +
    /// any edit added.
92 +
    key: std::sync::OnceLock<[u8; 32]>,
93 +
}
94 +
95 +
#[derive(Clone, Default)]
96 +
struct DeferredBody {
84 97
    base: Option<DeferredBase>,
85 98
    edits: Vec<(ChangeHash, Vec<Edit>)>,
86 99
    /// The text blob and line count of each insert in `edits` left without
87 100
    /// its lines, in order: read when the graph is built.
88 101
    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]>,
102 +
}
103 +
104 +
impl Deferred {
105 +
    fn from_body(body: DeferredBody) -> Deferred {
106 +
        Deferred {
107 +
            body: std::sync::OnceLock::from(body),
108 +
            ..Deferred::default()
109 +
        }
110 +
    }
111 +
112 +
    fn body(&self) -> &DeferredBody {
113 +
        self.body.get_or_init(|| match &self.raw {
114 +
            // The index was checked against its hash when read, and was
115 +
            // written by `write_index`: it decodes.
116 +
            Some((bytes, range)) => State::read_deferred_body(&bytes[range.clone()])
117 +
                .expect("a state index checked by its hash decodes"),
118 +
            None => DeferredBody::default(),
119 +
        })
120 +
    }
121 +
122 +
    fn body_mut(&mut self) -> &mut DeferredBody {
123 +
        self.body();
124 +
        self.raw = None;
125 +
        self.body.get_mut().expect("decoded above")
126 +
    }
127 +
128 +
    fn into_body(mut self) -> DeferredBody {
129 +
        self.body();
130 +
        self.body.take().expect("decoded above")
131 +
    }
92 132
}
93 133
94 134
#[derive(Clone)]
…
8 unchanged lines
103 143
/// command reads it rather than replaying history. Derived only: written
104 144
/// from a state loaded the long way, and read back as the same state.
105 145
impl State {
106 -
    const INDEX_MAGIC: &'static [u8] = b"oxy state index 1\n";
146 +
    const INDEX_MAGIC: &'static [u8] = b"oxy state index 2\n";
107 147
108 148
    /// The index of this state, or `None` when it has anything an index
109 149
    /// does not hold: a built graph, or text in memory (a change added
…
58 unchanged lines
168 208
        w.count(nodes.len());
169 209
        for (n, d) in nodes {
170 210
            w.fixed(&n.0);
211 +
            let mut h = blake3::Hasher::new();
212 +
            h.update(b"oxy graph key 1\0");
213 +
            h.update(&n.0);
214 +
            w.fixed(d.key.get_or_init(|| d.graph_key(h)));
215 +
            // Unchanged since read (a change clears `raw`): copied as it is.
216 +
            if let Some((bytes, range)) = &d.raw {
217 +
                w.bytes(&bytes[range.clone()]);
218 +
                continue;
219 +
            }
220 +
            let d = d.body();
221 +
            let outer_w = std::mem::take(&mut w);
171 222
            match &d.base {
172 223
                None => w.u8(0),
173 224
                Some(DeferredBase::Blob(r)) => {
…
77 unchanged lines
251 302
                    }
252 303
                }
253 304
            }
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)));
305 +
            let body = std::mem::replace(&mut w, outer_w);
306 +
            w.bytes(&body.0);
258 307
        }
259 308
        let mut outer = outer;
260 309
        outer.bytes(&w.0);
…
56 unchanged lines
317 366
        Ok(st)
318 367
    }
319 368
369 +
    /// The deferred block: each node with its graph key and its body,
370 +
    /// left encoded (see [`Deferred`]).
320 371
    fn read_deferred(bytes: &[u8]) -> Result<HashMap<NodeId, Arc<Deferred>>, ObjectError> {
321 372
        use crate::flat::Reader;
322 373
        let _t = crate::timing::phase("index: deferred");
374 +
        let all: Arc<[u8]> = Arc::from(bytes);
375 +
        let mut r = Reader::new(bytes);
376 +
        let mut deferred: HashMap<NodeId, Arc<Deferred>> = HashMap::new();
377 +
        let n = r.count()?;
378 +
        deferred.reserve(n);
379 +
        for _ in 0..n {
380 +
            let id = NodeId(r.fixed::<16>()?);
381 +
            let key = r.fixed::<32>()?;
382 +
            let body = r.bytes()?;
383 +
            let start = body.as_ptr() as usize - bytes.as_ptr() as usize;
384 +
            let d = Deferred {
385 +
                raw: Some((all.clone(), start..start + body.len())),
386 +
                key: std::sync::OnceLock::from(key),
387 +
                ..Deferred::default()
388 +
            };
389 +
            deferred.insert(id, Arc::new(d));
390 +
        }
391 +
        if !r.at_end() {
392 +
            return Err(ObjectError::Schema("state index: trailing bytes".into()));
393 +
        }
394 +
        Ok(deferred)
395 +
    }
396 +
397 +
    /// One node's deferred history as the index encodes it.
398 +
    fn read_deferred_body(bytes: &[u8]) -> Result<DeferredBody, ObjectError> {
399 +
        use crate::flat::Reader;
323 400
        let bad = |what: &str| ObjectError::Schema(format!("state index: {what}"));
324 401
        let node = |r: &mut Reader| -> Result<NodeId, ObjectError> { Ok(NodeId(r.fixed::<16>()?)) };
325 402
        let atom = |r: &mut Reader| -> Result<AtomRef, ObjectError> {
…
8 unchanged lines
334 411
            })
335 412
        };
336 413
        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();
414 +
        let base = match r.u8()? {
415 +
            0 => None,
416 +
            1 => Some(DeferredBase::Blob(r.object()?)),
417 +
            _ => Some(DeferredBase::Inline(cbor::decode(r.bytes()?)?)),
418 +
        };
419 +
        let mut edits = Vec::new();
420 +
        for _ in 0..r.count()? {
421 +
            let c = r.object()?;
422 +
            let mut es = Vec::new();
377 423
            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()?,
424 +
                es.push(match r.u8()? {
425 +
                    0 => Edit::Insert {
426 +
                        node: node(&mut r)?,
427 +
                        after: atom(&mut r)?,
428 +
                        before: atom(&mut r)?,
429 +
                        lines: crate::lines::Lines::default(),
392 430
                    },
431 +
                    1 => {
432 +
                        let node = node(&mut r)?;
433 +
                        let mut atoms = Vec::new();
434 +
                        for _ in 0..r.count()? {
435 +
                            atoms.push(crate::objects::AtomRange {
436 +
                                change: r.object()?,
437 +
                                from: r.u32()?,
438 +
                                to: r.u32()?,
439 +
                            });
440 +
                        }
441 +
                        Edit::Delete { node, atoms }
442 +
                    }
443 +
                    _ => return Err(bad("edit")),
393 444
                });
394 445
            }
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));
446 +
            edits.push((c, es));
447 +
        }
448 +
        let mut texts = Vec::new();
449 +
        for _ in 0..r.count()? {
450 +
            texts.push(match r.u8()? {
451 +
                0 => TextRef::Blob(
452 +
                    r.object()?,
453 +
                    r.u32()?,
454 +
                    match r.u8()? {
455 +
                        0 => crate::objects::TextForm::Cbor,
456 +
                        1 => crate::objects::TextForm::Raw,
457 +
                        _ => crate::objects::TextForm::CborFallback,
458 +
                    },
459 +
                ),
460 +
                _ => TextRef::Inline {
461 +
                    change: r.object()?,
462 +
                    start: r.u32()?,
463 +
                    end: r.u32()?,
464 +
                },
465 +
            });
402 466
        }
403 467
        if !r.at_end() {
404 -
            return Err(bad("trailing bytes"));
468 +
            return Err(bad("trailing bytes in a file's history"));
405 469
        }
406 -
        Ok(deferred)
470 +
        Ok(DeferredBody { base, edits, texts })
407 471
    }
408 472
}
409 473
…
137 unchanged lines
547 611
/// texts of inserts read without their lines are loaded in one call, then
548 612
/// the graphs are built in parallel (they are independent).
549 613
fn build_deferred(
550 -
    mut taken: Vec<(NodeId, Deferred)>,
614 +
    taken: Vec<(NodeId, Deferred)>,
551 615
    load: impl FnOnce(&[ObjectRef]) -> Result<Vec<Vec<u8>>, ObjectError>,
552 616
    read_change: impl Fn(&ChangeHash) -> Result<Arc<Vec<u8>>, ObjectError>,
553 617
) -> Result<Vec<(NodeId, FileGraph)>, ObjectError> {
618 +
    let mut taken: Vec<(NodeId, DeferredBody)> =
619 +
        taken.into_iter().map(|(n, d)| (n, d.into_body())).collect();
554 620
    let mut refs: Vec<ObjectRef> = taken
555 621
        .iter()
556 622
        .filter_map(|(_, d)| match &d.base {
…
31 unchanged lines
588 654
    let mut blobs = blobs.into_iter();
589 655
    // Pair each graph with its blob, then build them all in parallel:
590 656
    // graphs are independent.
591 -
    let mut work: Vec<(NodeId, Deferred, Option<Vec<u8>>)> = Vec::with_capacity(taken.len());
657 +
    let mut work: Vec<(NodeId, DeferredBody, Option<Vec<u8>>)> = Vec::with_capacity(taken.len());
592 658
    for (node, mut d) in taken.drain(..) {
593 659
        // Fill in the lines of inserts read without them, in order.
594 660
        let mut want = std::mem::take(&mut d.texts).into_iter();
…
232 unchanged lines
827 893
impl Deferred {
828 894
    /// See [`State::graph_key`]: `h` has taken the node already.
829 895
    fn graph_key(&self, mut h: blake3::Hasher) -> [u8; 32] {
830 -
        let d = self;
896 +
        let d = self.body();
831 897
        match &d.base {
832 898
            None => h.update(&[0]),
833 899
            Some(DeferredBase::Blob(r)) => h.update(&[1]).update(&r.0),
…
356 unchanged lines
1190 1256
            };
1191 1257
            deferred.insert(
1192 1258
                node,
1193 -
                Arc::new(Deferred {
1259 +
                Arc::new(Deferred::from_body(DeferredBody {
1194 1260
                    base: Some(base),
1195 -
                    edits: Vec::new(),
1196 -
                    texts: Vec::new(),
1197 -
                    key: Default::default(),
1198 -
                }),
1261 +
                    ..DeferredBody::default()
1262 +
                })),
1199 1263
            );
1200 1264
        }
1201 1265
        st.deferred = Some(deferred);
…
101 unchanged lines
1303 1367
                    }
1304 1368
                    None => {
1305 1369
                        let d = Arc::make_mut(deferred.entry(node).or_default());
1306 -
                        d.edits.push((h, edits));
1307 -
                        d.texts.extend(node_texts);
1370 +
                        let b = d.body_mut();
1371 +
                        b.edits.push((h, edits));
1372 +
                        b.texts.extend(node_texts);
1308 1373
                        d.key = Default::default();
1309 1374
                    }
1310 1375
                }
…
1068 unchanged lines