A new file's lines share its content 40a40bb7b798
A file read from the working copy is held behind a shared buffer, and the insert that adds it splits that buffer into lines without copying it (and without the copy the stored text made on the way). A first commit of a large tree held its text twice. Rendering times each of its paths (kept order, chain, single order, general), and an import times rendering its whole tree apart, under OXY_TIMINGS. refs #35
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-30 07:39 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
40a40bb7b798595b5bd6835f5f509f381a2b848f940e0e5869d74879d37d2c5a- Depends on
- 01a47cd7f0c8 2306a4d24779 598009a75f6c 6e723b81f5b9 75167672ebcd 79ab8528ff62 982864068a59 b7f102fcbb17 c3069537d7c5 cc7a00565161 cdb5beba7f05 dbf06d342e1d dfb23a3a8cfa e7ddd552b619 ff0af1ee299d
- Depended on by
- c3d2ef6104c2 5b4c51307b14 36e51aa806c6 1966f5db15ee
- 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
- 33 atoms, 0 tree operations, 5 files
Browse the tree at this change
modified crates/oxy-core/src/graph.rs +13 −1
| … | 541 unchanged lines |
||
| 542 | 542 | }, |
|
| 543 | 543 | }; |
|
| 544 | 544 | use std::sync::atomic::Ordering::Relaxed; |
|
| 545 | + | // Which way it went, and how long it took (`OXY_TIMINGS`). |
|
| 546 | + | let started = crate::timing::enabled().then(std::time::Instant::now); |
|
| 547 | + | let took = |name: &'static str| { |
|
| 548 | + | if let Some(t) = started { |
|
| 549 | + | crate::timing::add(name, t.elapsed()); |
|
| 550 | + | } |
|
| 551 | + | }; |
|
| 545 | 552 | if let Some(o) = self.order.get() { |
|
| 546 | 553 | let holds = o.confirmed.load(Relaxed) |
|
| 547 | 554 | || o.unchecked.iter().all(|k| { |
|
| … | 3 unchanged lines |
||
| 551 | 558 | }); |
|
| 552 | 559 | if holds { |
|
| 553 | 560 | o.confirmed.store(true, Relaxed); |
|
| 561 | + | took("graph: kept order"); |
|
| 554 | 562 | return lines(o.atoms.clone()); |
|
| 555 | 563 | } |
|
| 556 | 564 | self.order.set(None); |
|
| … | 1 unchanged lines |
||
| 558 | 566 | // A chain (nothing ever deleted) is quick to walk, and a fresh tree |
|
| 559 | 567 | // is all chains: no order is kept for one. |
|
| 560 | 568 | if let Some(order) = self.as_chain() { |
|
| 569 | + | took("graph: chain"); |
|
| 561 | 570 | return lines(order); |
|
| 562 | 571 | } |
|
| 563 | 572 | if let Some(order) = self.as_order(deps) { |
|
| 564 | 573 | self.order |
|
| 565 | 574 | .set(Some(std::sync::Arc::new(Order::known(order.clone())))); |
|
| 575 | + | took("graph: single order"); |
|
| 566 | 576 | return lines(order); |
|
| 567 | 577 | } |
|
| 568 | - | self.materialize_general(deps) |
|
| 578 | + | let m = self.materialize_general(deps); |
|
| 579 | + | took("graph: general"); |
|
| 580 | + | m |
|
| 569 | 581 | } |
|
| 570 | 582 | ||
| 571 | 583 | /// Take the kept order, to [`Order::splice`] it once a change's edits |
|
| … | 1228 unchanged lines |
modified crates/oxy-core/src/lines.rs +24 −13
| … | 7 unchanged lines |
||
| 8 | 8 | ||
| 9 | 9 | #[derive(Default, PartialEq, Eq, Hash)] |
|
| 10 | 10 | struct Inner { |
|
| 11 | - | buf: Vec<u8>, |
|
| 11 | + | /// Shared with the file it was read from, when there was one. |
|
| 12 | + | buf: Arc<Vec<u8>>, |
|
| 12 | 13 | /// The end offset of each line in `buf`. |
|
| 13 | 14 | ends: Vec<u64>, |
|
| 14 | 15 | } |
|
| … | 7 unchanged lines |
||
| 22 | 23 | /// unterminated line is its own line (the rule of |
|
| 23 | 24 | /// [`crate::diff::split_lines`]). |
|
| 24 | 25 | pub fn split(content: &[u8]) -> Lines { |
|
| 26 | + | Lines::split_shared(Arc::new(content.to_vec())) |
|
| 27 | + | } |
|
| 28 | + | ||
| 29 | + | /// [`Lines::split`] of a buffer held elsewhere too (a file's content), |
|
| 30 | + | /// shared rather than copied. |
|
| 31 | + | pub fn split_shared(buf: Arc<Vec<u8>>) -> Lines { |
|
| 32 | + | let content = &buf[..]; |
|
| 25 | 33 | let mut ends = Vec::new(); |
|
| 26 | 34 | for (i, &c) in content.iter().enumerate() { |
|
| 27 | 35 | if c == b'\n' { |
|
| … | 3 unchanged lines |
||
| 31 | 39 | if ends.last().copied().unwrap_or(0) as usize != content.len() { |
|
| 32 | 40 | ends.push(content.len() as u64); |
|
| 33 | 41 | } |
|
| 34 | - | Lines(Arc::new(Inner { |
|
| 35 | - | buf: content.to_vec(), |
|
| 36 | - | ends, |
|
| 37 | - | })) |
|
| 42 | + | Lines(Arc::new(Inner { buf, ends })) |
|
| 38 | 43 | } |
|
| 39 | 44 | ||
| 40 | 45 | pub fn len(&self) -> usize { |
|
| … | 44 unchanged lines |
||
| 85 | 90 | ||
| 86 | 91 | impl<'a> FromIterator<&'a [u8]> for Lines { |
|
| 87 | 92 | fn from_iter<I: IntoIterator<Item = &'a [u8]>>(iter: I) -> Lines { |
|
| 88 | - | let mut inner = Inner::default(); |
|
| 93 | + | let (mut buf, mut ends) = (Vec::new(), Vec::new()); |
|
| 89 | 94 | for l in iter { |
|
| 90 | - | inner.buf.extend_from_slice(l); |
|
| 91 | - | inner.ends.push(inner.buf.len() as u64); |
|
| 95 | + | buf.extend_from_slice(l); |
|
| 96 | + | ends.push(buf.len() as u64); |
|
| 92 | 97 | } |
|
| 93 | - | Lines(Arc::new(inner)) |
|
| 98 | + | Lines(Arc::new(Inner { |
|
| 99 | + | buf: Arc::new(buf), |
|
| 100 | + | ends, |
|
| 101 | + | })) |
|
| 94 | 102 | } |
|
| 95 | 103 | } |
|
| 96 | 104 | ||
| 97 | 105 | impl FromIterator<Vec<u8>> for Lines { |
|
| 98 | 106 | fn from_iter<I: IntoIterator<Item = Vec<u8>>>(iter: I) -> Lines { |
|
| 99 | - | let mut inner = Inner::default(); |
|
| 107 | + | let (mut buf, mut ends) = (Vec::new(), Vec::new()); |
|
| 100 | 108 | for l in iter { |
|
| 101 | - | inner.buf.extend_from_slice(&l); |
|
| 102 | - | inner.ends.push(inner.buf.len() as u64); |
|
| 109 | + | buf.extend_from_slice(&l); |
|
| 110 | + | ends.push(buf.len() as u64); |
|
| 103 | 111 | } |
|
| 104 | - | Lines(Arc::new(inner)) |
|
| 112 | + | Lines(Arc::new(Inner { |
|
| 113 | + | buf: Arc::new(buf), |
|
| 114 | + | ends, |
|
| 115 | + | })) |
|
| 105 | 116 | } |
|
| 106 | 117 | } |
|
| 107 | 118 | ||
| … | 56 unchanged lines |
modified crates/oxy-core/src/repo/git.rs +2 −2
| … | 906 unchanged lines |
||
| 907 | 907 | crate::timing::add("render: same and need", clock.elapsed()); |
|
| 908 | 908 | let t = std::time::Instant::now(); |
|
| 909 | 909 | self.load_graphs_of(&mut base, &need)?; |
|
| 910 | - | crate::timing::add("render: load graphs", t.elapsed()); |
|
| 910 | + | crate::timing::add("render: load graphs, whole tree", t.elapsed()); |
|
| 911 | 911 | let t = std::time::Instant::now(); |
|
| 912 | 912 | let snap = base.materialize_except(&skip); |
|
| 913 | - | crate::timing::add("render: materialize", t.elapsed()); |
|
| 913 | + | crate::timing::add("render: materialize, whole tree", t.elapsed()); |
|
| 914 | 914 | snap |
|
| 915 | 915 | }; |
|
| 916 | 916 | ||
| … | 672 unchanged lines |
modified crates/oxy-core/src/repo/mod.rs +5 −4
| … | 134 unchanged lines |
||
| 135 | 135 | pub kind: NodeKind, |
|
| 136 | 136 | pub exec: bool, |
|
| 137 | 137 | /// File content, or a symlink's target. Empty when `blob` is set. |
|
| 138 | - | pub content: Vec<u8>, |
|
| 138 | + | /// Shared, so a new file's insert holds it without a copy. |
|
| 139 | + | pub content: std::sync::Arc<Vec<u8>>, |
|
| 139 | 140 | /// The stored blob of an opaque file whose size and mtime are unchanged |
|
| 140 | 141 | /// since the last manifest (spec §12.3 stat cache); the content was not |
|
| 141 | 142 | /// read. |
|
| … | 14 unchanged lines |
||
| 156 | 157 | FsEntry { |
|
| 157 | 158 | kind, |
|
| 158 | 159 | exec, |
|
| 159 | - | content, |
|
| 160 | + | content: std::sync::Arc::new(content), |
|
| 160 | 161 | blob: None, |
|
| 161 | 162 | content_hash: None, |
|
| 162 | 163 | unchanged: false, |
|
| … | 1128 unchanged lines |
||
| 1291 | 1292 | for (rel, mut e) in read { |
|
| 1292 | 1293 | // A cached blob the store lost (gc, a partial clone): read the file after all. |
|
| 1293 | 1294 | if e.content.is_empty() && e.blob.is_some_and(|b| !self.store.has(&b)) { |
|
| 1294 | - | e.content = fs::read(self.root.join(&rel))?; |
|
| 1295 | + | e.content = std::sync::Arc::new(fs::read(self.root.join(&rel))?); |
|
| 1295 | 1296 | e.blob = None; |
|
| 1296 | 1297 | } |
|
| 1297 | 1298 | out.insert(rel, e); |
|
| … | 89 unchanged lines |
||
| 1387 | 1388 | ) -> Result<FsEntry> { |
|
| 1388 | 1389 | let mut e = read_file_entry(rel, path, meta, cache, None)?; |
|
| 1389 | 1390 | if e.content.is_empty() && e.blob.is_some_and(|b| !self.store.has(&b)) { |
|
| 1390 | - | e.content = fs::read(path)?; |
|
| 1391 | + | e.content = std::sync::Arc::new(fs::read(path)?); |
|
| 1391 | 1392 | e.blob = None; |
|
| 1392 | 1393 | } |
|
| 1393 | 1394 | Ok(e) |
|
| … | 344 unchanged lines |
modified crates/oxy-core/src/repo/snapshot.rs +12 −8
| … | 388 unchanged lines |
||
| 389 | 389 | Some(FileContent::Text { |
|
| 390 | 390 | bytes, |
|
| 391 | 391 | materialized, |
|
| 392 | - | }) if !materialized.is_conflicted() && *bytes == e.content => { |
|
| 392 | + | }) if !materialized.is_conflicted() && *bytes == *e.content => { |
|
| 393 | 393 | keys.get(p).cloned() |
|
| 394 | 394 | } |
|
| 395 | 395 | _ => None, |
|
| … | 1255 unchanged lines |
||
| 1651 | 1651 | } |
|
| 1652 | 1652 | ||
| 1653 | 1653 | /// Text content as stored: `\r\n` normalized for `eol=` files. |
|
| 1654 | - | fn stored_text(&self, path: &str, content: &[u8]) -> Vec<u8> { |
|
| 1654 | + | fn stored_text( |
|
| 1655 | + | &self, |
|
| 1656 | + | path: &str, |
|
| 1657 | + | content: &std::sync::Arc<Vec<u8>>, |
|
| 1658 | + | ) -> std::sync::Arc<Vec<u8>> { |
|
| 1655 | 1659 | if self.attrs.lookup(path).eol.is_some() { |
|
| 1656 | - | crate::attributes::normalize_eol(content) |
|
| 1660 | + | std::sync::Arc::new(crate::attributes::normalize_eol(content)) |
|
| 1657 | 1661 | } else { |
|
| 1658 | - | content.to_vec() |
|
| 1662 | + | content.clone() |
|
| 1659 | 1663 | } |
|
| 1660 | 1664 | } |
|
| 1661 | 1665 | ||
| … | 111 unchanged lines |
||
| 1773 | 1777 | node, |
|
| 1774 | 1778 | after: crate::objects::AtomRef::Start, |
|
| 1775 | 1779 | before: crate::objects::AtomRef::End, |
|
| 1776 | - | lines: crate::lines::Lines::split(&content), |
|
| 1780 | + | lines: crate::lines::Lines::split_shared(content), |
|
| 1777 | 1781 | }); |
|
| 1778 | 1782 | } |
|
| 1779 | 1783 | self.statuses.push(PathStatus { |
|
| … | 180 unchanged lines |
||
| 1960 | 1964 | materialized, |
|
| 1961 | 1965 | } => { |
|
| 1962 | 1966 | if materialized.is_conflicted() { |
|
| 1963 | - | if bytes == &disk_text { |
|
| 1967 | + | if bytes == &*disk_text { |
|
| 1964 | 1968 | return Ok(()); |
|
| 1965 | 1969 | } |
|
| 1966 | 1970 | if has_conflict_markers(&e.content) { |
|
| … | 65 unchanged lines |
||
| 2032 | 2036 | new: Some(blob), |
|
| 2033 | 2037 | }); |
|
| 2034 | 2038 | status = Some(ChangeStatus::Modified); |
|
| 2035 | - | } else if bytes != &disk_text { |
|
| 2039 | + | } else if bytes != &*disk_text { |
|
| 2036 | 2040 | let mut laps = crate::timing::Laps::start(); |
|
| 2037 | 2041 | // The base's rendering holds its live atoms in order, |
|
| 2038 | 2042 | // one per line (it is not conflicted here). |
|
| … | 419 unchanged lines |
||
| 2458 | 2462 | let score = match (&old, old_blob) { |
|
| 2459 | 2463 | (Some(o), _) => { |
|
| 2460 | 2464 | if exact_only { |
|
| 2461 | - | if *o == e.content { |
|
| 2465 | + | if *o == *e.content { |
|
| 2462 | 2466 | 100 |
|
| 2463 | 2467 | } else { |
|
| 2464 | 2468 | 0 |
|
| … | 56 unchanged lines |