OXYGENHUB

File graphs keep their order as edits apply dbf06d342e1d

A file graph whose live atoms have one order keeps it, found by a
render, and each change's edits splice it rather than dropping it: its
anchors must be in the order with only deleted lines between them, and
its runs kept apart by a line that stays, or the order is dropped. The
lines it deletes are checked for ghosts (spec 9.1) at the next render,
which knows how changes depend on each other. Any other change to the
graph drops it. A randomised test checks the kept order against the
general algorithm through sequential and concurrent edits.

So a long-lived file renders in time that follows its live lines rather
than every line it ever had. The first 5 000 vscode commits import in
38.8 s (from 78 s) and verify clean.

refs #25, refs #35
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 07:21 UTC
Signature
✔ signed by Andy McCall
Hash
dbf06d342e1db5047e0c8dfd881cee3b54ac0400f00d3853c920524600f7d2ea
Depends on
0bdc3f91a39f 598009a75f6c 75167672ebcd e83354cdac22 e8d38e47e9d7 ff0af1ee299d
Depended on by
aedfe465b611 7eac74abc586 40a40bb7b798
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
17 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified crates/oxy-core/src/graph.rs +317 −8
…
45 unchanged lines
46 46
    atoms: HashMap<AtomKey, AtomInfo>,
47 47
    /// Every vertex, with its direct successors.
48 48
    succ: HashMap<Vertex, Edges>,
49 +
    /// The live atoms' order, once known to be the graph's only one.
50 +
    order: OrderSlot,
49 51
}
52 +
53 +
/// A file graph's live atoms in order, when the graph orders them one way
54 +
/// only: found by [`FileGraph::materialize`] and brought up to date by
55 +
/// [`Order::splice`] as a change's edits apply, so a long-lived file
56 +
/// renders in time that follows its live lines rather than every line it
57 +
/// ever had. Any other change to the graph drops it.
58 +
#[derive(Debug)]
59 +
pub struct Order {
60 +
    atoms: Vec<AtomKey>,
61 +
    /// Atoms deleted since the order was last confirmed: it holds while
62 +
    /// none of them is a ghost (spec §9.1), which only a render can ask,
63 +
    /// since it needs to know how the changes depend on each other.
64 +
    unchecked: Vec<AtomKey>,
65 +
    confirmed: std::sync::atomic::AtomicBool,
66 +
}
67 +
68 +
/// Holds a graph's [`Order`]; filled in by a render, which only reads the
69 +
/// graph.
70 +
#[derive(Debug, Default)]
71 +
struct OrderSlot(std::sync::Mutex<Option<std::sync::Arc<Order>>>);
72 +
73 +
impl OrderSlot {
74 +
    fn get(&self) -> Option<std::sync::Arc<Order>> {
75 +
        self.0.lock().unwrap_or_else(|e| e.into_inner()).clone()
76 +
    }
77 +
    fn set(&self, o: Option<std::sync::Arc<Order>>) {
78 +
        *self.0.lock().unwrap_or_else(|e| e.into_inner()) = o;
79 +
    }
80 +
    fn take(&mut self) -> Option<std::sync::Arc<Order>> {
81 +
        self.0.get_mut().unwrap_or_else(|e| e.into_inner()).take()
82 +
    }
83 +
}
84 +
85 +
impl Clone for OrderSlot {
86 +
    fn clone(&self) -> Self {
87 +
        OrderSlot(std::sync::Mutex::new(self.get()))
88 +
    }
89 +
}
90 +
91 +
impl Order {
92 +
    fn known(atoms: Vec<AtomKey>) -> Order {
93 +
        Order {
94 +
            atoms,
95 +
            unchecked: Vec::new(),
96 +
            confirmed: std::sync::atomic::AtomicBool::new(true),
97 +
        }
98 +
    }
50 99
100 +
    /// The order after change `h` inserts the runs `inserts` (after,
101 +
    /// before, first index, count) and deletes `deleted`, as its edits
102 +
    /// apply; `None` where that is not certain to be the graph's only
103 +
    /// order: an anchor not in the order, live lines between an insert's
104 +
    /// anchors, or two inserts with nothing kept between them.
105 +
    pub fn splice(
106 +
        &self,
107 +
        h: ChangeHash,
108 +
        inserts: &[(Vertex, Vertex, u32, u32)],
109 +
        deleted: &[AtomKey],
110 +
    ) -> Option<std::sync::Arc<Order>> {
111 +
        let mut want: HashSet<AtomKey> = deleted.iter().copied().collect();
112 +
        for (a, b, _, _) in inserts {
113 +
            for v in [a, b] {
114 +
                if let Vertex::Atom(k) = v {
115 +
                    want.insert(*k);
116 +
                }
117 +
            }
118 +
        }
119 +
        let mut pos: HashMap<AtomKey, usize> = HashMap::default();
120 +
        for (i, k) in self.atoms.iter().enumerate() {
121 +
            if want.contains(k) {
122 +
                pos.insert(*k, i);
123 +
            }
124 +
        }
125 +
        let len = self.atoms.len();
126 +
        let mut gone = vec![false; len];
127 +
        let mut unchecked = if self.confirmed.load(std::sync::atomic::Ordering::Relaxed) {
128 +
            Vec::new()
129 +
        } else {
130 +
            self.unchecked.clone()
131 +
        };
132 +
        for k in deleted {
133 +
            if k.0 == h {
134 +
                return None;
135 +
            }
136 +
            if let Some(&i) = pos.get(k) {
137 +
                if !gone[i] {
138 +
                    gone[i] = true;
139 +
                    unchecked.push(*k);
140 +
                }
141 +
            }
142 +
        }
143 +
        // Each run goes in the gap its anchors leave (slot `i`: before
144 +
        // `atoms[i]`), which must hold nothing that stays.
145 +
        let mut slots: Vec<(usize, usize, u32, u32)> = Vec::with_capacity(inserts.len());
146 +
        for &(a, b, first, count) in inserts {
147 +
            let from = match a {
148 +
                Vertex::Start => 0,
149 +
                Vertex::Atom(k) => pos.get(&k)? + 1,
150 +
                Vertex::End => return None,
151 +
            };
152 +
            let to = match b {
153 +
                Vertex::End => len,
154 +
                Vertex::Atom(k) => *pos.get(&k)?,
155 +
                Vertex::Start => return None,
156 +
            };
157 +
            if from > to || gone[from..to].iter().any(|g| !g) {
158 +
                return None;
159 +
            }
160 +
            slots.push((from, to, first, count));
161 +
        }
162 +
        // Runs one after another need a kept line between them, or the
163 +
        // graph would not say which comes first.
164 +
        slots.sort_unstable();
165 +
        for w in slots.windows(2) {
166 +
            let (end, next) = (w[0].1, w[1].0);
167 +
            if end >= next || !gone[end..next].iter().any(|g| !g) {
168 +
                return None;
169 +
            }
170 +
        }
171 +
        let added: usize = slots.iter().map(|s| s.3 as usize).sum();
172 +
        let mut atoms = Vec::with_capacity(len + added);
173 +
        let mut slots = slots.into_iter().peekable();
174 +
        let mut runs_at = |i: usize, atoms: &mut Vec<AtomKey>| {
175 +
            while let Some(&(from, _, first, count)) = slots.peek() {
176 +
                if from != i {
177 +
                    break;
178 +
                }
179 +
                atoms.extend((first..first + count).map(|j| (h, j)));
180 +
                slots.next();
181 +
            }
182 +
        };
183 +
        for (i, (k, g)) in self.atoms.iter().zip(&gone).enumerate() {
184 +
            runs_at(i, &mut atoms);
185 +
            if !g {
186 +
                atoms.push(*k);
187 +
            }
188 +
        }
189 +
        runs_at(len, &mut atoms);
190 +
        let confirmed = unchecked.is_empty();
191 +
        Some(std::sync::Arc::new(Order {
192 +
            atoms,
193 +
            unchecked,
194 +
            confirmed: std::sync::atomic::AtomicBool::new(confirmed),
195 +
        }))
196 +
    }
197 +
}
198 +
51 199
/// A vertex's direct successors, sorted and without repeats. Nearly every
52 200
/// vertex has one, which is kept inline without an allocation.
53 201
#[derive(Clone, Debug, Default, PartialEq, Eq)]
…
90 unchanged lines
144 292
        let mut g = FileGraph {
145 293
            atoms: HashMap::default(),
146 294
            succ: HashMap::default(),
295 +
            order: OrderSlot::default(),
147 296
        };
148 297
        g.edge(Vertex::Start, Vertex::End);
149 298
        g
…
27 unchanged lines
177 326
        before: Vertex,
178 327
        lines: Lines,
179 328
    ) {
329 +
        self.order.take();
180 330
        self.atoms.reserve(lines.len());
181 331
        self.succ.reserve(lines.len() + 1);
182 332
        let mut prev = after;
…
11 unchanged lines
194 344
195 345
    /// Apply a `Delete` of `range` by change `by`.
196 346
    pub fn delete(&mut self, by: ChangeHash, range: &AtomRange) {
347 +
        self.order.take();
197 348
        for i in range.from..range.to {
198 349
            self.atoms
199 350
                .entry((range.change, i))
…
183 unchanged lines
383 534
384 535
    /// Order the live atoms and locate conflicts.
385 536
    pub fn materialize(&self, deps: &dyn DepOracle) -> Materialized {
386 -
        if let Some(order) = self.as_chain().or_else(|| self.as_order(deps)) {
387 -
            return Materialized {
388 -
                segments: if order.is_empty() {
389 -
                    vec![]
390 -
                } else {
391 -
                    vec![Segment::Lines(order)]
392 -
                },
393 -
            };
537 +
        let lines = |order: Vec<AtomKey>| Materialized {
538 +
            segments: if order.is_empty() {
539 +
                vec![]
540 +
            } else {
541 +
                vec![Segment::Lines(order)]
542 +
            },
543 +
        };
544 +
        use std::sync::atomic::Ordering::Relaxed;
545 +
        if let Some(o) = self.order.get() {
546 +
            let holds = o.confirmed.load(Relaxed)
547 +
                || o.unchecked.iter().all(|k| {
548 +
                    self.atoms
549 +
                        .get(k)
550 +
                        .is_none_or(|info| !self.is_ghost(k, info, deps))
551 +
                });
552 +
            if holds {
553 +
                o.confirmed.store(true, Relaxed);
554 +
                return lines(o.atoms.clone());
555 +
            }
556 +
            self.order.set(None);
557 +
        }
558 +
        // A chain (nothing ever deleted) is quick to walk, and a fresh tree
559 +
        // is all chains: no order is kept for one.
560 +
        if let Some(order) = self.as_chain() {
561 +
            return lines(order);
394 562
        }
563 +
        if let Some(order) = self.as_order(deps) {
564 +
            self.order
565 +
                .set(Some(std::sync::Arc::new(Order::known(order.clone()))));
566 +
            return lines(order);
567 +
        }
395 568
        self.materialize_general(deps)
569 +
    }
570 +
571 +
    /// Take the kept order, to [`Order::splice`] it once a change's edits
572 +
    /// have applied (they drop it).
573 +
    pub fn take_order(&mut self) -> Option<std::sync::Arc<Order>> {
574 +
        self.order.take()
575 +
    }
576 +
577 +
    /// Keep `order`, spliced by the edits just applied.
578 +
    pub fn keep_order(&mut self, order: Option<std::sync::Arc<Order>>) {
579 +
        *self.order.0.get_mut().unwrap_or_else(|e| e.into_inner()) = order;
396 580
    }
397 581
398 582
    /// The live atoms in order when, with the dead ones contracted away,
…
400 unchanged lines
799 983
        let mut g = FileGraph {
800 984
            atoms: HashMap::default(),
801 985
            succ: HashMap::default(),
986 +
            order: OrderSlot::default(),
802 987
        };
803 988
        let atoms = array(field(m, 0, "atoms")?, "atoms")?;
804 989
        // Every atom's line in one buffer.
…
185 unchanged lines
990 1175
        g.insert(h(3), 0, a, Vertex::End, &lines(&["c\n"]));
991 1176
        assert!(g.as_order(&NoDeps).is_none());
992 1177
        assert!(g.materialize(&NoDeps).is_conflicted());
1178 +
    }
1179 +
1180 +
    /// Applied as the state applies a change's edits: the kept order is
1181 +
    /// spliced by them.
1182 +
    fn apply_spliced(
1183 +
        g: &mut FileGraph,
1184 +
        h: ChangeHash,
1185 +
        inserts: &[(Vertex, Vertex, usize)],
1186 +
        deleted: &[AtomKey],
1187 +
    ) {
1188 +
        let order = g.take_order();
1189 +
        let mut runs = Vec::new();
1190 +
        let mut next = 0u32;
1191 +
        for (i, &(a, b, n)) in inserts.iter().enumerate() {
1192 +
            let text: Vec<String> = (0..n).map(|j| format!("{i}.{j}\n")).collect();
1193 +
            let text: Vec<&str> = text.iter().map(String::as_str).collect();
1194 +
            g.insert(h, next, a, b, &lines(&text));
1195 +
            runs.push((a, b, next, n as u32));
1196 +
            next += n as u32;
1197 +
        }
1198 +
        for k in deleted {
1199 +
            g.delete(
1200 +
                h,
1201 +
                &AtomRange {
1202 +
                    change: k.0,
1203 +
                    from: k.1,
1204 +
                    to: k.1 + 1,
1205 +
                },
1206 +
            );
1207 +
        }
1208 +
        if let Some(o) = order {
1209 +
            g.keep_order(o.splice(h, &runs, deleted));
1210 +
        }
1211 +
    }
1212 +
1213 +
    /// Edits as a diff makes them from the order `l`: hunks with a kept
1214 +
    /// line between them, each deleting some lines and inserting some.
1215 +
    fn random_edits(
1216 +
        l: &[AtomKey],
1217 +
        rnd: &mut impl FnMut(usize) -> usize,
1218 +
    ) -> (Vec<(Vertex, Vertex, usize)>, Vec<AtomKey>) {
1219 +
        let (mut inserts, mut deleted) = (Vec::new(), Vec::new());
1220 +
        let mut i = rnd(3);
1221 +
        while i <= l.len() {
1222 +
            let del = rnd(3).min(l.len() - i);
1223 +
            let add = rnd(3);
1224 +
            let after = if i == 0 {
1225 +
                Vertex::Start
1226 +
            } else {
1227 +
                Vertex::Atom(l[i - 1])
1228 +
            };
1229 +
            let before = l.get(i).map(|k| Vertex::Atom(*k)).unwrap_or(Vertex::End);
1230 +
            deleted.extend(&l[i..i + del]);
1231 +
            if add > 0 {
1232 +
                inserts.push((after, before, add));
1233 +
            }
1234 +
            i += del + 1 + rnd(6);
1235 +
        }
1236 +
        (inserts, deleted)
1237 +
    }
1238 +
1239 +
    /// The kept order, spliced change by change, is what the general
1240 +
    /// algorithm finds, through sequential edits and through concurrent
1241 +
    /// ones that leave conflicts and ghosts (where it must stand aside).
1242 +
    #[test]
1243 +
    fn the_kept_order_follows_edits() {
1244 +
        let hn = |i: usize| {
1245 +
            let mut b = [0u8; 32];
1246 +
            b[..2].copy_from_slice(&(i as u16).to_be_bytes());
1247 +
            ObjectRef(b)
1248 +
        };
1249 +
        let mut seed = 7u64;
1250 +
        let mut rnd = move |n: usize| {
1251 +
            seed = seed
1252 +
                .wrapping_mul(6364136223846793005)
1253 +
                .wrapping_add(1442695040888963407);
1254 +
            ((seed >> 33) as usize) % n.max(1)
1255 +
        };
1256 +
        let mut deps = Deps(HashMap::default());
1257 +
        let mut g = FileGraph::new();
1258 +
        let first: Vec<String> = (0..40).map(|i| format!("{i}\n")).collect();
1259 +
        let first: Vec<&str> = first.iter().map(String::as_str).collect();
1260 +
        g.insert(hn(1), 0, Vertex::Start, Vertex::End, &lines(&first));
1261 +
        let mut known: Vec<ChangeHash> = vec![hn(1)];
1262 +
        let mut c = 2;
1263 +
        let (mut spliced, mut conflicted) = (0, 0);
1264 +
        for step in 0..400 {
1265 +
            let l = g.materialize(&deps).live_atoms();
1266 +
            // Now and then two changes from the same base, neither knowing
1267 +
            // the other.
1268 +
            let pair = step % 7 == 3;
1269 +
            let base = known.clone();
1270 +
            for _ in 0..if pair { 2 } else { 1 } {
1271 +
                let h = hn(c);
1272 +
                c += 1;
1273 +
                let (inserts, deleted) = random_edits(&l, &mut rnd);
1274 +
                deps.0.insert(h, base.clone());
1275 +
                apply_spliced(&mut g, h, &inserts, &deleted);
1276 +
                known.push(h);
1277 +
                if g.order.get().is_some() {
1278 +
                    spliced += 1;
1279 +
                }
1280 +
                let m = g.materialize(&deps);
1281 +
                assert_eq!(m, g.materialize_general(&deps), "step {step}");
1282 +
                if m.is_conflicted() {
1283 +
                    conflicted += 1;
1284 +
                }
1285 +
            }
1286 +
            // A change that knows everything settles any conflict by
1287 +
            // rewriting the file.
1288 +
            if g.materialize(&deps).is_conflicted() {
1289 +
                let h = hn(c);
1290 +
                c += 1;
1291 +
                let all = g.materialize(&deps).live_atoms();
1292 +
                deps.0.insert(h, known.clone());
1293 +
                apply_spliced(&mut g, h, &[(Vertex::Start, Vertex::End, 5)], &all);
1294 +
                known.push(h);
1295 +
                assert_eq!(g.materialize(&deps), g.materialize_general(&deps));
1296 +
            }
1297 +
        }
1298 +
        assert!(
1299 +
            spliced > 100 && conflicted > 0,
1300 +
            "{spliced} spliced, {conflicted} conflicted"
1301 +
        );
993 1302
    }
994 1303
995 1304
    /// How long rendering a long-lived file takes (run by hand, in release).
…
495 unchanged lines
modified crates/oxy-core/src/state.rs +15 −9
…
197 unchanged lines
198 198
/// Apply one change's edits of one file (inserts and deletes; replaces
199 199
/// live beside the graphs) to that file's graph.
200 200
fn apply_file_edits(g: &mut FileGraph, h: ChangeHash, edits: Vec<Edit>) {
201 +
    // The graph's kept order, if any, follows the edits (see `Order`).
202 +
    let order = g.take_order();
203 +
    let mut inserts: Vec<(Vertex, Vertex, u32, u32)> = Vec::new();
204 +
    let mut deleted: Vec<crate::graph::AtomKey> = Vec::new();
201 205
    let mut next = 0u32;
202 206
    for e in edits {
203 207
        match e {
…
4 unchanged lines
208 212
                ..
209 213
            } => {
210 214
                let first = next;
211 -
                next += lines.len() as u32;
212 -
                g.insert_owned(
213 -
                    h,
214 -
                    first,
215 -
                    vertex(after.resolve(h)),
216 -
                    vertex(before.resolve(h)),
217 -
                    lines,
218 -
                );
215 +
                let count = lines.len() as u32;
216 +
                next += count;
217 +
                let (after, before) = (vertex(after.resolve(h)), vertex(before.resolve(h)));
218 +
                inserts.push((after, before, first, count));
219 +
                g.insert_owned(h, first, after, before, lines);
219 220
            }
220 221
            Edit::Delete { atoms, .. } => {
221 222
                for r in atoms {
222 -
                    g.delete(h, &r.resolve(h));
223 +
                    let r = r.resolve(h);
224 +
                    deleted.extend((r.from..r.to).map(|i| (r.change, i)));
225 +
                    g.delete(h, &r);
223 226
                }
224 227
            }
225 228
            Edit::Replace { .. } => {}
226 229
        }
230 +
    }
231 +
    if let Some(o) = order {
232 +
        g.keep_order(o.splice(h, &inserts, &deleted));
227 233
    }
228 234
}
229 235
…
1775 unchanged lines