Skip to content

mv: add --exchange to atomically swap two paths - #13215

Merged
sylvestre merged 2 commits into
uutils:mainfrom
sylvestre:mv-exchange
Aug 2, 2026
Merged

mv: add --exchange to atomically swap two paths#13215
sylvestre merged 2 commits into
uutils:mainfrom
sylvestre:mv-exchange

Conversation

@sylvestre

Copy link
Copy Markdown
Contributor

Should make test tests/mv/mv-exchange.sh pass

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown

GNU testsuite comparison:

Skip an intermittent issue tests/misc/tty-eof (fails in this run but passes in the 'main' branch)
Skip an intermittent issue tests/tail/inotify-dir-recreate (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/cut/bounded-memory (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/mv/mv-exchange is no longer failing!
Note: The gnu test tests/basenc/bounded-memory is now being skipped but was previously passing.

@codspeed-hq

codspeed-hq Bot commented Jun 29, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 8.14%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
✅ 343 untouched benchmarks
⏩ 46 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation mv_multiple_to_dir 28 ms 24.7 ms +13.41%
Simulation numfmt_to_si_precision[10000] 93.3 ms 90.5 ms +3.12%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing sylvestre:mv-exchange (486621f) with main (0950712)

Open in CodSpeed

Footnotes

  1. 46 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Comment thread tests/by-util/test_mv.rs
Comment thread tests/by-util/test_mv.rs
Comment thread tests/by-util/test_mv.rs
Comment thread src/uu/mv/src/mv.rs Outdated
Comment thread src/uu/mv/src/mv.rs Outdated
Comment thread src/uu/mv/src/mv.rs Outdated
@sylvestre
sylvestre force-pushed the mv-exchange branch 4 times, most recently from 7c63a6d to a3dc47b Compare July 25, 2026 18:13
@sylvestre

Copy link
Copy Markdown
Contributor Author

@codspeedbot fix this regression

@codspeed-hq

codspeed-hq Bot commented Jul 29, 2026

Copy link
Copy Markdown

@codspeedbot fix this regression

I profiled the two regressed benchmarks and pushed a fix that recovers most of the loss.

Root cause: it is CLI parsing, not --exchange.
mv_single_file and mv_force_overwrite call uumain 1000 times per iteration on tiny files, so ~80% of what they measure is clap building and parsing the Command (uu_app 23%, parse 38%, teardown of ArgMatches/Command 19%). Registering one additional argument costs ~5.8k instructions per invocation. I verified this by keeping the whole feature but removing only the .arg(OPT_EXCHANGE…) registration: mv_force_overwrite then lands on 144.28M instructions vs 144.32M on main, i.e. the entire delta was the flag registration. Nothing in the --exchange code path itself is slow, and the throughput-oriented benchmark (mv_multiple_to_dir, 1000 files in a single invocation) was unaffected at +0.02%.

What I changed (commit d7c0cc7) — removal of redundant work in mv's per-file hot path:

  • handle_two_paths now reuses one symlink_metadata lookup for the source instead of is_dir() + is_symlink(), and one for the target instead of is_symlink() + is_dir().
  • target.exists() is only evaluated when the source is a directory (its result is unused otherwise).
  • assert_not_same_file no longer builds the quoted "target display" error string on every successful move — it is now only built when an error is reported. This was the biggest win.
  • The BackupMode check in assert_not_same_file runs before the file-identity checks, so no stat calls happen when a backup mode makes them irrelevant.
  • rename replaces to.exists() && to.is_dir() && !to.is_symlink() with a single symlink_metadata lookup.
  • uucore::fs::are_hardlinks_to_same_file / are_hardlinks_or_one_way_symlink_to_same_file look up the target first and return early instead of always querying both paths (the target is usually the one that does not exist).

Measured locally with codspeed run --mode simulation (instruction counts, deterministic):

Benchmark main before this commit after
mv_single_file 143.1M 150.2M (+5.0%) 146.5M (+2.4%)
mv_force_overwrite 144.3M 151.8M (+5.2%) 148.5M (+2.9%)
mv_multiple_to_dir 31.9M 32.0M 28.4M (−11.2%)
mv_directory 7.747M 7.754M 7.750M

cp also benefits from the uucore change: cp_recursive_wide_tree −2.4%, and the balanced-tree/archive/preserve-metadata benchmarks ~−0.8% each.

Remaining delta: ~2.5% on the two single-file benchmarks. That is the intrinsic cost of one extra clap flag — unavoidable while adding a CLI option, and worth a few microseconds of process startup in real usage. I'd suggest acknowledging that residual in CodSpeed rather than reshaping the feature to avoid it.

Validation: cargo test --features mv test_mv (128 passed) and --features cp test_cp show no new failures versus main (the 3 mv / 9 cp failures in my sandbox are permission/cross-device tests that fail identically on main); cargo fmt --check and cargo clippy -p uu_mv -p uucore are clean.

Summary
  • Warming up the performance wizard...
  • Investigating the mv regression — Flamegraph shows clap Command construction (uu_app, 29%) and parsing dominate mv_single_file; the extra --exchange arg adds per-invocation CLI setup cost
  • Building mv benchmarks — mv_single_file: 143.1M -> 150.2M instructions (+5.0%), mv_force_overwrite +5.2%; clap CLI setup/parsing accounts for ~60% of these benchmarks
  • Recovering the lost performance — Removed redundant stat() calls and eager error-string building in mv's hot path: mv_single_file +5.0% -> +2.4%, mv_force_overwrite +5.2% -> +2.9%, mv_multiple_to_dir -11%
  • Validating the changes — mv/cp test suites show no new failures (3 pre-existing sandbox failures also fail on main), fmt and clippy clean, benchmarks re-measured
  • Committing and pushing — Committed and pushed the hot-path optimizations to the mv-exchange branch
  • Handling comment — Comment handling finished successfully

Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot.

@SuperMuel

SuperMuel commented Jul 29, 2026

Copy link
Copy Markdown

Hello @sylvestre, once again the agent opened another PR (#13633) because we don't support fork PRs at this time. I'm actively working on this. We're not blocking the agent so it can still do the work you request, even if it means you'll have to manually cherry-pick and manage multiple PRs.

@sylvestre

Copy link
Copy Markdown
Contributor Author

@SuperMuel pas de soucis, merci!

sylvestre and others added 2 commits August 2, 2026 11:09
Should make test tests/mv/mv-exchange.sh pass
The same-file detection and the two-path handling in mv issued several
stat calls whose result was already available, and assert_not_same_file
formatted the quoted target path used in error messages even when the
move succeeded. Reuse a single symlink_metadata lookup per path, only
build the error string when an error is reported, and make the hardlink
comparison helpers in uucore look up the target first so they can return
early when it does not exist.
@sylvestre
sylvestre merged commit 4d4a119 into uutils:main Aug 2, 2026
167 checks passed
@sylvestre
sylvestre deleted the mv-exchange branch August 2, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants