OXYGENHUB

Check the manifest with a parallel walk eaf3e9068180

Whether the working copy still matches the manifest is a check of each
file on its own, so the walk now runs on every core and stops at the
first difference; a file that cannot be read counts as changed. On the
Linux tip a clean status takes 0.44 s (from 0.72 s; Git 0.22 s) and a
one-file switch 3.3 s.

refs #35
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-29 22:44 UTC
Signature
✔ signed by Andy McCall
Hash
eaf3e9068180c0a3adc4a8bc3f9e3702f08afb80f8d5e83046e007d19b05dd99
Depends on
135212de5370 1d360c91a289 4dee277c327a 7421309f5372 75167672ebcd 9ac1fbe211b4 b7f102fcbb17 da868d5d2646
Depended on by
3604179679eb d4dbab97e948 76740263089c d0990c2bff74
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
6 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified crates/oxy-core/src/repo/prompt.rs +74 −44
…
383 unchanged lines
384 384
        if uncommitted {
385 385
            return Ok(Some(true));
386 386
        }
387 -
        let mut seen = 0usize;
387 +
        use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
388 388
        let nested = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
389 389
        let wb = self.walker(&nested);
390 -
        for entry in wb.build() {
391 -
            let Ok(entry) = entry else { continue };
392 -
            let Some(ft) = entry.file_type() else {
393 -
                continue;
394 -
            };
395 -
            if ft.is_dir() {
396 -
                continue;
397 -
            }
398 -
            let rel: String = entry
399 -
                .path()
400 -
                .strip_prefix(&self.root)
401 -
                .unwrap()
402 -
                .components()
403 -
                .map(|c| c.as_os_str().to_string_lossy().to_string())
404 -
                .collect::<Vec<_>>()
405 -
                .join("/");
406 -
            if !self.in_scope(&rel) {
407 -
                continue;
408 -
            }
409 -
            let Some(me) = manifest.entries.get(&rel) else {
410 -
                return Ok(Some(true));
411 -
            };
412 -
            seen += 1;
413 -
            let m = fs::symlink_metadata(entry.path())?;
414 -
            let now_mtime = mtime_ns(&m);
415 -
            if ft.is_symlink() {
416 -
                let target = fs::read_link(entry.path())?;
417 -
                if blake3::hash(target.to_string_lossy().as_bytes())
418 -
                    .to_hex()
419 -
                    .as_str()
420 -
                    != me.hash
421 -
                {
422 -
                    return Ok(Some(true));
390 +
        // Every file is checked on its own, so the walk runs in parallel and
391 +
        // stops at the first difference.
392 +
        let dirty = AtomicBool::new(false);
393 +
        let seen = AtomicUsize::new(0);
394 +
        let (root, sparse) = (&self.root, &self.config.sparse_paths);
395 +
        let in_scope = |p: &str| {
396 +
            sparse.is_empty()
397 +
                || sparse
398 +
                    .iter()
399 +
                    .any(|s| p == s || p.starts_with(&format!("{s}/")))
400 +
        };
401 +
        wb.build_parallel().run(|| {
402 +
            Box::new(|entry| {
403 +
                use ignore::WalkState;
404 +
                if dirty.load(Relaxed) {
405 +
                    return WalkState::Quit;
406 +
                }
407 +
                let Ok(entry) = entry else {
408 +
                    return WalkState::Continue;
409 +
                };
410 +
                let Some(ft) = entry.file_type() else {
411 +
                    return WalkState::Continue;
412 +
                };
413 +
                if ft.is_dir() {
414 +
                    return WalkState::Continue;
415 +
                }
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("/");
424 +
                if !in_scope(&rel) {
425 +
                    return WalkState::Continue;
426 +
                }
427 +
                // Whether this file differs from its entry; an unreadable
428 +
                // one counts as different.
429 +
                let differs = match manifest.entries.get(&rel) {
430 +
                    None => true,
431 +
                    Some(me) => {
432 +
                        seen.fetch_add(1, Relaxed);
433 +
                        match fs::symlink_metadata(entry.path()) {
434 +
                            Err(_) => true,
435 +
                            Ok(_) if ft.is_symlink() => {
436 +
                                !fs::read_link(entry.path()).is_ok_and(|t| {
437 +
                                    blake3::hash(t.to_string_lossy().as_bytes())
438 +
                                        .to_hex()
439 +
                                        .as_str()
440 +
                                        == me.hash
441 +
                                })
442 +
                            }
443 +
                            Ok(m) => {
444 +
                                m.len() != me.size
445 +
                                    || super::is_executable(&m) != me.exec
446 +
                                    || (mtime_ns(&m) != me.mtime_ns
447 +
                                        && !fs::read(entry.path()).is_ok_and(|b| {
448 +
                                            blake3::hash(&b).to_hex().as_str() == me.hash
449 +
                                        }))
450 +
                            }
451 +
                        }
452 +
                    }
453 +
                };
454 +
                if differs {
455 +
                    dirty.store(true, Relaxed);
456 +
                    return WalkState::Quit;
423 457
                }
424 -
                continue;
425 -
            }
426 -
            if m.len() != me.size || super::is_executable(&m) != me.exec {
427 -
                return Ok(Some(true));
428 -
            }
429 -
            if now_mtime != me.mtime_ns
430 -
                && blake3::hash(&fs::read(entry.path())?).to_hex().as_str() != me.hash
431 -
            {
432 -
                return Ok(Some(true));
433 -
            }
458 +
                WalkState::Continue
459 +
            })
460 +
        });
461 +
        if dirty.load(Relaxed) {
462 +
            return Ok(Some(true));
434 463
        }
464 +
        let seen = seen.load(Relaxed);
435 465
        if seen == manifest.entries.len() {
436 466
            return Ok(Some(false));
437 467
        }
…
96 unchanged lines
modified docs/benchmarks.md +1 −0
…
297 unchanged lines
298 298
| Before | killed at 14 GB | |
299 299
| Lazy text and keys at commit | 3.7 s | 768 MB |
300 300
| And answered from the manifest | 0.72 s | 120 MB |
301 +
| And the manifest checked by a parallel walk | 0.44 s | 123 MB |
301 302
| Git | 0.22 s | 31 MB |
302 303
303 304
A clean status is now answered from the manifest alone, loading no
…
9 unchanged lines