aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-05-27 23:50:06 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-05-27 23:50:06 -0500
commit2f5c85ff187d1dc49a8712e1b80c107012cb6a49 (patch)
treedececa68572653268cc16ea4a58a71bc5d54100d
parent44a1fa6fac19e3c323a7d7d8bf9d0443cc54b75b (diff)
Add FUSE io_uring negotiation and bottleneck logging
-rw-r--r--src/src/fuseconnector.cpp92
-rw-r--r--src/src/vfs/mo2filesystem.cpp304
-rw-r--r--src/src/vfs/mo2filesystem.h25
3 files changed, 381 insertions, 40 deletions
diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp
index cf13ba1..fb2a518 100644
--- a/src/src/fuseconnector.cpp
+++ b/src/src/fuseconnector.cpp
@@ -22,6 +22,7 @@
#include <chrono>
#include <cerrno>
#include <cstdio>
+#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <filesystem>
@@ -55,6 +56,69 @@ namespace
{
namespace fs = std::filesystem;
+bool parseVersionTriplet(const char* text, int* major, int* minor, int* patch)
+{
+ if (text == nullptr || major == nullptr || minor == nullptr || patch == nullptr) {
+ return false;
+ }
+
+ char* end = nullptr;
+ const long ma = std::strtol(text, &end, 10);
+ if (end == text || *end != '.') {
+ return false;
+ }
+ const long mi = std::strtol(end + 1, &end, 10);
+ if (*end != '.') {
+ return false;
+ }
+ const long pa = std::strtol(end + 1, &end, 10);
+ if (ma < 0 || mi < 0 || pa < 0) {
+ return false;
+ }
+
+ *major = static_cast<int>(ma);
+ *minor = static_cast<int>(mi);
+ *patch = static_cast<int>(pa);
+ return true;
+}
+
+bool runtimeLibfuseHasStableIoUring()
+{
+#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 18)
+ int major = 0;
+ int minor = 0;
+ int patch = 0;
+ if (!parseVersionTriplet(fuse_pkgversion(), &major, &minor, &patch)) {
+ return false;
+ }
+ return major > 3 || (major == 3 && (minor > 18 || (minor == 18 && patch >= 2)));
+#else
+ return false;
+#endif
+}
+
+void recordRequestTransport(fuse_req_t req) noexcept
+{
+ if (req == nullptr) {
+ return;
+ }
+
+ auto* ctx = static_cast<Mo2FsContext*>(fuse_req_userdata(req));
+ if (ctx == nullptr) {
+ return;
+ }
+
+#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 18)
+ if (fuse_req_is_uring(req)) {
+ ctx->uring_request_count.fetch_add(1, std::memory_order_relaxed);
+ } else {
+ ctx->legacy_request_count.fetch_add(1, std::memory_order_relaxed);
+ }
+#else
+ ctx->legacy_request_count.fetch_add(1, std::memory_order_relaxed);
+#endif
+}
+
std::string decodeProcMountField(const std::string& in)
{
std::string out;
@@ -208,18 +272,21 @@ void wrap_init(void* userdata, struct fuse_conn_info* conn) noexcept
void wrap_lookup(fuse_req_t req, fuse_ino_t parent, const char* name) noexcept
{
+ recordRequestTransport(req);
try { mo2_lookup(req, parent, name); }
MO2_TRY_REPLY(req, "lookup", parent, EIO)
}
void wrap_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_getattr(req, ino, fi); }
MO2_TRY_REPLY(req, "getattr", ino, EIO)
}
void wrap_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_opendir(req, ino, fi); }
MO2_TRY_REPLY(req, "opendir", ino, EIO)
}
@@ -227,6 +294,7 @@ void wrap_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noe
void wrap_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_readdir(req, ino, size, off, fi); }
MO2_TRY_REPLY(req, "readdir", ino, EIO)
}
@@ -234,12 +302,14 @@ void wrap_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
void wrap_readdirplus(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_readdirplus(req, ino, size, off, fi); }
MO2_TRY_REPLY(req, "readdirplus", ino, EIO)
}
void wrap_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_open(req, ino, fi); }
MO2_TRY_REPLY(req, "open", ino, EIO)
}
@@ -247,6 +317,7 @@ void wrap_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexce
void wrap_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_read(req, ino, size, off, fi); }
MO2_TRY_REPLY(req, "read", ino, EIO)
}
@@ -254,6 +325,7 @@ void wrap_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
void wrap_write(fuse_req_t req, fuse_ino_t ino, const char* buf, size_t size,
off_t off, struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_write(req, ino, buf, size, off, fi); }
MO2_TRY_REPLY(req, "write", ino, EIO)
}
@@ -261,6 +333,7 @@ void wrap_write(fuse_req_t req, fuse_ino_t ino, const char* buf, size_t size,
void wrap_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode,
struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_create(req, parent, name, mode, fi); }
MO2_TRY_REPLY(req, "create", parent, EIO)
}
@@ -268,6 +341,7 @@ void wrap_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mod
void wrap_rename(fuse_req_t req, fuse_ino_t parent, const char* name,
fuse_ino_t newparent, const char* newname, unsigned int flags) noexcept
{
+ recordRequestTransport(req);
try { mo2_rename(req, parent, name, newparent, newname, flags); }
MO2_TRY_REPLY(req, "rename", parent, EIO)
}
@@ -275,48 +349,56 @@ void wrap_rename(fuse_req_t req, fuse_ino_t parent, const char* name,
void wrap_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set,
struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_setattr(req, ino, attr, to_set, fi); }
MO2_TRY_REPLY(req, "setattr", ino, EIO)
}
void wrap_unlink(fuse_req_t req, fuse_ino_t parent, const char* name) noexcept
{
+ recordRequestTransport(req);
try { mo2_unlink(req, parent, name); }
MO2_TRY_REPLY(req, "unlink", parent, EIO)
}
void wrap_mkdir(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode) noexcept
{
+ recordRequestTransport(req);
try { mo2_mkdir(req, parent, name, mode); }
MO2_TRY_REPLY(req, "mkdir", parent, EIO)
}
void wrap_rmdir(fuse_req_t req, fuse_ino_t parent, const char* name) noexcept
{
+ recordRequestTransport(req);
try { mo2_rmdir(req, parent, name); }
MO2_TRY_REPLY(req, "rmdir", parent, EIO)
}
void wrap_release(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_release(req, ino, fi); }
MO2_TRY_REPLY(req, "release", ino, EIO)
}
void wrap_releasedir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_releasedir(req, ino, fi); }
MO2_TRY_REPLY(req, "releasedir", ino, EIO)
}
void wrap_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) noexcept
{
+ recordRequestTransport(req);
try { mo2_forget(req, ino, nlookup); }
catch (...) { fuse_reply_none(req); }
}
void wrap_flush(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_flush(req, ino, fi); }
MO2_TRY_REPLY(req, "flush", ino, EIO)
}
@@ -324,6 +406,7 @@ void wrap_flush(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexc
void wrap_fsync(fuse_req_t req, fuse_ino_t ino, int datasync,
struct fuse_file_info* fi) noexcept
{
+ recordRequestTransport(req);
try { mo2_fsync(req, ino, datasync, fi); }
MO2_TRY_REPLY(req, "fsync", ino, EIO)
}
@@ -557,6 +640,15 @@ bool FuseConnector::mount(
std::vector<std::string> argvStorage = {
"mo2fuse", "-o", "fsname=mo2linux", "-o", "noatime",
"-o", "default_permissions", "-o", "max_read=1048576"};
+ const bool requestIoUring = runtimeLibfuseHasStableIoUring();
+ if (requestIoUring) {
+ argvStorage.push_back("-o");
+ argvStorage.push_back("io_uring");
+ }
+ std::fprintf(stderr,
+ "[VFS] libfuse=%s headers=%d.%d io_uring_mount_option=%d\n",
+ fuse_pkgversion(), FUSE_MAJOR_VERSION, FUSE_MINOR_VERSION,
+ requestIoUring ? 1 : 0);
std::vector<char*> argv;
argv.reserve(argvStorage.size());
diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp
index 814d2ce..3cbe435 100644
--- a/src/src/vfs/mo2filesystem.cpp
+++ b/src/src/vfs/mo2filesystem.cpp
@@ -9,8 +9,10 @@
#include <algorithm>
#include <chrono>
#include <climits>
+#include <cstdint>
#include <cstring>
#include <filesystem>
+#include <utility>
#if __has_include(<linux/msdos_fs.h>)
#include <linux/msdos_fs.h>
@@ -27,6 +29,7 @@ constexpr double TTL_SECONDS = 86400.0; // 24 hours
constexpr double NEGATIVE_TTL_SECONDS = 3600.0; // 1 hour — Wine probes many non-existent files
constexpr double ATTR_CACHE_SECONDS = 86400.0;
constexpr size_t MAX_RETAINED_RO_FDS = 1024;
+constexpr uint64_t SLOW_OP_LOG_NS = 100ull * 1000ull * 1000ull;
void fillStatForDir(struct stat* st, fuse_ino_t ino, uid_t uid, gid_t gid);
void fillStatForFile(struct stat* st, fuse_ino_t ino, uid_t uid, gid_t gid,
@@ -43,9 +46,13 @@ bool pathTouchesMutation(const std::string& cachedPath,
struct OpTimer
{
std::atomic<uint64_t>* sink;
+ const char* op;
+ std::string path;
std::chrono::steady_clock::time_point start;
- explicit OpTimer(std::atomic<uint64_t>* s)
- : sink(s), start(std::chrono::steady_clock::now()) {}
+ explicit OpTimer(std::atomic<uint64_t>* s, const char* opName = nullptr,
+ std::string opPath = {})
+ : sink(s), op(opName), path(std::move(opPath)),
+ start(std::chrono::steady_clock::now()) {}
~OpTimer()
{
if (sink == nullptr) return;
@@ -53,11 +60,68 @@ struct OpTimer
const uint64_t ns = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count());
sink->fetch_add(ns, std::memory_order_relaxed);
+ if (op != nullptr && ns >= SLOW_OP_LOG_NS) {
+ std::fprintf(stderr, "[VFS] slow op=%s elapsed=%.1fms path='%s'\n",
+ op, ns / 1e6, path.c_str());
+ }
}
OpTimer(const OpTimer&) = delete;
OpTimer& operator=(const OpTimer&) = delete;
};
+bool fuseHasFeature(const struct fuse_conn_info* conn, uint64_t flag)
+{
+ if (conn == nullptr) {
+ return false;
+ }
+#ifdef FUSE_CAP_OVER_IO_URING
+ return (conn->capable_ext & flag) != 0 ||
+ (flag <= UINT32_MAX && (conn->capable & static_cast<uint32_t>(flag)) != 0);
+#else
+ return (conn->capable & static_cast<uint32_t>(flag)) != 0;
+#endif
+}
+
+bool fuseWantsFeature(const struct fuse_conn_info* conn, uint64_t flag)
+{
+ if (conn == nullptr) {
+ return false;
+ }
+#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 17)
+ return fuse_get_feature_flag(const_cast<struct fuse_conn_info*>(conn), flag);
+#else
+ return (conn->want & static_cast<uint32_t>(flag)) != 0;
+#endif
+}
+
+bool fuseRequestFeature(struct fuse_conn_info* conn, uint64_t flag)
+{
+ if (conn == nullptr) {
+ return false;
+ }
+#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 17)
+ return fuse_set_feature_flag(conn, flag);
+#else
+ if ((conn->capable & static_cast<uint32_t>(flag)) == 0) {
+ return false;
+ }
+ conn->want |= static_cast<uint32_t>(flag);
+ return true;
+#endif
+}
+
+void fuseDropFeature(struct fuse_conn_info* conn, uint64_t flag)
+{
+ if (conn == nullptr) {
+ return;
+ }
+#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 17)
+ fuse_unset_feature_flag(conn, flag);
+#else
+ conn->want &= ~static_cast<uint32_t>(flag);
+#endif
+}
+
void maybeLogCounters(Mo2FsContext* ctx)
{
if (ctx == nullptr) {
@@ -74,15 +138,31 @@ void maybeLogCounters(Mo2FsContext* ctx)
const uint64_t rc = ctx->readdir_count.load(std::memory_order_relaxed);
const uint64_t oc = ctx->open_count.load(std::memory_order_relaxed);
const uint64_t rdc = ctx->read_count.load(std::memory_order_relaxed);
+ const uint64_t wc = ctx->write_count.load(std::memory_order_relaxed);
+ const uint64_t cc = ctx->create_count.load(std::memory_order_relaxed);
+ const uint64_t rnc = ctx->rename_count.load(std::memory_order_relaxed);
+ const uint64_t sc = ctx->setattr_count.load(std::memory_order_relaxed);
+ const uint64_t uc = ctx->unlink_count.load(std::memory_order_relaxed);
+ const uint64_t fc = ctx->flush_count.load(std::memory_order_relaxed);
+ const uint64_t fsc = ctx->fsync_count.load(std::memory_order_relaxed);
const uint64_t ic = ctx->ioctl_count.load(std::memory_order_relaxed);
std::fprintf(stderr,
- "[VFS] ops lookup=%llu getattr=%llu readdir=%llu open=%llu read=%llu ioctl=%llu",
+ "[VFS] ops lookup=%llu getattr=%llu readdir=%llu open=%llu read=%llu "
+ "write=%llu create=%llu rename=%llu setattr=%llu unlink=%llu "
+ "flush=%llu fsync=%llu ioctl=%llu",
static_cast<unsigned long long>(lc),
static_cast<unsigned long long>(gc),
static_cast<unsigned long long>(rc),
static_cast<unsigned long long>(oc),
static_cast<unsigned long long>(rdc),
+ static_cast<unsigned long long>(wc),
+ static_cast<unsigned long long>(cc),
+ static_cast<unsigned long long>(rnc),
+ static_cast<unsigned long long>(sc),
+ static_cast<unsigned long long>(uc),
+ static_cast<unsigned long long>(fc),
+ static_cast<unsigned long long>(fsc),
static_cast<unsigned long long>(ic));
{
std::scoped_lock lock(ctx->open_files_mutex);
@@ -90,6 +170,8 @@ void maybeLogCounters(Mo2FsContext* ctx)
}
std::fprintf(stderr,
"[VFS] cache lookup_hit=%llu lookup_miss=%llu lookup_inval=%llu "
+ "attr_hit=%llu attr_miss=%llu dir_hit=%llu dir_miss=%llu "
+ "readdir_blob_hit=%llu readdirplus_blob_hit=%llu "
"lazy_ro_open=%llu ro_fd_hit=%llu ro_fd_evict=%llu\n",
static_cast<unsigned long long>(
ctx->lookup_cache_hits.load(std::memory_order_relaxed)),
@@ -98,11 +180,72 @@ void maybeLogCounters(Mo2FsContext* ctx)
static_cast<unsigned long long>(
ctx->lookup_cache_invalidations.load(std::memory_order_relaxed)),
static_cast<unsigned long long>(
+ ctx->attr_cache_hits.load(std::memory_order_relaxed)),
+ static_cast<unsigned long long>(
+ ctx->attr_cache_misses.load(std::memory_order_relaxed)),
+ static_cast<unsigned long long>(
+ ctx->dir_cache_hits.load(std::memory_order_relaxed)),
+ static_cast<unsigned long long>(
+ ctx->dir_cache_misses.load(std::memory_order_relaxed)),
+ static_cast<unsigned long long>(
+ ctx->readdir_blob_hits.load(std::memory_order_relaxed)),
+ static_cast<unsigned long long>(
+ ctx->readdirplus_blob_hits.load(std::memory_order_relaxed)),
+ static_cast<unsigned long long>(
ctx->lazy_ro_fd_opens.load(std::memory_order_relaxed)),
static_cast<unsigned long long>(
ctx->retained_ro_fd_hits.load(std::memory_order_relaxed)),
static_cast<unsigned long long>(
ctx->retained_ro_fd_evictions.load(std::memory_order_relaxed)));
+ std::fprintf(stderr,
+ "[VFS] io bytes_read=%llu bytes_written=%llu cow_writes=%llu "
+ "transport_uring=%llu transport_legacy=%llu\n",
+ static_cast<unsigned long long>(
+ ctx->read_bytes.load(std::memory_order_relaxed)),
+ static_cast<unsigned long long>(
+ ctx->write_bytes.load(std::memory_order_relaxed)),
+ static_cast<unsigned long long>(
+ ctx->cow_write_count.load(std::memory_order_relaxed)),
+ static_cast<unsigned long long>(
+ ctx->uring_request_count.load(std::memory_order_relaxed)),
+ static_cast<unsigned long long>(
+ ctx->legacy_request_count.load(std::memory_order_relaxed)));
+ {
+ size_t lookupSize = 0;
+ size_t attrSize = 0;
+ size_t dirSize = 0;
+ size_t readdirBlobSize = 0;
+ size_t readdirPlusBlobSize = 0;
+ size_t openDirSize = 0;
+ size_t nodeSize = 0;
+ {
+ std::scoped_lock lock(ctx->lookup_cache_mutex);
+ lookupSize = ctx->lookup_cache.size();
+ }
+ {
+ std::scoped_lock lock(ctx->attr_cache_mutex);
+ attrSize = ctx->attr_cache.size();
+ }
+ {
+ std::scoped_lock lock(ctx->dir_cache_mutex);
+ dirSize = ctx->dir_cache.size();
+ readdirBlobSize = ctx->readdir_blob_cache.size();
+ readdirPlusBlobSize = ctx->readdirplus_blob_cache.size();
+ }
+ {
+ std::scoped_lock lock(ctx->open_dirs_mutex);
+ openDirSize = ctx->open_dirs.size();
+ }
+ {
+ std::scoped_lock lock(ctx->node_cache_mutex);
+ nodeSize = ctx->node_cache.size();
+ }
+ std::fprintf(stderr,
+ "[VFS] cache_size lookup=%zu attr=%zu dir=%zu readdir_blob=%zu "
+ "readdirplus_blob=%zu open_dirs=%zu node=%zu\n",
+ lookupSize, attrSize, dirSize, readdirBlobSize,
+ readdirPlusBlobSize, openDirSize, nodeSize);
+ }
// Per-op wall-clock totals and averages (microseconds).
auto avgUs = [](uint64_t ns, uint64_t count) -> double {
@@ -113,14 +256,32 @@ void maybeLogCounters(Mo2FsContext* ctx)
const uint64_t rns = ctx->readdir_ns.load(std::memory_order_relaxed);
const uint64_t ons = ctx->open_ns.load(std::memory_order_relaxed);
const uint64_t rdns = ctx->read_ns.load(std::memory_order_relaxed);
+ const uint64_t wns = ctx->write_ns.load(std::memory_order_relaxed);
+ const uint64_t cns = ctx->create_ns.load(std::memory_order_relaxed);
+ const uint64_t rnns = ctx->rename_ns.load(std::memory_order_relaxed);
+ const uint64_t sns = ctx->setattr_ns.load(std::memory_order_relaxed);
+ const uint64_t uns = ctx->unlink_ns.load(std::memory_order_relaxed);
+ const uint64_t fns = ctx->flush_ns.load(std::memory_order_relaxed);
+ const uint64_t fsns = ctx->fsync_ns.load(std::memory_order_relaxed);
std::fprintf(stderr,
"[VFS] time lookup=%.1fms/%.1fus-avg getattr=%.1fms/%.1fus readdir=%.1fms/%.1fus "
- "open=%.1fms/%.1fus read=%.1fms/%.1fus\n",
+ "open=%.1fms/%.1fus read=%.1fms/%.1fus write=%.1fms/%.1fus\n",
lns / 1e6, avgUs(lns, lc),
gns / 1e6, avgUs(gns, gc),
rns / 1e6, avgUs(rns, rc),
ons / 1e6, avgUs(ons, oc),
- rdns / 1e6, avgUs(rdns, rdc));
+ rdns / 1e6, avgUs(rdns, rdc),
+ wns / 1e6, avgUs(wns, wc));
+ std::fprintf(stderr,
+ "[VFS] time_mut create=%.1fms/%.1fus rename=%.1fms/%.1fus "
+ "setattr=%.1fms/%.1fus unlink=%.1fms/%.1fus flush=%.1fms/%.1fus "
+ "fsync=%.1fms/%.1fus\n",
+ cns / 1e6, avgUs(cns, cc),
+ rnns / 1e6, avgUs(rnns, rnc),
+ sns / 1e6, avgUs(sns, sc),
+ uns / 1e6, avgUs(uns, uc),
+ fns / 1e6, avgUs(fns, fc),
+ fsns / 1e6, avgUs(fsns, fsc));
// Process-wide CPU usage + delta since last tick. Lets us tell whether
// high per-op wall time is spent burning CPU (parsing, mutex contention)
@@ -669,9 +830,11 @@ std::shared_ptr<std::vector<Mo2FsContext::DirEntry>> getOrBuildDirEntries(
auto it = ctx->dir_cache.find(path);
if (it != ctx->dir_cache.end()) {
*ok = true;
+ ctx->dir_cache_hits.fetch_add(1, std::memory_order_relaxed);
return it->second;
}
}
+ ctx->dir_cache_misses.fetch_add(1, std::memory_order_relaxed);
auto entries = std::make_shared<std::vector<Mo2FsContext::DirEntry>>(
buildDirEntries(ctx, path, ino, ok));
@@ -983,24 +1146,24 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn)
// reduction. Our VFS tree is immutable during a session — we handle
// invalidation ourselves via fuse_lowlevel_notify_inval_inode() when
// files are created/renamed/deleted through our own handlers.
- conn->want &= ~FUSE_CAP_AUTO_INVAL_DATA;
+ fuseDropFeature(conn, FUSE_CAP_AUTO_INVAL_DATA);
// Let us control page cache invalidation explicitly.
- if (conn->capable & FUSE_CAP_EXPLICIT_INVAL_DATA) {
- conn->want |= FUSE_CAP_EXPLICIT_INVAL_DATA;
+ if (fuseHasFeature(conn, FUSE_CAP_EXPLICIT_INVAL_DATA)) {
+ fuseRequestFeature(conn, FUSE_CAP_EXPLICIT_INVAL_DATA);
}
// Plain readdir is the conservative default for large modlists. Wine and
// game startup often enumerate large directories but only stat/open a small
// subset, so forced readdirplus can send a lot of unused metadata.
- if (conn->capable & FUSE_CAP_READDIRPLUS) {
- conn->want &= ~FUSE_CAP_READDIRPLUS;
- conn->want &= ~FUSE_CAP_READDIRPLUS_AUTO;
+ if (fuseHasFeature(conn, FUSE_CAP_READDIRPLUS)) {
+ fuseDropFeature(conn, FUSE_CAP_READDIRPLUS);
+ fuseDropFeature(conn, FUSE_CAP_READDIRPLUS_AUTO);
}
#ifdef FUSE_CAP_NO_OPENDIR_SUPPORT
- if (conn->capable & FUSE_CAP_NO_OPENDIR_SUPPORT) {
- conn->want |= FUSE_CAP_NO_OPENDIR_SUPPORT;
+ if (fuseHasFeature(conn, FUSE_CAP_NO_OPENDIR_SUPPORT)) {
+ fuseRequestFeature(conn, FUSE_CAP_NO_OPENDIR_SUPPORT);
}
#endif
@@ -1009,41 +1172,44 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn)
// our read-heavy VFS more than the write buffering helps.
// Cache symlink targets in the kernel page cache.
- if (conn->capable & FUSE_CAP_CACHE_SYMLINKS) {
- conn->want |= FUSE_CAP_CACHE_SYMLINKS;
+ if (fuseHasFeature(conn, FUSE_CAP_CACHE_SYMLINKS)) {
+ fuseRequestFeature(conn, FUSE_CAP_CACHE_SYMLINKS);
}
// Allow concurrent lookup()/readdir() on the same directory.
- if (conn->capable & FUSE_CAP_PARALLEL_DIROPS) {
- conn->want |= FUSE_CAP_PARALLEL_DIROPS;
+ if (fuseHasFeature(conn, FUSE_CAP_PARALLEL_DIROPS)) {
+ fuseRequestFeature(conn, FUSE_CAP_PARALLEL_DIROPS);
}
// Splice: reduce kernel↔userspace data copies for reads and writes.
- if (conn->capable & FUSE_CAP_SPLICE_WRITE) {
- conn->want |= FUSE_CAP_SPLICE_WRITE;
+ if (fuseHasFeature(conn, FUSE_CAP_SPLICE_WRITE)) {
+ fuseRequestFeature(conn, FUSE_CAP_SPLICE_WRITE);
}
- if (conn->capable & FUSE_CAP_SPLICE_MOVE) {
- conn->want |= FUSE_CAP_SPLICE_MOVE;
+ if (fuseHasFeature(conn, FUSE_CAP_SPLICE_MOVE)) {
+ fuseRequestFeature(conn, FUSE_CAP_SPLICE_MOVE);
}
- if (conn->capable & FUSE_CAP_SPLICE_READ) {
- conn->want |= FUSE_CAP_SPLICE_READ;
+ if (fuseHasFeature(conn, FUSE_CAP_SPLICE_READ)) {
+ fuseRequestFeature(conn, FUSE_CAP_SPLICE_READ);
}
// Allow concurrent submission of split direct I/O requests.
// Harmless when not triggered; helps if Wine opens files with O_DIRECT.
- if (conn->capable & FUSE_CAP_ASYNC_DIO) {
- conn->want |= FUSE_CAP_ASYNC_DIO;
+ if (fuseHasFeature(conn, FUSE_CAP_ASYNC_DIO)) {
+ fuseRequestFeature(conn, FUSE_CAP_ASYNC_DIO);
}
// Softer dentry invalidation: mark entries as expired rather than
// forcefully removing them, reducing cascading cache evictions.
- if (conn->capable & FUSE_CAP_EXPIRE_ONLY) {
- conn->want |= FUSE_CAP_EXPIRE_ONLY;
+ if (fuseHasFeature(conn, FUSE_CAP_EXPIRE_ONLY)) {
+ fuseRequestFeature(conn, FUSE_CAP_EXPIRE_ONLY);
}
+ bool uringCapable = false;
+ bool uringWanted = false;
#ifdef FUSE_CAP_OVER_IO_URING
- if (conn->capable & FUSE_CAP_OVER_IO_URING) {
- std::fprintf(stderr, "[VFS] init: kernel reports FUSE over io_uring support\n");
+ uringCapable = fuseHasFeature(conn, FUSE_CAP_OVER_IO_URING);
+ if (uringCapable) {
+ uringWanted = fuseRequestFeature(conn, FUSE_CAP_OVER_IO_URING);
}
#endif
@@ -1073,16 +1239,29 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn)
std::fprintf(stderr,
"[VFS] init: auto_inval=%d explicit_inval=%d readdirplus=%d "
- "no_opendir=%d max_bg=%u max_readahead=%u\n",
- (conn->want & FUSE_CAP_AUTO_INVAL_DATA) ? 1 : 0,
- (conn->want & FUSE_CAP_EXPLICIT_INVAL_DATA) ? 1 : 0,
- (conn->want & FUSE_CAP_READDIRPLUS) ? 1 : 0,
+ "no_opendir=%d uring_capable=%d uring_wanted=%d "
+ "max_bg=%u max_readahead=%u\n",
+ fuseWantsFeature(conn, FUSE_CAP_AUTO_INVAL_DATA) ? 1 : 0,
+ fuseWantsFeature(conn, FUSE_CAP_EXPLICIT_INVAL_DATA) ? 1 : 0,
+ fuseWantsFeature(conn, FUSE_CAP_READDIRPLUS) ? 1 : 0,
#ifdef FUSE_CAP_NO_OPENDIR_SUPPORT
- (conn->want & FUSE_CAP_NO_OPENDIR_SUPPORT) ? 1 : 0,
+ fuseWantsFeature(conn, FUSE_CAP_NO_OPENDIR_SUPPORT) ? 1 : 0,
#else
0,
#endif
+ uringCapable ? 1 : 0,
+ uringWanted ? 1 : 0,
conn->max_background, conn->max_readahead);
+ std::fprintf(stderr,
+ "[VFS] init_caps: libfuse=%s headers=%d.%d proto=%u.%u "
+ "capable=0x%08x capable_ext=0x%016llx want=0x%08x "
+ "want_ext=0x%016llx max_write=%u max_read=%u\n",
+ fuse_pkgversion(), FUSE_MAJOR_VERSION, FUSE_MINOR_VERSION,
+ conn->proto_major, conn->proto_minor, conn->capable,
+ static_cast<unsigned long long>(conn->capable_ext),
+ conn->want,
+ static_cast<unsigned long long>(conn->want_ext),
+ conn->max_write, conn->max_read);
}
void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name)
@@ -1092,7 +1271,7 @@ void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name)
fuse_reply_err(req, EINVAL);
return;
}
- OpTimer _t(&ctx->lookup_ns);
+ OpTimer _t(&ctx->lookup_ns, "lookup");
ctx->lookup_count.fetch_add(1, std::memory_order_relaxed);
maybeLogCounters(ctx);
@@ -1134,6 +1313,7 @@ void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name)
return;
}
const std::string childPath = joinPath(parentPath, lr.canonical_name);
+ _t.path = childPath;
samplePathStat(ctx, "lookup", childPath, false);
fuse_ino_t childIno = 0;
@@ -1188,7 +1368,7 @@ void mo2_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* /*fi*/)
fuse_reply_err(req, EINVAL);
return;
}
- OpTimer _t(&ctx->getattr_ns);
+ OpTimer _t(&ctx->getattr_ns, "getattr");
ctx->getattr_count.fetch_add(1, std::memory_order_relaxed);
maybeLogCounters(ctx);
@@ -1197,12 +1377,15 @@ void mo2_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* /*fi*/)
auto it = ctx->attr_cache.find(ino);
if (it != ctx->attr_cache.end() && it->second.valid &&
std::chrono::steady_clock::now() < it->second.expires_at) {
+ ctx->attr_cache_hits.fetch_add(1, std::memory_order_relaxed);
fuse_reply_attr(req, &it->second.st, TTL_SECONDS);
return;
}
}
+ ctx->attr_cache_misses.fetch_add(1, std::memory_order_relaxed);
if (ino == 1) {
+ _t.path = "/";
struct stat st;
fillStatForDir(&st, 1, ctx->uid, ctx->gid);
{
@@ -1299,7 +1482,7 @@ void mo2_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
fuse_reply_err(req, EINVAL);
return;
}
- OpTimer _t(&ctx->readdir_ns);
+ OpTimer _t(&ctx->readdir_ns, "readdir");
ctx->readdir_count.fetch_add(1, std::memory_order_relaxed);
maybeLogCounters(ctx);
@@ -1319,6 +1502,8 @@ void mo2_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
}
if (readdirBlob != nullptr) {
+ ctx->readdir_blob_hits.fetch_add(1, std::memory_order_relaxed);
+ _t.path = path;
samplePathStat(ctx, "readdir", path);
const size_t start = static_cast<size_t>(off);
if (start >= readdirBlob->size()) {
@@ -1346,6 +1531,7 @@ void mo2_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
}
}
samplePathStat(ctx, "readdir", path);
+ _t.path = path;
// Cap the buffer. size comes from the kernel and is normally bounded by
// FUSE protocol limits, but a corrupt/oversized request would otherwise
@@ -1389,7 +1575,7 @@ void mo2_readdirplus(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
fuse_reply_err(req, EINVAL);
return;
}
- OpTimer _t(&ctx->readdir_ns);
+ OpTimer _t(&ctx->readdir_ns, "readdirplus");
ctx->readdir_count.fetch_add(1, std::memory_order_relaxed);
maybeLogCounters(ctx);
@@ -1423,6 +1609,8 @@ void mo2_readdirplus(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
}
if (readdirPlusBlob != nullptr) {
+ ctx->readdirplus_blob_hits.fetch_add(1, std::memory_order_relaxed);
+ _t.path = path;
samplePathStat(ctx, "readdir", path);
const size_t start = static_cast<size_t>(off);
if (start >= readdirPlusBlob->size()) {
@@ -1450,6 +1638,7 @@ void mo2_readdirplus(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
}
}
samplePathStat(ctx, "readdir", path);
+ _t.path = path;
constexpr size_t kReaddirPlusBufMax = 1 * 1024 * 1024;
if (size > kReaddirPlusBufMax) {
@@ -1492,12 +1681,13 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi)
fuse_reply_err(req, EINVAL);
return;
}
- OpTimer _t(&ctx->open_ns);
+ OpTimer _t(&ctx->open_ns, "open");
ctx->open_count.fetch_add(1, std::memory_order_relaxed);
maybeLogCounters(ctx);
bool ok = false;
const std::string path = inodeToPath(ctx, ino, &ok);
+ _t.path = path;
if (!ok) {
fuse_reply_err(req, ENOENT);
return;
@@ -1617,7 +1807,7 @@ void mo2_read(fuse_req_t req, fuse_ino_t /*ino*/, size_t size, off_t off,
fuse_reply_err(req, EINVAL);
return;
}
- OpTimer _t(&ctx->read_ns);
+ OpTimer _t(&ctx->read_ns, "read");
ctx->read_count.fetch_add(1, std::memory_order_relaxed);
maybeLogCounters(ctx);
@@ -1636,7 +1826,9 @@ void mo2_read(fuse_req_t req, fuse_ino_t /*ino*/, size_t size, off_t off,
realPath = it->second.real_path;
isBacking = it->second.is_backing;
writable = it->second.writable;
+ _t.path = it->second.relative_path;
}
+ ctx->read_bytes.fetch_add(static_cast<uint64_t>(size), std::memory_order_relaxed);
int localFd = fd;
if (localFd < 0) {
@@ -1696,6 +1888,9 @@ void mo2_write(fuse_req_t req, fuse_ino_t /*ino*/, const char* buf, size_t size,
fuse_reply_err(req, EINVAL);
return;
}
+ OpTimer _t(&ctx->write_ns, "write");
+ ctx->write_count.fetch_add(1, std::memory_order_relaxed);
+ maybeLogCounters(ctx);
int fd = -1;
std::string relativePath;
@@ -1716,6 +1911,7 @@ void mo2_write(fuse_req_t req, fuse_ino_t /*ino*/, const char* buf, size_t size,
isBacking = it->second.is_backing;
relativePath = it->second.relative_path;
realPath = it->second.real_path;
+ _t.path = relativePath;
}
if (!writable) {
@@ -1727,6 +1923,7 @@ void mo2_write(fuse_req_t req, fuse_ino_t /*ino*/, const char* buf, size_t size,
// stay in place so config writes update the source mod rather than creating
// a shadow copy in overwrite.
if (cowPending) {
+ ctx->cow_write_count.fetch_add(1, std::memory_order_relaxed);
try {
std::string newPath;
if (isBacking && ctx->backing_dir_fd >= 0) {
@@ -1772,6 +1969,8 @@ void mo2_write(fuse_req_t req, fuse_ino_t /*ino*/, const char* buf, size_t size,
fuse_reply_err(req, EIO);
return;
}
+ ctx->write_bytes.fetch_add(static_cast<uint64_t>(written),
+ std::memory_order_relaxed);
if (!cowPending) {
// Update VFS tree size/mtime for in-place writes.
@@ -1788,6 +1987,9 @@ void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode
fuse_reply_err(req, EINVAL);
return;
}
+ OpTimer _t(&ctx->create_ns, "create");
+ ctx->create_count.fetch_add(1, std::memory_order_relaxed);
+ maybeLogCounters(ctx);
bool ok = false;
const std::string parentPath = inodeToPath(ctx, parent, &ok);
@@ -1798,6 +2000,7 @@ void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode
const std::string relative =
joinPath(parentPath, canonicalChildName(ctx, parentPath, name));
+ _t.path = relative;
std::string realPath;
@@ -1892,6 +2095,9 @@ void mo2_rename(fuse_req_t req, fuse_ino_t parent, const char* name,
fuse_reply_err(req, EINVAL);
return;
}
+ OpTimer _t(&ctx->rename_ns, "rename");
+ ctx->rename_count.fetch_add(1, std::memory_order_relaxed);
+ maybeLogCounters(ctx);
// Reject unsupported flags (only RENAME_NOREPLACE is supported).
// Wine uses renameat2(RENAME_NOREPLACE) for MoveFileW() calls where
@@ -1913,6 +2119,7 @@ void mo2_rename(fuse_req_t req, fuse_ino_t parent, const char* name,
const std::string oldRelative = joinPath(parentPath, canonicalChildName(ctx, parentPath, name));
const std::string newRelative =
joinPath(newParentPath, canonicalChildName(ctx, newParentPath, newname));
+ _t.path = oldRelative + " -> " + newRelative;
const auto oldSnap = snapshotForPath(ctx, oldRelative);
if (!oldSnap.found) {
@@ -2009,8 +2216,12 @@ void mo2_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set,
fuse_reply_err(req, EINVAL);
return;
}
+ OpTimer _t(&ctx->setattr_ns, "setattr");
+ ctx->setattr_count.fetch_add(1, std::memory_order_relaxed);
+ maybeLogCounters(ctx);
if (ino == 1) {
+ _t.path = "/";
struct stat st;
fillStatForDir(&st, 1, ctx->uid, ctx->gid);
fuse_reply_attr(req, &st, TTL_SECONDS);
@@ -2019,6 +2230,7 @@ void mo2_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set,
bool ok = false;
const std::string path = inodeToPath(ctx, ino, &ok);
+ _t.path = path;
if (!ok) {
fuse_reply_err(req, ENOENT);
return;
@@ -2221,6 +2433,9 @@ void mo2_unlink(fuse_req_t req, fuse_ino_t parent, const char* name)
fuse_reply_err(req, EINVAL);
return;
}
+ OpTimer _t(&ctx->unlink_ns, "unlink");
+ ctx->unlink_count.fetch_add(1, std::memory_order_relaxed);
+ maybeLogCounters(ctx);
bool ok = false;
const std::string parentPath = inodeToPath(ctx, parent, &ok);
@@ -2230,6 +2445,7 @@ void mo2_unlink(fuse_req_t req, fuse_ino_t parent, const char* name)
}
const std::string relative = joinPath(parentPath, canonicalChildName(ctx, parentPath, name));
+ _t.path = relative;
if (!ctx->overwrite->removeFile(relative)) {
const auto snap = snapshotForPath(ctx, relative);
if (!snap.found || snap.is_directory) {
@@ -2423,6 +2639,9 @@ void mo2_flush(fuse_req_t req, fuse_ino_t /*ino*/, struct fuse_file_info* fi)
fuse_reply_err(req, EINVAL);
return;
}
+ OpTimer _t(&ctx->flush_ns, "flush");
+ ctx->flush_count.fetch_add(1, std::memory_order_relaxed);
+ maybeLogCounters(ctx);
int fd = -1;
bool writable = false;
@@ -2435,6 +2654,7 @@ void mo2_flush(fuse_req_t req, fuse_ino_t /*ino*/, struct fuse_file_info* fi)
}
fd = it->second.fd;
writable = it->second.writable;
+ _t.path = it->second.relative_path;
}
if (writable && fd >= 0 && fdatasync(fd) != 0) {
@@ -2452,6 +2672,9 @@ void mo2_fsync(fuse_req_t req, fuse_ino_t /*ino*/, int datasync,
fuse_reply_err(req, EINVAL);
return;
}
+ OpTimer _t(&ctx->fsync_ns, "fsync");
+ ctx->fsync_count.fetch_add(1, std::memory_order_relaxed);
+ maybeLogCounters(ctx);
int fd = -1;
{
@@ -2462,6 +2685,7 @@ void mo2_fsync(fuse_req_t req, fuse_ino_t /*ino*/, int datasync,
return;
}
fd = it->second.fd;
+ _t.path = it->second.relative_path;
}
if (fd >= 0) {
diff --git a/src/src/vfs/mo2filesystem.h b/src/src/vfs/mo2filesystem.h
index 39e4b32..b52f5ad 100644
--- a/src/src/vfs/mo2filesystem.h
+++ b/src/src/vfs/mo2filesystem.h
@@ -112,6 +112,13 @@ struct Mo2FsContext
std::atomic<uint64_t> readdir_count{0};
std::atomic<uint64_t> open_count{0};
std::atomic<uint64_t> read_count{0};
+ std::atomic<uint64_t> write_count{0};
+ std::atomic<uint64_t> create_count{0};
+ std::atomic<uint64_t> rename_count{0};
+ std::atomic<uint64_t> setattr_count{0};
+ std::atomic<uint64_t> unlink_count{0};
+ std::atomic<uint64_t> flush_count{0};
+ std::atomic<uint64_t> fsync_count{0};
std::atomic<uint64_t> ioctl_count{0};
std::atomic<uint64_t> op_tick{0};
// Cumulative wall-clock nanoseconds spent inside each op handler.
@@ -121,12 +128,30 @@ struct Mo2FsContext
std::atomic<uint64_t> readdir_ns{0};
std::atomic<uint64_t> open_ns{0};
std::atomic<uint64_t> read_ns{0};
+ std::atomic<uint64_t> write_ns{0};
+ std::atomic<uint64_t> create_ns{0};
+ std::atomic<uint64_t> rename_ns{0};
+ std::atomic<uint64_t> setattr_ns{0};
+ std::atomic<uint64_t> unlink_ns{0};
+ std::atomic<uint64_t> flush_ns{0};
+ std::atomic<uint64_t> fsync_ns{0};
std::atomic<uint64_t> lookup_cache_hits{0};
std::atomic<uint64_t> lookup_cache_misses{0};
std::atomic<uint64_t> lookup_cache_invalidations{0};
+ std::atomic<uint64_t> attr_cache_hits{0};
+ std::atomic<uint64_t> attr_cache_misses{0};
+ std::atomic<uint64_t> dir_cache_hits{0};
+ std::atomic<uint64_t> dir_cache_misses{0};
+ std::atomic<uint64_t> readdir_blob_hits{0};
+ std::atomic<uint64_t> readdirplus_blob_hits{0};
std::atomic<uint64_t> lazy_ro_fd_opens{0};
std::atomic<uint64_t> retained_ro_fd_hits{0};
std::atomic<uint64_t> retained_ro_fd_evictions{0};
+ std::atomic<uint64_t> read_bytes{0};
+ std::atomic<uint64_t> write_bytes{0};
+ std::atomic<uint64_t> cow_write_count{0};
+ std::atomic<uint64_t> uring_request_count{0};
+ std::atomic<uint64_t> legacy_request_count{0};
// CPU snapshot from previous stats tick (microseconds, from getrusage).
// Used to compute per-tick CPU delta so we can distinguish disk-bound vs
// CPU-bound slowness in the VFS layer.