Keep an insert's lines in one shared buffer 01a47cd7f0c8
Every line of an insert was its own Vec, in the change and again in the file graph's atoms: the Linux tree's 40 million lines were as many allocations twice over, 2.6 GB for 1.8 GB of text. An insert's lines are now one buffer and the offsets where each ends (Lines, in the new lines module), cloned by reference; a graph atom holds a LineRef into its insert's buffer instead of a copy, and a graph read from a checkpoint puts all its lines in one buffer. Decoding builds Lines straight from the byte strings. The diff of the whole Linux tree now takes 3.3 s and 1.1 million allocations (from 6.2 s and 45 million), and the first commit peaks at 4.3 GB (4.9 GB RSS, from 5.8 GB). refs #35
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-29 23:39 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
01a47cd7f0c8deefd0326b8e498cf2c2ad6422e2dd7da1d139a73c27fe968ef9- Depends on
- 0bdc3f91a39f 119bbf50cb86 17f94b13ae54 192fad1e46a2 1d360c91a289 252e8cb23fbc 387335ff7f4d 614541c3cebd 6f8aeb996558 728da0f92161 75167672ebcd 86e1824f2d0b 982864068a59 a4fad5b085c7 afaba5f1306d c07e0e7d2d85 cc7a00565161 cdb5beba7f05 e8d38e47e9d7 ff0af1ee299d
- Depended on by
- 91334b7fa574 5b4c51307b14 40a40bb7b798 2306a4d24779 0f93f49ce059
- 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
- 82 atoms, 1 tree operation, 12 files
Browse the tree at this change
added crates/oxy-core/src/lines.rs +163 −0
| 1 | + | //! The lines of an insert (spec §7.4) held as one buffer and where each |
|
| 2 | + | //! line ends, shared by the change and the file graph it builds: a tree of |
|
| 3 | + | //! forty million lines is then a few allocations per insert, not one per |
|
| 4 | + | //! line, and a graph's atoms point into their insert's buffer rather than |
|
| 5 | + | //! copying it. |
|
| 6 | + | ||
| 7 | + | use std::sync::Arc; |
|
| 8 | + | ||
| 9 | + | #[derive(Default, PartialEq, Eq, Hash)] |
|
| 10 | + | struct Inner { |
|
| 11 | + | buf: Vec<u8>, |
|
| 12 | + | /// The end offset of each line in `buf`. |
|
| 13 | + | ends: Vec<u64>, |
|
| 14 | + | } |
|
| 15 | + | ||
| 16 | + | /// An insert's lines. Cloning is cheap: the buffer is shared. |
|
| 17 | + | #[derive(Clone, Default, PartialEq, Eq, Hash)] |
|
| 18 | + | pub struct Lines(Arc<Inner>); |
|
| 19 | + | ||
| 20 | + | impl Lines { |
|
| 21 | + | /// Lines from their bytes, each keeping its terminator; a final |
|
| 22 | + | /// unterminated line is its own line (the rule of |
|
| 23 | + | /// [`crate::diff::split_lines`]). |
|
| 24 | + | pub fn split(content: &[u8]) -> Lines { |
|
| 25 | + | let mut ends = Vec::new(); |
|
| 26 | + | for (i, &c) in content.iter().enumerate() { |
|
| 27 | + | if c == b'\n' { |
|
| 28 | + | ends.push(i as u64 + 1); |
|
| 29 | + | } |
|
| 30 | + | } |
|
| 31 | + | if ends.last().copied().unwrap_or(0) as usize != content.len() { |
|
| 32 | + | ends.push(content.len() as u64); |
|
| 33 | + | } |
|
| 34 | + | Lines(Arc::new(Inner { |
|
| 35 | + | buf: content.to_vec(), |
|
| 36 | + | ends, |
|
| 37 | + | })) |
|
| 38 | + | } |
|
| 39 | + | ||
| 40 | + | pub fn len(&self) -> usize { |
|
| 41 | + | self.0.ends.len() |
|
| 42 | + | } |
|
| 43 | + | ||
| 44 | + | pub fn is_empty(&self) -> bool { |
|
| 45 | + | self.0.ends.is_empty() |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | /// Line `i`, with its terminator. |
|
| 49 | + | pub fn get(&self, i: usize) -> Option<&[u8]> { |
|
| 50 | + | let end = *self.0.ends.get(i)? as usize; |
|
| 51 | + | let start = if i == 0 { |
|
| 52 | + | 0 |
|
| 53 | + | } else { |
|
| 54 | + | self.0.ends[i - 1] as usize |
|
| 55 | + | }; |
|
| 56 | + | Some(&self.0.buf[start..end]) |
|
| 57 | + | } |
|
| 58 | + | ||
| 59 | + | pub fn iter(&self) -> impl ExactSizeIterator<Item = &[u8]> + Clone + '_ { |
|
| 60 | + | (0..self.len()).map(move |i| self.get(i).unwrap()) |
|
| 61 | + | } |
|
| 62 | + | ||
| 63 | + | /// Every line, joined: the bytes they were split from. |
|
| 64 | + | pub fn bytes(&self) -> &[u8] { |
|
| 65 | + | &self.0.buf |
|
| 66 | + | } |
|
| 67 | + | ||
| 68 | + | /// Lines `range` as lines of their own. |
|
| 69 | + | pub fn slice(&self, range: std::ops::Range<usize>) -> Lines { |
|
| 70 | + | self.iter().skip(range.start).take(range.len()).collect() |
|
| 71 | + | } |
|
| 72 | + | ||
| 73 | + | /// The lines as owned vectors, for code that edits them one by one. |
|
| 74 | + | pub fn to_vec(&self) -> Vec<Vec<u8>> { |
|
| 75 | + | self.iter().map(<[u8]>::to_vec).collect() |
|
| 76 | + | } |
|
| 77 | + | } |
|
| 78 | + | ||
| 79 | + | impl std::ops::Index<usize> for Lines { |
|
| 80 | + | type Output = [u8]; |
|
| 81 | + | fn index(&self, i: usize) -> &[u8] { |
|
| 82 | + | self.get(i).expect("line index out of range") |
|
| 83 | + | } |
|
| 84 | + | } |
|
| 85 | + | ||
| 86 | + | impl<'a> FromIterator<&'a [u8]> for Lines { |
|
| 87 | + | fn from_iter<I: IntoIterator<Item = &'a [u8]>>(iter: I) -> Lines { |
|
| 88 | + | let mut inner = Inner::default(); |
|
| 89 | + | for l in iter { |
|
| 90 | + | inner.buf.extend_from_slice(l); |
|
| 91 | + | inner.ends.push(inner.buf.len() as u64); |
|
| 92 | + | } |
|
| 93 | + | Lines(Arc::new(inner)) |
|
| 94 | + | } |
|
| 95 | + | } |
|
| 96 | + | ||
| 97 | + | impl FromIterator<Vec<u8>> for Lines { |
|
| 98 | + | fn from_iter<I: IntoIterator<Item = Vec<u8>>>(iter: I) -> Lines { |
|
| 99 | + | let mut inner = Inner::default(); |
|
| 100 | + | for l in iter { |
|
| 101 | + | inner.buf.extend_from_slice(&l); |
|
| 102 | + | inner.ends.push(inner.buf.len() as u64); |
|
| 103 | + | } |
|
| 104 | + | Lines(Arc::new(inner)) |
|
| 105 | + | } |
|
| 106 | + | } |
|
| 107 | + | ||
| 108 | + | impl From<Vec<Vec<u8>>> for Lines { |
|
| 109 | + | fn from(v: Vec<Vec<u8>>) -> Lines { |
|
| 110 | + | v.into_iter().collect() |
|
| 111 | + | } |
|
| 112 | + | } |
|
| 113 | + | ||
| 114 | + | impl std::fmt::Debug for Lines { |
|
| 115 | + | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
|
| 116 | + | f.debug_list() |
|
| 117 | + | .entries(self.iter().map(String::from_utf8_lossy)) |
|
| 118 | + | .finish() |
|
| 119 | + | } |
|
| 120 | + | } |
|
| 121 | + | ||
| 122 | + | /// One line of a [`Lines`], held by a file graph's atom. |
|
| 123 | + | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 124 | + | pub struct LineRef { |
|
| 125 | + | lines: Lines, |
|
| 126 | + | index: u32, |
|
| 127 | + | } |
|
| 128 | + | ||
| 129 | + | impl LineRef { |
|
| 130 | + | pub fn new(lines: Lines, index: u32) -> LineRef { |
|
| 131 | + | LineRef { lines, index } |
|
| 132 | + | } |
|
| 133 | + | ||
| 134 | + | pub fn bytes(&self) -> &[u8] { |
|
| 135 | + | &self.lines[self.index as usize] |
|
| 136 | + | } |
|
| 137 | + | } |
|
| 138 | + | ||
| 139 | + | #[cfg(test)] |
|
| 140 | + | mod tests { |
|
| 141 | + | use super::*; |
|
| 142 | + | ||
| 143 | + | #[test] |
|
| 144 | + | fn lines_split_index_and_join_as_the_line_rule_says() { |
|
| 145 | + | for text in [&b""[..], b"a\n", b"a\nb", b"a\n\nb\n", b"\n", b"no end"] { |
|
| 146 | + | let l = Lines::split(text); |
|
| 147 | + | let want = crate::diff::split_lines(text); |
|
| 148 | + | assert_eq!(l.len(), want.len(), "{text:?}"); |
|
| 149 | + | assert_eq!(l.iter().collect::<Vec<_>>(), want); |
|
| 150 | + | assert_eq!(l.bytes(), text); |
|
| 151 | + | let v: Lines = want.iter().copied().collect(); |
|
| 152 | + | assert_eq!(v, l); |
|
| 153 | + | assert_eq!(Lines::from(l.to_vec()), l); |
|
| 154 | + | } |
|
| 155 | + | let l = Lines::split(b"a\nb\nc\n"); |
|
| 156 | + | assert_eq!(&l[1], b"b\n"); |
|
| 157 | + | assert_eq!( |
|
| 158 | + | l.slice(1..3).to_vec(), |
|
| 159 | + | vec![b"b\n".to_vec(), b"c\n".to_vec()] |
|
| 160 | + | ); |
|
| 161 | + | assert_eq!(LineRef::new(l.clone(), 2).bytes(), b"c\n"); |
|
| 162 | + | } |
|
| 163 | + | } |
modified crates/oxy-core/src/cbor.rs +3 −3
| … | 122 unchanged lines |
||
| 123 | 123 | ||
| 124 | 124 | /// The canonical encoding of an array of byte strings, written without |
|
| 125 | 125 | /// building a [`Value`] (large inserts stored as text blobs, spec §6.1). |
|
| 126 | - | pub fn encode_byte_strings(items: &[Vec<u8>]) -> Vec<u8> { |
|
| 126 | + | pub fn encode_byte_strings(items: &crate::lines::Lines) -> Vec<u8> { |
|
| 127 | 127 | let mut out = Vec::with_capacity(byte_strings_len(items)); |
|
| 128 | 128 | head(&mut out, 4, items.len() as u64); |
|
| 129 | - | for b in items { |
|
| 129 | + | for b in items.iter() { |
|
| 130 | 130 | head(&mut out, 2, b.len() as u64); |
|
| 131 | 131 | out.extend_from_slice(b); |
|
| 132 | 132 | } |
|
| … | 1 unchanged lines |
||
| 134 | 134 | } |
|
| 135 | 135 | ||
| 136 | 136 | /// The length of [`encode_byte_strings`]' output, without encoding. |
|
| 137 | - | pub fn byte_strings_len(items: &[Vec<u8>]) -> usize { |
|
| 137 | + | pub fn byte_strings_len(items: &crate::lines::Lines) -> usize { |
|
| 138 | 138 | head_len(items.len() as u64) |
|
| 139 | 139 | + items |
|
| 140 | 140 | .iter() |
|
| … | 460 unchanged lines |
modified crates/oxy-core/src/diff.rs +4 −4
| … | 103 unchanged lines |
||
| 104 | 104 | before, |
|
| 105 | 105 | lines: new_lines[new_index..new_index + new_len] |
|
| 106 | 106 | .iter() |
|
| 107 | - | .map(|l| l.to_vec()) |
|
| 107 | + | .copied() |
|
| 108 | 108 | .collect(), |
|
| 109 | 109 | }); |
|
| 110 | 110 | } |
|
| … | 11 unchanged lines |
||
| 122 | 122 | before, |
|
| 123 | 123 | lines: new_lines[new_index..new_index + new_len] |
|
| 124 | 124 | .iter() |
|
| 125 | - | .map(|l| l.to_vec()) |
|
| 125 | + | .copied() |
|
| 126 | 126 | .collect(), |
|
| 127 | 127 | }); |
|
| 128 | 128 | } |
|
| … | 150 unchanged lines |
||
| 279 | 279 | change: h(1), |
|
| 280 | 280 | index: 1 |
|
| 281 | 281 | }, |
|
| 282 | - | lines: vec![b"B\n".to_vec()] |
|
| 282 | + | lines: vec![b"B\n".to_vec()].into() |
|
| 283 | 283 | }, |
|
| 284 | 284 | ] |
|
| 285 | 285 | ); |
|
| … | 37 unchanged lines |
||
| 323 | 323 | node, |
|
| 324 | 324 | after: AtomRef::Start, |
|
| 325 | 325 | before: AtomRef::End, |
|
| 326 | - | lines: vec![b"x\n".to_vec()] |
|
| 326 | + | lines: vec![b"x\n".to_vec()].into() |
|
| 327 | 327 | }] |
|
| 328 | 328 | ); |
|
| 329 | 329 | } |
|
| … | 1 unchanged lines |
modified crates/oxy-core/src/graph.rs +31 −14
| … | 5 unchanged lines |
||
| 6 | 6 | ||
| 7 | 7 | use crate::fasthash::{FastMap as HashMap, FastSet as HashSet}; |
|
| 8 | 8 | use crate::hash::ChangeHash; |
|
| 9 | + | use crate::lines::{LineRef, Lines}; |
|
| 9 | 10 | use crate::objects::AtomRange; |
|
| 10 | 11 | use std::collections::{BTreeMap, BTreeSet}; |
|
| 11 | 12 | ||
| … | 22 unchanged lines |
||
| 34 | 35 | ||
| 35 | 36 | #[derive(Default, Clone, Debug)] |
|
| 36 | 37 | struct AtomInfo { |
|
| 37 | - | /// `None` until the introducing change has been applied. |
|
| 38 | - | content: Option<Vec<u8>>, |
|
| 38 | + | /// `None` until the introducing change has been applied; a line of the |
|
| 39 | + | /// insert's shared buffer. |
|
| 40 | + | content: Option<LineRef>, |
|
| 39 | 41 | deleted_by: BTreeSet<ChangeHash>, |
|
| 40 | 42 | } |
|
| 41 | 43 | ||
| … | 117 unchanged lines |
||
| 159 | 161 | first_index: u32, |
|
| 160 | 162 | after: Vertex, |
|
| 161 | 163 | before: Vertex, |
|
| 162 | - | lines: &[Vec<u8>], |
|
| 164 | + | lines: &Lines, |
|
| 163 | 165 | ) { |
|
| 164 | - | self.insert_owned(change, first_index, after, before, lines.to_vec()); |
|
| 166 | + | self.insert_owned(change, first_index, after, before, lines.clone()); |
|
| 165 | 167 | } |
|
| 166 | 168 | ||
| 167 | 169 | /// [`FileGraph::insert`] taking the lines, so loading a state moves |
|
| … | 5 unchanged lines |
||
| 173 | 175 | first_index: u32, |
|
| 174 | 176 | after: Vertex, |
|
| 175 | 177 | before: Vertex, |
|
| 176 | - | lines: Vec<Vec<u8>>, |
|
| 178 | + | lines: Lines, |
|
| 177 | 179 | ) { |
|
| 178 | 180 | self.atoms.reserve(lines.len()); |
|
| 179 | 181 | self.succ.reserve(lines.len() + 1); |
|
| 180 | 182 | let mut prev = after; |
|
| 181 | - | for (i, line) in lines.into_iter().enumerate() { |
|
| 183 | + | for i in 0..lines.len() { |
|
| 182 | 184 | let key = (change, first_index + i as u32); |
|
| 183 | - | self.atoms.entry(key).or_default().content = Some(line); |
|
| 185 | + | self.atoms.entry(key).or_default().content = |
|
| 186 | + | Some(LineRef::new(lines.clone(), i as u32)); |
|
| 184 | 187 | let v = Vertex::Atom(key); |
|
| 185 | 188 | self.succ.entry(prev).or_default().insert(v); |
|
| 186 | 189 | prev = v; |
|
| … | 20 unchanged lines |
||
| 207 | 210 | } |
|
| 208 | 211 | ||
| 209 | 212 | pub fn content(&self, k: &AtomKey) -> Option<&[u8]> { |
|
| 210 | - | self.atoms.get(k).and_then(|a| a.content.as_deref()) |
|
| 213 | + | self.atoms |
|
| 214 | + | .get(k) |
|
| 215 | + | .and_then(|a| a.content.as_ref().map(LineRef::bytes)) |
|
| 211 | 216 | } |
|
| 212 | 217 | ||
| 213 | 218 | pub fn deleters(&self, k: &AtomKey) -> impl Iterator<Item = &ChangeHash> { |
|
| … | 387 unchanged lines |
||
| 601 | 606 | Value::Uint(*i as u64), |
|
| 602 | 607 | info.content |
|
| 603 | 608 | .as_ref() |
|
| 604 | - | .map(|b| Value::Bytes(b.clone())) |
|
| 609 | + | .map(|b| Value::Bytes(b.bytes().to_vec())) |
|
| 605 | 610 | .unwrap_or(Value::Null), |
|
| 606 | 611 | Value::Array( |
|
| 607 | 612 | info.deleted_by |
|
| … | 25 unchanged lines |
||
| 633 | 638 | atoms: HashMap::default(), |
|
| 634 | 639 | succ: HashMap::default(), |
|
| 635 | 640 | }; |
|
| 636 | - | for a in array(field(m, 0, "atoms")?, "atoms")? { |
|
| 641 | + | let atoms = array(field(m, 0, "atoms")?, "atoms")?; |
|
| 642 | + | // Every atom's line in one buffer. |
|
| 643 | + | let mut texts: Vec<&[u8]> = Vec::new(); |
|
| 644 | + | for a in atoms { |
|
| 645 | + | let e = array(a, "atom")?; |
|
| 646 | + | if e.len() == 4 && !e[2].is_null() { |
|
| 647 | + | texts.push(bytes(&e[2], "atom content")?); |
|
| 648 | + | } |
|
| 649 | + | } |
|
| 650 | + | let all: Lines = texts.into_iter().collect(); |
|
| 651 | + | let mut next = 0u32; |
|
| 652 | + | for a in atoms { |
|
| 637 | 653 | let e = array(a, "atom")?; |
|
| 638 | 654 | if e.len() != 4 { |
|
| 639 | 655 | return Err(ObjectError::Schema("atom: expected 4 fields".into())); |
|
| … | 2 unchanged lines |
||
| 642 | 658 | let content = if e[2].is_null() { |
|
| 643 | 659 | None |
|
| 644 | 660 | } else { |
|
| 645 | - | Some(bytes(&e[2], "atom content")?.to_vec()) |
|
| 661 | + | next += 1; |
|
| 662 | + | Some(LineRef::new(all.clone(), next - 1)) |
|
| 646 | 663 | }; |
|
| 647 | 664 | let deleted_by = array(&e[3], "deleters")? |
|
| 648 | 665 | .iter() |
|
| … | 61 unchanged lines |
||
| 710 | 727 | fn h(n: u8) -> ChangeHash { |
|
| 711 | 728 | ObjectRef([n; 32]) |
|
| 712 | 729 | } |
|
| 713 | - | fn lines(v: &[&str]) -> Vec<Vec<u8>> { |
|
| 714 | - | v.iter().map(|s| s.as_bytes().to_vec()).collect() |
|
| 730 | + | fn lines(v: &[&str]) -> Lines { |
|
| 731 | + | v.iter().map(|s| s.as_bytes()).collect() |
|
| 715 | 732 | } |
|
| 716 | 733 | fn text(g: &FileGraph, m: &Materialized) -> String { |
|
| 717 | 734 | String::from_utf8(g.render(m, &|c| c.short(4))).unwrap() |
|
| … | 358 unchanged lines |
||
| 1076 | 1093 | use rand::seq::SliceRandom; |
|
| 1077 | 1094 | type Op = ( |
|
| 1078 | 1095 | ChangeHash, |
|
| 1079 | - | Vec<(u32, Vertex, Vertex, Vec<Vec<u8>>)>, |
|
| 1096 | + | Vec<(u32, Vertex, Vertex, Lines)>, |
|
| 1080 | 1097 | Vec<AtomRange>, |
|
| 1081 | 1098 | ); |
|
| 1082 | 1099 | let ops: Vec<Op> = vec![ |
|
| … | 83 unchanged lines |
modified crates/oxy-core/src/lib.rs +1 −0
| … | 13 unchanged lines |
||
| 14 | 14 | pub mod identity; |
|
| 15 | 15 | pub mod ids; |
|
| 16 | 16 | pub mod lanes; |
|
| 17 | + | pub mod lines; |
|
| 17 | 18 | pub mod objects; |
|
| 18 | 19 | pub mod oplog; |
|
| 19 | 20 | pub mod policy; |
|
| … | 10 unchanged lines |
modified crates/oxy-core/src/objects.rs +23 −26
| … | 9 unchanged lines |
||
| 10 | 10 | use crate::cbor::{self, Value}; |
|
| 11 | 11 | use crate::hash::{hash_object, ChangeHash, KeyId, ObjectRef, ObjectType}; |
|
| 12 | 12 | use crate::ids::{ChangeId, NodeId}; |
|
| 13 | + | use crate::lines::Lines; |
|
| 13 | 14 | use std::collections::BTreeMap; |
|
| 14 | 15 | ||
| 15 | 16 | pub const FORMAT_VERSION: u64 = 1; |
|
| … | 414 unchanged lines |
||
| 430 | 431 | node: NodeId, |
|
| 431 | 432 | after: AtomRef, |
|
| 432 | 433 | before: AtomRef, |
|
| 433 | - | lines: Vec<Vec<u8>>, |
|
| 434 | + | lines: crate::lines::Lines, |
|
| 434 | 435 | }, |
|
| 435 | 436 | Delete { |
|
| 436 | 437 | node: NodeId, |
|
| … | 61 unchanged lines |
||
| 498 | 499 | (3, before.to_value()), |
|
| 499 | 500 | ( |
|
| 500 | 501 | 4, |
|
| 501 | - | Value::Array(lines.iter().map(|l| Value::Bytes(l.clone())).collect()), |
|
| 502 | + | Value::Array(lines.iter().map(|l| Value::Bytes(l.to_vec())).collect()), |
|
| 502 | 503 | ), |
|
| 503 | 504 | ]), |
|
| 504 | 505 | (Edit::Delete { node, atoms }, _) => Value::map([ |
|
| … | 28 unchanged lines |
||
| 533 | 534 | 0 => { |
|
| 534 | 535 | let lines = match (m.get(&4), m.get(&5)) { |
|
| 535 | 536 | (Some(inline), None) => { |
|
| 536 | - | let lines = array(inline, "lines")? |
|
| 537 | - | .iter() |
|
| 538 | - | .map(|l| bytes(l, "line").map(|b| b.to_vec())) |
|
| 539 | - | .collect::<Result<Vec<_>, _>>()?; |
|
| 537 | + | let lines = array_lines(inline)?; |
|
| 540 | 538 | if version >= 2 && cbor::byte_strings_len(&lines) >= TEXT_BLOB_MIN { |
|
| 541 | 539 | return schema("an insert this large is stored as a text blob in a version 2 change"); |
|
| 542 | 540 | } |
|
| … | 299 unchanged lines |
||
| 842 | 840 | /// the insert claims; [`text_lines`] reads and checks a blob's content. |
|
| 843 | 841 | /// A resolver may also return no lines, leaving a placeholder for a reader |
|
| 844 | 842 | /// that fills it in later (history-only states, spec §11). |
|
| 845 | - | pub type Resolver<'a> = dyn FnMut(&ObjectRef, u32) -> Result<Vec<Vec<u8>>, ObjectError> + 'a; |
|
| 843 | + | pub type Resolver<'a> = dyn FnMut(&ObjectRef, u32) -> Result<Lines, ObjectError> + 'a; |
|
| 846 | 844 | ||
| 847 | 845 | /// Receives the stored texts of a change being encoded, a batch at a |
|
| 848 | 846 | /// time; returns false to stop making more. |
|
| … | 16 unchanged lines |
||
| 865 | 863 | ||
| 866 | 864 | /// The lines of an inline insert from its array's bytes (see |
|
| 867 | 865 | /// [`TextRef::Inline`]). |
|
| 868 | - | pub fn inline_lines(array: &[u8]) -> Result<Vec<Vec<u8>>, ObjectError> { |
|
| 866 | + | pub fn inline_lines(array: &[u8]) -> Result<Lines, ObjectError> { |
|
| 869 | 867 | let v = cbor::decode(array)?; |
|
| 870 | 868 | let lines = array_lines(&v)?; |
|
| 871 | 869 | if lines.is_empty() { |
|
| … | 2 unchanged lines |
||
| 874 | 872 | Ok(lines) |
|
| 875 | 873 | } |
|
| 876 | 874 | ||
| 877 | - | fn array_lines(v: &Value) -> Result<Vec<Vec<u8>>, ObjectError> { |
|
| 878 | - | array(v, "lines")? |
|
| 875 | + | /// The lines of a CBOR array of byte strings, copied once into one buffer. |
|
| 876 | + | fn array_lines(v: &Value) -> Result<Lines, ObjectError> { |
|
| 877 | + | Ok(array(v, "lines")? |
|
| 879 | 878 | .iter() |
|
| 880 | - | .map(|l| bytes(l, "line").map(|b| b.to_vec())) |
|
| 881 | - | .collect() |
|
| 879 | + | .map(|l| bytes(l, "line")) |
|
| 880 | + | .collect::<Result<Vec<&[u8]>, _>>()? |
|
| 881 | + | .into_iter() |
|
| 882 | + | .collect()) |
|
| 882 | 883 | } |
|
| 883 | 884 | ||
| 884 | 885 | impl Change { |
|
| … | 90 unchanged lines |
||
| 975 | 976 | let Some((start, end, count)) = inline else { |
|
| 976 | 977 | return Edit::from_value(&Value::Map(fields), version, &mut |text, count| { |
|
| 977 | 978 | texts.push(TextRef::Blob(*text, count)); |
|
| 978 | - | Ok(Vec::new()) |
|
| 979 | + | Ok(Lines::default()) |
|
| 979 | 980 | }); |
|
| 980 | 981 | }; |
|
| 981 | 982 | if uint(field(&fields, 0, "edit tag")?, "edit tag")? != 0 || fields.contains_key(&5) { |
|
| … | 17 unchanged lines |
||
| 999 | 1000 | node: node_id(field(&fields, 1, "edit.node")?, "edit.node")?, |
|
| 1000 | 1001 | after: AtomRef::from_value(field(&fields, 2, "after")?, "after")?, |
|
| 1001 | 1002 | before: AtomRef::from_value(field(&fields, 3, "before")?, "before")?, |
|
| 1002 | - | lines: Vec::new(), |
|
| 1003 | + | lines: Lines::default(), |
|
| 1003 | 1004 | }) |
|
| 1004 | 1005 | } |
|
| 1005 | 1006 | ||
| … | 8 unchanged lines |
||
| 1014 | 1015 | /// The lines a text blob's `content` holds, checked against the rules for |
|
| 1015 | 1016 | /// one: canonical, as many as the insert claims, and not short enough to |
|
| 1016 | 1017 | /// have been inline. |
|
| 1017 | - | pub fn text_lines(content: &[u8], count: u32) -> Result<Vec<Vec<u8>>, ObjectError> { |
|
| 1018 | + | pub fn text_lines(content: &[u8], count: u32) -> Result<Lines, ObjectError> { |
|
| 1018 | 1019 | if content.len() < TEXT_BLOB_MIN { |
|
| 1019 | 1020 | return schema(format!( |
|
| 1020 | 1021 | "a text blob of {} bytes is short enough to be inline", |
|
| 1021 | 1022 | content.len() |
|
| 1022 | 1023 | )); |
|
| 1023 | 1024 | } |
|
| 1024 | - | let v = cbor::decode(content)?; |
|
| 1025 | - | let lines = array(&v, "text blob")? |
|
| 1026 | - | .iter() |
|
| 1027 | - | .map(|l| bytes(l, "line").map(|b| b.to_vec())) |
|
| 1028 | - | .collect::<Result<Vec<_>, _>>()?; |
|
| 1025 | + | let lines = array_lines(&cbor::decode(content)?)?; |
|
| 1029 | 1026 | if lines.len() != count as usize { |
|
| 1030 | 1027 | return schema(format!( |
|
| 1031 | 1028 | "a text blob holds {} lines, the insert says {count}", |
|
| … | 192 unchanged lines |
||
| 1224 | 1221 | node: n, |
|
| 1225 | 1222 | after: AtomRef::Start, |
|
| 1226 | 1223 | before: AtomRef::End, |
|
| 1227 | - | lines: vec![b"hello\n".to_vec(), b"world\n".to_vec()], |
|
| 1224 | + | lines: vec![b"hello\n".to_vec(), b"world\n".to_vec()].into(), |
|
| 1228 | 1225 | }, |
|
| 1229 | 1226 | Edit::Delete { |
|
| 1230 | 1227 | node: n, |
|
| … | 60 unchanged lines |
||
| 1291 | 1288 | ); |
|
| 1292 | 1289 | } |
|
| 1293 | 1290 | ||
| 1294 | - | fn big_lines(n: usize) -> Vec<Vec<u8>> { |
|
| 1291 | + | fn big_lines(n: usize) -> Lines { |
|
| 1295 | 1292 | (0..n) |
|
| 1296 | 1293 | .map(|i| format!("line {i} of a large insert\n").into_bytes()) |
|
| 1297 | 1294 | .collect() |
|
| 1298 | 1295 | } |
|
| 1299 | 1296 | ||
| 1300 | - | fn with_insert(mut c: Change, lines: Vec<Vec<u8>>) -> Change { |
|
| 1297 | + | fn with_insert(mut c: Change, lines: Lines) -> Change { |
|
| 1301 | 1298 | c.edits[0] = Edit::Insert { |
|
| 1302 | 1299 | node: NodeId([7; 16]), |
|
| 1303 | 1300 | after: AtomRef::Start, |
|
| … | 57 unchanged lines |
||
| 1361 | 1358 | node: NodeId([8; 16]), |
|
| 1362 | 1359 | after: AtomRef::Start, |
|
| 1363 | 1360 | before: AtomRef::End, |
|
| 1364 | - | lines: vec![b"small\n".to_vec(), b"insert\n".to_vec()], |
|
| 1361 | + | lines: vec![b"small\n".to_vec(), b"insert\n".to_vec()].into(), |
|
| 1365 | 1362 | }); |
|
| 1366 | 1363 | let (bytes, stored) = c.encode_parts(); |
|
| 1367 | 1364 | let me = ObjectRef([4; 32]); |
|
| … | 36 unchanged lines |
||
| 1404 | 1401 | assert_eq!( |
|
| 1405 | 1402 | content, |
|
| 1406 | 1403 | cbor::encode(&Value::Array( |
|
| 1407 | - | lines.iter().map(|l| Value::Bytes(l.clone())).collect() |
|
| 1404 | + | lines.iter().map(|l| Value::Bytes(l.to_vec())).collect() |
|
| 1408 | 1405 | )) |
|
| 1409 | 1406 | ); |
|
| 1410 | 1407 | assert_eq!(text_lines(&content, 500).unwrap(), lines); |
|
| 1411 | 1408 | assert!(text_lines(&content, 499).is_err()); |
|
| 1412 | - | let small = cbor::encode_byte_strings(&[b"x\n".to_vec()]); |
|
| 1409 | + | let small = cbor::encode_byte_strings(&vec![b"x\n".to_vec()].into()); |
|
| 1413 | 1410 | assert!(text_lines(&small, 1).is_err(), "short enough to be inline"); |
|
| 1414 | 1411 | } |
|
| 1415 | 1412 | ||
| … | 81 unchanged lines |
modified crates/oxy-core/src/repo/fsck.rs +1 −1
| … | 89 unchanged lines |
||
| 90 | 90 | ObjectType::Blob => Blob::decode(body).map(|_| ()), |
|
| 91 | 91 | // Structure only: the text blobs are checked with the change graph. |
|
| 92 | 92 | ObjectType::Change => { |
|
| 93 | - | Change::decode_with(body, &mut |_, _| Ok(vec![Vec::new()])).map(|_| ()) |
|
| 93 | + | Change::decode_with(body, &mut |_, _| Ok(crate::lines::Lines::default())).map(|_| ()) |
|
| 94 | 94 | } |
|
| 95 | 95 | ObjectType::Checkpoint => crate::objects::Checkpoint::decode(body).map(|_| ()), |
|
| 96 | 96 | ObjectType::Op => match Op::decode(body) { |
|
| … | 391 unchanged lines |
modified crates/oxy-core/src/repo/history.rs +1 −1
| … | 1070 unchanged lines |
||
| 1071 | 1071 | if let Edit::Insert { node, lines, .. } = e { |
|
| 1072 | 1072 | let off = per_node.get(node).copied().unwrap_or(0); |
|
| 1073 | 1073 | let idx = counts.entry(*node).or_default(); |
|
| 1074 | - | for _ in lines { |
|
| 1074 | + | for _ in lines.iter() { |
|
| 1075 | 1075 | corr.atoms.insert((*node, *idx), *idx + off); |
|
| 1076 | 1076 | *idx += 1; |
|
| 1077 | 1077 | } |
|
| … | 128 unchanged lines |
modified crates/oxy-core/src/repo/snapshot.rs +4 −4
| … | 1476 unchanged lines |
||
| 1477 | 1477 | }; |
|
| 1478 | 1478 | f.hunks.push(DiffHunk { |
|
| 1479 | 1479 | base_line, |
|
| 1480 | - | added: lines.clone(), |
|
| 1480 | + | added: lines.to_vec(), |
|
| 1481 | 1481 | removed: vec![], |
|
| 1482 | 1482 | }); |
|
| 1483 | 1483 | } |
|
| … | 226 unchanged lines |
||
| 1710 | 1710 | node, |
|
| 1711 | 1711 | after: crate::objects::AtomRef::Start, |
|
| 1712 | 1712 | before: crate::objects::AtomRef::End, |
|
| 1713 | - | lines: split_lines(&content).iter().map(|l| l.to_vec()).collect(), |
|
| 1713 | + | lines: crate::lines::Lines::split(&content), |
|
| 1714 | 1714 | }); |
|
| 1715 | 1715 | } |
|
| 1716 | 1716 | self.statuses.push(PathStatus { |
|
| … | 254 unchanged lines |
||
| 1971 | 1971 | node, |
|
| 1972 | 1972 | after: crate::objects::AtomRef::Start, |
|
| 1973 | 1973 | before: crate::objects::AtomRef::End, |
|
| 1974 | - | lines: split_lines(&e.content).iter().map(|l| l.to_vec()).collect(), |
|
| 1974 | + | lines: crate::lines::Lines::split(&e.content), |
|
| 1975 | 1975 | }); |
|
| 1976 | 1976 | } |
|
| 1977 | 1977 | } else { |
|
| … | 235 unchanged lines |
||
| 2213 | 2213 | } |
|
| 2214 | 2214 | match e { |
|
| 2215 | 2215 | Edit::Insert { lines, .. } => { |
|
| 2216 | - | for l in lines { |
|
| 2216 | + | for l in lines.iter() { |
|
| 2217 | 2217 | out.push('+'); |
|
| 2218 | 2218 | out.push_str(&String::from_utf8_lossy(l)); |
|
| 2219 | 2219 | if !l.ends_with(b"\n") { |
|
| … | 140 unchanged lines |
modified crates/oxy-core/src/rewrite.rs +11 −9
| … | 32 unchanged lines |
||
| 33 | 33 | for e in &change.edits { |
|
| 34 | 34 | if let Edit::Insert { node, lines, .. } = e { |
|
| 35 | 35 | let idx = per_node.entry(*node).or_default(); |
|
| 36 | - | for _ in lines { |
|
| 36 | + | for _ in lines.iter() { |
|
| 37 | 37 | atoms.insert((*node, *idx), *idx); |
|
| 38 | 38 | *idx += 1; |
|
| 39 | 39 | } |
|
| … | 11 unchanged lines |
||
| 51 | 51 | let mut m: HashMap<crate::ids::NodeId, Vec<Vec<u8>>> = HashMap::new(); |
|
| 52 | 52 | for e in &c.edits { |
|
| 53 | 53 | if let Edit::Insert { node, lines, .. } = e { |
|
| 54 | - | m.entry(*node).or_default().extend(lines.iter().cloned()); |
|
| 54 | + | m.entry(*node) |
|
| 55 | + | .or_default() |
|
| 56 | + | .extend(lines.iter().map(<[u8]>::to_vec)); |
|
| 55 | 57 | } |
|
| 56 | 58 | } |
|
| 57 | 59 | m |
|
| … | 430 unchanged lines |
||
| 488 | 490 | node: f, |
|
| 489 | 491 | after: AtomRef::Start, |
|
| 490 | 492 | before: AtomRef::End, |
|
| 491 | - | lines: vec![b"a\n".to_vec(), b"b\n".to_vec()], |
|
| 493 | + | lines: vec![b"a\n".to_vec(), b"b\n".to_vec()].into(), |
|
| 492 | 494 | }], |
|
| 493 | 495 | "one", |
|
| 494 | 496 | ); |
|
| … | 17 unchanged lines |
||
| 512 | 514 | index: 1, |
|
| 513 | 515 | }, |
|
| 514 | 516 | before: AtomRef::End, |
|
| 515 | - | lines: vec![b"c\n".to_vec()], |
|
| 517 | + | lines: vec![b"c\n".to_vec()].into(), |
|
| 516 | 518 | }, |
|
| 517 | 519 | ], |
|
| 518 | 520 | "two", |
|
| … | 19 unchanged lines |
||
| 538 | 540 | index: 0, |
|
| 539 | 541 | }, |
|
| 540 | 542 | before: AtomRef::End, |
|
| 541 | - | lines: vec![b"d\n".to_vec()], |
|
| 543 | + | lines: vec![b"d\n".to_vec()].into(), |
|
| 542 | 544 | }], |
|
| 543 | 545 | "three", |
|
| 544 | 546 | ); |
|
| … | 56 unchanged lines |
||
| 601 | 603 | node: f, |
|
| 602 | 604 | after: AtomRef::Start, |
|
| 603 | 605 | before: AtomRef::End, |
|
| 604 | - | lines: vec![b"a\n".to_vec(), b"b\n".to_vec()], |
|
| 606 | + | lines: vec![b"a\n".to_vec(), b"b\n".to_vec()].into(), |
|
| 605 | 607 | }], |
|
| 606 | 608 | "one", |
|
| 607 | 609 | ); |
|
| … | 8 unchanged lines |
||
| 616 | 618 | index: 1, |
|
| 617 | 619 | }, |
|
| 618 | 620 | before: AtomRef::End, |
|
| 619 | - | lines: vec![b"c\n".to_vec()], |
|
| 621 | + | lines: vec![b"c\n".to_vec()].into(), |
|
| 620 | 622 | }], |
|
| 621 | 623 | "two", |
|
| 622 | 624 | ); |
|
| … | 7 unchanged lines |
||
| 630 | 632 | node: f, |
|
| 631 | 633 | after: AtomRef::Start, |
|
| 632 | 634 | before: AtomRef::End, |
|
| 633 | - | lines: vec![b"a\n".to_vec()], |
|
| 635 | + | lines: vec![b"a\n".to_vec()].into(), |
|
| 634 | 636 | }]; |
|
| 635 | 637 | let corr = Correspondence { |
|
| 636 | 638 | atoms: [((f, 0), 0)].into_iter().collect(), |
|
| … | 89 unchanged lines |
||
| 726 | 728 | node: g, |
|
| 727 | 729 | after: AtomRef::Start, |
|
| 728 | 730 | before: AtomRef::End, |
|
| 729 | - | lines: vec![b"x\n".to_vec()], |
|
| 731 | + | lines: vec![b"x\n".to_vec()].into(), |
|
| 730 | 732 | }], |
|
| 731 | 733 | "three", |
|
| 732 | 734 | ); |
|
| … | 42 unchanged lines |
modified crates/oxy-core/src/state.rs +15 −15
| … | 1243 unchanged lines |
||
| 1244 | 1244 | node: f, |
|
| 1245 | 1245 | after: AtomRef::Start, |
|
| 1246 | 1246 | before: AtomRef::End, |
|
| 1247 | - | lines: vec![b"one\n".to_vec(), b"two\n".to_vec()], |
|
| 1247 | + | lines: vec![b"one\n".to_vec(), b"two\n".to_vec()].into(), |
|
| 1248 | 1248 | }], |
|
| 1249 | 1249 | ); |
|
| 1250 | 1250 | let h1 = st.add(c1).unwrap(); |
|
| … | 17 unchanged lines |
||
| 1268 | 1268 | index: 1, |
|
| 1269 | 1269 | }, |
|
| 1270 | 1270 | before: AtomRef::End, |
|
| 1271 | - | lines: vec![b"three\n".to_vec()], |
|
| 1271 | + | lines: vec![b"three\n".to_vec()].into(), |
|
| 1272 | 1272 | }, |
|
| 1273 | 1273 | ], |
|
| 1274 | 1274 | ); |
|
| … | 47 unchanged lines |
||
| 1322 | 1322 | node: f, |
|
| 1323 | 1323 | after: AtomRef::Start, |
|
| 1324 | 1324 | before: AtomRef::End, |
|
| 1325 | - | lines: vec![b"a\n".to_vec(), b"c\n".to_vec()], |
|
| 1325 | + | lines: vec![b"a\n".to_vec(), b"c\n".to_vec()].into(), |
|
| 1326 | 1326 | }, |
|
| 1327 | 1327 | // Insert between the two lines just inserted, using self references. |
|
| 1328 | 1328 | Edit::Insert { |
|
| … | 6 unchanged lines |
||
| 1335 | 1335 | change: ObjectRef::ZERO, |
|
| 1336 | 1336 | index: 1, |
|
| 1337 | 1337 | }, |
|
| 1338 | - | lines: vec![b"b\n".to_vec()], |
|
| 1338 | + | lines: vec![b"b\n".to_vec()].into(), |
|
| 1339 | 1339 | }, |
|
| 1340 | 1340 | ], |
|
| 1341 | 1341 | ); |
|
| … | 80 unchanged lines |
||
| 1422 | 1422 | node: f, |
|
| 1423 | 1423 | after: AtomRef::Start, |
|
| 1424 | 1424 | before: AtomRef::End, |
|
| 1425 | - | lines: vec![b"1\n".to_vec(), b"2\n".to_vec()], |
|
| 1425 | + | lines: vec![b"1\n".to_vec(), b"2\n".to_vec()].into(), |
|
| 1426 | 1426 | }], |
|
| 1427 | 1427 | )) |
|
| 1428 | 1428 | .unwrap(); |
|
| … | 28 unchanged lines |
||
| 1457 | 1457 | index: 1, |
|
| 1458 | 1458 | }, |
|
| 1459 | 1459 | before: AtomRef::End, |
|
| 1460 | - | lines: vec![b"3\n".to_vec()], |
|
| 1460 | + | lines: vec![b"3\n".to_vec()].into(), |
|
| 1461 | 1461 | }], |
|
| 1462 | 1462 | ); |
|
| 1463 | 1463 | let mut full = st.clone(); |
|
| … | 29 unchanged lines |
||
| 1493 | 1493 | node, |
|
| 1494 | 1494 | after: AtomRef::Start, |
|
| 1495 | 1495 | before: AtomRef::End, |
|
| 1496 | - | lines: vec![line.to_vec()], |
|
| 1496 | + | lines: vec![line.to_vec()].into(), |
|
| 1497 | 1497 | }; |
|
| 1498 | 1498 | let c1 = change( |
|
| 1499 | 1499 | vec![], |
|
| … | 74 unchanged lines |
||
| 1574 | 1574 | parent: NodeId::ROOT, |
|
| 1575 | 1575 | name: name.into(), |
|
| 1576 | 1576 | }; |
|
| 1577 | - | let lines: Vec<Vec<u8>> = (0..300) |
|
| 1577 | + | let lines: crate::lines::Lines = (0..300) |
|
| 1578 | 1578 | .map(|i| format!("line {i}\n").into_bytes()) |
|
| 1579 | 1579 | .collect(); |
|
| 1580 | 1580 | let c1 = change( |
|
| … | 4 unchanged lines |
||
| 1585 | 1585 | node: small, |
|
| 1586 | 1586 | after: AtomRef::Start, |
|
| 1587 | 1587 | before: AtomRef::End, |
|
| 1588 | - | lines: vec![b"x\n".to_vec()], |
|
| 1588 | + | lines: vec![b"x\n".to_vec()].into(), |
|
| 1589 | 1589 | }, |
|
| 1590 | 1590 | Edit::Insert { |
|
| 1591 | 1591 | node: big, |
|
| … | 26 unchanged lines |
||
| 1618 | 1618 | change: h1, |
|
| 1619 | 1619 | index: 21, |
|
| 1620 | 1620 | }, |
|
| 1621 | - | lines: vec![b"new a\n".to_vec(), b"new b\n".to_vec()], |
|
| 1621 | + | lines: vec![b"new a\n".to_vec(), b"new b\n".to_vec()].into(), |
|
| 1622 | 1622 | }, |
|
| 1623 | 1623 | Edit::Insert { |
|
| 1624 | 1624 | node: small, |
|
| … | 2 unchanged lines |
||
| 1627 | 1627 | index: 0, |
|
| 1628 | 1628 | }, |
|
| 1629 | 1629 | before: AtomRef::End, |
|
| 1630 | - | lines: vec![b"y\n".to_vec()], |
|
| 1630 | + | lines: vec![b"y\n".to_vec()].into(), |
|
| 1631 | 1631 | }, |
|
| 1632 | 1632 | ], |
|
| 1633 | 1633 | ); |
|
| … | 96 unchanged lines |
||
| 1730 | 1730 | node: f, |
|
| 1731 | 1731 | after: AtomRef::Start, |
|
| 1732 | 1732 | before: AtomRef::End, |
|
| 1733 | - | lines: vec![b"a\n".to_vec()], |
|
| 1733 | + | lines: vec![b"a\n".to_vec()].into(), |
|
| 1734 | 1734 | }], |
|
| 1735 | 1735 | )) |
|
| 1736 | 1736 | .unwrap(); |
|
| … | 18 unchanged lines |
||
| 1755 | 1755 | node: f, |
|
| 1756 | 1756 | after: AtomRef::Start, |
|
| 1757 | 1757 | before: AtomRef::End, |
|
| 1758 | - | lines: vec![b"a\n".to_vec()], |
|
| 1758 | + | lines: vec![b"a\n".to_vec()].into(), |
|
| 1759 | 1759 | }], |
|
| 1760 | 1760 | )) |
|
| 1761 | 1761 | .unwrap(); |
|
| … | 14 unchanged lines |
||
| 1776 | 1776 | parent: NodeId::ROOT, |
|
| 1777 | 1777 | name: name.into(), |
|
| 1778 | 1778 | }; |
|
| 1779 | - | let big_lines: Vec<Vec<u8>> = (0..400) |
|
| 1779 | + | let big_lines: crate::lines::Lines = (0..400) |
|
| 1780 | 1780 | .map(|i| format!("line {i} of a long file\n").into_bytes()) |
|
| 1781 | 1781 | .collect(); |
|
| 1782 | 1782 | let h1 = st |
|
| … | 5 unchanged lines |
||
| 1788 | 1788 | node: small, |
|
| 1789 | 1789 | after: AtomRef::Start, |
|
| 1790 | 1790 | before: AtomRef::End, |
|
| 1791 | - | lines: vec![b"x\n".to_vec()], |
|
| 1791 | + | lines: vec![b"x\n".to_vec()].into(), |
|
| 1792 | 1792 | }, |
|
| 1793 | 1793 | Edit::Insert { |
|
| 1794 | 1794 | node: big, |
|
| … | 129 unchanged lines |
modified docs/benchmarks.md +8 −2
| … | 383 unchanged lines |
||
| 384 | 384 | | Before | 61 s | 7.1 GB | 10.1 GB | |
|
| 385 | 385 | | After | 40.7 s | 7.4 GB | 8.7 GB | |
|
| 386 | 386 | | Texts stored a batch at a time while encoding | 41.6 s | 4.8 GB | 5.8 GB | |
|
| 387 | + | | Lines in one shared buffer per insert (Earth under load) | 50.1 s | 4.3 GB | 4.9 GB | |
|
| 387 | 388 | | Git | 33.7 to 37.9 s | | 42 MB | |
|
| 388 | 389 | ||
| 389 | 390 | Storing a change's texts a batch at a time as they are made (the |
|
| 390 | 391 | encoder hands each batch of 256 to the store) removed the storing peak. |
|
| 391 | - | The peak is now the working copy's content (1.6 GB) and its lines, a Vec |
|
| 392 | - | each (2.6 GB): M2. |
|
| 392 | + | The peak was then the working copy's content (1.6 GB) and its lines, a |
|
| 393 | + | Vec each (2.6 GB). An insert's lines are now one buffer with line ends |
|
| 394 | + | (`Lines`), shared by the change and by the file graph, whose atoms point |
|
| 395 | + | into it: the diff of the whole tree takes 3.3 s and 1.1 million |
|
| 396 | + | allocations (from 6.2 s and 45 million), and the peak is 4.3 GB (4.9 GB |
|
| 397 | + | RSS). That run shared Earth with Flow jobs (load average 6.5 on 4 cores), |
|
| 398 | + | so its total time is not comparable with the rows above. |
|
| 393 | 399 |