OXYGENHUB

git import: graphs shed before large merges and from kept states (refs #25) fd280228bdc2

The full vscode import passed 14 GB on merges of long-lived branches:
thousands of changes were applied to built graphs that the kept states
shared, so each was copied and grown. State::shed_graphs turns a
history-only state's built graphs back into deferred histories, each
starting from the graph stored as a checkpoint-form blob. The importer
sheds before a merge bringing more than 100 commits, so their changes go
in as skeletons, and when a kept state holds more than 2 000 graphs.
The 10 000-commit slice: 146 s and 1.6 GB (from 136 s and 2.2 GB), the
last 1 000 commits verified.
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-01 05:21 UTC
Signature
✔ signed by Andy McCall
Hash
fd280228bdc252bb48aecdfb07489c7f3fbc5304362888c85a8f22b400add118
Depends on
2306a4d24779 30ea9c1e70fa 79ab8528ff62 958fb27dda7c 982864068a59 c3069537d7c5 d4dbab97e948 fb5b6181618a ff0af1ee299d
Depended on by
9935d490ddfb 33be24095b4b d45dab0484ea c3d2ef6104c2 360aa74a6277
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
9 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/oxy-core/src/repo/git.rs +30 −2
…
285 unchanged lines
286 286
    pub heads: usize,
287 287
}
288 288
289 +
/// A merge bringing more commits than this sheds its base's graphs first.
290 +
const MERGE_SHED_COMMITS: usize = 100;
291 +
/// A kept state with more built graphs than this sheds them.
292 +
const KEPT_GRAPHS: usize = 2000;
293 +
289 294
fn git(dir: &Path, args: &[&str]) -> Result<String> {
290 295
    let out = Command::new("git")
291 296
        .arg("-C")
…
462 unchanged lines
754 759
                .unwrap_or(0)
755 760
        };
756 761
        let mut done = 0usize;
762 +
        // Graphs turned back into history (see `State::shed_graphs`).
763 +
        let mut shed = 0usize;
757 764
        let mut lap = |i: usize, clock: &mut std::time::Instant| {
758 765
            let now = std::time::Instant::now();
759 766
            spent[i] += now - *clock;
…
54 unchanged lines
814 821
                let mut args: Vec<String> = gc.parents[1..].to_vec();
815 822
                args.push(format!("^{}", gc.parents[0]));
816 823
                let refs: Vec<&str> = args.iter().map(String::as_str).collect();
824 +
                // A merge of a long-lived branch brings thousands of
825 +
                // changes: added to built graphs, which kept states share,
826 +
                // they were copied and applied whole (vscode's import passed
827 +
                // 14 GB on such merges). Shed the graphs first, so the
828 +
                // changes go in as skeletons and only what this commit
829 +
                // touches is built again.
830 +
                if base.built_count() > 0 {
831 +
                    let mut count_args = vec!["rev-list", "--count"];
832 +
                    count_args.extend(refs.iter().copied());
833 +
                    let others: usize = git(git_dir, &count_args)?.trim().parse().unwrap_or(0);
834 +
                    if others > MERGE_SHED_COMMITS {
835 +
                        shed += base.shed_graphs(|b| self.store_blob(b))?;
836 +
                    }
837 +
                }
817 838
                let before = base.len();
818 839
                self.add_git_ancestors_touching(&mut base, git_dir, &refs, &known, &mut touched)?;
819 840
                merged = base.len() - before;
…
302 unchanged lines
1122 1143
            crate::timing::add("store: add to state", t.elapsed());
1123 1144
            known.imported.insert(gc.sha.clone(), hash);
1124 1145
            if waiting.get(gc.sha.as_str()).is_some_and(|n| *n > 0) {
1146 +
                // A kept state holds its graphs until its children come:
1147 +
                // past a few thousand, they go back into its history.
1148 +
                if base.built_count() > KEPT_GRAPHS {
1149 +
                    shed += base.shed_graphs(|b| self.store_blob(b))?;
1150 +
                }
1125 1151
                kept.insert(gc.sha.clone(), (base, tree));
1126 1152
                kept_order.push_back(gc.sha.clone());
1127 1153
                if kept_order.len() > max_kept {
…
12 unchanged lines
1140 1166
                    .map(|(st, _)| st.built_count())
1141 1167
                    .unwrap_or(0);
1142 1168
                eprintln!(
1143 -
                    "trace {done} {} parents={} base={base_how} merged={merged} built={built} kept={} rss_mb={}",
1169 +
                    "trace {done} {} parents={} base={base_how} merged={merged} built={built} kept={} shed={shed} rss_mb={}",
1144 1170
                    &gc.sha[..12.min(gc.sha.len())],
1145 1171
                    gc.parents.len(),
1146 1172
                    kept.len(),
…
16 unchanged lines
1163 1189
        }
1164 1190
        crate::timing::report_totals();
1165 1191
        if crate::timing::enabled() {
1166 -
            eprintln!("timing git import: {rebuilt} bases rebuilt from history");
1192 +
            eprintln!(
1193 +
                "timing git import: {rebuilt} bases rebuilt from history, {shed} graphs shed"
1194 +
            );
1167 1195
        }
1168 1196
1169 1197
        // The bookmark: heads of the old state plus every new change.
…
509 unchanged lines
modified crates/oxy-core/src/repo/tests.rs +34 −0
…
6418 unchanged lines
6419 6419
    let snap = repo.snapshot_status("status").unwrap();
6420 6420
    assert!(snap.statuses.iter().any(|s| s.path == "kept.log"));
6421 6421
}
6422 +
6423 +
#[test]
6424 +
fn shed_graphs_come_back_the_same() {
6425 +
    hermetic();
6426 +
    let dir = tempfile::tempdir().unwrap();
6427 +
    let root = dir.path();
6428 +
    let repo = Repo::init(root).unwrap();
6429 +
    write(root, "a.txt", b"1\n2\n3\n");
6430 +
    write(root, "b.txt", b"x\n");
6431 +
    repo.commit("first", &[]).unwrap();
6432 +
    write(root, "a.txt", b"1\ntwo\n3\n4\n");
6433 +
    repo.commit("second", &[]).unwrap();
6434 +
    let heads = repo.bookmark_heads().unwrap();
6435 +
    let mut st = repo.load_state_history(&heads).unwrap();
6436 +
    let view = st.tree().view();
6437 +
    let nodes: Vec<NodeId> = ["a.txt", "b.txt"]
6438 +
        .iter()
6439 +
        .map(|p| view.entries[*p].node)
6440 +
        .collect();
6441 +
    repo.load_graphs_of(&mut st, &nodes).unwrap();
6442 +
    let before: Vec<Vec<u8>> = nodes.iter().map(|n| st.render_file(n).0).collect();
6443 +
    assert_eq!(st.built_count(), 2);
6444 +
    let shed = st.shed_graphs(|b| repo.store_blob(b)).unwrap();
6445 +
    assert_eq!((shed, st.built_count()), (2, 0));
6446 +
    // A later change applies on top of the shed history.
6447 +
    write(root, "a.txt", b"1\ntwo\n3\n4\n5\n");
6448 +
    let h3 = repo.commit("third", &[]).unwrap();
6449 +
    let (skeleton, texts) = repo.store().get_change_lazy(&h3).unwrap();
6450 +
    st.add_known(h3, skeleton, texts);
6451 +
    repo.load_graphs_of(&mut st, &nodes).unwrap();
6452 +
    let after: Vec<Vec<u8>> = nodes.iter().map(|n| st.render_file(n).0).collect();
6453 +
    assert_eq!(after[1], before[1]);
6454 +
    assert_eq!(after[0], b"1\ntwo\n3\n4\n5\n");
6455 +
}
modified crates/oxy-core/src/state.rs +35 −0
…
920 unchanged lines
921 921
        self.changes.len()
922 922
    }
923 923
924 +
    /// Turn this history-only state's built graphs back into deferred
925 +
    /// histories, each starting from its graph encoded as a checkpoint
926 +
    /// graph and stored by `store` (it returns the blob's reference): what
927 +
    /// they held stays, and memory goes back to what a history-only state
928 +
    /// costs. A graph built later again decodes that blob and applies only
929 +
    /// the edits that came after. Graph keys change (the base is now a
930 +
    /// blob), which only means a cached rendering is not reused.
931 +
    pub fn shed_graphs<E>(
932 +
        &mut self,
933 +
        mut store: impl FnMut(&[u8]) -> Result<ObjectRef, E>,
934 +
    ) -> Result<usize, E> {
935 +
        use rayon::prelude::*;
936 +
        let Some(deferred) = self.deferred.as_mut() else {
937 +
            return Ok(0);
938 +
        };
939 +
        let graphs: Vec<(NodeId, Arc<FileGraph>)> = self.files.drain().collect();
940 +
        let encoded: Vec<(NodeId, Vec<u8>)> = graphs
941 +
            .par_iter()
942 +
            .map(|(n, g)| (*n, cbor::encode(&g.to_value())))
943 +
            .collect();
944 +
        drop(graphs);
945 +
        let shed = encoded.len();
946 +
        for (node, bytes) in encoded {
947 +
            let blob = store(&bytes)?;
948 +
            deferred.insert(
949 +
                node,
950 +
                Arc::new(Deferred::from_body(DeferredBody {
951 +
                    base: Some(DeferredBase::Blob(blob)),
952 +
                    ..DeferredBody::default()
953 +
                })),
954 +
            );
955 +
        }
956 +
        Ok(shed)
957 +
    }
958 +
924 959
    /// How many file graphs this state has built.
925 960
    pub fn built_count(&self) -> usize {
926 961
        self.files.len()
…
1554 unchanged lines