OXYGENHUB

Blame prints through one buffered lock; the tree view builds in one step (refs #41) fddf64baa3d2

oxy blame wrote each of MAINTAINERS' 30 219 lines with its own println and
formatted each line's change id, author and age again; it now formats
each change's prefix once and writes through one buffered stdout lock
(print 4 ms), stopping quietly on a closed pipe. The state index's tree
view was decoded into a BTreeMap one insert at a time; it is collected
and built from the sorted paths in one step (37 ms to 21 ms), for every
command that loads the index. Blame on the Linux tree 0.08 s (from 0.16 s;
tuned Git 0.06 s).
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-02 00:04 UTC
Signature
✔ signed by Andy McCall
Hash
fddf64baa3d2b8c8efd78df0677341036711292c17775f4c02499546736c037e
Depends on
1d360c91a289 33472ccc08cc 8fd78eb52ad2 957fee30826e c07406bcc237 e96a430c78f3 f9575918477f fce44ae9ad29 ff0af1ee299d
Depended on by
b35df36b7dc5 0389c3e82f6c
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
14 atoms, 0 tree operations, 5 files

Browse the tree at this change

modified CHANGELOG.md +2 −0
…
1 unchanged lines
2 2
3 3
## Unreleased
4 4
5 +
- **Blame twice as fast (#41).** `oxy blame` prints through a buffered writer and formats each change once, and the state index's tree view loads in one step: blame of the Linux tree's MAINTAINERS (30 000 lines) is 0.08 s, from 0.16 s; tuned Git 0.06 s. The faster tree view also takes 16 ms off diff, commit and checkout.
6 +
5 7
- **`oxy log` in milliseconds (#41).** Log no longer checks the files on disk: its working-copy row shows the working copy as last recorded, "(working copy)" or "(working copy, uncommitted changes)", as Git's log never looks at the working tree; `oxy status` checks the disk. `log -n 100` is 0.03 s on the Linux tree (from 0.17 s) and 0.02 s on a 10 000-commit history.
6 8
7 9
- **Status twice as fast as Git on the Linux tree (#41).** With uncommitted work, `oxy status` answers from a status cache while every file is as the last snapshot left it, and the manifest keeps every directory's time, so the tree is listed only when a directory's entries changed. One edited file is 0.10 to 0.12 s (from 0.44 s; Git 0.22 s) and a clean status 0.09 to 0.12 s (Git 0.20 to 0.26 s), in 80 MB. `oxy diff` renders the working copy from the cache too (0.20 s, from 0.41 s; Git 0.08 s). A commit after a status takes that status's snapshot instead of walking the tree again (0.37 s, from 0.68 s; Git 0.15 s). A file written within the racy window is now hashed by the prompt's check too, as it was by status.
…
56 unchanged lines
modified crates/oxy-core/src/repo/inspect.rs +1 −0
…
101 unchanged lines
102 102
        if f.kind != crate::objects::NodeKind::File || f.mode.opaque {
103 103
            return fail(format!("{path} is not a text file"));
104 104
        }
105 +
        let _t = crate::timing::phase("blame: lines");
105 106
        let mut out = Vec::new();
106 107
        for (i, ((hash, index), text)) in state.live_lines(&f.node).into_iter().enumerate() {
107 108
            let meta = state.get(&hash);
…
360 unchanged lines
modified crates/oxy-core/src/tree.rs +5 −3
…
1115 unchanged lines
1116 1116
            Ok((r.object()?, r.u32()?))
1117 1117
        };
1118 1118
        let n = r.count()?;
1119 -
        let mut entries = BTreeMap::new();
1119 +
        // Collected, then built in one step: the map is made from sorted
1120 +
        // input in a single pass rather than by searching for each insert.
1121 +
        let mut entries = Vec::with_capacity(n);
1120 1122
        for _ in 0..n {
1121 1123
            let path = r.text()?;
1122 1124
            let e = TreeEntry {
…
7 unchanged lines
1130 1132
                    _ => Some(key(&mut r)?),
1131 1133
                },
1132 1134
            };
1133 -
            entries.insert(path, e);
1135 +
            entries.push((path, e));
1134 1136
        }
1135 1137
        Ok(Some(TreeView {
1136 -
            entries: std::sync::Arc::new(entries),
1138 +
            entries: std::sync::Arc::new(entries.into_iter().collect()),
1137 1139
            conflicts: Vec::new(),
1138 1140
        }))
1139 1141
    }
…
403 unchanged lines
modified docs/benchmarks.md +17 −0
…
878 unchanged lines
879 879
calls themselves, the same work Git does; doing less needs a file watcher
880 880
(Git's fsmonitor), which oxy does not have.
881 881
882 +
### Blame, and the tree view (2026-10-02, oxygen#41)
883 +
884 +
`oxy blame` writes through one buffered lock on stdout and formats each
885 +
change's id, author and age once (30 219 lines of MAINTAINERS share a few
886 +
changes); printing went to 4 ms. The state index's tree view is built in
887 +
one step from its sorted paths rather than by an insert per path: 37 ms
888 +
to 21 ms, for every command that loads the index. Quiet Earth (load
889 +
average 0.4), three runs each, alternating:
890 +
891 +
| | oxy | Git | Git, tuned |
892 +
|---|---:|---:|---:|
893 +
| status, clean | 0.10 s | 0.19 to 0.23 s | 0.09 to 0.10 s |
894 +
| status, one file edited | 0.10 s | 0.19 s | 0.09 s |
895 +
| diff, one file | 0.17 to 0.19 s | 0.08 s | 0.08 s |
896 +
| log -n 100 | 0.03 s | 0.00 s | 0.00 s |
897 +
| blame MAINTAINERS | **0.08 s** (from 0.16) | 0.06 s | 0.06 s |
898 +
modified src/cli/workspace.rs +28 −8
…
10 unchanged lines
11 11
use oxy_core::objects::ChangeKind;
12 12
use oxy_core::repo::SignatureStatus;
13 13
use oxy_core::repo::{render_change, ChangeStatus, Repo};
14 +
use std::io::Write;
14 15
use std::path::PathBuf;
15 16
16 17
pub fn init(path: Option<PathBuf>, bare: bool) -> Result<i32> {
…
321 unchanged lines
338 339
        .unwrap_or(0)
339 340
        .min(20);
340 341
    let digits = lines.len().to_string().len();
342 +
    let _t = oxy_core::timing::phase("blame: print");
343 +
    // Most lines share their change with many others: each change's id,
344 +
    // author and age are formatted once, and the lines go out through one
345 +
    // buffered lock on stdout.
346 +
    let mut prefix: std::collections::HashMap<oxy_core::hash::ChangeHash, String> =
347 +
        std::collections::HashMap::new();
348 +
    let bar = ui::dim(ui::glyph("│", "|"));
349 +
    let mut out = std::io::BufWriter::with_capacity(1 << 16, std::io::stdout().lock());
341 350
    for l in &lines {
342 -
        let author: String = l.author.chars().take(20).collect();
351 +
        let p = prefix.entry(l.hash).or_insert_with(|| {
352 +
            let author: String = l.author.chars().take(20).collect();
353 +
            format!(
354 +
                "{} {} {}",
355 +
                ui::id(l.change_id.short(8)),
356 +
                ui::author(format!("{author:<width$}")),
357 +
                ui::dim(ui::ago(l.when_ms))
358 +
            )
359 +
        });
343 360
        let text = String::from_utf8_lossy(&l.text);
344 -
        println!(
345 -
            "{} {} {} {:>digits$}{} {}",
346 -
            ui::id(l.change_id.short(8)),
347 -
            ui::author(format!("{author:<width$}")),
348 -
            ui::dim(ui::ago(l.when_ms)),
361 +
        if writeln!(
362 +
            out,
363 +
            "{p} {:>digits$}{bar} {}",
349 364
            l.line,
350 -
            ui::dim(ui::glyph("│", "|")),
351 365
            text.trim_end_matches('\n')
352 -
        );
366 +
        )
367 +
        .is_err()
368 +
        {
369 +
            // A closed pipe (blame | head): stop quietly.
370 +
            return Ok(0);
371 +
        }
353 372
    }
373 +
    let _ = out.flush();
354 374
    Ok(0)
355 375
}
356 376
…
2016 unchanged lines