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 |