aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/util
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-23 11:48:01 +0900
committernsfisis <nsfisis@gmail.com>2026-08-23 12:00:07 +0900
commit2f07bd12c0c77e8985646a9f21a62a91311d82c0 (patch)
tree92f995eee73d58b38a850f8ba9b0ab09d8a59f3a /crates/shirabe/src/util
parent88b5c4b9c5f7941292b918b6eefe54947c4a0d96 (diff)
downloadphp-shirabe-2f07bd12c0c77e8985646a9f21a62a91311d82c0.tar.gz
php-shirabe-2f07bd12c0c77e8985646a9f21a62a91311d82c0.tar.zst
php-shirabe-2f07bd12c0c77e8985646a9f21a62a91311d82c0.zip
perf(filesystem): delete directories without spawning rm -rf
Composer shells out to `rm -rf` because PHP has no recursive directory removal. Rust has one, and every package installed from a dist archive pays for a removal: the extraction pipeline drops its temporary directory once per package, and uninstalling a package removes its whole tree. The asynchronous path goes through `tokio::fs` rather than `std::fs`, so the walk runs on a blocking thread and the sibling installs the reactor is driving keep making progress, the way they did while the subprocess was working. Installing laravel/laravel (109 packages) from a warm cache drops from 3.85 to 3.19 CPU seconds. The removals run concurrently, so on an idle 16-core machine they never reach the critical path and wall time is unchanged at 1.65 s; pinned to two cores it falls from 2.33 s to 2.20 s. Pruning the 33 dev packages with `install --no-dev`, where whole package trees are removed rather than empty temporary directories, drops from 844 ms to 806 ms even on 16 cores. Windows keeps the `rmdir /S /Q` subprocess, and both platforms keep falling back to `remove_directory_php` when the fast path does not clear the directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/util')
-rw-r--r--crates/shirabe/src/util/filesystem.rs87
1 files changed, 57 insertions, 30 deletions
diff --git a/crates/shirabe/src/util/filesystem.rs b/crates/shirabe/src/util/filesystem.rs
index bca18771..05146fd3 100644
--- a/crates/shirabe/src/util/filesystem.rs
+++ b/crates/shirabe/src/util/filesystem.rs
@@ -116,9 +116,9 @@ impl Filesystem {
/// Recursively remove a directory
pub fn remove_directory(&mut self, directory: impl AsRef<Path>) -> anyhow::Result<bool> {
// TODO(bytes):
- // This path is matched against a regex (remove_edge_cases) and passed to an
- // `rm -rf`/`rmdir` subprocess via the String-based ProcessExecutor, so it has to be
- // representable as UTF-8.
+ // This path is matched against a regex (remove_edge_cases) and, on Windows, passed to an
+ // `rmdir` subprocess via the String-based ProcessExecutor, so it has to be representable as
+ // UTF-8.
let directory = directory.as_ref();
let directory = directory.to_str().ok_or_else(|| {
RuntimeException::new(format!(
@@ -131,24 +131,25 @@ impl Filesystem {
return Ok(r);
}
- let cmd: Vec<String> = if Platform::is_windows() {
- vec![
+ let result = if Platform::is_windows() {
+ // TODO(perf,windows): `std::fs::remove_dir_all` is expected to be faster here too,
+ // but how it differs from the `rmdir` command has to be investigated first.
+ let cmd: Vec<String> = vec![
"rmdir".to_string(),
"/S".to_string(),
"/Q".to_string(),
Platform::realpath(directory),
- ]
+ ];
+
+ let mut output = PhpMixed::Null;
+ self.get_process()
+ .execute(&cmd, &mut output, None)
+ .map(|n| n == 0)
+ .unwrap_or(false)
} else {
- vec!["rm".to_string(), "-rf".to_string(), directory.to_string()]
+ remove_directory_recursively(Path::new(directory)).is_ok()
};
- let mut output = PhpMixed::Null;
- let result = self
- .get_process()
- .execute(&cmd, &mut output, None)
- .map(|n| n == 0)
- .unwrap_or(false);
-
// clear stat cache because external processes aren't tracked by the php stat cache
clearstatcache2(false, "");
@@ -164,7 +165,7 @@ impl Filesystem {
this: &std::rc::Rc<std::cell::RefCell<Filesystem>>,
directory: &str,
) -> anyhow::Result<bool> {
- let (process_executor, cmd) = {
+ {
let mut fs = this.borrow_mut();
if let Some(mock) = fs.mock.as_mut()
@@ -179,28 +180,33 @@ impl Filesystem {
if let Some(r) = edge_case_result {
return Ok(r);
}
+ }
- let cmd: Vec<String> = if Platform::is_windows() {
- vec![
- "rmdir".to_string(),
- "/S".to_string(),
- "/Q".to_string(),
- Platform::realpath(directory),
- ]
- } else {
- vec!["rm".to_string(), "-rf".to_string(), directory.to_string()]
- };
+ let result = if Platform::is_windows() {
+ // TODO(perf,windows): `std::fs::remove_dir_all` is expected to be faster here too,
+ // but how it differs from the `rmdir` command has to be investigated first.
+ let cmd: Vec<String> = vec![
+ "rmdir".to_string(),
+ "/S".to_string(),
+ "/Q".to_string(),
+ Platform::realpath(directory),
+ ];
- (fs.get_process_handle(), cmd)
- };
+ let process_executor = this.borrow_mut().get_process_handle();
+ let process_future = process_executor.borrow_mut().execute_async(&cmd, None);
+ let mut process = process_future.await?;
- let process_future = process_executor.borrow_mut().execute_async(&cmd, None);
- let mut process = process_future.await?;
+ process.is_successful()
+ } else {
+ remove_directory_recursively_async(Path::new(directory))
+ .await
+ .is_ok()
+ };
// clear stat cache because external processes aren't tracked by the php stat cache
clearstatcache2(false, "");
- if process.is_successful() && !is_dir(directory) {
+ if result && !is_dir(directory) {
return Ok(true);
}
@@ -1084,3 +1090,24 @@ impl Filesystem {
result
}
}
+
+/// Removes `path` and everything below it, standing in for the `rm -rf` that Composer shells out
+/// to on non-Windows platforms. Symbolic links below `path` are unlinked rather than followed, and
+/// a `path` that is already gone counts as removed.
+///
+/// Unlike `rm -rf`, this stops at the first entry it cannot remove instead of removing the rest of
+/// the tree first. Callers fall back to `Filesystem::remove_directory_php` on failure, which walks
+/// the remainder and surfaces the error.
+fn remove_directory_recursively(path: &Path) -> std::io::Result<()> {
+ match std::fs::remove_dir_all(path) {
+ Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(e),
+ _ => Ok(()),
+ }
+}
+
+async fn remove_directory_recursively_async(path: &Path) -> std::io::Result<()> {
+ match tokio::fs::remove_dir_all(path).await {
+ Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(e),
+ _ => Ok(()),
+ }
+}