Repository: denoland/deno
Pull request: perf(ext/node): optimize fs.exists[Sync]

Use `access` on *nix and `GetFileAttributesW` on Windows.

[Benchmark](https://paste.divy.work/p/-gq8Ark.js):
```
$ deno run -A bench.mjs # main (568dd)
existsSync: 8980.636629ms

$ target/release/deno run -A bench.mjs # this PR
existsSync: 6448.7604519999995ms

$ bun bench.mjs
existsSync: 6562.88671ms

$ node bench.mjs
existsSync: 7740.064653ms
```

Ref https://github.com/denoland/deno/pull/24434#discussion_r1679777912

Changed files:
filename: ext/fs/interface.rs
patch: @@ -315,6 +315,9 @@ pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
   fn exists_sync(&self, path: &Path) -> bool {
     self.stat_sync(path).is_ok()
   }
+  async fn exists_async(&self, path: PathBuf) -> FsResult<bool> {
+    Ok(self.stat_async(path).await.is_ok())
+  }
 
   fn read_text_file_lossy_sync(
     &self,

filename: ext/fs/std_fs.rs
patch: @@ -173,6 +173,15 @@ impl FileSystem for RealFs {
     spawn_blocking(move || lstat(&path)).await?.map(Into::into)
   }
 
+  fn exists_sync(&self, path: &Path) -> bool {
+    exists(path)
+  }
+  async fn exists_async(&self, path: PathBuf) -> FsResult<bool> {
+    spawn_blocking(move || exists(&path))
+      .await
+      .map_err(Into::into)
+  }
+
   fn realpath_sync(&self, path: &Path) -> FsResult<PathBuf> {
     realpath(path)
   }
@@ -837,6 +846,32 @@ fn stat_extra(
   }
 }
 
+fn exists(path: &Path) -> bool {
+  #[cfg(unix)]
+  {
+    use nix::unistd::access;
+    use nix::unistd::AccessFlags;
+    access(path, AccessFlags::F_OK).is_ok()
+  }
+
+  #[cfg(windows)]
+  {
+    use std::os::windows::ffi::OsStrExt;
+    use winapi::um::fileapi::GetFileAttributesW;
+    use winapi::um::fileapi::INVALID_FILE_ATTRIBUTES;
+
+    let path = path
+      .as_os_str()
+      .encode_wide()
+      .chain(std::iter::once(0))
+      .collect::<Vec<_>>();
+    // Safety: `path` is a null-terminated string
+    let attrs = unsafe { GetFileAttributesW(path.as_ptr()) };
+
+    attrs != INVALID_FILE_ATTRIBUTES
+  }
+}
+
 fn realpath(path: &Path) -> FsResult<PathBuf> {
   Ok(deno_core::strip_unc_prefix(path.canonicalize()?))
 }

filename: ext/node/lib.rs
patch: @@ -313,6 +313,7 @@ deno_core::extension!(deno_node,
     ops::crypto::x509::op_node_x509_get_serial_number,
     ops::crypto::x509::op_node_x509_key_usage,
     ops::fs::op_node_fs_exists_sync<P>,
+    ops::fs::op_node_fs_exists<P>,
     ops::fs::op_node_cp_sync<P>,
     ops::fs::op_node_cp<P>,
     ops::fs::op_node_lchown_sync<P>,

filename: ext/node/ops/fs.rs
patch: @@ -26,7 +26,28 @@ where
     .borrow_mut::<P>()
     .check_read_with_api_name(&path, Some("node:fs.existsSync()"))?;
   let fs = state.borrow::<FileSystemRc>();
-  Ok(fs.lstat_sync(&path).is_ok())
+  Ok(fs.exists_sync(&path))
+}
+
+#[op2(async)]
+pub async fn op_node_fs_exists<P>(
+  state: Rc<RefCell<OpState>>,
+  #[string] path: String,
+) -> Result<bool, AnyError>
+where
+  P: NodePermissions + 'static,
+{
+  let path = PathBuf::from(path);
+
+  let fs = {
+    let mut state = state.borrow_mut();
+    state
+      .borrow_mut::<P>()
+      .check_read_with_api_name(&path, Some("node:fs.exists()"))?;
+    state.borrow::<FileSystemRc>().clone()
+  };
+
+  Ok(fs.exists_async(path).await?)
 }
 
 #[op2(fast)]

filename: ext/node/polyfills/_fs/_fs_exists.ts
patch: @@ -3,7 +3,7 @@
 // TODO(petamoriken): enable prefer-primordials for node polyfills
 // deno-lint-ignore-file prefer-primordials
 
-import { op_node_fs_exists_sync } from "ext:core/ops";
+import { op_node_fs_exists, op_node_fs_exists_sync } from "ext:core/ops";
 
 import { pathFromURL } from "ext:deno_web/00_infra.js";
 
@@ -16,7 +16,7 @@ type ExistsCallback = (exists: boolean) => void;
  */
 export function exists(path: string | URL, callback: ExistsCallback) {
   path = path instanceof URL ? pathFromURL(path) : path;
-  Deno.lstat(path).then(() => callback(true), () => callback(false));
+  op_node_fs_exists(path).then(callback);
 }
 
 // The callback of fs.exists doesn't have standard callback signature.