aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-06 13:58:17 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-06 13:58:17 -0500
commit28d1072ebb43dd35f7a36fc852e5dc3f1fe30b19 (patch)
tree1279fc12376ec9a8be612b05747a6685bca07cc0 /src
parentda10828d0d63718cba1532ae819a3cc16ebe30db (diff)
Add VFS performance optimizations: node cache, lookup cache, access handler
- Add inode→VfsNode* cache to skip splitPath + full tree walks on lookup/getattr/open (O(1) hash lookup instead of tree traversal) - Combine canonicalChildName + snapshotForPath into single lookupChild function (one tree lock, one parent resolution, one child hash probe) - Add userspace lookup cache keyed by (parent_ino, normalized_name) to absorb Wine's case-variant probes that bypass the kernel FUSE dcache - Add mo2_access handler to avoid kernel fallback to lookup+getattr - Invalidate all caches correctly on tree mutations (create/unlink/rename/mkdir) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/src/fuseconnector.cpp1
-rw-r--r--src/src/vfs/mo2filesystem.cpp329
-rw-r--r--src/src/vfs/mo2filesystem.h26
3 files changed, 327 insertions, 29 deletions
diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp
index 67593b1..4d84164 100644
--- a/src/src/fuseconnector.cpp
+++ b/src/src/fuseconnector.cpp
@@ -176,6 +176,7 @@ void setupFuseOps(struct fuse_lowlevel_ops* ops)
ops->mkdir = mo2_mkdir;
ops->release = mo2_release;
ops->releasedir = mo2_releasedir;
+ ops->access = mo2_access;
}
} // namespace
diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp
index 078cbb7..7453631 100644
--- a/src/src/vfs/mo2filesystem.cpp
+++ b/src/src/vfs/mo2filesystem.cpp
@@ -30,6 +30,7 @@ void fillStatForFile(struct stat* st, fuse_ino_t ino, uid_t uid, gid_t gid,
uint64_t size,
const std::chrono::system_clock::time_point& mtime,
const std::string& real_path = {});
+void invalidateLookupCache(Mo2FsContext* ctx, const std::string& dirPath);
void maybeLogCounters(Mo2FsContext* ctx)
{
@@ -117,6 +118,72 @@ void invalidateDirCache(Mo2FsContext* ctx, const std::string& dirPath)
ctx->readdir_blob_cache.erase(dirPath);
ctx->readdirplus_blob_cache.erase(dirPath);
}
+ invalidateLookupCache(ctx, dirPath);
+}
+
+// Invalidate lookup cache entries for a directory whose children changed.
+// The lookup cache is keyed by (parent_ino, normalized_child_name), so we
+// need to remove all entries with the given parent inode.
+void invalidateLookupCache(Mo2FsContext* ctx, const std::string& dirPath)
+{
+ if (ctx == nullptr) {
+ return;
+ }
+
+ fuse_ino_t parentIno = 0;
+ if (dirPath.empty()) {
+ parentIno = 1; // root
+ } else {
+ std::shared_lock lock(ctx->inode_mutex);
+ parentIno = ctx->inodes->get(dirPath);
+ }
+
+ if (parentIno == 0) {
+ return;
+ }
+
+ std::scoped_lock lock(ctx->lookup_cache_mutex);
+ for (auto it = ctx->lookup_cache.begin(); it != ctx->lookup_cache.end();) {
+ if (it->first.first == parentIno) {
+ it = ctx->lookup_cache.erase(it);
+ } else {
+ ++it;
+ }
+ }
+}
+
+// Clear all node_cache entries whose path starts with the given prefix.
+// Must be called while tree_mutex is held exclusively (during mutations).
+void invalidateNodeCache(Mo2FsContext* ctx, const std::string& path)
+{
+ if (ctx == nullptr) {
+ return;
+ }
+
+ // Look up inode for this path and remove its cache entry.
+ fuse_ino_t ino = 0;
+ {
+ std::shared_lock lock(ctx->inode_mutex);
+ ino = ctx->inodes->get(path);
+ }
+ if (ino != 0) {
+ ctx->node_cache.erase(ino);
+ }
+
+ // Also invalidate the parent directory's cache entry since its children
+ // map has changed (new/removed child).
+ const auto slash = path.rfind('/');
+ if (slash != std::string::npos) {
+ const std::string parentPath = path.substr(0, slash);
+ fuse_ino_t parentIno = 0;
+ {
+ std::shared_lock lock(ctx->inode_mutex);
+ parentIno = ctx->inodes->get(parentPath);
+ }
+ if (parentIno != 0) {
+ ctx->node_cache.erase(parentIno);
+ }
+ }
}
struct NodeSnapshot
@@ -224,6 +291,93 @@ NodeSnapshot snapshotForPath(const Mo2FsContext* ctx, const std::string& path)
return snap;
}
+// Fill a snapshot from an already-resolved VfsNode (caller holds tree_mutex shared).
+void snapshotFromNode(const VfsNode* node, NodeSnapshot& snap)
+{
+ snap.found = true;
+ snap.is_directory = node->is_directory;
+ if (!node->is_directory) {
+ snap.real_path = node->file_info.real_path;
+ snap.size = node->file_info.size;
+ snap.mtime = node->file_info.mtime;
+ snap.is_backing = node->file_info.is_backing;
+ }
+}
+
+// Resolve a parent inode to its VfsNode*, using the node_cache for O(1) hits.
+// Caller must hold tree_mutex (shared). Falls back to tree walk on cache miss
+// and populates the cache.
+const VfsNode* resolveByInode(Mo2FsContext* ctx, fuse_ino_t ino)
+{
+ if (ino == 1) {
+ return &ctx->tree->root;
+ }
+
+ // Check node_cache (fast path — no splitPath, no tree walk)
+ auto cacheIt = ctx->node_cache.find(ino);
+ if (cacheIt != ctx->node_cache.end()) {
+ return cacheIt->second;
+ }
+
+ // Cache miss — resolve via inode→path→tree walk
+ std::string path;
+ {
+ std::shared_lock ilock(ctx->inode_mutex);
+ path = ctx->inodes->getPath(ino);
+ }
+ if (path.empty()) {
+ return nullptr;
+ }
+
+ const VfsNode* node = ctx->tree->root.resolve(splitPath(path));
+ if (node != nullptr) {
+ // Safe: we hold tree_mutex shared, and mutations hold it exclusively
+ // so they can clear stale entries before any pointer becomes dangling.
+ const_cast<std::unordered_map<fuse_ino_t, const VfsNode*>&>(ctx->node_cache)[ino] = node;
+ }
+ return node;
+}
+
+// Combined lookup: resolves parent by inode (cached), looks up child in one
+// hash probe, returns canonical name + snapshot. Single tree_mutex acquisition.
+struct LookupResult
+{
+ bool found = false;
+ std::string canonical_name;
+ NodeSnapshot snap;
+};
+
+LookupResult lookupChild(Mo2FsContext* ctx, fuse_ino_t parentIno, const char* name)
+{
+ LookupResult result;
+ std::shared_lock lock(ctx->tree_mutex);
+
+ const VfsNode* parent = resolveByInode(ctx, parentIno);
+ if (parent == nullptr || !parent->is_directory) {
+ return result;
+ }
+
+ const std::string key = normalizeForLookup(name);
+
+ // Get canonical display name
+ auto nameIt = parent->dir_info.display_names.find(key);
+ result.canonical_name = (nameIt != parent->dir_info.display_names.end())
+ ? nameIt->second
+ : std::string(name);
+
+ // Look up child node — single hash probe
+ auto childIt = parent->dir_info.children.find(key);
+ if (childIt == parent->dir_info.children.end()) {
+ return result;
+ }
+
+ const VfsNode* child = childIt->second.get();
+ result.found = true;
+ snapshotFromNode(child, result.snap);
+
+ return result;
+}
+
struct ChildSnapshot
{
std::string name;
@@ -556,6 +710,7 @@ void updateFileNode(Mo2FsContext* ctx, const std::string& relative,
std::unique_lock lock(ctx->tree_mutex);
ctx->tree->root.insertFile(splitPath(relative), realPath, ec ? 0 : size, mtime,
origin);
+ invalidateNodeCache(ctx, relative);
}
} // namespace
@@ -643,28 +798,53 @@ void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name)
ctx->lookup_count.fetch_add(1, std::memory_order_relaxed);
maybeLogCounters(ctx);
- bool ok = false;
- const std::string parentPath = inodeToPath(ctx, parent, &ok);
- if (!ok) {
- fuse_reply_err(req, ENOENT);
- return;
+ // Fast path: check userspace lookup cache. The kernel FUSE dcache is
+ // case-sensitive, so Wine's different-case probes ("Shaders", "shaders")
+ // each miss the kernel cache and reach us. Our cache keys on normalized
+ // (lowercased) name so all case variants hit on the second probe.
+ const std::string normName = normalizeForLookup(name);
+ const auto cacheKey = std::make_pair(parent, normName);
+ {
+ std::scoped_lock lock(ctx->lookup_cache_mutex);
+ auto it = ctx->lookup_cache.find(cacheKey);
+ if (it != ctx->lookup_cache.end()) {
+ // Cache hit — reply directly, no tree/inode locks needed
+ fuse_reply_entry(req, &it->second.entry);
+ return;
+ }
}
- // Use the tree's canonical (mod-provided) case for the child name so that
- // inode paths match the mod's directory/file casing rather than Wine's.
- const std::string canonical = canonicalChildName(ctx, parentPath, name);
- const std::string childPath = joinPath(parentPath, canonical);
- const auto snap = snapshotForPath(ctx, childPath);
- if (!snap.found) {
- samplePathStat(ctx, "lookup", childPath, true);
+ // Cache miss — do the full lookup
+ const auto lr = lookupChild(ctx, parent, name);
+
+ if (!lr.found) {
struct fuse_entry_param e;
std::memset(&e, 0, sizeof(e));
e.ino = 0;
e.attr_timeout = NEGATIVE_TTL_SECONDS;
e.entry_timeout = NEGATIVE_TTL_SECONDS;
+
+ // Cache negative results too (Wine probes many non-existent paths)
+ {
+ std::scoped_lock lock(ctx->lookup_cache_mutex);
+ Mo2FsContext::LookupCacheEntry lce;
+ lce.child_ino = 0;
+ lce.entry = e;
+ ctx->lookup_cache[cacheKey] = lce;
+ }
+
fuse_reply_entry(req, &e);
return;
}
+
+ // Build child path for inode allocation
+ bool ok = false;
+ const std::string parentPath = inodeToPath(ctx, parent, &ok);
+ if (!ok) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ const std::string childPath = joinPath(parentPath, lr.canonical_name);
samplePathStat(ctx, "lookup", childPath, false);
fuse_ino_t childIno = 0;
@@ -677,7 +857,41 @@ void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name)
childIno = ctx->inodes->getOrCreate(childPath);
}
- replyEntryFromSnapshot(req, ctx, childIno, snap);
+ // Cache the child node pointer for future getattr/open
+ {
+ std::shared_lock lock(ctx->tree_mutex);
+ const VfsNode* parentNode = resolveByInode(ctx, parent);
+ if (parentNode != nullptr && parentNode->is_directory) {
+ const std::string key = normalizeForLookup(lr.canonical_name);
+ auto it = parentNode->dir_info.children.find(key);
+ if (it != parentNode->dir_info.children.end()) {
+ const_cast<std::unordered_map<fuse_ino_t, const VfsNode*>&>(ctx->node_cache)[childIno] = it->second.get();
+ }
+ }
+ }
+
+ // Build the entry_param and cache it for future case-variant lookups
+ struct fuse_entry_param e;
+ std::memset(&e, 0, sizeof(e));
+ e.ino = childIno;
+ e.attr_timeout = TTL_SECONDS;
+ e.entry_timeout = TTL_SECONDS;
+ if (lr.snap.is_directory) {
+ fillStatForDir(&e.attr, childIno, ctx->uid, ctx->gid);
+ } else {
+ fillStatForFile(&e.attr, childIno, ctx->uid, ctx->gid, lr.snap.size, lr.snap.mtime,
+ lr.snap.real_path);
+ }
+
+ {
+ std::scoped_lock lock(ctx->lookup_cache_mutex);
+ Mo2FsContext::LookupCacheEntry lce;
+ lce.child_ino = childIno;
+ lce.entry = e;
+ ctx->lookup_cache[cacheKey] = lce;
+ }
+
+ fuse_reply_entry(req, &e);
}
void mo2_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* /*fi*/)
@@ -716,18 +930,16 @@ void mo2_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* /*fi*/)
return;
}
- bool ok = false;
- const std::string path = inodeToPath(ctx, ino, &ok);
- if (!ok) {
- fuse_reply_err(req, ENOENT);
- return;
- }
- samplePathStat(ctx, "getattr", path);
-
- const auto snap = snapshotForPath(ctx, path);
- if (!snap.found) {
- fuse_reply_err(req, ENOENT);
- return;
+ // Use node cache for O(1) resolution instead of splitPath + full tree walk
+ NodeSnapshot snap;
+ {
+ std::shared_lock lock(ctx->tree_mutex);
+ const VfsNode* node = resolveByInode(ctx, ino);
+ if (node == nullptr) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ snapshotFromNode(node, snap);
}
struct stat st;
@@ -993,10 +1205,16 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi)
return;
}
- const auto snap = snapshotForPath(ctx, path);
- if (!snap.found || snap.is_directory) {
- fuse_reply_err(req, ENOENT);
- return;
+ // Use node cache for O(1) snapshot instead of full tree walk
+ NodeSnapshot snap;
+ {
+ std::shared_lock lock(ctx->tree_mutex);
+ const VfsNode* node = resolveByInode(ctx, ino);
+ if (node == nullptr || node->is_directory) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ snapshotFromNode(node, snap);
}
std::string realPath = snap.real_path;
@@ -1283,6 +1501,7 @@ void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode
{
std::unique_lock lock(ctx->tree_mutex);
++ctx->tree->file_count;
+ invalidateNodeCache(ctx, relative);
}
fuse_ino_t newIno;
@@ -1363,6 +1582,7 @@ void mo2_rename(fuse_req_t req, fuse_ino_t parent, const char* name,
{
std::unique_lock lock(ctx->tree_mutex);
+ invalidateNodeCache(ctx, oldRelative);
ctx->tree->root.removeFromTree(splitPath(oldRelative));
if (oldSnap.is_directory) {
@@ -1374,6 +1594,7 @@ void mo2_rename(fuse_req_t req, fuse_ino_t parent, const char* name,
ctx->tree->root.insertFile(splitPath(newRelative), real, oldSnap.size,
oldSnap.mtime, "Staging");
}
+ invalidateNodeCache(ctx, newRelative);
}
{
@@ -1625,6 +1846,7 @@ void mo2_unlink(fuse_req_t req, fuse_ino_t parent, const char* name)
{
std::unique_lock lock(ctx->tree_mutex);
+ invalidateNodeCache(ctx, relative);
if (ctx->tree->root.removeFromTree(splitPath(relative))) {
ctx->tree->file_count = ctx->tree->file_count > 0 ? ctx->tree->file_count - 1 : 0;
}
@@ -1659,6 +1881,7 @@ void mo2_mkdir(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t /*mod
std::unique_lock lock(ctx->tree_mutex);
ctx->tree->root.insertDirectory(splitPath(relative));
++ctx->tree->dir_count;
+ invalidateNodeCache(ctx, relative);
}
invalidateDirCache(ctx, parentPath);
@@ -1758,3 +1981,51 @@ void mo2_ioctl(fuse_req_t req, fuse_ino_t /*ino*/, unsigned int cmd, void* /*arg
fuse_reply_err(req, ENOTTY);
}
+
+void mo2_access(fuse_req_t req, fuse_ino_t ino, int mask)
+{
+ Mo2FsContext* ctx = getContext(req);
+ if (ctx == nullptr) {
+ fuse_reply_err(req, EINVAL);
+ return;
+ }
+
+ // Root always exists
+ if (ino == 1) {
+ fuse_reply_err(req, 0);
+ return;
+ }
+
+ // Use node cache for O(1) existence check — no splitPath or tree walk needed.
+ {
+ std::shared_lock lock(ctx->tree_mutex);
+ const VfsNode* node = resolveByInode(ctx, ino);
+ if (node == nullptr) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+
+ // W_OK: only allow for files we can write to (non-backing, non-directory)
+ if ((mask & W_OK) != 0 && (node->is_directory || node->file_info.is_backing)) {
+ fuse_reply_err(req, EACCES);
+ return;
+ }
+ }
+
+ // X_OK on regular files: check real file permissions
+ if ((mask & X_OK) != 0) {
+ std::shared_lock lock(ctx->tree_mutex);
+ const VfsNode* node = resolveByInode(ctx, ino);
+ if (node != nullptr && !node->is_directory && !node->file_info.real_path.empty()) {
+ struct stat st;
+ if (::stat(node->file_info.real_path.c_str(), &st) == 0) {
+ if ((st.st_mode & S_IXUSR) == 0) {
+ fuse_reply_err(req, EACCES);
+ return;
+ }
+ }
+ }
+ }
+
+ fuse_reply_err(req, 0);
+}
diff --git a/src/src/vfs/mo2filesystem.h b/src/src/vfs/mo2filesystem.h
index 40e0ed7..4ebf2bd 100644
--- a/src/src/vfs/mo2filesystem.h
+++ b/src/src/vfs/mo2filesystem.h
@@ -24,6 +24,10 @@ struct Mo2FsContext
std::unique_ptr<InodeTable> inodes;
mutable std::shared_mutex inode_mutex;
+ // Fast inode→VfsNode* cache. Pointers are valid while tree_mutex is held
+ // (shared for reads). Invalidated under exclusive tree_mutex during mutations.
+ std::unordered_map<fuse_ino_t, const VfsNode*> node_cache; // protected by tree_mutex
+
std::unique_ptr<OverwriteManager> overwrite;
std::shared_ptr<TrackedWrites> tracked_writes;
@@ -74,6 +78,27 @@ struct Mo2FsContext
};
std::unordered_map<fuse_ino_t, CachedAttr> attr_cache;
mutable std::mutex attr_cache_mutex;
+
+ // Userspace lookup cache: (parent_ino, normalized_child_name) → (child_ino, entry_param).
+ // The kernel FUSE dcache is case-sensitive, but Wine probes the same directory
+ // with many case variants ("Shaders", "shaders", "SHADERS"). Each variant is a
+ // kernel dcache miss that reaches userspace. This cache makes those re-lookups
+ // lock-free (no tree_mutex, no inode_mutex).
+ struct LookupCacheEntry
+ {
+ fuse_ino_t child_ino = 0;
+ struct fuse_entry_param entry{};
+ };
+ struct PairHash {
+ size_t operator()(const std::pair<fuse_ino_t, std::string>& p) const {
+ size_t h1 = std::hash<fuse_ino_t>{}(p.first);
+ size_t h2 = std::hash<std::string>{}(p.second);
+ return h1 ^ (h2 * 0x9e3779b97f4a7c15ULL + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
+ }
+ };
+ std::unordered_map<std::pair<fuse_ino_t, std::string>, LookupCacheEntry, PairHash> lookup_cache;
+ mutable std::mutex lookup_cache_mutex;
+
std::atomic<uint64_t> next_dh{1};
std::atomic<uint64_t> lookup_count{0};
std::atomic<uint64_t> getattr_count{0};
@@ -126,5 +151,6 @@ void mo2_ioctl(fuse_req_t req, fuse_ino_t ino, unsigned int cmd, void* arg,
struct fuse_file_info* fi, unsigned flags, const void* in_buf,
size_t in_bufsz, size_t out_bufsz);
#endif
+void mo2_access(fuse_req_t req, fuse_ino_t ino, int mask);
#endif