OXYGENHUB

Store: a cache of change metadata (refs #41) ce14f0d68b15

A change's metadata is read from its object, which can be as large as the
tree the change adds. The store now keeps what it has read in a small
checksummed cache file beside the objects, served only while the store
still has the change; a torn tail is cut off. log -n 100 on the Linux tree
gathers its entries in 9 ms (from 181 ms) and takes 0.20 s in all, most of
it the working-copy check.
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 21:55 UTC
Signature
✔ signed by Andy McCall
Hash
ce14f0d68b155ff7b6bfc4d9c7ed1d182a5ea88aad74e638abf33852904b3d04
Depends on
252e8cb23fbc 4cce912a0953 4d6c9c1b358c 69816d688300 75167672ebcd 8f10a07a1dbe 982864068a59 a07f60a37fb2 f9575918477f ff0af1ee299d
Depended on by
ef7d4ea6f33b 74b4a3c62ea4 648189388f1a ff1f62fee654 f9466021286a
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
11 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/oxy-core/src/repo/tests.rs +24 −0
…
6229 unchanged lines
6230 6230
        assert_ne!(o.token.as_deref(), Some("secret"), "{other}");
6231 6231
    }
6232 6232
}
6233 +
6234 +
#[test]
6235 +
fn the_metadata_cache_survives_a_torn_tail() {
6236 +
    hermetic();
6237 +
    let dir = tempfile::tempdir().unwrap();
6238 +
    let root = dir.path();
6239 +
    let repo = Repo::init(root).unwrap();
6240 +
    write(root, "a.txt", b"1\n");
6241 +
    let h = repo.commit("first", &[]).unwrap();
6242 +
    let meta = repo.store().get_change_meta(&h).unwrap();
6243 +
    let cache = repo.store().dir().join("metas");
6244 +
    let len = fs::metadata(&cache).unwrap().len();
6245 +
    assert!(len > 0, "a change read is cached");
6246 +
    // A torn record at the end: the cache reads up to it and cuts it off.
6247 +
    fs::OpenOptions::new()
6248 +
        .append(true)
6249 +
        .open(&cache)
6250 +
        .unwrap()
6251 +
        .write_all(&[7; 20])
6252 +
        .unwrap();
6253 +
    let again = Repo::open(root).unwrap();
6254 +
    assert_eq!(again.store().get_change_meta(&h).unwrap(), meta);
6255 +
    assert_eq!(fs::metadata(&cache).unwrap().len(), len);
6256 +
}
modified crates/oxy-core/src/state.rs +35 −27
…
40 unchanged lines
41 41
}
42 42
43 43
impl ChangeMeta {
44 +
    /// In the flat layout of local indexes and caches ([`crate::flat`]).
45 +
    pub(crate) fn write_flat(&self, w: &mut crate::flat::Writer) {
46 +
        w.fixed(&self.change_id.0);
47 +
        w.count(self.deps.len());
48 +
        for d in &self.deps {
49 +
            w.object(d);
50 +
        }
51 +
        w.object(&self.author);
52 +
        w.u64(self.hlc.wall_ms);
53 +
        w.u32(self.hlc.logical);
54 +
        w.bytes(self.message.as_bytes());
55 +
        w.u8(self.kind.tag() as u8);
56 +
    }
57 +
58 +
    pub(crate) fn read_flat(r: &mut crate::flat::Reader) -> Result<ChangeMeta, ObjectError> {
59 +
        let change_id = crate::ids::ChangeId(r.fixed::<16>()?);
60 +
        let mut deps = Vec::new();
61 +
        for _ in 0..r.count()? {
62 +
            deps.push(r.object()?);
63 +
        }
64 +
        Ok(ChangeMeta {
65 +
            change_id,
66 +
            deps,
67 +
            author: r.object()?,
68 +
            hlc: Hlc {
69 +
                wall_ms: r.u64()?,
70 +
                logical: r.u32()?,
71 +
            },
72 +
            message: r.text()?,
73 +
            kind: ChangeKind::from_tag(r.u8()? as u64)?,
74 +
        })
75 +
    }
76 +
44 77
    pub fn of(c: &Change) -> ChangeMeta {
45 78
        ChangeMeta {
46 79
            change_id: c.change_id,
…
125 unchanged lines
172 205
        w.count(changes.len());
173 206
        for (h, m) in changes {
174 207
            w.object(h);
175 -
            w.fixed(&m.change_id.0);
176 -
            w.count(m.deps.len());
177 -
            for d in &m.deps {
178 -
                w.object(d);
179 -
            }
180 -
            w.object(&m.author);
181 -
            w.u64(m.hlc.wall_ms);
182 -
            w.u32(m.hlc.logical);
183 -
            w.bytes(m.message.as_bytes());
184 -
            w.u8(m.kind.tag() as u8);
208 +
            m.write_flat(&mut w);
185 209
        }
186 210
        let changes_block = std::mem::take(&mut w);
187 211
        w.bytes(&changes_block.0);
…
172 unchanged lines
360 384
        let mut changes = HashMap::with_capacity(n);
361 385
        for _ in 0..n {
362 386
            let h = r.object()?;
363 -
            let change_id = crate::ids::ChangeId(r.fixed::<16>()?);
364 -
            let mut deps = Vec::new();
365 -
            for _ in 0..r.count()? {
366 -
                deps.push(r.object()?);
367 -
            }
368 -
            let meta = ChangeMeta {
369 -
                change_id,
370 -
                deps,
371 -
                author: r.object()?,
372 -
                hlc: Hlc {
373 -
                    wall_ms: r.u64()?,
374 -
                    logical: r.u32()?,
375 -
                },
376 -
                message: r.text()?,
377 -
                kind: ChangeKind::from_tag(r.u8()? as u64)?,
378 -
            };
379 -
            changes.insert(h, Arc::new(meta));
387 +
            changes.insert(h, Arc::new(ChangeMeta::read_flat(&mut r)?));
380 388
        }
381 389
        if !r.at_end() {
382 390
            return Err(ObjectError::Schema("state index: trailing bytes".into()));
…
2079 unchanged lines
modified crates/oxy-core/src/store.rs +89 −1
…
185 unchanged lines
186 186
    /// files, and are written out with one `fsync` when the batch finishes.
187 187
    pending: RefCell<Option<Pending>>,
188 188
    batch_depth: std::cell::Cell<u32>,
189 +
    /// Change metadata already read, from the `metas` cache file.
190 +
    metas: RefCell<Option<crate::fasthash::FastMap<ObjectRef, crate::state::ChangeMeta>>>,
189 191
}
190 192
191 193
/// Objects written since `begin_batch`. Small batches stay in memory and
…
88 unchanged lines
280 282
            packs: RefCell::new(Vec::new()),
281 283
            pending: RefCell::new(None),
282 284
            batch_depth: std::cell::Cell::new(0),
285 +
            metas: RefCell::new(None),
283 286
        };
284 287
        store
285 288
            .reload_packs()
…
805 unchanged lines
1091 1094
    /// A change's metadata (id, dependencies, author, clock, message, kind)
1092 1095
    /// without its contents: read from its skeleton, so neither its text
1093 1096
    /// blobs nor its inline lines are read (spec §11).
1097 +
    ///
1098 +
    /// Served from the `metas` cache when this store read it before (a
1099 +
    /// change object can be as large as the tree it adds), as long as the
1100 +
    /// store still has the change.
1094 1101
    pub fn get_change_meta(&self, r: &ObjectRef) -> Result<crate::state::ChangeMeta> {
1102 +
        if self.metas.borrow().is_none() {
1103 +
            *self.metas.borrow_mut() = Some(self.read_metas_cache());
1104 +
        }
1105 +
        let cached = self.metas.borrow().as_ref().and_then(|m| m.get(r).cloned());
1106 +
        if let Some(meta) = cached {
1107 +
            if self.has(r) {
1108 +
                #[cfg(test)]
1109 +
                assert_eq!(
1110 +
                    meta,
1111 +
                    Change::decode_meta(&self.get_typed(r, ObjectType::Change)?).unwrap(),
1112 +
                    "the metadata cache holds a change's metadata"
1113 +
                );
1114 +
                return Ok(meta);
1115 +
            }
1116 +
        }
1095 1117
        let bytes = self.get_typed(r, ObjectType::Change)?;
1096 -
        Change::decode_meta(&bytes).map_err(|e| StoreError::Corrupt(*r, e.to_string()))
1118 +
        let meta =
1119 +
            Change::decode_meta(&bytes).map_err(|e| StoreError::Corrupt(*r, e.to_string()))?;
1120 +
        self.remember_meta(r, &meta);
1121 +
        Ok(meta)
1122 +
    }
1123 +
1124 +
    /// The metadata cache: records of a change hash, its metadata in the
1125 +
    /// flat layout, and the first 8 bytes of BLAKE3 over both, each
1126 +
    /// appended in one write. Derived only: a record that does not check
1127 +
    /// ends the reading (a torn write), and the cache can be deleted.
1128 +
    fn read_metas_cache(&self) -> crate::fasthash::FastMap<ObjectRef, crate::state::ChangeMeta> {
1129 +
        let mut out = crate::fasthash::FastMap::default();
1130 +
        let Ok(bytes) = fs::read(self.dir.join("metas")) else {
1131 +
            return out;
1132 +
        };
1133 +
        let mut r = crate::flat::Reader::new(&bytes);
1134 +
        let mut good = 0;
1135 +
        while !r.at_end() {
1136 +
            let mut record = || -> std::result::Result<_, ObjectError> {
1137 +
                let h = r.object()?;
1138 +
                let body = r.bytes()?;
1139 +
                let check = r.fixed::<8>()?;
1140 +
                Ok((h, body, check))
1141 +
            };
1142 +
            let Ok((h, body, check)) = record() else {
1143 +
                break;
1144 +
            };
1145 +
            let mut sum = blake3::Hasher::new();
1146 +
            sum.update(&h.0).update(body);
1147 +
            if sum.finalize().as_bytes()[..8] != check {
1148 +
                break;
1149 +
            }
1150 +
            match crate::state::ChangeMeta::read_flat(&mut crate::flat::Reader::new(body)) {
1151 +
                Ok(m) => out.insert(h, m),
1152 +
                Err(_) => break,
1153 +
            };
1154 +
            good += 32 + 4 + body.len() + 8;
1155 +
        }
1156 +
        // Cut a bad tail, so records appended later are read.
1157 +
        if good < bytes.len() {
1158 +
            let _ = fs::OpenOptions::new()
1159 +
                .write(true)
1160 +
                .open(self.dir.join("metas"))
1161 +
                .and_then(|f| f.set_len(good as u64));
1162 +
        }
1163 +
        out
1164 +
    }
1165 +
1166 +
    /// Add a change's metadata to the cache; quietly nothing when it
1167 +
    /// cannot be written (it is only a cache).
1168 +
    fn remember_meta(&self, r: &ObjectRef, meta: &crate::state::ChangeMeta) {
1169 +
        let mut body = crate::flat::Writer::default();
1170 +
        meta.write_flat(&mut body);
1171 +
        let mut w = crate::flat::Writer::default();
1172 +
        w.object(r);
1173 +
        w.bytes(&body.0);
1174 +
        let mut sum = blake3::Hasher::new();
1175 +
        sum.update(&r.0).update(&body.0);
1176 +
        w.fixed(&sum.finalize().as_bytes()[..8]);
1177 +
        let _ = fs::OpenOptions::new()
1178 +
            .create(true)
1179 +
            .append(true)
1180 +
            .open(self.dir.join("metas"))
1181 +
            .and_then(|mut f| f.write_all(&w.0));
1182 +
        if let Some(m) = self.metas.borrow_mut().as_mut() {
1183 +
            m.insert(*r, meta.clone());
1184 +
        }
1097 1185
    }
1098 1186
1099 1187
    fn read_text(&self, r: &ObjectRef) -> Result<(Blob, Vec<u8>)> {
…
787 unchanged lines