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 |