A command parses the manifest once d0990c2bff74
The parsed manifest is kept for the rest of the command while its file is the same one (size, time and inode), and dropped whenever it is written or removed: a status that found the working copy dirty no longer parses it a second time for the snapshot (0.1 s on the Linux tree). The walks take a path under the root as it is where the separator is already a slash, rather than splitting and joining it. Snapshot times say how long building graphs and rendering take apart. refs #41
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-30 10:41 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
d0990c2bff74c79d895c2a4d5f978455d138c133a85610bceb3303fb39b869b7- Depends on
- 135212de5370 4dee277c327a 75167672ebcd 86e1824f2d0b 8b276e0106d1 982864068a59 b7f102fcbb17 cc7a00565161 eaf3e9068180
- Depended on by
- 236f35c0baba 0389c3e82f6c 33472ccc08cc d616faf5d136 1966f5db15ee 76740263089c 992467d31d26
- 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
- 18 atoms, 0 tree operations, 3 files
Browse the tree at this change
modified crates/oxy-core/src/repo/mod.rs +17 −7
| … | 167 unchanged lines |
||
| 168 | 168 | pub struct Repo { |
|
| 169 | 169 | /// Oxygen-pinned subrepos skipped by the running Git export. |
|
| 170 | 170 | pub(crate) git_export_skipped: std::cell::Cell<usize>, |
|
| 171 | + | /// The manifest as last read, with the file identity it was read from, |
|
| 172 | + | /// so one command parses it once (see `read_manifest`). |
|
| 173 | + | pub(crate) manifest_cache: std::cell::RefCell<Option<prompt::CachedManifest>>, |
|
| 171 | 174 | root: PathBuf, |
|
| 172 | 175 | oxy: PathBuf, |
|
| 173 | 176 | store: Store, |
|
| … | 71 unchanged lines |
||
| 245 | 248 | let store = Store::open(oxy.join("objects"))?; |
|
| 246 | 249 | let repo = Repo { |
|
| 247 | 250 | git_export_skipped: Default::default(), |
|
| 251 | + | manifest_cache: Default::default(), |
|
| 248 | 252 | root, |
|
| 249 | 253 | oxy, |
|
| 250 | 254 | store, |
|
| … | 43 unchanged lines |
||
| 294 | 298 | let store = Store::open(oxy.join("objects"))?; |
|
| 295 | 299 | let repo = Repo { |
|
| 296 | 300 | git_export_skipped: Default::default(), |
|
| 301 | + | manifest_cache: Default::default(), |
|
| 297 | 302 | root, |
|
| 298 | 303 | oxy, |
|
| 299 | 304 | store, |
|
| … | 65 unchanged lines |
||
| 365 | 370 | let store = Store::open(shared.join("objects"))?; |
|
| 366 | 371 | let mut repo = Repo { |
|
| 367 | 372 | git_export_skipped: Default::default(), |
|
| 373 | + | manifest_cache: Default::default(), |
|
| 368 | 374 | root: d.to_path_buf(), |
|
| 369 | 375 | oxy: shared, |
|
| 370 | 376 | store, |
|
| … | 20 unchanged lines |
||
| 391 | 397 | let store = Store::open(oxy.join("objects"))?; |
|
| 392 | 398 | let mut repo = Repo { |
|
| 393 | 399 | git_export_skipped: Default::default(), |
|
| 400 | + | manifest_cache: Default::default(), |
|
| 394 | 401 | root: d.to_path_buf(), |
|
| 395 | 402 | oxy, |
|
| 396 | 403 | store, |
|
| … | 773 unchanged lines |
||
| 1170 | 1177 | if path == root { |
|
| 1171 | 1178 | return Ok(None); |
|
| 1172 | 1179 | } |
|
| 1173 | - | let rel: String = path |
|
| 1174 | - | .strip_prefix(root) |
|
| 1175 | - | .unwrap() |
|
| 1176 | - | .components() |
|
| 1177 | - | .map(|c| c.as_os_str().to_string_lossy().to_string()) |
|
| 1178 | - | .collect::<Vec<_>>() |
|
| 1179 | - | .join("/"); |
|
| 1180 | + | let under = path.strip_prefix(root).unwrap(); |
|
| 1181 | + | // Already `/`-separated where that is the separator. |
|
| 1182 | + | let rel: String = match under.to_str() { |
|
| 1183 | + | Some(s) if std::path::MAIN_SEPARATOR == '/' => s.to_string(), |
|
| 1184 | + | _ => under |
|
| 1185 | + | .components() |
|
| 1186 | + | .map(|c| c.as_os_str().to_string_lossy().to_string()) |
|
| 1187 | + | .collect::<Vec<_>>() |
|
| 1188 | + | .join("/"), |
|
| 1189 | + | }; |
|
| 1180 | 1190 | let Some(ft) = entry.file_type() else { |
|
| 1181 | 1191 | return Ok(None); |
|
| 1182 | 1192 | }; |
|
| … | 515 unchanged lines |
modified crates/oxy-core/src/repo/prompt.rs +46 −10
| … | 147 unchanged lines |
||
| 148 | 148 | pub entries: BTreeMap<String, ManifestEntry>, |
|
| 149 | 149 | } |
|
| 150 | 150 | ||
| 151 | + | /// A parsed manifest and the identity (size, time, inode) of the file it |
|
| 152 | + | /// came from: reused while the file is the same one. |
|
| 153 | + | pub(crate) type CachedManifest = ((u64, u128, u64), (bool, bool, std::sync::Arc<Manifest>)); |
|
| 154 | + | ||
| 151 | 155 | pub(crate) fn mtime_ns(m: &fs::Metadata) -> u128 { |
|
| 152 | 156 | m.modified() |
|
| 153 | 157 | .ok() |
|
| … | 52 unchanged lines |
||
| 206 | 210 | entries: &BTreeMap<String, FsEntry>, |
|
| 207 | 211 | blobs: &HashMap<String, ObjectRef>, |
|
| 208 | 212 | ) -> Result<()> { |
|
| 213 | + | self.manifest_cache.take(); |
|
| 209 | 214 | let written = std::time::SystemTime::now() |
|
| 210 | 215 | .duration_since(UNIX_EPOCH) |
|
| 211 | 216 | .map(|d| d.as_nanos()) |
|
| … | 61 unchanged lines |
||
| 273 | 278 | uncommitted: bool, |
|
| 274 | 279 | keys: &HashMap<String, String>, |
|
| 275 | 280 | ) -> Result<()> { |
|
| 281 | + | self.manifest_cache.take(); |
|
| 276 | 282 | let p = self.manifest_path(); |
|
| 277 | 283 | let text = match fs::read_to_string(&p) { |
|
| 278 | 284 | Ok(t) => t, |
|
| … | 26 unchanged lines |
||
| 305 | 311 | /// change without a snapshot (undo, rewrites); the prompt shows blank |
|
| 306 | 312 | /// markers until the next snapshot. |
|
| 307 | 313 | pub(crate) fn drop_manifest(&self) { |
|
| 314 | + | self.manifest_cache.take(); |
|
| 308 | 315 | let _ = fs::remove_file(self.manifest_path()); |
|
| 316 | + | } |
|
| 317 | + | ||
| 318 | + | /// The manifest, with whether it records a conflict and uncommitted |
|
| 319 | + | /// work; parsed once while the file stays the same one. |
|
| 320 | + | pub(crate) fn read_manifest(&self) -> Result<Option<(bool, bool, std::sync::Arc<Manifest>)>> { |
|
| 321 | + | let path = self.manifest_path(); |
|
| 322 | + | let id = match fs::metadata(&path) { |
|
| 323 | + | Ok(m) => { |
|
| 324 | + | #[cfg(unix)] |
|
| 325 | + | let ino = std::os::unix::fs::MetadataExt::ino(&m); |
|
| 326 | + | #[cfg(not(unix))] |
|
| 327 | + | let ino = 0; |
|
| 328 | + | (m.len(), mtime_ns(&m), ino) |
|
| 329 | + | } |
|
| 330 | + | Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), |
|
| 331 | + | Err(e) => return Err(e.into()), |
|
| 332 | + | }; |
|
| 333 | + | if let Some((cached_id, m)) = &*self.manifest_cache.borrow() { |
|
| 334 | + | if *cached_id == id { |
|
| 335 | + | return Ok(Some(m.clone())); |
|
| 336 | + | } |
|
| 337 | + | } |
|
| 338 | + | let Some((c, u, m)) = self.parse_manifest(&path)? else { |
|
| 339 | + | return Ok(None); |
|
| 340 | + | }; |
|
| 341 | + | let m = (c, u, std::sync::Arc::new(m)); |
|
| 342 | + | *self.manifest_cache.borrow_mut() = Some((id, m.clone())); |
|
| 343 | + | Ok(Some(m)) |
|
| 309 | 344 | } |
|
| 310 | 345 | ||
| 311 | - | pub(crate) fn read_manifest(&self) -> Result<Option<(bool, bool, Manifest)>> { |
|
| 312 | - | let text = match fs::read_to_string(self.manifest_path()) { |
|
| 346 | + | fn parse_manifest(&self, path: &std::path::Path) -> Result<Option<(bool, bool, Manifest)>> { |
|
| 347 | + | let text = match fs::read_to_string(path) { |
|
| 313 | 348 | Ok(t) => t, |
|
| 314 | 349 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), |
|
| 315 | 350 | Err(e) => return Err(e.into()), |
|
| … | 97 unchanged lines |
||
| 413 | 448 | if ft.is_dir() { |
|
| 414 | 449 | return WalkState::Continue; |
|
| 415 | 450 | } |
|
| 416 | - | let rel: String = entry |
|
| 417 | - | .path() |
|
| 418 | - | .strip_prefix(root) |
|
| 419 | - | .unwrap() |
|
| 420 | - | .components() |
|
| 421 | - | .map(|c| c.as_os_str().to_string_lossy().to_string()) |
|
| 422 | - | .collect::<Vec<_>>() |
|
| 423 | - | .join("/"); |
|
| 451 | + | let under = entry.path().strip_prefix(root).unwrap(); |
|
| 452 | + | let rel: String = match under.to_str() { |
|
| 453 | + | Some(s) if std::path::MAIN_SEPARATOR == '/' => s.to_string(), |
|
| 454 | + | _ => under |
|
| 455 | + | .components() |
|
| 456 | + | .map(|c| c.as_os_str().to_string_lossy().to_string()) |
|
| 457 | + | .collect::<Vec<_>>() |
|
| 458 | + | .join("/"), |
|
| 459 | + | }; |
|
| 424 | 460 | if !in_scope(&rel) { |
|
| 425 | 461 | return WalkState::Continue; |
|
| 426 | 462 | } |
|
| … | 137 unchanged lines |
modified crates/oxy-core/src/repo/snapshot.rs +6 −2
| … | 321 unchanged lines |
||
| 322 | 322 | }; |
|
| 323 | 323 | let mut fs_entries = { |
|
| 324 | 324 | let _t = crate::timing::phase("snapshot: read working copy"); |
|
| 325 | - | self.walk_working_copy_with(manifest.as_ref(), &tracked, Some(&keys))? |
|
| 325 | + | self.walk_working_copy_with(manifest.as_deref(), &tracked, Some(&keys))? |
|
| 326 | 326 | }; |
|
| 327 | 327 | let base_snapshot = { |
|
| 328 | 328 | let _t = crate::timing::phase("snapshot: load and render base"); |
|
| … | 9 unchanged lines |
||
| 338 | 338 | .filter(|e| e.kind == NodeKind::File && !skip.contains(&e.node)) |
|
| 339 | 339 | .map(|e| e.node) |
|
| 340 | 340 | .collect(); |
|
| 341 | - | self.load_graphs_of(&mut base, &need)?; |
|
| 341 | + | { |
|
| 342 | + | let _t = crate::timing::phase("snapshot: of which building graphs"); |
|
| 343 | + | self.load_graphs_of(&mut base, &need)?; |
|
| 344 | + | } |
|
| 345 | + | let _t = crate::timing::phase("snapshot: of which rendering"); |
|
| 342 | 346 | base.materialize_except(&skip) |
|
| 343 | 347 | } else { |
|
| 344 | 348 | self.load_all_graphs(&mut base)?; |
|
| … | 2003 unchanged lines |