diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-06 13:58:17 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-06 13:58:17 -0500 |
| commit | 28d1072ebb43dd35f7a36fc852e5dc3f1fe30b19 (patch) | |
| tree | 1279fc12376ec9a8be612b05747a6685bca07cc0 /src | |
| parent | da10828d0d63718cba1532ae819a3cc16ebe30db (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.cpp | 1 | ||||
| -rw-r--r-- | src/src/vfs/mo2filesystem.cpp | 329 | ||||
| -rw-r--r-- | src/src/vfs/mo2filesystem.h | 26 |
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 |
