Read a change's metadata without its operations 4d6c9c1b358c
Reading a change's metadata (for log, signature checks and walks of the dependency graph) decoded its skeleton, all of its tree operations and edit headers: for the Linux tree's first change, 96 000 of them. It now steps over both with the CBOR cursor (Change::decode_meta). And log walks the changes' metadata instead of loading a history state, unless the repository is shallow, where a checkpoint's metadata tells of changes beyond the boundary. log -n 100 on the Linux tip takes 1.15 s (from 1.6 to 1.7 s, and 28 s yesterday). refs #35
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-30 03:20 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
4d6c9c1b358cb39e29dc4afdd962b3efbebea2ae9934416e8ced44ffd8a9f54d- Depends on
- 252e8cb23fbc 2863456a0f6d 69816d688300 6f8aeb996558 75167672ebcd 982864068a59 a4fad5b085c7 e02bf0cd0533 e8d38e47e9d7 ff0af1ee299d
- Depended on by
- c6e69e4b8d8f 74b4a3c62ea4 79b5a90ffd43 ce14f0d68b15 4cce912a0953
- 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
- 8 atoms, 0 tree operations, 3 files
Browse the tree at this change
modified crates/oxy-core/src/objects.rs +38 −0
| … | 864 unchanged lines |
||
| 865 | 865 | } |
|
| 866 | 866 | } |
|
| 867 | 867 | ||
| 868 | + | impl Change { |
|
| 869 | + | /// A change's metadata alone: its tree operations and edits are |
|
| 870 | + | /// stepped over, not decoded. For log, signature checks and walks of |
|
| 871 | + | /// the dependency graph, which read nothing else. |
|
| 872 | + | pub fn decode_meta(bytes: &[u8]) -> Result<crate::state::ChangeMeta, ObjectError> { |
|
| 873 | + | let mut c = cbor::Cursor::new(bytes); |
|
| 874 | + | let (major, n) = c.head()?; |
|
| 875 | + | if major != 5 { |
|
| 876 | + | return schema("change: expected map"); |
|
| 877 | + | } |
|
| 878 | + | let mut fields = Map::new(); |
|
| 879 | + | let mut last: Option<u64> = None; |
|
| 880 | + | for _ in 0..n { |
|
| 881 | + | let k = map_key(&mut c, &mut last)?; |
|
| 882 | + | if k == 6 || k == 7 { |
|
| 883 | + | c.skip()?; |
|
| 884 | + | fields.insert(k, Value::Array(Vec::new())); |
|
| 885 | + | } else { |
|
| 886 | + | fields.insert(k, c.value()?); |
|
| 887 | + | } |
|
| 888 | + | } |
|
| 889 | + | if !c.at_end() { |
|
| 890 | + | return Err(ObjectError::Cbor(cbor::CborError::TrailingBytes)); |
|
| 891 | + | } |
|
| 892 | + | Ok(crate::state::ChangeMeta::of(&Change::from_value( |
|
| 893 | + | &Value::Map(fields), |
|
| 894 | + | )?)) |
|
| 895 | + | } |
|
| 896 | + | } |
|
| 897 | + | ||
| 868 | 898 | /// A canonical map key: an unsigned integer above the previous one. |
|
| 869 | 899 | fn map_key(c: &mut cbor::Cursor, last: &mut Option<u64>) -> Result<u64, ObjectError> { |
|
| 870 | 900 | let (km, k) = c.head()?; |
|
| … | 456 unchanged lines |
||
| 1327 | 1357 | m.insert(0, Value::Uint(2)); |
|
| 1328 | 1358 | } |
|
| 1329 | 1359 | assert!(Change::decode_skeleton(&cbor::encode(&v), me).is_err()); |
|
| 1360 | + | } |
|
| 1361 | + | ||
| 1362 | + | #[test] |
|
| 1363 | + | fn metadata_reads_without_the_operations() { |
|
| 1364 | + | let c = with_insert(sample_change(), big_lines(500)); |
|
| 1365 | + | let (bytes, _) = c.encode_parts(); |
|
| 1366 | + | let m = Change::decode_meta(&bytes).unwrap(); |
|
| 1367 | + | assert_eq!(m, crate::state::ChangeMeta::of(&c)); |
|
| 1330 | 1368 | } |
|
| 1331 | 1369 | ||
| 1332 | 1370 | #[test] |
|
| … | 97 unchanged lines |
modified crates/oxy-core/src/repo/mod.rs +61 −8
| … | 1371 unchanged lines |
||
| 1372 | 1372 | let (wc_hash, _) = self.working_copy()?; |
|
| 1373 | 1373 | let mut all = heads.clone(); |
|
| 1374 | 1374 | all.push(wc_hash); |
|
| 1375 | - | let state = self.load_state_history(&all)?; |
|
| 1376 | 1375 | let head_set: BTreeSet<ChangeHash> = heads.into_iter().collect(); |
|
| 1377 | 1376 | let mut entries: Vec<LogEntry> = Vec::new(); |
|
| 1378 | - | for (h, meta) in state.changes() { |
|
| 1377 | + | // A shallow clone knows changes beyond its boundary from a |
|
| 1378 | + | // checkpoint's metadata only: its log comes from the state. |
|
| 1379 | + | let metas = if self.is_shallow() { |
|
| 1380 | + | self.load_state_history(&all)? |
|
| 1381 | + | .changes() |
|
| 1382 | + | .map(|(h, m)| (*h, m.clone())) |
|
| 1383 | + | .collect() |
|
| 1384 | + | } else { |
|
| 1385 | + | self.metas_of(&all)? |
|
| 1386 | + | }; |
|
| 1387 | + | for (h, meta) in metas { |
|
| 1379 | 1388 | entries.push(LogEntry { |
|
| 1380 | - | hash: *h, |
|
| 1381 | - | change: meta.clone(), |
|
| 1382 | - | is_working_copy: *h == wc_hash, |
|
| 1383 | - | is_head: head_set.contains(h), |
|
| 1389 | + | hash: h, |
|
| 1390 | + | change: meta, |
|
| 1391 | + | is_working_copy: h == wc_hash, |
|
| 1392 | + | is_head: head_set.contains(&h), |
|
| 1384 | 1393 | }); |
|
| 1385 | 1394 | } |
|
| 1386 | 1395 | entries.sort_by(|a, b| { |
|
| … | 21 unchanged lines |
||
| 1408 | 1417 | /// The log of the state with these heads, newest first, with no working |
|
| 1409 | 1418 | /// copy involved (for hosted repositories and any explicit state). |
|
| 1410 | 1419 | pub fn log_state(&self, heads: &[ChangeHash]) -> Result<Vec<LogEntry>> { |
|
| 1411 | - | // The log needs change metadata only: no file graph is built. |
|
| 1412 | - | Ok(self.log_of(&self.load_state_history(heads)?)) |
|
| 1420 | + | // The log needs change metadata only: neither graphs nor the tree. |
|
| 1421 | + | if self.is_shallow() { |
|
| 1422 | + | return Ok(self.log_of(&self.load_state_history(heads)?)); |
|
| 1423 | + | } |
|
| 1424 | + | let metas = self.metas_of(heads)?; |
|
| 1425 | + | let depended: BTreeSet<ChangeHash> = metas |
|
| 1426 | + | .iter() |
|
| 1427 | + | .flat_map(|(_, m)| m.deps.iter().copied()) |
|
| 1428 | + | .collect(); |
|
| 1429 | + | let mut entries: Vec<LogEntry> = metas |
|
| 1430 | + | .into_iter() |
|
| 1431 | + | .map(|(h, meta)| LogEntry { |
|
| 1432 | + | hash: h, |
|
| 1433 | + | is_head: !depended.contains(&h), |
|
| 1434 | + | change: meta, |
|
| 1435 | + | is_working_copy: false, |
|
| 1436 | + | }) |
|
| 1437 | + | .collect(); |
|
| 1438 | + | entries.sort_by(|a, b| b.change.hlc.cmp(&a.change.hlc).then(b.hash.cmp(&a.hash))); |
|
| 1439 | + | Ok(entries) |
|
| 1440 | + | } |
|
| 1441 | + | ||
| 1442 | + | /// Every change of the state with these heads and its metadata, read |
|
| 1443 | + | /// without its operations ([`Store::get_change_meta`]). A change beyond |
|
| 1444 | + | /// a shallow clone's boundary is left out. |
|
| 1445 | + | pub fn metas_of( |
|
| 1446 | + | &self, |
|
| 1447 | + | heads: &[ChangeHash], |
|
| 1448 | + | ) -> Result<Vec<(ChangeHash, crate::state::ChangeMeta)>> { |
|
| 1449 | + | let boundary = self.boundary(); |
|
| 1450 | + | let mut seen: HashSet<ChangeHash> = HashSet::new(); |
|
| 1451 | + | let mut stack: Vec<ChangeHash> = heads.to_vec(); |
|
| 1452 | + | let mut out = Vec::new(); |
|
| 1453 | + | while let Some(h) = stack.pop() { |
|
| 1454 | + | if !seen.insert(h) { |
|
| 1455 | + | continue; |
|
| 1456 | + | } |
|
| 1457 | + | let meta = match self.store.get_change_meta(&h) { |
|
| 1458 | + | Ok(m) => m, |
|
| 1459 | + | Err(crate::store::StoreError::NotFound(_)) if boundary.contains(&h) => continue, |
|
| 1460 | + | Err(e) => return Err(e.into()), |
|
| 1461 | + | }; |
|
| 1462 | + | stack.extend(meta.deps.iter().copied()); |
|
| 1463 | + | out.push((h, meta)); |
|
| 1464 | + | } |
|
| 1465 | + | Ok(out) |
|
| 1413 | 1466 | } |
|
| 1414 | 1467 | ||
| 1415 | 1468 | /// The log of a state already loaded, newest first. |
|
| … | 256 unchanged lines |
modified crates/oxy-core/src/store.rs +2 −1
| … | 941 unchanged lines |
||
| 942 | 942 | /// without its contents: read from its skeleton, so neither its text |
|
| 943 | 943 | /// blobs nor its inline lines are read (spec §11). |
|
| 944 | 944 | pub fn get_change_meta(&self, r: &ObjectRef) -> Result<crate::state::ChangeMeta> { |
|
| 945 | - | Ok(crate::state::ChangeMeta::of(&self.get_change_lazy(r)?.0)) |
|
| 945 | + | let bytes = self.get_typed(r, ObjectType::Change)?; |
|
| 946 | + | Change::decode_meta(&bytes).map_err(|e| StoreError::Corrupt(*r, e.to_string())) |
|
| 946 | 947 | } |
|
| 947 | 948 | ||
| 948 | 949 | fn read_text(&self, r: &ObjectRef) -> Result<(Blob, Vec<u8>)> { |
|
| … | 753 unchanged lines |