OXYGENHUB

Verify a Git import from each parent's state 57d8dc05c9d4

oxy git verify loaded each commit's whole history again, which grows as
the square of the history. It now keeps the state of a commit whose
first-parent children are still to come, as the import does, and a
child adds only the changes its parent lacks. The states stay history
only: the graphs of files to compare are built beside them
(State::build_graphs_detached, Repo::build_graphs_detached; load_graphs
shares its construction) and dropped after rendering, so changes are
added without their text and every file keeps the graph key the cache
of verified files relies on. The first 5 000 vscode commits verify in
166.5 s (from 978 s), all 13.4 million files equal.

refs #35
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 02:01 UTC
Signature
✔ signed by Andy McCall
Hash
57d8dc05c9d42681cd78f1212d8b1c8b23da0f8e699e64dc05ae6f04f0b619b6
Depends on
1d360c91a289 519c7c061bcb 62794ecc7f74 79ab8528ff62 86e1824f2d0b 982864068a59 a4fad5b085c7 a87c7b9d563a b493d993a9df cc7a00565161 dfb23a3a8cfa e8d38e47e9d7 ff0af1ee299d
Depended on by
5552e69c7325 ee6aa3e81bc8 33be24095b4b 360aa74a6277 091fef80755d acadb5ed28a1 638fb21420d7 3f1b8fe25ec9 f6451fbbae3f 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
16 atoms, 0 tree operations, 4 files

Browse the tree at this change

modified crates/oxy-core/src/repo/git.rs +56 −4
…
1123 unchanged lines
1124 1124
        // Graphs (by key) and blobs already found equal to a Git blob.
1125 1125
        let mut verified_graphs: HashMap<[u8; 32], String> = HashMap::new();
1126 1126
        let mut verified_blobs: HashMap<ObjectRef, String> = HashMap::new();
1127 +
        // The state of a commit whose first-parent children are still to
1128 +
        // come, so a child adds its own changes instead of loading all its
1129 +
        // history again (as the import does).
1130 +
        let mut waiting: HashMap<&str, usize> = HashMap::new();
1127 1131
        for c in &commits {
1132 +
            if let Some(p1) = c.parents.first() {
1133 +
                *waiting.entry(p1.as_str()).or_default() += 1;
1134 +
            }
1135 +
        }
1136 +
        let mut kept: HashMap<String, State> = HashMap::new();
1137 +
        let mut kept_order: std::collections::VecDeque<String> = std::collections::VecDeque::new();
1138 +
        for c in &commits {
1128 1139
            rep.commits += 1;
1129 1140
            let mut set: BTreeSet<ChangeHash> = BTreeSet::new();
1130 1141
            for p in &c.parents {
…
21 unchanged lines
1152 1163
            // History only: a file whose graph (by key) or blob was already
1153 1164
            // found equal to this Git blob is not built, rendered or read
1154 1165
            // again; the rest are.
1155 -
            let mut state = self.load_state_history(&heads)?;
1166 +
            let from_parent = c.parents.first().and_then(|p1| {
1167 +
                let left = waiting.get_mut(p1.as_str()).map(|n| {
1168 +
                    *n -= 1;
1169 +
                    *n
1170 +
                });
1171 +
                if left == Some(0) {
1172 +
                    kept_order.retain(|k| k != p1);
1173 +
                    kept.remove(p1.as_str())
1174 +
                } else {
1175 +
                    kept.get(p1.as_str()).cloned()
1176 +
                }
1177 +
            });
1178 +
            let state = match from_parent {
1179 +
                Some(mut st) => {
1180 +
                    // The changes the parent's state lacks. The state never
1181 +
                    // builds a graph (they are built beside it), so they
1182 +
                    // are read without their text.
1183 +
                    for h in set.iter() {
1184 +
                        if !st.contains(h) {
1185 +
                            let (change, texts) = self.store.get_change_lazy(h)?;
1186 +
                            st.add_known(*h, change, texts);
1187 +
                        }
1188 +
                    }
1189 +
                    st
1190 +
                }
1191 +
                None => self.load_state_history(&heads)?,
1192 +
            };
1156 1193
            let view = state.tree().view();
1157 1194
            let tree = list_tree(git_dir, &c.sha)?;
1158 1195
            let short = &c.sha[..12.min(c.sha.len())];
…
24 unchanged lines
1183 1220
                .filter_map(|n| Some((*n, state.graph_key(n)?)))
1184 1221
                .collect();
1185 1222
            let need_set: HashSet<NodeId> = need.iter().copied().collect();
1186 -
            self.load_graphs_of(&mut state, &need)?;
1223 +
            let graphs = self.build_graphs_detached(&state, &need)?;
1187 1224
            for (path, (mode, sha)) in &tree {
1188 1225
                let Some(e) = view.entries.get(path) else {
1189 1226
                    differ(format!("{short}: {path} is missing"));
…
34 unchanged lines
1224 1261
                                // Known equal: skipped when the graphs were chosen.
1225 1262
                                continue;
1226 1263
                            }
1227 -
                            let (have, m) = state.render_file(&e.node);
1264 +
                            let (have, m) = match graphs.get(&e.node) {
1265 +
                                Some(g) => state.render_graph(g),
1266 +
                                None => (Vec::new(), Default::default()),
1267 +
                            };
1228 1268
                            if m.is_conflicted() {
1229 -
                                let who: Vec<String> = state
1269 +
                                // Who conflicts: a copy of the state with this graph built.
1270 +
                                let mut detail = state.clone();
1271 +
                                self.load_graphs_of(&mut detail, &[e.node])?;
1272 +
                                let who: Vec<String> = detail
1230 1273
                                    .conflict_regions(&e.node)
1231 1274
                                    .contributors
1232 1275
                                    .iter()
…
58 unchanged lines
1291 1334
                    && !(has_submodules && path == ".oxymodules")
1292 1335
                {
1293 1336
                    differ(format!("{short}: {path} is extra"));
1337 +
                }
1338 +
            }
1339 +
            if waiting.get(c.sha.as_str()).is_some_and(|n| *n > 0) {
1340 +
                kept.insert(c.sha.clone(), state);
1341 +
                kept_order.push_back(c.sha.clone());
1342 +
                if kept_order.len() > 8 {
1343 +
                    if let Some(old) = kept_order.pop_front() {
1344 +
                        kept.remove(&old);
1345 +
                    }
1294 1346
                }
1295 1347
            }
1296 1348
            if rep.mismatches.len() >= 50 {
…
31 unchanged lines
modified crates/oxy-core/src/repo/mod.rs +21 −0
…
841 unchanged lines
842 842
        )?)
843 843
    }
844 844
845 +
    /// The graphs of `nodes` of a history-only state, built beside it (see
846 +
    /// [`State::build_graphs_detached`]).
847 +
    pub fn build_graphs_detached(
848 +
        &self,
849 +
        state: &State,
850 +
        nodes: &[crate::ids::NodeId],
851 +
    ) -> Result<HashMap<crate::ids::NodeId, crate::graph::FileGraph>> {
852 +
        Ok(state
853 +
            .build_graphs_detached(
854 +
                nodes,
855 +
                |refs| {
856 +
                    self.read_blobs(refs).map_err(|e| {
857 +
                        crate::objects::ObjectError::Schema(format!("graph and text blobs: {e}"))
858 +
                    })
859 +
                },
860 +
                |h| self.change_bytes(h),
861 +
            )?
862 +
            .into_iter()
863 +
            .collect())
864 +
    }
865 +
845 866
    /// Build every graph a history-only state still lacks, making it full.
846 867
    pub fn load_all_graphs(&self, state: &mut State) -> Result<()> {
847 868
        Ok(state.load_all_graphs(
…
882 unchanged lines
modified crates/oxy-core/src/state.rs +141 −101
…
226 unchanged lines
227 227
    }
228 228
}
229 229
230 +
/// Build the graphs of deferred entries: their checkpoint bases and the
231 +
/// texts of inserts read without their lines are loaded in one call, then
232 +
/// the graphs are built in parallel (they are independent).
233 +
fn build_deferred(
234 +
    mut taken: Vec<(NodeId, Deferred)>,
235 +
    load: impl FnOnce(&[ObjectRef]) -> Result<Vec<Vec<u8>>, ObjectError>,
236 +
    read_change: impl Fn(&ChangeHash) -> Result<Arc<Vec<u8>>, ObjectError>,
237 +
) -> Result<Vec<(NodeId, FileGraph)>, ObjectError> {
238 +
    let mut refs: Vec<ObjectRef> = taken
239 +
        .iter()
240 +
        .filter_map(|(_, d)| match &d.base {
241 +
            Some(DeferredBase::Blob(r)) => Some(*r),
242 +
            _ => None,
243 +
        })
244 +
        .collect();
245 +
    // Text blobs of inserts read without their lines, after the bases.
246 +
    let bases = refs.len();
247 +
    refs.extend(taken.iter().flat_map(|(_, d)| {
248 +
        d.texts.iter().filter_map(|t| match t {
249 +
            TextRef::Blob(r, _) => Some(*r),
250 +
            TextRef::Inline { .. } => None,
251 +
        })
252 +
    }));
253 +
    // And the encodings of changes whose inline inserts are needed,
254 +
    // each read once.
255 +
    let mut changes: HashMap<ChangeHash, Arc<Vec<u8>>> = HashMap::new();
256 +
    for t in taken.iter().flat_map(|(_, d)| d.texts.iter()) {
257 +
        if let TextRef::Inline { change, .. } = t {
258 +
            if !changes.contains_key(change) {
259 +
                changes.insert(*change, read_change(change)?);
260 +
            }
261 +
        }
262 +
    }
263 +
    let mut blobs = if refs.is_empty() {
264 +
        Vec::new()
265 +
    } else {
266 +
        load(&refs)?
267 +
    };
268 +
    if blobs.len() != refs.len() {
269 +
        return Err(ObjectError::Schema("graph blobs: wrong count".into()));
270 +
    }
271 +
    let mut texts = blobs.split_off(bases).into_iter();
272 +
    let mut blobs = blobs.into_iter();
273 +
    // Pair each graph with its blob, then build them all in parallel:
274 +
    // graphs are independent.
275 +
    let mut work: Vec<(NodeId, Deferred, Option<Vec<u8>>)> = Vec::with_capacity(taken.len());
276 +
    for (node, mut d) in taken.drain(..) {
277 +
        // Fill in the lines of inserts read without them, in order.
278 +
        let mut want = std::mem::take(&mut d.texts).into_iter();
279 +
        for (_, edits) in d.edits.iter_mut() {
280 +
            for e in edits.iter_mut() {
281 +
                if let Edit::Insert { lines, .. } = e {
282 +
                    if lines.is_empty() {
283 +
                        *lines = match want.next() {
284 +
                            Some(TextRef::Blob(_, count)) => {
285 +
                                let content = texts.next().ok_or_else(|| {
286 +
                                    ObjectError::Schema("text blobs: wrong count".into())
287 +
                                })?;
288 +
                                crate::objects::text_lines(&content, count)?
289 +
                            }
290 +
                            Some(TextRef::Inline { change, start, end }) => {
291 +
                                let bytes = &changes[&change];
292 +
                                let range =
293 +
                                    bytes.get(start as usize..end as usize).ok_or_else(|| {
294 +
                                        ObjectError::Schema(
295 +
                                            "an inline insert lies outside its change".into(),
296 +
                                        )
297 +
                                    })?;
298 +
                                crate::objects::inline_lines(range)?
299 +
                            }
300 +
                            None => {
301 +
                                return Err(ObjectError::Schema(
302 +
                                    "an insert's text is missing".into(),
303 +
                                ))
304 +
                            }
305 +
                        };
306 +
                    }
307 +
                }
308 +
            }
309 +
        }
310 +
        let bytes = match &d.base {
311 +
            Some(DeferredBase::Blob(_)) => Some(
312 +
                blobs
313 +
                    .next()
314 +
                    .ok_or_else(|| ObjectError::Schema("graph blobs: wrong count".into()))?,
315 +
            ),
316 +
            _ => None,
317 +
        };
318 +
        work.push((node, d, bytes));
319 +
    }
320 +
    use rayon::prelude::*;
321 +
    let built: Vec<(NodeId, FileGraph)> = work
322 +
        .into_par_iter()
323 +
        .map(|(node, mut d, bytes)| {
324 +
            let mut graph = match (d.base.take(), bytes) {
325 +
                (Some(DeferredBase::Inline(v)), _) => FileGraph::from_value(&v)?,
326 +
                (Some(DeferredBase::Blob(_)), Some(bytes)) => {
327 +
                    FileGraph::from_value(&cbor::decode(&bytes)?)?
328 +
                }
329 +
                _ => FileGraph::default(),
330 +
            };
331 +
            for (h, edits) in std::mem::take(&mut d.edits) {
332 +
                apply_file_edits(&mut graph, h, edits);
333 +
            }
334 +
            Ok((node, graph))
335 +
        })
336 +
        .collect::<Result<_, ObjectError>>()?;
337 +
    Ok(built)
338 +
}
339 +
230 340
impl State {
231 341
    pub fn new() -> Self {
232 342
        Self::default()
…
54 unchanged lines
287 397
        let Some(deferred) = self.deferred.as_mut() else {
288 398
            return Ok(());
289 399
        };
290 -
        let mut taken: Vec<(NodeId, Deferred)> = nodes
400 +
        let taken: Vec<(NodeId, Deferred)> = nodes
291 401
            .iter()
292 402
            .filter_map(|n| deferred.remove(n).map(|d| (*n, d)))
293 403
            .collect();
294 -
        let mut refs: Vec<ObjectRef> = taken
295 -
            .iter()
296 -
            .filter_map(|(_, d)| match &d.base {
297 -
                Some(DeferredBase::Blob(r)) => Some(*r),
298 -
                _ => None,
299 -
            })
300 -
            .collect();
301 -
        // Text blobs of inserts read without their lines, after the bases.
302 -
        let bases = refs.len();
303 -
        refs.extend(taken.iter().flat_map(|(_, d)| {
304 -
            d.texts.iter().filter_map(|t| match t {
305 -
                TextRef::Blob(r, _) => Some(*r),
306 -
                TextRef::Inline { .. } => None,
307 -
            })
308 -
        }));
309 -
        // And the encodings of changes whose inline inserts are needed,
310 -
        // each read once.
311 -
        let mut changes: HashMap<ChangeHash, Arc<Vec<u8>>> = HashMap::new();
312 -
        for t in taken.iter().flat_map(|(_, d)| d.texts.iter()) {
313 -
            if let TextRef::Inline { change, .. } = t {
314 -
                if !changes.contains_key(change) {
315 -
                    changes.insert(*change, read_change(change)?);
316 -
                }
317 -
            }
318 -
        }
319 -
        let mut blobs = if refs.is_empty() {
320 -
            Vec::new()
321 -
        } else {
322 -
            load(&refs)?
323 -
        };
324 -
        if blobs.len() != refs.len() {
325 -
            return Err(ObjectError::Schema("graph blobs: wrong count".into()));
326 -
        }
327 -
        let mut texts = blobs.split_off(bases).into_iter();
328 -
        let mut blobs = blobs.into_iter();
329 -
        // Pair each graph with its blob, then build them all in parallel:
330 -
        // graphs are independent.
331 -
        let mut work: Vec<(NodeId, Deferred, Option<Vec<u8>>)> = Vec::with_capacity(taken.len());
332 -
        for (node, mut d) in taken.drain(..) {
333 -
            // Fill in the lines of inserts read without them, in order.
334 -
            let mut want = std::mem::take(&mut d.texts).into_iter();
335 -
            for (_, edits) in d.edits.iter_mut() {
336 -
                for e in edits.iter_mut() {
337 -
                    if let Edit::Insert { lines, .. } = e {
338 -
                        if lines.is_empty() {
339 -
                            *lines = match want.next() {
340 -
                                Some(TextRef::Blob(_, count)) => {
341 -
                                    let content = texts.next().ok_or_else(|| {
342 -
                                        ObjectError::Schema("text blobs: wrong count".into())
343 -
                                    })?;
344 -
                                    crate::objects::text_lines(&content, count)?
345 -
                                }
346 -
                                Some(TextRef::Inline { change, start, end }) => {
347 -
                                    let bytes = &changes[&change];
348 -
                                    let range = bytes
349 -
                                        .get(start as usize..end as usize)
350 -
                                        .ok_or_else(|| {
351 -
                                            ObjectError::Schema(
352 -
                                                "an inline insert lies outside its change".into(),
353 -
                                            )
354 -
                                        })?;
355 -
                                    crate::objects::inline_lines(range)?
356 -
                                }
357 -
                                None => {
358 -
                                    return Err(ObjectError::Schema(
359 -
                                        "an insert's text is missing".into(),
360 -
                                    ))
361 -
                                }
362 -
                            };
363 -
                        }
364 -
                    }
365 -
                }
366 -
            }
367 -
            let bytes = match &d.base {
368 -
                Some(DeferredBase::Blob(_)) => Some(
369 -
                    blobs
370 -
                        .next()
371 -
                        .ok_or_else(|| ObjectError::Schema("graph blobs: wrong count".into()))?,
372 -
                ),
373 -
                _ => None,
374 -
            };
375 -
            work.push((node, d, bytes));
376 -
        }
377 -
        use rayon::prelude::*;
378 -
        let built: Vec<(NodeId, FileGraph)> = work
379 -
            .into_par_iter()
380 -
            .map(|(node, mut d, bytes)| {
381 -
                let mut graph = match (d.base.take(), bytes) {
382 -
                    (Some(DeferredBase::Inline(v)), _) => FileGraph::from_value(&v)?,
383 -
                    (Some(DeferredBase::Blob(_)), Some(bytes)) => {
384 -
                        FileGraph::from_value(&cbor::decode(&bytes)?)?
385 -
                    }
386 -
                    _ => FileGraph::default(),
387 -
                };
388 -
                for (h, edits) in std::mem::take(&mut d.edits) {
389 -
                    apply_file_edits(&mut graph, h, edits);
390 -
                }
391 -
                Ok((node, graph))
392 -
            })
393 -
            .collect::<Result<_, ObjectError>>()?;
404 +
        let built = build_deferred(taken, load, read_change)?;
394 405
        self.files
395 406
            .extend(built.into_iter().map(|(n, g)| (n, Arc::new(g))));
396 407
        Ok(())
408 +
    }
409 +
410 +
    /// The graphs of `nodes`, built from this history-only state's deferred
411 +
    /// edits without building them in it: the state stays history only, so
412 +
    /// changes read without their lines can still be added to it and every
413 +
    /// graph keeps its key. For a caller that renders a few files of many
414 +
    /// states in turn (verifying an import).
415 +
    pub fn build_graphs_detached(
416 +
        &self,
417 +
        nodes: &[NodeId],
418 +
        load: impl FnOnce(&[ObjectRef]) -> Result<Vec<Vec<u8>>, ObjectError>,
419 +
        read_change: impl Fn(&ChangeHash) -> Result<Arc<Vec<u8>>, ObjectError>,
420 +
    ) -> Result<Vec<(NodeId, FileGraph)>, ObjectError> {
421 +
        let Some(deferred) = self.deferred.as_ref() else {
422 +
            return Ok(Vec::new());
423 +
        };
424 +
        let copies: Vec<(NodeId, Deferred)> = nodes
425 +
            .iter()
426 +
            .map(|n| (*n, deferred.get(n).cloned().unwrap_or_default()))
427 +
            .collect();
428 +
        build_deferred(copies, load, read_change)
429 +
    }
430 +
431 +
    /// Render a graph built for this state (see
432 +
    /// [`State::build_graphs_detached`]), as [`State::render_file`] does.
433 +
    pub fn render_graph(&self, g: &FileGraph) -> (Vec<u8>, Materialized) {
434 +
        let m = g.materialize(self);
435 +
        let bytes = g.render(&m, &|h| self.label(h));
436 +
        (bytes, m)
397 437
    }
398 438
399 439
    /// Every file graph a history-only state has not built, deleted files'
…
1524 unchanged lines
modified docs/benchmarks.md +7 −2
…
467 unchanged lines
468 468
| Before (2026-09-29) | 412 s | 6.3 GB | |
469 469
| After | 309 s | 2.3 GB | every commit (13.4 million files) in 978 s |
470 470
471 -
Verify still loads each commit's history from scratch, which grows with
472 -
the length of the history.
471 +
Verify loaded each commit's history from scratch. It now keeps the
472 +
state of a commit whose children are still to come, as the import does,
473 +
and a child adds only the changes its parent lacks. Those states stay
474 +
history only: graphs are built beside them (`State::build_graphs_detached`)
475 +
and dropped after rendering, so changes are added without their text and
476 +
every file keeps its graph key, which the cache of verified files needs.
477 +
The same 5 000 commits verify in 166.5 s (from 978 s), 1.3 GB.
473 478