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 |