OXYGENHUB

Git import: a large merge renders its tree in batches c3d2ef6104c2

A merge onto a base with no built graphs built the graphs of every
text file at once, then rendered and diffed them: on vscode's history
that was the peak that stopped the full import. Now, past 500 files,
the graphs are built beside the state a batch at a time and dropped
once rendered, so what is held for the whole tree is the renderings.
The few files the change builder reads graphs for (conflicted ones,
and renderings whose lines are not one per live atom) are built in
the state afterwards. The existing import test runs again with
batches of one file.

refs #25
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-04 00:07 UTC
Signature
✔ signed by Andy McCall
Hash
c3d2ef6104c2c9afdcdffdc9851cd4b746f0b8def7cd85773239e847b68f4efb
Depends on
2306a4d24779 40a40bb7b798 79ab8528ff62 982864068a59 c3069537d7c5 e7ddd552b619 fd280228bdc2 ff0af1ee299d
Depended on by
ce118287d9da 33be24095b4b
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.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
5 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/oxy-core/src/repo/git.rs +123 −5
…
290 unchanged lines
291 291
/// A kept state with more built graphs than this sheds them.
292 292
const KEPT_GRAPHS: usize = 2000;
293 293
294 +
/// Text files of a merge rendered this many at a time (oxygen#25): a
295 +
/// batch's graphs are built beside the state and dropped once rendered,
296 +
/// so what is held for the whole tree is the renderings, not the graphs
297 +
/// with every deleted line of their history.
298 +
const MERGE_BATCH: usize = 500;
299 +
300 +
thread_local! {
301 +
    /// A smaller batch for this thread's imports: tests set 1, so a small
302 +
    /// merge takes the batched path.
303 +
    static MERGE_BATCH_OVERRIDE: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
304 +
}
305 +
306 +
#[cfg(test)]
307 +
pub(crate) fn set_merge_batch_for_tests(n: Option<usize>) {
308 +
    MERGE_BATCH_OVERRIDE.with(|c| c.set(n));
309 +
}
310 +
311 +
fn merge_batch() -> usize {
312 +
    MERGE_BATCH_OVERRIDE
313 +
        .with(|c| c.get())
314 +
        .filter(|n| *n > 0)
315 +
        .unwrap_or(MERGE_BATCH)
316 +
}
317 +
318 +
impl Repo {
319 +
    /// As `State::materialize_except`, for a tree whose graphs would not
320 +
    /// fit in memory at once. The few files the change builder reads
321 +
    /// graphs for (conflicted ones, and renderings whose lines are not
322 +
    /// one per live atom) are built in the state afterwards.
323 +
    fn materialize_batched(
324 +
        &self,
325 +
        base: &mut State,
326 +
        need: &[crate::ids::NodeId],
327 +
        skip: &HashSet<crate::ids::NodeId>,
328 +
    ) -> Result<crate::state::Snapshot> {
329 +
        use crate::graph::Materialized;
330 +
        use crate::state::{FileContent, FileState};
331 +
        use rayon::prelude::*;
332 +
        let view = base.tree().view();
333 +
        let mut rendered: HashMap<crate::ids::NodeId, (Vec<u8>, Materialized)> =
334 +
            HashMap::with_capacity(need.len());
335 +
        let mut keep_built: Vec<crate::ids::NodeId> = Vec::new();
336 +
        for batch in need.chunks(merge_batch()) {
337 +
            let t = std::time::Instant::now();
338 +
            let graphs = self.build_graphs_detached(base, batch)?;
339 +
            crate::timing::add("render: build graphs, batch", t.elapsed());
340 +
            let t = std::time::Instant::now();
341 +
            let batch_rendered: Vec<(crate::ids::NodeId, Vec<u8>, Materialized)> = graphs
342 +
                .par_iter()
343 +
                .map(|(node, g)| {
344 +
                    let (bytes, m) = base.render_graph(g);
345 +
                    (*node, bytes, m)
346 +
                })
347 +
                .collect();
348 +
            drop(graphs);
349 +
            crate::timing::add("render: render, batch", t.elapsed());
350 +
            for (node, bytes, m) in batch_rendered {
351 +
                if m.is_conflicted()
352 +
                    || m.live_atoms().len() != crate::diff::split_lines(&bytes).len()
353 +
                {
354 +
                    keep_built.push(node);
355 +
                }
356 +
                rendered.insert(node, (bytes, m));
357 +
            }
358 +
        }
359 +
        if !keep_built.is_empty() {
360 +
            self.load_graphs_of(base, &keep_built)?;
361 +
        }
362 +
        let mut files = BTreeMap::new();
363 +
        let mut unrendered = BTreeSet::new();
364 +
        for (path, e) in view.entries.iter() {
365 +
            let f = if e.kind == NodeKind::File && !e.mode.opaque {
366 +
                match rendered.remove(&e.node) {
367 +
                    Some((bytes, materialized)) => FileState {
368 +
                        node: e.node,
369 +
                        kind: e.kind,
370 +
                        mode: e.mode,
371 +
                        content: FileContent::Text {
372 +
                            bytes,
373 +
                            materialized,
374 +
                        },
375 +
                    },
376 +
                    None if skip.contains(&e.node) => {
377 +
                        unrendered.insert(path.clone());
378 +
                        FileState {
379 +
                            node: e.node,
380 +
                            kind: e.kind,
381 +
                            mode: e.mode,
382 +
                            content: FileContent::Text {
383 +
                                bytes: Vec::new(),
384 +
                                materialized: Materialized::default(),
385 +
                            },
386 +
                        }
387 +
                    }
388 +
                    None => {
389 +
                        // Neither asked for nor known unchanged: render it
390 +
                        // from a built graph, as the whole-tree path does.
391 +
                        self.load_graphs_of(base, &[e.node])?;
392 +
                        base.entry_state(e)
393 +
                    }
394 +
                }
395 +
            } else {
396 +
                base.entry_state(e)
397 +
            };
398 +
            files.insert(path.clone(), f);
399 +
        }
400 +
        Ok(crate::state::Snapshot {
401 +
            tree: view,
402 +
            files,
403 +
            unrendered,
404 +
        })
405 +
    }
406 +
}
407 +
294 408
fn git(dir: &Path, args: &[&str]) -> Result<String> {
295 409
    let out = Command::new("git")
296 410
        .arg("-C")
…
654 unchanged lines
951 1065
                    .collect();
952 1066
                crate::timing::add("render: same and need", clock.elapsed());
953 1067
                let t = std::time::Instant::now();
954 -
                self.load_graphs_of(&mut base, &need)?;
955 -
                crate::timing::add("render: load graphs, whole tree", t.elapsed());
956 -
                let t = std::time::Instant::now();
957 -
                let snap = base.materialize_except(&skip);
958 -
                crate::timing::add("render: materialize, whole tree", t.elapsed());
1068 +
                let snap = if need.len() > merge_batch() {
1069 +
                    // A merge onto a base with no built graphs: thousands
1070 +
                    // of files, whose graphs would not fit at once.
1071 +
                    self.materialize_batched(&mut base, &need, &skip)?
1072 +
                } else {
1073 +
                    self.load_graphs_of(&mut base, &need)?;
1074 +
                    base.materialize_except(&skip)
1075 +
                };
1076 +
                crate::timing::add("render: whole tree", t.elapsed());
959 1077
                snap
960 1078
            };
961 1079
…
745 unchanged lines
modified crates/oxy-core/src/repo/tests.rs +12 −0
…
1438 unchanged lines
1439 1439
1440 1440
#[test]
1441 1441
fn git_import_reproduces_trees() {
1442 +
    git_import_reproduces_trees_with(None);
1443 +
}
1444 +
1445 +
/// The same import with every merge rendered in batches of one file
1446 +
/// (oxygen#25): what a merge onto a base of thousands of files takes.
1447 +
#[test]
1448 +
fn git_import_reproduces_trees_in_batches() {
1449 +
    git_import_reproduces_trees_with(Some(1));
1450 +
}
1451 +
1452 +
fn git_import_reproduces_trees_with(batch: Option<usize>) {
1453 +
    crate::repo::git::set_merge_batch_for_tests(batch);
1442 1454
    hermetic();
1443 1455
    if std::process::Command::new("git")
1444 1456
        .arg("--version")
…
5011 unchanged lines
modified crates/oxy-core/src/state.rs +6 −0
…
1759 unchanged lines
1760 1760
        }
1761 1761
    }
1762 1762
1763 +
    /// The state of one tree entry: a directory, an opaque file's live
1764 +
    /// blobs, or a text file rendered from its graph (which must be built).
1765 +
    pub fn entry_state(&self, e: &crate::tree::TreeEntry) -> FileState {
1766 +
        self.file_state(e)
1767 +
    }
1768 +
1763 1769
    fn file_state(&self, e: &crate::tree::TreeEntry) -> FileState {
1764 1770
        {
1765 1771
            let content = match e.kind {
…
750 unchanged lines