OXYGENHUB

Checkout: files added once are written from their text, new files written in parallel (refs #41) 091fef80755d

- A file whose history is one insert of its whole text (added once, never
  edited, no checkpoint base) is written from that text without building
  its graph; test builds render the graph as well and compare.
- Each directory of a batch is checked and made once; files are compared
  on every core, new files written on every core, and files that exist
  rewritten one at a time (on ext4, rewriting in parallel was six times
  slower).
- fetch has phases under OXY_TIMINGS.

Local clone of the Linux tree: 16.8 to 17.3 s (from 32 s; Git 17.0 s).
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 23:27 UTC
Signature
✔ signed by Andy McCall
Hash
091fef80755d422062f2abeaebd66da497997257dcd4c3955ca0cac9eba178be
Depends on
1c2bd9abdd4f 57d8dc05c9d4 598009a75f6c 83e0677b07fe 982864068a59 b3c5a59a875a c5ce0367eddf cc7a00565161 ff0af1ee299d
Depended on by
2f0b4b0651ea c397a8c36a9f f3811eb20bea 5552e69c7325 11e2b98a3a39
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
15 atoms, 0 tree operations, 4 files

Browse the tree at this change

modified crates/oxy-core/src/repo/checkout.rs +158 −13
…
233 unchanged lines
234 234
        for batch in batches {
235 235
            use rayon::prelude::*;
236 236
            let nodes: Vec<NodeId> = batch.iter().map(|(_, n, _)| *n).collect();
237 +
            // A file added once and never edited is its text: no graph.
238 +
            let single: Vec<Option<crate::objects::TextRef>> =
239 +
                nodes.iter().map(|n| target.single_text(n)).collect();
240 +
            let graphs: Vec<NodeId> = nodes
241 +
                .iter()
242 +
                .zip(&single)
243 +
                .filter(|(_, s)| s.is_none())
244 +
                .map(|(n, _)| *n)
245 +
                .collect();
246 +
            let mut texts = {
247 +
                let _t = crate::timing::phase("checkout: read single texts");
248 +
                let wanted: Vec<crate::objects::TextRef> =
249 +
                    single.iter().flatten().copied().collect();
250 +
                self.single_texts(&wanted)?.into_iter()
251 +
            };
237 252
            {
238 253
                let _t = crate::timing::phase("checkout: build graphs");
239 -
                self.load_graphs_of(&mut target, &nodes)?;
254 +
                self.load_graphs_of(&mut target, &graphs)?;
240 255
            }
241 256
            let rendered: Vec<(Vec<u8>, bool)> = {
242 257
                let _t = crate::timing::phase("checkout: render");
243 -
                nodes
258 +
                let mut out: Vec<Option<(Vec<u8>, bool)>> = single
259 +
                    .iter()
260 +
                    .map(|s| s.as_ref().and_then(|_| texts.next().map(|t| (t, false))))
261 +
                    .collect();
262 +
                let from_graphs: Vec<(Vec<u8>, bool)> = graphs
244 263
                    .par_iter()
245 264
                    .map(|n| {
246 265
                        let (bytes, m) = target.render_file(n);
247 266
                        (bytes, m.is_conflicted())
248 267
                    })
268 +
                    .collect();
269 +
                let mut from_graphs = from_graphs.into_iter();
270 +
                for o in out.iter_mut().filter(|o| o.is_none()) {
271 +
                    *o = from_graphs.next();
272 +
                }
273 +
                out.into_iter()
274 +
                    .map(|o| o.expect("every file rendered"))
249 275
                    .collect()
250 276
            };
277 +
            #[cfg(test)]
278 +
            {
279 +
                let singles: Vec<NodeId> = nodes
280 +
                    .iter()
281 +
                    .zip(&single)
282 +
                    .filter(|(_, s)| s.is_some())
283 +
                    .map(|(n, _)| *n)
284 +
                    .collect();
285 +
                self.load_graphs_of(&mut target, &singles)?;
286 +
                for (n, (bytes, _)) in nodes.iter().zip(&rendered) {
287 +
                    assert_eq!(
288 +
                        *bytes,
289 +
                        target.render_file(n).0,
290 +
                        "a single text is its file's rendering"
291 +
                    );
292 +
                }
293 +
            }
251 294
            target.drop_graphs(&nodes);
252 295
            let _t = crate::timing::phase("checkout: write");
253 296
            if done == 0 && attr_count > 0 {
…
5 unchanged lines
259 302
                );
260 303
            }
261 304
            done += batch.len();
262 -
            for ((path, node, mode), (bytes, conflicted)) in batch.iter().zip(rendered) {
263 -
                let a = attrs.lookup(path);
264 -
                let crlf = a.eol == Some(crate::attributes::Eol::Crlf);
265 -
                let on_disk: Vec<u8> = if crlf {
266 -
                    crate::attributes::to_crlf(&bytes)
267 -
                } else {
268 -
                    bytes
269 -
                };
270 -
                if !self.same_on_disk(&self.root.join(path), &on_disk, mode.exec) {
271 -
                    self.write_file(path, &on_disk, mode.exec)?;
272 -
                    report.written += 1;
305 +
            let on_disk: Vec<Vec<u8>> = batch
306 +
                .iter()
307 +
                .zip(rendered.iter())
308 +
                .map(|((path, _, _), (bytes, _))| {
309 +
                    if attrs.lookup(path).eol == Some(crate::attributes::Eol::Crlf) {
310 +
                        crate::attributes::to_crlf(bytes)
311 +
                    } else {
312 +
                        bytes.clone()
313 +
                    }
314 +
                })
315 +
                .collect();
316 +
            // Each directory checked (no symlink on the way) and made once,
317 +
            // then the files compared and written on every core.
318 +
            let mut dirs: BTreeSet<&str> = BTreeSet::new();
319 +
            for (path, _, _) in batch {
320 +
                if let Some((dir, _)) = path.rsplit_once('/') {
321 +
                    dirs.insert(dir);
322 +
                }
323 +
            }
324 +
            for dir in dirs {
325 +
                let full = self.safe_path(dir)?;
326 +
                if !full.is_dir() {
327 +
                    if fs::symlink_metadata(&full).is_ok() {
328 +
                        remove_any(&full)?;
329 +
                    }
330 +
                    fs::create_dir_all(&full)?;
273 331
                }
332 +
            }
333 +
            for (path, _, _) in batch {
334 +
                check_path_name(path)?;
335 +
            }
336 +
            let (root, exec_bits) = (&self.root, self.platform.exec_bits);
337 +
            // Compared on every core; new files written on every core too,
338 +
            // but files that exist rewritten one at a time: on ext4,
339 +
            // rewriting in parallel was six times slower (each truncation
340 +
            // starts writing back the old blocks).
341 +
            let plans: Vec<Disk> = batch
342 +
                .par_iter()
343 +
                .zip(on_disk.par_iter())
344 +
                .map(|((path, _, mode), bytes)| {
345 +
                    compare_on_disk(&root.join(path), bytes, mode.exec, exec_bits)
346 +
                })
347 +
                .collect();
348 +
            let fresh: Vec<usize> = (0..batch.len())
349 +
                .filter(|&i| plans[i] == Disk::Absent)
350 +
                .collect();
351 +
            fresh
352 +
                .par_iter()
353 +
                .map(|&i| write_file_at(&root.join(&batch[i].0), &on_disk[i], batch[i].2.exec))
354 +
                .collect::<std::io::Result<()>>()?;
355 +
            for (i, plan) in plans.iter().enumerate() {
356 +
                if *plan == Disk::Differs {
357 +
                    write_file_at(&root.join(&batch[i].0), &on_disk[i], batch[i].2.exec)?;
358 +
                }
359 +
            }
360 +
            report.written += plans.iter().filter(|p| **p != Disk::Same).count();
361 +
            for (((path, node, mode), (_, conflicted)), on_disk) in
362 +
                batch.iter().zip(rendered).zip(on_disk)
363 +
            {
364 +
                let a = attrs.lookup(path);
274 365
                report.conflicted |= conflicted;
275 366
                entries.insert(
276 367
                    path.clone(),
…
137 unchanged lines
414 505
        ChangeStatus::ModeChanged => "mode changed",
415 506
        ChangeStatus::ConflictEdited => "conflict edited",
416 507
        ChangeStatus::Resolved => "resolved",
508 +
    }
509 +
}
510 +
511 +
/// Refuse a path that could escape the workspace (spec §7.7): an empty,
512 +
/// `.` or `..` component, a backslash, or a first component `.oxy`.
513 +
fn check_path_name(rel: &str) -> Result<()> {
514 +
    for (i, comp) in rel.split('/').enumerate() {
515 +
        if comp.is_empty() || comp == "." || comp == ".." || comp.contains('\\') {
516 +
            return fail(format!("refusing unsafe path {rel:?}"));
517 +
        }
518 +
        if i == 0 && comp == OXY_DIR {
519 +
            return fail(format!("refusing to write inside {OXY_DIR}: {rel:?}"));
520 +
        }
521 +
    }
522 +
    Ok(())
523 +
}
524 +
525 +
/// A file on disk against what a checkout would write there.
526 +
#[derive(Clone, Copy, PartialEq, Eq)]
527 +
enum Disk {
528 +
    Same,
529 +
    Differs,
530 +
    Absent,
531 +
}
532 +
533 +
/// Whether the file at `full` already holds `bytes` with the right
534 +
/// executable bit. Touches no repository state, so it runs in parallel.
535 +
fn compare_on_disk(full: &Path, bytes: &[u8], exec: bool, exec_bits: bool) -> Disk {
536 +
    match fs::symlink_metadata(full) {
537 +
        Ok(m)
538 +
            if m.is_file()
539 +
                && m.len() == bytes.len() as u64
540 +
                && (!exec_bits || is_executable(&m) == exec)
541 +
                && fs::read(full).is_ok_and(|b| b == bytes) =>
542 +
        {
543 +
            Disk::Same
544 +
        }
545 +
        Ok(_) => Disk::Differs,
546 +
        Err(_) => Disk::Absent,
547 +
    }
548 +
}
549 +
550 +
/// Write a file whose directory exists, replacing whatever is there.
551 +
fn write_file_at(full: &Path, bytes: &[u8], exec: bool) -> std::io::Result<()> {
552 +
    match fs::symlink_metadata(full) {
553 +
        Ok(m) if m.is_file() => {
554 +
            if m.permissions().readonly() {
555 +
                set_readonly(full, false)?;
556 +
            }
557 +
        }
558 +
        Ok(_) => remove_any(full)?,
559 +
        Err(_) => {}
417 560
    }
561 +
    fs::write(full, bytes)?;
562 +
    set_executable(full, exec)
418 563
}
modified crates/oxy-core/src/repo/mod.rs +34 −0
…
862 unchanged lines
863 863
            .collect())
864 864
    }
865 865
866 +
    /// The contents of texts [`State::single_text`] gave: blobs read in
867 +
    /// one go, inline ones from their changes.
868 +
    pub(crate) fn single_texts(&self, texts: &[crate::objects::TextRef]) -> Result<Vec<Vec<u8>>> {
869 +
        use crate::objects::TextRef;
870 +
        let refs: Vec<ObjectRef> = texts
871 +
            .iter()
872 +
            .filter_map(|t| match t {
873 +
                TextRef::Blob(r, _, _) => Some(*r),
874 +
                TextRef::Inline { .. } => None,
875 +
            })
876 +
            .collect();
877 +
        let mut blobs = self.read_blobs(&refs)?.into_iter();
878 +
        let mut out = Vec::with_capacity(texts.len());
879 +
        for t in texts {
880 +
            let lines = match t {
881 +
                TextRef::Blob(_, count, form) => {
882 +
                    let content = blobs
883 +
                        .next()
884 +
                        .ok_or_else(|| RepoError::Msg("text blobs: wrong count".into()))?;
885 +
                    crate::objects::text_lines(&content, *count, *form)?
886 +
                }
887 +
                TextRef::Inline { change, start, end } => {
888 +
                    let bytes = self.change_bytes(change)?;
889 +
                    let range = bytes.get(*start as usize..*end as usize).ok_or_else(|| {
890 +
                        RepoError::Msg("an inline insert lies outside its change".into())
891 +
                    })?;
892 +
                    crate::objects::inline_lines(range)?
893 +
                }
894 +
            };
895 +
            out.push(lines.bytes().to_vec());
896 +
        }
897 +
        Ok(out)
898 +
    }
899 +
866 900
    /// Build every graph a history-only state still lacks, making it full.
867 901
    pub fn load_all_graphs(&self, state: &mut State) -> Result<()> {
868 902
        Ok(state.load_all_graphs(
…
882 unchanged lines
modified crates/oxy-core/src/repo/sync.rs +12 −3
…
1148 unchanged lines
1149 1149
        let mut report = Vec::new();
1150 1150
        let max_blob = self.config.max_blob;
1151 1151
        for r in resp.refs {
1152 -
            let needed = remote.negotiate(&have, &r.heads)?;
1153 -
            let bundle = remote.fetch(&needed, max_blob)?;
1152 +
            let needed = {
1153 +
                let _t = crate::timing::phase("fetch: negotiate");
1154 +
                remote.negotiate(&have, &r.heads)?
1155 +
            };
1156 +
            let bundle = {
1157 +
                let _t = crate::timing::phase("fetch: bundle");
1158 +
                remote.fetch(&needed, max_blob)?
1159 +
            };
1154 1160
            // Changes are not individually re-verified here: the bookmark's
1155 1161
            // attestation (checked below) binds its whole state, and the
1156 1162
            // receiver of the push verified the added changes at that time.
1157 -
            let imported = self.import_bundle(&bundle, false)?;
1163 +
            let imported = {
1164 +
                let _t = crate::timing::phase("fetch: import");
1165 +
                self.import_bundle(&bundle, false)?
1166 +
            };
1158 1167
            if max_blob.is_some() {
1159 1168
                self.register_missing_blobs(&imported, remote_name)?;
1160 1169
            }
…
579 unchanged lines
modified crates/oxy-core/src/state.rs +27 −0
…
468 unchanged lines
469 469
        Ok(())
470 470
    }
471 471
472 +
    /// The one text a file's history is, when it is a single insert of
473 +
    /// the whole file into nothing (a file added once and never edited,
474 +
    /// with no checkpoint base): its rendering is that text, so it needs
475 +
    /// no graph. `None` otherwise, or when the graph is built.
476 +
    pub fn single_text(&self, node: &NodeId) -> Option<TextRef> {
477 +
        if self.files.contains_key(node) {
478 +
            return None;
479 +
        }
480 +
        let d = self.deferred.as_ref()?.get(node)?.body();
481 +
        let [(_, edits)] = d.edits.as_slice() else {
482 +
            return None;
483 +
        };
484 +
        let [Edit::Insert {
485 +
            after: AtomRef::Start,
486 +
            before: AtomRef::End,
487 +
            lines,
488 +
            ..
489 +
        }] = edits.as_slice()
490 +
        else {
491 +
            return None;
492 +
        };
493 +
        match (&d.base, d.texts.as_slice()) {
494 +
            (None, [t]) if lines.is_empty() => Some(*t),
495 +
            _ => None,
496 +
        }
497 +
    }
498 +
472 499
    /// A key for the graph of `node` in a history-only state that has not
473 500
    /// built it: equal keys mean equal graphs, so equal renderings (a
474 501
    /// rendering depends only on the graph and on the changes' own
…
1489 unchanged lines