A commit writes the manifest once (refs #41) 236f35c0baba
The commit's snapshot hands its manifest rows to the commit, which writes them with the new base's graph keys, stamped when the snapshot read them, instead of the snapshot writing the manifest and the commit reading and rewriting it. Finer timing phases for a commit. Direct commit on the Linux tree 0.55 s to 0.34-0.45 s.
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-02 02:15 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
236f35c0baba91063400fb690083c90cd205cad77987bd3fa53379952431421a- Depends on
- 0389c3e82f6c 135212de5370 19555025a2ac 1ec3f008e96a 3604179679eb 4dcb8c0916b7 75167672ebcd 8457a6cc6984 86e1824f2d0b 957fee30826e 982864068a59 992467d31d26 b30a880fa409 d02e545dc265 d0990c2bff74 d4dbab97e948 e226a6cc2f71 f9466021286a
- Depended on by
- 157d02e95586 dd21fd82687d
- 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.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
- 20 atoms, 0 tree operations, 3 files
Browse the tree at this change
modified crates/oxy-core/src/repo/mod.rs +2 −0
| … | 696 unchanged lines |
||
| 697 | 697 | }; |
|
| 698 | 698 | let r = self.store.put(ObjectType::Op, &op.encode())?; |
|
| 699 | 699 | // The head is the commit point: everything it names must be on disk first. |
|
| 700 | + | let flushing = crate::timing::phase("op: flush the store"); |
|
| 700 | 701 | self.store.flush()?; |
|
| 702 | + | drop(flushing); |
|
| 701 | 703 | // Compare and swap under a short lock (spec §12.1): when another |
|
| 702 | 704 | // process recorded an operation since this one read the head, the |
|
| 703 | 705 | // two are merged instead of one overwriting the other. |
|
| … | 1402 unchanged lines |
modified crates/oxy-core/src/repo/prompt.rs +16 −7
| … | 158 unchanged lines |
||
| 159 | 159 | pub(crate) type CachedManifest = ((u64, u128, u64), (bool, bool, std::sync::Arc<Manifest>)); |
|
| 160 | 160 | ||
| 161 | 161 | /// Now, in nanoseconds since the epoch. |
|
| 162 | - | fn now_ns() -> u128 { |
|
| 162 | + | pub(crate) fn now_ns() -> u128 { |
|
| 163 | 163 | std::time::SystemTime::now() |
|
| 164 | 164 | .duration_since(UNIX_EPOCH) |
|
| 165 | 165 | .map(|d| d.as_nanos()) |
|
| … | 310 unchanged lines |
||
| 476 | 476 | entries: &BTreeMap<String, FsEntry>, |
|
| 477 | 477 | blobs: &HashMap<String, ObjectRef>, |
|
| 478 | 478 | ) -> Result<()> { |
|
| 479 | + | let dirs = self.walked_dirs.take().unwrap_or_default(); |
|
| 480 | + | self.write_manifest_entries_as_of(conflicted, uncommitted, entries, blobs, now_ns(), dirs) |
|
| 481 | + | } |
|
| 482 | + | ||
| 483 | + | /// `write_manifest_entries` stamped `written` (when the entries were |
|
| 484 | + | /// read), with the directories of the walk they came from. |
|
| 485 | + | pub(crate) fn write_manifest_entries_as_of( |
|
| 486 | + | &self, |
|
| 487 | + | conflicted: bool, |
|
| 488 | + | uncommitted: bool, |
|
| 489 | + | entries: &BTreeMap<String, FsEntry>, |
|
| 490 | + | blobs: &HashMap<String, ObjectRef>, |
|
| 491 | + | written: u128, |
|
| 492 | + | (dirs, nested): WalkedDirs, |
|
| 493 | + | ) -> Result<()> { |
|
| 479 | 494 | self.manifest_cache.take(); |
|
| 480 | 495 | self.listing_cache.take(); |
|
| 481 | - | let written = std::time::SystemTime::now() |
|
| 482 | - | .duration_since(UNIX_EPOCH) |
|
| 483 | - | .map(|d| d.as_nanos()) |
|
| 484 | - | .unwrap_or(0); |
|
| 485 | 496 | let mut newest_ns: u128 = 0; |
|
| 486 | 497 | let mut rows = entries.iter().map(|(path, e)| { |
|
| 487 | 498 | let (size, mtime_ns) = match e.stat { |
|
| … | 16 unchanged lines |
||
| 504 | 515 | }; |
|
| 505 | 516 | (path.as_str(), entry) |
|
| 506 | 517 | }); |
|
| 507 | - | // The directories of the walk these entries came from, if any. |
|
| 508 | - | let (dirs, nested) = self.walked_dirs.take().unwrap_or_default(); |
|
| 509 | 518 | let new = encode_manifest(conflicted, uncommitted, written, &dirs, &nested, &mut rows); |
|
| 510 | 519 | drop(rows); |
|
| 511 | 520 | let p = self.manifest_path(); |
|
| … | 581 unchanged lines |
modified crates/oxy-core/src/repo/snapshot.rs +67 −6
| … | 61 unchanged lines |
||
| 62 | 62 | /// Folders holding a repository of their own (`.git` or `.oxy`) that |
|
| 63 | 63 | /// are not subrepos, so nothing in them is tracked (oxygen#43). |
|
| 64 | 64 | pub untracked_nested: Vec<String>, |
|
| 65 | + | /// A commit's snapshot leaves the manifest to the commit, which writes |
|
| 66 | + | /// it once with the new base's graph keys. |
|
| 67 | + | pub(crate) manifest: Option<PendingManifest>, |
|
| 65 | 68 | } |
|
| 66 | 69 | ||
| 70 | + | /// The manifest a commit's snapshot would have written, as of the moment |
|
| 71 | + | /// it would have written it (that moment is the racy-window stamp). |
|
| 72 | + | pub(crate) struct PendingManifest { |
|
| 73 | + | entries: BTreeMap<String, FsEntry>, |
|
| 74 | + | blobs: HashMap<String, ObjectRef>, |
|
| 75 | + | dirs: Option<super::WalkedDirs>, |
|
| 76 | + | written: u128, |
|
| 77 | + | } |
|
| 78 | + | ||
| 67 | 79 | pub fn is_binary(content: &[u8]) -> bool { |
|
| 68 | 80 | content[..content.len().min(BINARY_SNIFF)].contains(&0) |
|
| 69 | 81 | } |
|
| … | 564 unchanged lines |
||
| 634 | 646 | })) |
|
| 635 | 647 | } |
|
| 636 | 648 | }; |
|
| 637 | - | if worth { |
|
| 638 | - | self.write_manifest_entries(conflicted, uncommitted, &fs_entries, &built.blobs)?; |
|
| 639 | - | } |
|
| 640 | - | self.write_status_cache((uncommitted && !conflicted).then_some(&new_hash), &statuses); |
|
| 641 | 649 | let attributes = crate::attributes::Attributes::from_files( |
|
| 642 | 650 | fs_entries |
|
| 643 | 651 | .iter() |
|
| 644 | 652 | .filter(|(p, _)| is_attributes(p)) |
|
| 645 | 653 | .map(|(p, e)| (p.as_str(), e.content.as_slice())), |
|
| 646 | 654 | ); |
|
| 655 | + | let mut manifest = None; |
|
| 656 | + | if for_commit { |
|
| 657 | + | manifest = Some(PendingManifest { |
|
| 658 | + | entries: fs_entries, |
|
| 659 | + | blobs: built.blobs.clone(), |
|
| 660 | + | dirs: self.walked_dirs.take(), |
|
| 661 | + | written: super::prompt::now_ns(), |
|
| 662 | + | }); |
|
| 663 | + | } else { |
|
| 664 | + | if worth { |
|
| 665 | + | self.write_manifest_entries(conflicted, uncommitted, &fs_entries, &built.blobs)?; |
|
| 666 | + | } |
|
| 667 | + | self.write_status_cache((uncommitted && !conflicted).then_some(&new_hash), &statuses); |
|
| 668 | + | } |
|
| 647 | 669 | Ok(SnapshotResult { |
|
| 648 | 670 | wc_hash: new_hash, |
|
| 649 | 671 | wc, |
|
| … | 5 unchanged lines |
||
| 655 | 677 | contributors, |
|
| 656 | 678 | attributes, |
|
| 657 | 679 | untracked_nested: self.untracked_nested.borrow().clone(), |
|
| 680 | + | manifest, |
|
| 658 | 681 | }) |
|
| 659 | 682 | } |
|
| 660 | 683 | ||
| … | 156 unchanged lines |
||
| 817 | 840 | out |
|
| 818 | 841 | } |
|
| 819 | 842 | ||
| 843 | + | /// Write the manifest a commit's snapshot left to the commit, with the |
|
| 844 | + | /// graph keys `keys` sets. |
|
| 845 | + | fn write_pending_manifest( |
|
| 846 | + | &self, |
|
| 847 | + | mut m: PendingManifest, |
|
| 848 | + | conflicted: bool, |
|
| 849 | + | keys: HashMap<String, [u8; 32]>, |
|
| 850 | + | ) -> Result<()> { |
|
| 851 | + | for (path, k) in keys { |
|
| 852 | + | if let Some(e) = m.entries.get_mut(&path) { |
|
| 853 | + | e.graph_key = Some(k); |
|
| 854 | + | } |
|
| 855 | + | } |
|
| 856 | + | self.write_manifest_entries_as_of( |
|
| 857 | + | conflicted, |
|
| 858 | + | false, |
|
| 859 | + | &m.entries, |
|
| 860 | + | &m.blobs, |
|
| 861 | + | m.written, |
|
| 862 | + | m.dirs.unwrap_or_default(), |
|
| 863 | + | ) |
|
| 864 | + | } |
|
| 865 | + | ||
| 820 | 866 | fn commit_selection_batched( |
|
| 821 | 867 | &self, |
|
| 822 | 868 | message: &str, |
|
| … | 5 unchanged lines |
||
| 828 | 874 | Some(snap) => snap, |
|
| 829 | 875 | None => self.snapshot_as("oxy commit", true, true)?, |
|
| 830 | 876 | }; |
|
| 877 | + | let pending = snap.manifest.take(); |
|
| 831 | 878 | if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() { |
|
| 879 | + | if let Some(m) = pending { |
|
| 880 | + | let conflicted = snap.base_snapshot.is_conflicted(); |
|
| 881 | + | self.write_pending_manifest(m, conflicted, HashMap::new())?; |
|
| 882 | + | } |
|
| 832 | 883 | return fail("nothing to commit: the working copy matches its base"); |
|
| 833 | 884 | } |
|
| 834 | 885 | if let Some(s) = snap |
|
| … | 165 unchanged lines |
||
| 1000 | 1051 | // commit just extended: a file whose graph it built has none, and |
|
| 1001 | 1052 | // is read by the next snapshot; a key the next base does not share |
|
| 1002 | 1053 | // (the bookmark held other heads) is simply not trusted then. |
|
| 1054 | + | let keying = crate::timing::phase("commit: graph keys of the files"); |
|
| 1003 | 1055 | let committed_keys = |
|
| 1004 | 1056 | (!partial).then(|| self.committed_graph_keys(&st, &changed_paths, &snap.attributes)); |
|
| 1057 | + | drop(keying); |
|
| 1005 | 1058 | let recording = crate::timing::phase("commit: record"); |
|
| 1006 | 1059 | let wc = self.new_working_copy(wc_base, ""); |
|
| 1007 | 1060 | let wc_hash = self.store.put_change(&wc)?; |
|
| … | 5 unchanged lines |
||
| 1013 | 1066 | view.workspaces.insert(self.workspace, wc_hash); |
|
| 1014 | 1067 | self.record_op(&[op_ref], &format!("oxy commit -m {message:?}"), view)?; |
|
| 1015 | 1068 | drop(recording); |
|
| 1016 | - | let _rewrite = crate::timing::phase("commit: manifest and hooks"); |
|
| 1069 | + | let rewriting = crate::timing::phase("commit: manifest"); |
|
| 1017 | 1070 | if partial { |
|
| 1018 | 1071 | // The rest is rebuilt from disk by the next snapshot. |
|
| 1019 | 1072 | self.drop_manifest(); |
|
| … | 7 unchanged lines |
||
| 1027 | 1080 | } else { |
|
| 1028 | 1081 | committed_keys.unwrap_or_default() |
|
| 1029 | 1082 | }; |
|
| 1030 | - | self.rewrite_manifest(conflicted, false, &keys)?; |
|
| 1083 | + | match pending { |
|
| 1084 | + | Some(m) => self.write_pending_manifest(m, conflicted, keys)?, |
|
| 1085 | + | None => self.rewrite_manifest(conflicted, false, &keys)?, |
|
| 1086 | + | } |
|
| 1031 | 1087 | } |
|
| 1088 | + | drop(rewriting); |
|
| 1089 | + | let packing = crate::timing::phase("commit: autopack"); |
|
| 1032 | 1090 | self.maybe_autopack()?; |
|
| 1091 | + | drop(packing); |
|
| 1092 | + | let _hook = crate::timing::phase("commit: hook"); |
|
| 1033 | 1093 | let _ = self.run_hook( |
|
| 1034 | 1094 | "post-commit", |
|
| 1035 | 1095 | &format!( |
|
| … | 575 unchanged lines |
||
| 1611 | 1671 | contributors: Vec::new(), |
|
| 1612 | 1672 | attributes, |
|
| 1613 | 1673 | untracked_nested: self.untracked_nested.borrow().clone(), |
|
| 1674 | + | manifest: None, |
|
| 1614 | 1675 | })) |
|
| 1615 | 1676 | } |
|
| 1616 | 1677 | ||
| … | 1260 unchanged lines |