OXYGENHUB

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