OXYGENHUB

First commit: 12 s and 2.1 GB on the Linux tree (refs #41) 5b4c51307b14

A commit's own snapshot no longer encodes the working-copy change (the
commit encodes and stores its own); oxy commit prints the new change's
id from its metadata instead of reading back the whole change; line
ends take four bytes and exactly their room; a change's inline lines
are encoded as their bytes rather than a CBOR value per line (same
encoding, tested); stored texts go a batch of at most 32 MiB at a time.
The first commit of the Linux tip: 25.2 s and 2.7 GiB to 12.4 s and
2.07 GiB (Git 30.8 s).
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-02 03:00 UTC
Signature
✔ signed by Andy McCall
Hash
5b4c51307b14eee794c356ac0d086b4fb3f7e36170b9095817e52a889d455cb5
Depends on
01a47cd7f0c8 0f93f49ce059 17f94b13ae54 192fad1e46a2 40a40bb7b798 614541c3cebd 6f8aeb996558 75167672ebcd 957fee30826e 982864068a59 c07e0e7d2d85 cd4eb55e3b3d cdb5beba7f05 e7ddd552b619 e96a430c78f3 e993a086c13f fce44ae9ad29 ff0af1ee299d
Depended on by
7505194116e7
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
35 atoms, 0 tree operations, 8 files

Browse the tree at this change

modified Cargo.lock +1 −0
…
1271 unchanged lines
1272 1272
 "hex",
1273 1273
 "ignore",
1274 1274
 "libc",
1275 +
 "memchr",
1275 1276
 "ml-dsa",
1276 1277
 "proptest",
1277 1278
 "rand 0.8.8",
…
1284 unchanged lines
modified crates/oxy-core/Cargo.toml +1 −0
…
19 unchanged lines
20 20
webpki-roots = "1"
21 21
zstd = "0.13"
22 22
globset = "0.4"
23 +
memchr = "2"
23 24
rayon = "1"
24 25
25 26
[target.'cfg(unix)'.dependencies]
…
5 unchanged lines
modified crates/oxy-core/src/cbor.rs +5 −0
…
13 unchanged lines
14 14
    Map(BTreeMap<u64, Value>),
15 15
    Bool(bool),
16 16
    Null,
17 +
    /// An item already in its canonical encoding, written out as it is.
18 +
    /// Only built for encoding (never decoded): a large change's inline
19 +
    /// lines then cost their bytes, not a value per line.
20 +
    Encoded(Vec<u8>),
17 21
}
18 22
19 23
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
…
98 unchanged lines
118 122
        Value::Bool(false) => out.push(0xf4),
119 123
        Value::Bool(true) => out.push(0xf5),
120 124
        Value::Null => out.push(0xf6),
125 +
        Value::Encoded(b) => out.extend_from_slice(b),
121 126
    }
122 127
}
123 128
…
477 unchanged lines
modified crates/oxy-core/src/lines.rs +69 −19
…
10 unchanged lines
11 11
    /// Shared with the file it was read from, when there was one.
12 12
    buf: Arc<Vec<u8>>,
13 13
    /// The end offset of each line in `buf`.
14 -
    ends: Vec<u64>,
14 +
    ends: Ends,
15 15
}
16 16
17 +
/// Line end offsets, four bytes each unless the buffer is over 4 GiB: a
18 +
/// first commit of the Linux tree holds 36 million of them at once. Which
19 +
/// form follows from the buffer's length alone, so equal lines compare
20 +
/// equal.
21 +
#[derive(PartialEq, Eq, Hash)]
22 +
enum Ends {
23 +
    Small(Vec<u32>),
24 +
    Large(Vec<u64>),
25 +
}
26 +
27 +
impl Default for Ends {
28 +
    fn default() -> Self {
29 +
        Ends::Small(Vec::new())
30 +
    }
31 +
}
32 +
33 +
impl Ends {
34 +
    fn from_u64(ends: Vec<u64>, buf_len: usize) -> Ends {
35 +
        if u32::try_from(buf_len).is_ok() {
36 +
            Ends::Small(ends.into_iter().map(|e| e as u32).collect())
37 +
        } else {
38 +
            Ends::Large(ends)
39 +
        }
40 +
    }
41 +
42 +
    fn len(&self) -> usize {
43 +
        match self {
44 +
            Ends::Small(v) => v.len(),
45 +
            Ends::Large(v) => v.len(),
46 +
        }
47 +
    }
48 +
49 +
    fn is_empty(&self) -> bool {
50 +
        self.len() == 0
51 +
    }
52 +
53 +
    fn get(&self, i: usize) -> Option<usize> {
54 +
        match self {
55 +
            Ends::Small(v) => v.get(i).map(|&e| e as usize),
56 +
            Ends::Large(v) => v.get(i).map(|&e| e as usize),
57 +
        }
58 +
    }
59 +
}
60 +
17 61
/// An insert's lines. Cloning is cheap: the buffer is shared.
18 62
#[derive(Clone, Default, PartialEq, Eq, Hash)]
19 63
pub struct Lines(Arc<Inner>);
…
10 unchanged lines
30 74
    /// shared rather than copied.
31 75
    pub fn split_shared(buf: Arc<Vec<u8>>) -> Lines {
32 76
        let content = &buf[..];
33 -
        let mut ends = Vec::new();
34 -
        for (i, &c) in content.iter().enumerate() {
35 -
            if c == b'\n' {
36 -
                ends.push(i as u64 + 1);
37 -
            }
38 -
        }
39 -
        if ends.last().copied().unwrap_or(0) as usize != content.len() {
40 -
            ends.push(content.len() as u64);
41 -
        }
77 +
        // Counted first, so the offsets take exactly their room.
78 +
        let newlines = memchr::memchr_iter(b'\n', content).count();
79 +
        let unterminated = content.last().is_some_and(|&c| c != b'\n');
80 +
        let n = newlines + usize::from(unterminated);
81 +
        let ends_at = memchr::memchr_iter(b'\n', content)
82 +
            .map(|i| i + 1)
83 +
            .chain(unterminated.then_some(content.len()));
84 +
        let ends = if u32::try_from(content.len()).is_ok() {
85 +
            let mut v = Vec::with_capacity(n);
86 +
            v.extend(ends_at.map(|e| e as u32));
87 +
            Ends::Small(v)
88 +
        } else {
89 +
            let mut v = Vec::with_capacity(n);
90 +
            v.extend(ends_at.map(|e| e as u64));
91 +
            Ends::Large(v)
92 +
        };
42 93
        Lines(Arc::new(Inner { buf, ends }))
43 94
    }
44 95
…
7 unchanged lines
52 103
53 104
    /// Line `i`, with its terminator.
54 105
    pub fn get(&self, i: usize) -> Option<&[u8]> {
55 -
        let end = *self.0.ends.get(i)? as usize;
56 -
        let start = if i == 0 {
57 -
            0
58 -
        } else {
59 -
            self.0.ends[i - 1] as usize
60 -
        };
106 +
        let end = self.0.ends.get(i)?;
107 +
        let start = if i == 0 { 0 } else { self.0.ends.get(i - 1)? };
61 108
        Some(&self.0.buf[start..end])
62 109
    }
63 110
…
13 unchanged lines
77 124
    pub fn is_split_form(&self) -> bool {
78 125
        let buf = &self.0.buf;
79 126
        let mut start = 0usize;
80 -
        for (i, &end) in self.0.ends.iter().enumerate() {
81 -
            let line = &buf[start..end as usize];
127 +
        for i in 0..self.0.ends.len() {
128 +
            let end = self.0.ends.get(i).unwrap_or(start);
129 +
            let line = &buf[start..end];
82 130
            if line.is_empty() {
83 131
                return false;
84 132
            }
…
8 unchanged lines
93 141
            if body.contains(&b'\n') {
94 142
                return false;
95 143
            }
96 -
            start = end as usize;
144 +
            start = end;
97 145
        }
98 146
        true
99 147
    }
…
23 unchanged lines
123 171
            buf.extend_from_slice(l);
124 172
            ends.push(buf.len() as u64);
125 173
        }
174 +
        let ends = Ends::from_u64(ends, buf.len());
126 175
        Lines(Arc::new(Inner {
127 176
            buf: Arc::new(buf),
128 177
            ends,
…
8 unchanged lines
137 186
            buf.extend_from_slice(&l);
138 187
            ends.push(buf.len() as u64);
139 188
        }
189 +
        let ends = Ends::from_u64(ends, buf.len());
140 190
        Lines(Arc::new(Inner {
141 191
            buf: Arc::new(buf),
142 192
            ends,
…
65 unchanged lines
modified crates/oxy-core/src/objects.rs +63 −18
…
486 unchanged lines
487 487
488 488
    /// The edit's value; `stored` is the text blob of an insert stored as
489 489
    /// one, and its form (see [`Change::value_with`], which works them out).
490 -
    fn to_value(&self, stored: Option<(ObjectRef, TextForm)>) -> Value {
490 +
    /// The edit's value; `compact` writes an inline insert's lines as
491 +
    /// their encoding rather than a value per line (same bytes encoded).
492 +
    fn to_value(&self, stored: Option<(ObjectRef, TextForm)>, compact: bool) -> Value {
491 493
        let nid = |n: &NodeId| Value::Bytes(n.0.to_vec());
492 494
        let opt = |o: &Option<ObjectRef>| match o {
493 495
            Some(r) => Value::Bytes(r.0.to_vec()),
…
37 unchanged lines
531 533
                (3, before.to_value()),
532 534
                (
533 535
                    4,
534 -
                    Value::Array(lines.iter().map(|l| Value::Bytes(l.to_vec())).collect()),
536 +
                    if compact {
537 +
                        Value::Encoded(cbor::encode_byte_strings(lines))
538 +
                    } else {
539 +
                        Value::Array(lines.iter().map(|l| Value::Bytes(l.to_vec())).collect())
540 +
                    },
535 541
                ),
536 542
            ]),
537 543
            (Edit::Delete { node, atoms }, _) => Value::map([
…
135 unchanged lines
673 679
    }
674 680
675 681
    pub fn to_value(&self) -> Value {
676 -
        self.value_with(&mut |_| true)
682 +
        self.value_with(&mut |_| true, false)
677 683
    }
678 684
679 685
    /// The text blob of each edit stored as one, `None` for the others;
…
6 unchanged lines
686 692
        let stored: Vec<usize> = (0..self.edits.len())
687 693
            .filter(|i| self.edits[*i].stored_text(self.version))
688 694
            .collect();
689 -
        for batch in stored.chunks(256) {
695 +
        // At most 256 texts or 32 MiB of them a batch: some directories
696 +
        // hold hundreds of generated files of megabytes each, and a batch
697 +
        // is copied and compressed at once.
698 +
        let mut batches: Vec<&[usize]> = Vec::new();
699 +
        let (mut start, mut bytes) = (0, 0usize);
700 +
        for (k, i) in stored.iter().enumerate() {
701 +
            if let Edit::Insert { lines, .. } = &self.edits[*i] {
702 +
                bytes += lines.bytes().len();
703 +
            }
704 +
            if k + 1 - start == 256 || bytes >= 32 << 20 {
705 +
                batches.push(&stored[start..=k]);
706 +
                (start, bytes) = (k + 1, 0);
707 +
            }
708 +
        }
709 +
        if start < stored.len() {
710 +
            batches.push(&stored[start..]);
711 +
        }
712 +
        for batch in batches {
690 713
            let made: Vec<StoredText> = batch
691 714
                .par_iter()
692 715
                .map(|i| {
…
22 unchanged lines
715 738
        refs
716 739
    }
717 740
718 -
    fn value_with(&self, sink: &mut TextSink) -> Value {
741 +
    /// The change's value, its stored texts handed to `sink`; `compact`
742 +
    /// as [`Edit::to_value`] takes it, for a value that is only encoded.
743 +
    fn value_with(&self, sink: &mut TextSink, compact: bool) -> Value {
719 744
        Value::map([
720 745
            (0, Value::Uint(self.version)),
721 746
            (1, Value::Bytes(self.change_id.0.to_vec())),
…
19 unchanged lines
741 766
                    self.edits
742 767
                        .iter()
743 768
                        .zip(self.stored_texts(sink))
744 -
                        .map(|(e, stored)| e.to_value(stored))
769 +
                        .map(|(e, stored)| e.to_value(stored, compact))
745 770
                        .collect(),
746 771
                ),
747 772
            ),
…
7 unchanged lines
755 780
            self.deps.windows(2).all(|w| w[0] < w[1]),
756 781
            "deps must be normalized"
757 782
        );
758 -
        cbor::encode(&self.to_value())
783 +
        cbor::encode(&self.value_with(&mut |_| true, true))
759 784
    }
760 785
761 786
    /// The encoding and the text blobs its large inserts are stored as,
…
4 unchanged lines
766 791
            "deps must be normalized"
767 792
        );
768 793
        let mut texts = Vec::new();
769 -
        let bytes = cbor::encode(&self.value_with(&mut |batch| {
770 -
            texts.extend(batch);
771 -
            true
772 -
        }));
794 +
        let bytes = cbor::encode(&self.value_with(
795 +
            &mut |batch| {
796 +
                texts.extend(batch);
797 +
                true
798 +
            },
799 +
            true,
800 +
        ));
773 801
        (bytes, texts)
774 802
    }
775 803
…
9 unchanged lines
785 813
            "deps must be normalized"
786 814
        );
787 815
        let mut failed = None;
788 -
        let value = self.value_with(&mut |batch| match store(batch) {
789 -
            Ok(()) => true,
790 -
            Err(e) => {
791 -
                failed = Some(e);
792 -
                false
793 -
            }
794 -
        });
816 +
        let value = self.value_with(
817 +
            &mut |batch| match store(batch) {
818 +
                Ok(()) => true,
819 +
                Err(e) => {
820 +
                    failed = Some(e);
821 +
                    false
822 +
                }
823 +
            },
824 +
            true,
825 +
        );
795 826
        match failed {
796 827
            Some(e) => Err(e),
797 828
            None => Ok(cbor::encode(&value)),
…
548 unchanged lines
1346 1377
        let mut e = c.clone();
1347 1378
        e.message.push('!');
1348 1379
        assert_ne!(e.hash(), c.hash());
1380 +
    }
1381 +
1382 +
    #[test]
1383 +
    fn compact_encoding_is_the_plain_one() {
1384 +
        // Inline lines written as their encoding give the same bytes, so
1385 +
        // the same hash, as a value per line.
1386 +
        let c = sample_change();
1387 +
        assert!(!c.has_stored_text(), "the sample's insert is inline");
1388 +
        let plain = cbor::encode(&c.to_value());
1389 +
        assert_eq!(c.encode(), plain);
1390 +
        assert_eq!(c.encode_parts().0, plain);
1391 +
        let streamed: Result<Vec<u8>, ()> = c.encode_streaming(|_| Ok(()));
1392 +
        assert_eq!(streamed.unwrap(), plain);
1393 +
        assert_eq!(Change::decode(&plain).unwrap(), c);
1349 1394
    }
1350 1395
1351 1396
    #[test]
…
278 unchanged lines
modified crates/oxy-core/src/repo/snapshot.rs +15 −5
…
44 unchanged lines
45 45
46 46
/// Result of snapshotting the working copy.
47 47
pub struct SnapshotResult {
48 +
    /// The working-copy change's hash; for a commit's own snapshot, which
49 +
    /// does not encode it, the hash it had before.
48 50
    pub wc_hash: ChangeHash,
49 51
    pub wc: Change,
50 52
    /// The base state (closure of the working-copy change's dependencies).
…
304 unchanged lines
355 357
        if self.config.sign_changes && ours && self.secret_key()?.is_none() {
356 358
            return fail("core.sign is \"change\" but no secret key is available for author.key");
357 359
        }
360 +
        let storing = crate::timing::phase("store: the change");
358 361
        let hash = self.store.put_change(change)?;
362 +
        drop(storing);
359 363
        if self.config.sign_changes && ours {
364 +
            let _t = crate::timing::phase("store: sign it");
360 365
            self.sign_change(&hash)?;
361 366
        }
362 367
        Ok(hash)
…
213 unchanged lines
576 581
        // Encoded once, its texts stored a batch at a time as they are
577 582
        // made (a stored blob is not stored again); the change itself only
578 583
        // when it changed.
579 -
        let encoded = wc.encode_streaming(|batch| self.store.put_texts(&batch))?;
580 -
        let new_hash = crate::hash::hash_object(crate::hash::ObjectType::Change, &encoded);
581 -
        let changed = new_hash != old_hash;
582 -
        // For a commit the change is neither stored nor recorded: the commit
583 -
        // stores its own, built from this one's operations.
584 +
        // For a commit the change is neither encoded, stored nor recorded:
585 +
        // the commit stores its own, built from this one's operations, and
586 +
        // its texts with it. The hash it would have is not needed.
587 +
        let (new_hash, changed, encoded) = if for_commit {
588 +
            (old_hash, true, Vec::new())
589 +
        } else {
590 +
            let encoded = wc.encode_streaming(|batch| self.store.put_texts(&batch))?;
591 +
            let new_hash = crate::hash::hash_object(crate::hash::ObjectType::Change, &encoded);
592 +
            (new_hash, new_hash != old_hash, encoded)
593 +
        };
584 594
        if changed && !for_commit {
585 595
            self.store.put(crate::hash::ObjectType::Change, &encoded)?;
586 596
            let mut view = op.view.clone();
…
2290 unchanged lines
modified crates/oxy-core/src/store.rs +3 −0
…
1092 unchanged lines
1093 1093
    /// Store a change and the text blobs (and their chunks) its large
1094 1094
    /// inserts are stored as (spec §6.1).
1095 1095
    pub fn put_change(&self, c: &Change) -> Result<ObjectRef> {
1096 +
        let encoding = crate::timing::phase("store: encode the change and its texts");
1096 1097
        let bytes = c.encode_streaming(|batch| self.put_texts(&batch))?;
1098 +
        drop(encoding);
1099 +
        let _t = crate::timing::phase("store: put the encoding");
1097 1100
        self.put(ObjectType::Change, &bytes)
1098 1101
    }
1099 1102
…
947 unchanged lines
modified src/cli/workspace.rs +3 −2
…
286 unchanged lines
287 287
    };
288 288
    let sp = ui::spinner("Committing…");
289 289
    let h = repo.commit_selection(message, &deps, &selection)?;
290 -
    let c = repo.store().get_change(&h)?;
290 +
    // The metadata only: a first commit's change holds the whole tree.
291 +
    let meta = repo.store().get_change_meta(&h)?;
291 292
    sp.done(&format!(
292 293
        "{} {} {}  {}",
293 294
        ui::done("Committed"),
294 -
        ui::id(short_id(&c)),
295 +
        ui::id(meta.change_id.short(8)),
295 296
        ui::hash(format!("#{}", h.short(12))),
296 297
        ui::subject(subject_of(message))
297 298
    ));
…
2075 unchanged lines