OXYGENHUB

Share a state's changes and deferred files between clones f6451fbbae3f

A clone of a state (a Git import keeps several, one per branch point
whose children are still to come) copied every change's metadata and
every file's deferred edits. They are now shared behind Arc and copied
on write, and a clone starts with an empty closure cache. The first
5 000 vscode commits import as before (317 s, 2.1 GB); 20 000 take
4 530 s and 6.7 GB, which shows the growth is the base state itself and
the import's work per commit, costed in the backlog (oxygen#25).

refs #35
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 05:16 UTC
Signature
✔ signed by Andy McCall
Hash
f6451fbbae3f5bd13fe87928d1cc2ae109df40394ec1d46ac3cb12e82b89d8b1
Depends on
0bdc3f91a39f 119bbf50cb86 1681323d1dff 2517abe64654 252e8cb23fbc 33e77a5526fa 57d8dc05c9d4 86e1824f2d0b e8d38e47e9d7 ff0af1ee299d
Depended on by
aedfe465b611 ac03130a2b95 5552e69c7325 33be24095b4b d45dab0484ea 36e51aa806c6 3f1b8fe25ec9 f9575918477f 75b6c8665219
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
14 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified crates/oxy-core/src/state.rs +20 −12
…
54 unchanged lines
55 55
56 56
#[derive(Default)]
57 57
pub struct State {
58 -
    changes: HashMap<ChangeHash, ChangeMeta>,
58 +
    /// Shared between clones, like `files` and `deferred`: a state kept
59 +
    /// beside another that differs by a few changes shares the rest.
60 +
    changes: HashMap<ChangeHash, Arc<ChangeMeta>>,
59 61
    tree: TreeGraph,
60 62
    /// Shared between clones: a clone copies a graph only when an edit
61 63
    /// changes it (`Arc::make_mut`), so keeping several states that differ
…
6 unchanged lines
68 70
    /// `None` for a full state. For a history-only state (loaded for its
69 71
    /// changes and tree), the file graphs not built yet, by node: `files`
70 72
    /// holds only those loaded with [`State::load_graphs`].
71 -
    deferred: Option<HashMap<NodeId, Deferred>>,
73 +
    deferred: Option<HashMap<NodeId, Arc<Deferred>>>,
72 74
}
73 75
74 76
use crate::objects::TextRef;
…
24 unchanged lines
99 101
            tree: self.tree.clone(),
100 102
            files: self.files.clone(),
101 103
            replaces: self.replaces.clone(),
102 -
            closure: Mutex::new(self.closure.lock().unwrap().clone()),
104 +
            // A cache: the clone builds its own.
105 +
            closure: Mutex::new(HashMap::new()),
103 106
            deferred: self.deferred.clone(),
104 107
        }
105 108
    }
…
293 unchanged lines
399 402
        };
400 403
        let taken: Vec<(NodeId, Deferred)> = nodes
401 404
            .iter()
402 -
            .filter_map(|n| deferred.remove(n).map(|d| (*n, d)))
405 +
            .filter_map(|n| deferred.remove(n).map(|d| (*n, Arc::unwrap_or_clone(d))))
403 406
            .collect();
404 407
        let built = build_deferred(taken, load, read_change)?;
405 408
        self.files
…
17 unchanged lines
423 426
        };
424 427
        let copies: Vec<(NodeId, Deferred)> = nodes
425 428
            .iter()
426 -
            .map(|n| (*n, deferred.get(n).cloned().unwrap_or_default()))
429 +
            .map(|n| {
430 +
                (
431 +
                    *n,
432 +
                    deferred.get(n).map(|d| (**d).clone()).unwrap_or_default(),
433 +
                )
434 +
            })
427 435
            .collect();
428 436
        build_deferred(copies, load, read_change)
429 437
    }
…
85 unchanged lines
515 523
        self.changes.contains_key(h)
516 524
    }
517 525
    pub fn get(&self, h: &ChangeHash) -> Option<&ChangeMeta> {
518 -
        self.changes.get(h)
526 +
        self.changes.get(h).map(|m| &**m)
519 527
    }
520 528
    pub fn changes(&self) -> impl Iterator<Item = (&ChangeHash, &ChangeMeta)> {
521 -
        self.changes.iter()
529 +
        self.changes.iter().map(|(h, m)| (h, &**m))
522 530
    }
523 531
    pub fn tree(&self) -> &TreeGraph {
524 532
        &self.tree
…
41 unchanged lines
566 574
        }
567 575
        let meta = ChangeMeta::of(&change);
568 576
        self.apply_with(h, change, texts);
569 -
        self.changes.insert(h, meta);
577 +
        self.changes.insert(h, Arc::new(meta));
570 578
        self.closure.lock().unwrap().clear();
571 579
    }
572 580
…
223 unchanged lines
796 804
            };
797 805
            deferred.insert(
798 806
                node,
799 -
                Deferred {
807 +
                Arc::new(Deferred {
800 808
                    base: Some(base),
801 809
                    edits: Vec::new(),
802 810
                    texts: Vec::new(),
803 -
                },
811 +
                }),
804 812
            );
805 813
        }
806 814
        st.deferred = Some(deferred);
…
24 unchanged lines
831 839
                kind: ChangeKind::from_tag(o::uint(&e[6], "kind")?)?,
832 840
                author: o::oref(&e[7], "author")?,
833 841
            };
834 -
            st.changes.insert(hash, meta);
842 +
            st.changes.insert(hash, Arc::new(meta));
835 843
        }
836 844
        st.tree = TreeGraph::from_value(o::field(m, 2, "tree")?)?;
837 845
        let opt = |v: &Value| -> Result<Option<ObjectRef>, ObjectError> {
…
69 unchanged lines
907 915
                        apply_file_edits(Arc::make_mut(g), h, edits)
908 916
                    }
909 917
                    None => {
910 -
                        let d = deferred.entry(node).or_default();
918 +
                        let d = Arc::make_mut(deferred.entry(node).or_default());
911 919
                        d.edits.push((h, edits));
912 920
                        d.texts.extend(node_texts);
913 921
                    }
…
1050 unchanged lines
modified docs/backlog-overnight-3.md +15 −0
…
209 unchanged lines
210 210
- 2026-09-30 **H4 attempted**: the full vscode import (166 425 commits) under a 14 GB cap. With changes added mixed (e15074c7), it still grew about 0.4 MB per commit: 4.3 GB after 35 minutes and roughly 9 000 commits, far past the cap long before the end. It was stopped. The growth is the importer's kept states: each is a deep clone of a history-only state (change metadata, tree maps, the deferred edits of every file), up to eight at a time, with a new clone at every branch point.
211 211
  - **Next (H2b)**: make a state cheap to clone by sharing its parts (per-file deferred entries and graphs behind `Arc`, copied on write; the change map and tree likewise), so kept states share what they have in common.
212 212
  - **Also**: an import should report progress on long runs.
213 +
- 2026-09-30 **H2b, first part**: a state's change metadata and per-file deferred entries are shared between clones and copied on write, and a clone starts with an empty closure cache.
214 +
  - Measured: 5 000 vscode commits 317 s and 2.07 GB (unchanged); 20 000 commits 4 530 s and 6.7 GB. So most of the growth is the base state itself, not its clones.
215 +
  - Phases from 2 000 to 5 000 commits (2.5 times as many):
216 +
217 +
    | Phase | 2 000 commits | 5 000 commits |
218 +
    |---|---:|---:|
219 +
    | bases | 5.3 s | 24.4 s |
220 +
    | ls-tree | 20.8 s | 63.8 s |
221 +
    | render base | 20.8 s | 89.0 s |
222 +
    | build change | 97.3 s | 139.7 s |
223 +
224 +
  - Listing, rendering and rebuilding bases cost what the whole tree is, on every commit: oxygen#25. H3 to H5 need it. The plan:
225 +
    - list a commit's changes with `git diff-tree` against its first parent rather than `ls-tree`;
226 +
    - keep the tree view up to date as changes apply, rather than recomputing it;
227 +
    - render only the paths a commit touches.
213 228