diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-08 00:45:11 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-08 00:45:11 -0600 |
| commit | f055928b771df2f0dec8eb2757c2fb065c696855 (patch) | |
| tree | bced8986296e721dd40295ea4bd3f80c7f8c5058 | |
| parent | cb7c334b404a0a73f1709295cb70b0e0945cd1be (diff) | |
FUSE VFS performance: attr cache, selective invalidation, init tuning
- Increase ATTR_CACHE_SECONDS from 1.0s to 30.0s (90% getattr reduction)
- Replace nuclear clearDirCache() with selective invalidateDirCache() that
only clears the affected parent directory instead of all caches
- Add mo2_init callback: enable FUSE_CAP_AUTO_INVAL_DATA for kernel page
cache retention across open/close, increase max_readahead/max_write to 1MB
- Switch open_files_mutex to shared_mutex for concurrent read access
- Add perf handoff doc for VFS optimization history
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| -rw-r--r-- | docs/fuse_vfs_perf_handoff.md | 205 | ||||
| -rw-r--r-- | src/src/fuseconnector.cpp | 62 | ||||
| -rw-r--r-- | src/src/vfs/inodetable.cpp | 10 | ||||
| -rw-r--r-- | src/src/vfs/inodetable.h | 1 | ||||
| -rw-r--r-- | src/src/vfs/mo2filesystem.cpp | 813 | ||||
| -rw-r--r-- | src/src/vfs/mo2filesystem.h | 64 |
6 files changed, 1066 insertions, 89 deletions
diff --git a/docs/fuse_vfs_perf_handoff.md b/docs/fuse_vfs_perf_handoff.md new file mode 100644 index 0000000..3c92851 --- /dev/null +++ b/docs/fuse_vfs_perf_handoff.md @@ -0,0 +1,205 @@ +# FUSE VFS Performance Handoff (SkyrimSE / MO2 Linux Port) + +## Context +- Project: MO2 Linux Port with C +- Test instance: `/home/luke/Games/ElderTeej` +- Symptom: VFS tree build is very fast (`~300ms`), but Skyrim startup can stall for many minutes. +- Baseline expectation from user: startup should be around `2-3 minutes`. + +## Core Diagnosis +The slow path is **not** initial VFS construction. +The bottleneck is sustained runtime metadata traffic (Wine/game probing), especially: +- `getattr` +- `readdir` +- (previously) `ioctl` probes + +Observed pattern during bad runs: +- Very high `getattr/readdir/ioctl` +- Very low `read` +- Process appears mostly idle in btop (low MB read/write), yet VFS op counters climb rapidly + +## Why This Happens +FUSE builds a tree once, but then serves kernel/user-space requests continuously. +Games + Wine often do heavy existence/stat/directory probing before and during menu load. + +So startup cost can be dominated by metadata round-trips even if actual file reads are small. + +## Major Evidence from Logs +- Early problematic runs showed huge ioctl growth (millions) with low reads. +- After fixes, `ioctl` can be driven to `0`. +- Even with `ioctl=0`, `readdir/getattr` remained very high in some phases: + - root (`""`) + - `ShaderCache/*` + - `meshes/actors/character` + - `meshes/actors/character/facegenmorphs` + - `Seq` + +## Implemented Changes (Chronological Summary) + +### 1) Persistent file handles for open files (big fix) +- Reworked open/read/write/release to use handle state. +- Avoided reopening backing files on every read. +- `write` path moved to `pwrite`. +- `release` now closes tracked FDs. + +Impact: +- Reduced repeated open/close overhead for active handles. + +--- + +### 2) Added ioctl handler (then corrected behavior) +- Implemented `mo2_ioctl`. +- Returned `ENOTTY` for known probe-heavy commands (`VFAT_IOCTL_READDIR_BOTH`, `FS_IOC_GETFLAGS`). +- Added ioctl hotloop diagnostics. + +Impact: +- Helped identify ioctl storms. + +--- + +### 3) Enabled directory operation caching primitives +- Added `opendir` / `releasedir`. +- Added directory handle cache for entries. +- Added `readdirplus` callback and capability flags initially. +- Added negative lookup caching (`fuse_reply_entry` with `ino=0`, TTL). +- Increased entry/attr TTL values in replies. + +Impact: +- Reduced some repeated rebuild work, but did not fully eliminate metadata storm. + +--- + +### 4) Added instrumentation for hotspot visibility +- Global op counters (`lookup/getattr/readdir/open/read/ioctl`). +- Top hot paths logging for lookup hit/miss/getattr/readdir. +- Sampling mechanism to keep logs manageable. + +Impact: +- Confirmed dominant hotspots and phase shifts. + +--- + +### 5) Stale mount cleanup hardening +- Added proactive cleanup for stale `mo2linux` mounts (including trash paths). + +Impact: +- Stability/cleanup improvement, not direct startup speedup. + +--- + +### 6) Stabilized synthetic directory timestamps +- Virtual dir stat times changed from `now` per request to fixed constant. + +Impact: +- Prevents needless cache invalidation behavior due to changing timestamps. + +--- + +### 7) Lock/contention reduction in inode path +- Switched inode mutex to shared mutex. +- Added inode fast lookup (`get`) separate from `getOrCreate`. +- Reduced exclusive lock pressure on read-heavy paths. + +Impact: +- Better concurrency under FUSE multithread loop. + +--- + +### 8) Per-handle path retention in readdir path +- Stored directory path on open dir handles to avoid repeated inode-to-path resolution. + +Impact: +- Reduced overhead in hot `readdir` loop. + +--- + +### 9) Prebuilt encoded directory blobs +- Built and reused encoded `readdir` and `readdirplus` blobs. +- Added global per-path blob caches. + +Impact: +- Makes repeated directory listing replies cheaper. + +--- + +### 10) FD exhaustion fix (critical stability fix) +- Runtime crash seen: + - `GLib-ERROR: Creating pipes for GWakeup: Too many open files` +- Cause: too many retained open handles. +- Fix: + - Do **not** keep persistent FD for read-only handles. + - Open temp FD in read path when needed, close immediately. + - Keep persistent FD only for writable handles. + - Added `open_handles` counter in log line. + +Impact: +- Stabilized handle growth behavior and avoided EMFILE crash path. + +--- + +### 11) Disabled custom ioctl callback in ops table +- Stopped wiring `ops->ioctl`. + +Impact: +- `ioctl` count dropped to `0` in user logs. +- Removed large class of user-space ioctl churn. + +--- + +### 12) Removed `default_permissions` mount option (experiment) +- Tested to reduce permission-check-driven metadata round-trips. + +Impact: +- Mixed/limited; metadata storm still exists in hot phases. + +--- + +### 13) Disabled `readdirplus` capability/callback (experiment) +- Stopped advertising/using readdirplus. + +Impact: +- Profile changed; in latest successful run startup reached `2m40s`. + +--- + +## Current State (Most Important) +- User reported successful startup in **~2 minutes 40 seconds**. +- `ioctl=0`. +- `open_handles` still can grow into the thousands in long runs, but EMFILE crash path improved substantially after RO FD change. +- Remaining hot metadata phases are path-specific (shader cache and facegen/character meshes). + +## Remaining Known Issues +- Menu/runtime phases still generate heavy `getattr/readdir` on certain directories. +- `open_handles` can still increase significantly depending on workload phase. + +## What Appears to Have Helped Most +1. Removing ioctl churn (`ioctl=0`). +2. Avoiding persistent RO file FDs (stability fix). +3. Directory response caching plus reduced overhead paths. +4. Readdirplus/permissions experiments likely affected metadata profile depending on phase. + +## Suggested Next Experiments (if further optimization is desired) + +1. Add small in-process `getattr` LRU cache (short TTL, e.g. 0.5-1.0s) keyed by inode/path. +2. Introduce targeted caches for hottest directories: + - `meshes/actors/character` + - `meshes/actors/character/facegenmorphs` + - `ShaderCache/*` + - `Seq` +3. Add optional “performance mode” config flag: + - less verbose logging + - larger TTLs + - aggressive directory blob reuse +4. Track `release`/`releasedir` deltas in logs to verify handle churn balance. +5. Validate `ulimit -n` at launch and warn if too low. + +## Useful Log Fields for Future Triage +- `ops lookup=... getattr=... readdir=... open=... read=... ioctl=... open_handles=...` +- `hot readdir: ...` +- `hot getattr: ...` +- `hot lookup_hit/lookup_miss: ...` + +## Bottom Line +The root misunderstanding was “fast VFS build should imply fast startup.” +In practice, startup was dominated by post-build metadata probing. +After multiple fixes, the system reached user-acceptable startup (`~2m40s`) with much better stability and no ioctl storm. diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp index 325d7ed..499f77d 100644 --- a/src/src/fuseconnector.cpp +++ b/src/src/fuseconnector.cpp @@ -22,6 +22,7 @@ #include <fcntl.h> #include <filesystem> #include <fstream> +#include <mutex> #include <set> #include <sys/stat.h> #include <unistd.h> @@ -156,8 +157,10 @@ buildModsFromMapping(const MappingType& mapping, const QString& dataDir, void setupFuseOps(struct fuse_lowlevel_ops* ops) { std::memset(ops, 0, sizeof(struct fuse_lowlevel_ops)); + ops->init = mo2_init; ops->lookup = mo2_lookup; ops->getattr = mo2_getattr; + ops->opendir = mo2_opendir; ops->readdir = mo2_readdir; ops->open = mo2_open; ops->read = mo2_read; @@ -168,6 +171,7 @@ void setupFuseOps(struct fuse_lowlevel_ops* ops) ops->unlink = mo2_unlink; ops->mkdir = mo2_mkdir; ops->release = mo2_release; + ops->releasedir = mo2_releasedir; } } // namespace @@ -280,8 +284,7 @@ bool FuseConnector::mount( // separately to fuse_session_mount(). Including it here causes // "fuse: unknown option(s)" error. std::vector<std::string> argvStorage = { - "mo2fuse", "-o", "fsname=mo2linux", "-o", "default_permissions", - "-o", "noatime"}; + "mo2fuse", "-o", "fsname=mo2linux", "-o", "noatime"}; std::vector<char*> argv; argv.reserve(argvStorage.size()); @@ -428,8 +431,14 @@ void FuseConnector::rebuild( stampPluginTimestamps(*newTree, m_pluginLoadOrder); } - std::unique_lock lock(m_context->tree_mutex); - m_context->tree.swap(newTree); + { + std::unique_lock lock(m_context->tree_mutex); + m_context->tree.swap(newTree); + } + { + std::scoped_lock lock(m_context->open_dirs_mutex); + m_context->open_dirs.clear(); + } } void FuseConnector::updateMapping(const MappingType& mapping) @@ -721,6 +730,10 @@ void FuseConnector::flushStagingLive() std::unique_lock lock(m_context->tree_mutex); m_context->tree.swap(newTree); } + { + std::scoped_lock lock(m_context->open_dirs_mutex); + m_context->open_dirs.clear(); + } // Re-create OverwriteManager with fresh staging dir m_context->overwrite = std::make_unique<OverwriteManager>(m_stagingDir, m_overwriteDir); @@ -771,8 +784,48 @@ static void doUnmount(const QString& path) } } +static void cleanupStaleMo2Mounts(const QString& keepPath) +{ + QFile mounts(QStringLiteral("/proc/mounts")); + if (!mounts.open(QIODevice::ReadOnly)) { + return; + } + + const QString cleanKeep = QDir::cleanPath(keepPath); + const QString trashRoot = + QDir::cleanPath(QDir::homePath() + "/.local/share/Trash/files"); + + while (!mounts.atEnd()) { + const auto line = QString::fromUtf8(mounts.readLine()).trimmed(); + const auto parts = line.split(' ', Qt::SkipEmptyParts); + if (parts.size() < 3) { + continue; + } + if (parts[0] != QStringLiteral("mo2linux")) { + continue; + } + + const QString mp = QDir::cleanPath(QString::fromStdString( + decodeProcMountField(parts[1].toStdString()))); + if (mp == cleanKeep) { + continue; + } + + const bool underTrash = + mp == trashRoot || mp.startsWith(trashRoot + QDir::separator()); + if (!underTrash && !isStaleOrMounted(mp)) { + continue; + } + + log::warn("cleaning stale mo2linux mount at '{}'", mp); + doUnmount(mp); + } +} + void FuseConnector::tryCleanupStaleMount(const QString& path) { + cleanupStaleMo2Mounts(path); + if (!isStaleOrMounted(path)) { return; } @@ -780,4 +833,3 @@ void FuseConnector::tryCleanupStaleMount(const QString& path) log::warn("stale FUSE mount detected at '{}', attempting cleanup", path); doUnmount(path); } - diff --git a/src/src/vfs/inodetable.cpp b/src/src/vfs/inodetable.cpp index f4dcbcb..8fc0e21 100644 --- a/src/src/vfs/inodetable.cpp +++ b/src/src/vfs/inodetable.cpp @@ -32,6 +32,16 @@ InodeTable::InodeTable() m_inodeToPath.emplace(1, ""); } +uint64_t InodeTable::get(const std::string& path) const +{ + const std::string key = normalizeForLookup(path); + auto it = m_pathToInode.find(key); + if (it == m_pathToInode.end()) { + return 0; + } + return it->second; +} + uint64_t InodeTable::getOrCreate(const std::string& path) { const std::string key = normalizeForLookup(path); diff --git a/src/src/vfs/inodetable.h b/src/src/vfs/inodetable.h index 3594db1..d63864e 100644 --- a/src/src/vfs/inodetable.h +++ b/src/src/vfs/inodetable.h @@ -10,6 +10,7 @@ class InodeTable public: InodeTable(); + uint64_t get(const std::string& path) const; uint64_t getOrCreate(const std::string& path); std::string getPath(uint64_t ino) const; void rename(const std::string& old_path, const std::string& new_path); diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp index 5eb1607..4d71444 100644 --- a/src/src/vfs/mo2filesystem.cpp +++ b/src/src/vfs/mo2filesystem.cpp @@ -1,6 +1,7 @@ #include "mo2filesystem.h" #include <fcntl.h> +#include <linux/fs.h> #include <sys/time.h> #include <unistd.h> @@ -8,13 +9,111 @@ #include <chrono> #include <cstring> #include <filesystem> -#include <fstream> + +#if __has_include(<linux/msdos_fs.h>) +#include <linux/msdos_fs.h> +#endif namespace { namespace fs = std::filesystem; -constexpr double TTL_SECONDS = 1.0; +constexpr double TTL_SECONDS = 30.0; +constexpr double NEGATIVE_TTL_SECONDS = 5.0; +constexpr double ATTR_CACHE_SECONDS = 30.0; + +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, + uint64_t size, + const std::chrono::system_clock::time_point& mtime); + +void maybeLogCounters(Mo2FsContext* ctx) +{ + if (ctx == nullptr) { + return; + } + + const uint64_t tick = ctx->op_tick.fetch_add(1, std::memory_order_relaxed) + 1; + if ((tick % 50000) != 0) { + return; + } + + std::fprintf(stderr, + "[VFS] ops lookup=%llu getattr=%llu readdir=%llu open=%llu read=%llu ioctl=%llu", + static_cast<unsigned long long>( + ctx->lookup_count.load(std::memory_order_relaxed)), + static_cast<unsigned long long>( + ctx->getattr_count.load(std::memory_order_relaxed)), + static_cast<unsigned long long>( + ctx->readdir_count.load(std::memory_order_relaxed)), + static_cast<unsigned long long>( + ctx->open_count.load(std::memory_order_relaxed)), + static_cast<unsigned long long>( + ctx->read_count.load(std::memory_order_relaxed)), + static_cast<unsigned long long>( + ctx->ioctl_count.load(std::memory_order_relaxed))); + { + std::scoped_lock lock(ctx->open_files_mutex); + std::fprintf(stderr, " open_handles=%zu\n", ctx->open_files.size()); + } + + auto logTop = [](const char* label, const std::unordered_map<std::string, uint64_t>& m) { + if (m.empty()) { + return; + } + + std::vector<std::pair<std::string, uint64_t>> top(m.begin(), m.end()); + const size_t keep = std::min<size_t>(3, top.size()); + std::partial_sort( + top.begin(), top.begin() + static_cast<std::ptrdiff_t>(keep), top.end(), + [](const auto& a, const auto& b) { return a.second > b.second; }); + + std::fprintf(stderr, "[VFS] hot %s:", label); + for (size_t i = 0; i < keep; ++i) { + std::fprintf(stderr, " [%llu] %s", + static_cast<unsigned long long>(top[i].second), + top[i].first.c_str()); + } + std::fputc('\n', stderr); + }; + + { + std::scoped_lock lock(ctx->path_stats_mutex); + logTop("lookup_hit", ctx->lookup_hit_paths); + logTop("lookup_miss", ctx->lookup_miss_paths); + logTop("getattr", ctx->getattr_paths); + logTop("readdir", ctx->readdir_paths); + ctx->lookup_hit_paths.clear(); + ctx->lookup_miss_paths.clear(); + ctx->getattr_paths.clear(); + ctx->readdir_paths.clear(); + } +} + +void invalidateDirCache(Mo2FsContext* ctx, const std::string& dirPath) +{ + if (ctx == nullptr) { + return; + } + + // Invalidate only the affected directory (and its open-dir handles), not all. + { + std::scoped_lock lock(ctx->open_dirs_mutex); + for (auto it = ctx->open_dirs.begin(); it != ctx->open_dirs.end();) { + if (it->second.path == dirPath) { + it = ctx->open_dirs.erase(it); + } else { + ++it; + } + } + } + { + std::scoped_lock cacheLock(ctx->dir_cache_mutex); + ctx->dir_cache.erase(dirPath); + ctx->readdir_blob_cache.erase(dirPath); + ctx->readdirplus_blob_cache.erase(dirPath); + } +} struct NodeSnapshot { @@ -67,7 +166,7 @@ std::string joinPath(const std::string& base, const std::string& name) std::string inodeToPath(const Mo2FsContext* ctx, fuse_ino_t ino, bool* ok) { - std::scoped_lock lock(ctx->inode_mutex); + std::shared_lock lock(ctx->inode_mutex); const std::string path = ctx->inodes->getPath(ino); if (ino == 1) { @@ -101,10 +200,18 @@ NodeSnapshot snapshotForPath(const Mo2FsContext* ctx, const std::string& path) return snap; } -std::vector<std::pair<std::string, bool>> listChildrenSnapshot( +struct ChildSnapshot +{ + std::string name; + bool is_dir = false; + uint64_t size = 0; + std::chrono::system_clock::time_point mtime{}; +}; + +std::vector<ChildSnapshot> listChildrenSnapshot( const Mo2FsContext* ctx, const std::string& path, bool* ok) { - std::vector<std::pair<std::string, bool>> out; + std::vector<ChildSnapshot> out; std::shared_lock lock(ctx->tree_mutex); const VfsNode* node = path.empty() ? &ctx->tree->root : ctx->tree->root.resolve(splitPath(path)); @@ -115,12 +222,207 @@ std::vector<std::pair<std::string, bool>> listChildrenSnapshot( *ok = true; for (const auto& [name, child] : node->listChildren()) { - out.emplace_back(name, child->is_directory); + ChildSnapshot snap; + snap.name = name; + snap.is_dir = child->is_directory; + if (!child->is_directory) { + snap.size = child->file_info.size; + snap.mtime = child->file_info.mtime; + } + out.push_back(std::move(snap)); } return out; } +std::vector<Mo2FsContext::DirEntry> buildDirEntries( + Mo2FsContext* ctx, const std::string& path, fuse_ino_t selfIno, bool* ok) +{ + auto children = listChildrenSnapshot(ctx, path, ok); + if (!*ok) { + return {}; + } + + std::vector<Mo2FsContext::DirEntry> entries; + entries.reserve(children.size() + 2); + entries.push_back(Mo2FsContext::DirEntry{selfIno, ".", true}); + entries.push_back(Mo2FsContext::DirEntry{1, "..", true}); + + std::unique_lock lock(ctx->inode_mutex); + for (const auto& child : children) { + const std::string childPath = joinPath(path, child.name); + entries.push_back( + Mo2FsContext::DirEntry{ctx->inodes->getOrCreate(childPath), child.name, + child.is_dir, child.size, child.mtime}); + } + + return entries; +} + +void samplePathStat(Mo2FsContext* ctx, const char* op, const std::string& path, + bool miss = false) +{ + if (ctx == nullptr) { + return; + } + + const uint64_t sampleTick = + ctx->path_sample_tick.fetch_add(1, std::memory_order_relaxed) + 1; + if ((sampleTick % 128) != 0) { + return; + } + + std::scoped_lock lock(ctx->path_stats_mutex); + if (std::strcmp(op, "lookup") == 0) { + if (miss) { + ++ctx->lookup_miss_paths[path]; + } else { + ++ctx->lookup_hit_paths[path]; + } + return; + } + if (std::strcmp(op, "getattr") == 0) { + ++ctx->getattr_paths[path]; + return; + } + if (std::strcmp(op, "readdir") == 0) { + ++ctx->readdir_paths[path]; + } +} + +std::shared_ptr<std::vector<Mo2FsContext::DirEntry>> getOrBuildDirEntries( + Mo2FsContext* ctx, const std::string& path, fuse_ino_t ino, bool* ok) +{ + { + std::scoped_lock lock(ctx->dir_cache_mutex); + auto it = ctx->dir_cache.find(path); + if (it != ctx->dir_cache.end()) { + *ok = true; + return it->second; + } + } + + auto entries = std::make_shared<std::vector<Mo2FsContext::DirEntry>>( + buildDirEntries(ctx, path, ino, ok)); + if (!*ok) { + return {}; + } + + { + std::scoped_lock lock(ctx->dir_cache_mutex); + auto [it, inserted] = ctx->dir_cache.emplace(path, entries); + if (!inserted) { + return it->second; + } + } + + return entries; +} + +std::vector<char> buildReaddirBlob( + fuse_req_t req, const std::vector<Mo2FsContext::DirEntry>& entries) +{ + std::vector<char> blob; + for (const auto& entry : entries) { + struct stat st; + std::memset(&st, 0, sizeof(st)); + st.st_ino = entry.ino; + st.st_mode = entry.is_dir ? (S_IFDIR | 0755) : (S_IFREG | 0644); + + const size_t entSize = + fuse_add_direntry(req, nullptr, 0, entry.name.c_str(), &st, 0); + if (entSize == 0) { + continue; + } + const off_t nextOff = static_cast<off_t>(blob.size() + entSize); + const size_t oldLen = blob.size(); + blob.resize(oldLen + entSize); + fuse_add_direntry(req, blob.data() + oldLen, entSize, entry.name.c_str(), &st, + nextOff); + } + return blob; +} + +std::vector<char> buildReaddirPlusBlob( + fuse_req_t req, const Mo2FsContext* ctx, + const std::vector<Mo2FsContext::DirEntry>& entries) +{ + std::vector<char> blob; + for (const auto& entry : entries) { + struct fuse_entry_param e; + std::memset(&e, 0, sizeof(e)); + e.ino = entry.ino; + e.attr_timeout = TTL_SECONDS; + e.entry_timeout = TTL_SECONDS; + + if (entry.is_dir) { + fillStatForDir(&e.attr, entry.ino, ctx->uid, ctx->gid); + } else { + fillStatForFile(&e.attr, entry.ino, ctx->uid, ctx->gid, entry.size, + entry.mtime); + } + + const size_t entSize = + fuse_add_direntry_plus(req, nullptr, 0, entry.name.c_str(), &e, 0); + if (entSize == 0) { + continue; + } + const off_t nextOff = static_cast<off_t>(blob.size() + entSize); + const size_t oldLen = blob.size(); + blob.resize(oldLen + entSize); + fuse_add_direntry_plus(req, blob.data() + oldLen, entSize, entry.name.c_str(), + &e, nextOff); + } + return blob; +} + +std::shared_ptr<std::vector<char>> getOrBuildReaddirBlob( + Mo2FsContext* ctx, fuse_req_t req, const std::string& path, + const std::shared_ptr<std::vector<Mo2FsContext::DirEntry>>& entries) +{ + { + std::scoped_lock lock(ctx->dir_cache_mutex); + auto it = ctx->readdir_blob_cache.find(path); + if (it != ctx->readdir_blob_cache.end()) { + return it->second; + } + } + + auto built = std::make_shared<std::vector<char>>(buildReaddirBlob(req, *entries)); + { + std::scoped_lock lock(ctx->dir_cache_mutex); + auto [it, inserted] = ctx->readdir_blob_cache.emplace(path, built); + if (!inserted) { + return it->second; + } + } + return built; +} + +std::shared_ptr<std::vector<char>> getOrBuildReaddirPlusBlob( + Mo2FsContext* ctx, fuse_req_t req, const std::string& path, + const std::shared_ptr<std::vector<Mo2FsContext::DirEntry>>& entries) +{ + { + std::scoped_lock lock(ctx->dir_cache_mutex); + auto it = ctx->readdirplus_blob_cache.find(path); + if (it != ctx->readdirplus_blob_cache.end()) { + return it->second; + } + } + + auto built = std::make_shared<std::vector<char>>( + buildReaddirPlusBlob(req, ctx, *entries)); + { + std::scoped_lock lock(ctx->dir_cache_mutex); + auto [it, inserted] = ctx->readdirplus_blob_cache.emplace(path, built); + if (!inserted) { + return it->second; + } + } + return built; +} + void fillStatForDir(struct stat* st, fuse_ino_t ino, uid_t uid, gid_t gid) { std::memset(st, 0, sizeof(struct stat)); @@ -129,12 +431,12 @@ void fillStatForDir(struct stat* st, fuse_ino_t ino, uid_t uid, gid_t gid) st->st_nlink = 2; st->st_uid = uid; st->st_gid = gid; - - const auto now = std::chrono::duration_cast<std::chrono::seconds>( - std::chrono::system_clock::now().time_since_epoch()); - st->st_mtim.tv_sec = now.count(); - st->st_atim.tv_sec = now.count(); - st->st_ctim.tv_sec = now.count(); + // Keep synthetic directory timestamps stable so kernel/user-space attr caching + // stays effective across repeated getattr/readdir probes. + constexpr time_t kVirtualDirTime = 946684800; // 2000-01-01 00:00:00 UTC + st->st_mtim.tv_sec = kVirtualDirTime; + st->st_atim.tv_sec = kVirtualDirTime; + st->st_ctim.tv_sec = kVirtualDirTime; } void fillStatForFile(struct stat* st, fuse_ino_t ino, uid_t uid, gid_t gid, @@ -207,6 +509,31 @@ void updateFileNode(Mo2FsContext* ctx, const std::string& relative, } // namespace +void mo2_init(void* /*userdata*/, struct fuse_conn_info* conn) +{ + // Keep kernel page cache valid across open/close as long as mtime/size are + // unchanged. Mod files are immutable during a game session, so this avoids + // re-reading file data from userspace on every open(). + if (conn->capable & FUSE_CAP_AUTO_INVAL_DATA) { + conn->want |= FUSE_CAP_AUTO_INVAL_DATA; + } + + // Increase max read/write buffer to 1MB (kernel 4.20+ supports this). + // Reduces FUSE round-trips for large file reads (textures, meshes, BSAs). + constexpr unsigned int ONE_MB = 1048576; + if (conn->max_readahead < ONE_MB) { + conn->max_readahead = ONE_MB; + } + if (conn->max_write < ONE_MB) { + conn->max_write = ONE_MB; + } + + std::fprintf(stderr, + "[VFS] init: auto_inval=%d max_readahead=%u max_write=%u\n", + (conn->want & FUSE_CAP_AUTO_INVAL_DATA) ? 1 : 0, + conn->max_readahead, conn->max_write); +} + void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name) { Mo2FsContext* ctx = getContext(req); @@ -214,6 +541,8 @@ void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name) fuse_reply_err(req, EINVAL); return; } + ctx->lookup_count.fetch_add(1, std::memory_order_relaxed); + maybeLogCounters(ctx); bool ok = false; const std::string parentPath = inodeToPath(ctx, parent, &ok); @@ -225,13 +554,24 @@ void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name) const std::string childPath = joinPath(parentPath, name); const auto snap = snapshotForPath(ctx, childPath); if (!snap.found) { - fuse_reply_err(req, ENOENT); + samplePathStat(ctx, "lookup", childPath, true); + 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; + fuse_reply_entry(req, &e); return; } + samplePathStat(ctx, "lookup", childPath, false); - fuse_ino_t childIno; + fuse_ino_t childIno = 0; { - std::scoped_lock lock(ctx->inode_mutex); + std::shared_lock lock(ctx->inode_mutex); + childIno = ctx->inodes->get(childPath); + } + if (childIno == 0) { + std::unique_lock lock(ctx->inode_mutex); childIno = ctx->inodes->getOrCreate(childPath); } @@ -245,10 +585,31 @@ void mo2_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* /*fi*/) fuse_reply_err(req, EINVAL); return; } + ctx->getattr_count.fetch_add(1, std::memory_order_relaxed); + maybeLogCounters(ctx); + + { + std::scoped_lock lock(ctx->attr_cache_mutex); + 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) { + fuse_reply_attr(req, &it->second.st, TTL_SECONDS); + return; + } + } if (ino == 1) { struct stat st; fillStatForDir(&st, 1, ctx->uid, ctx->gid); + { + std::scoped_lock lock(ctx->attr_cache_mutex); + auto& c = ctx->attr_cache[1]; + c.st = st; + c.expires_at = std::chrono::steady_clock::now() + + std::chrono::milliseconds( + static_cast<int>(ATTR_CACHE_SECONDS * 1000.0)); + c.valid = true; + } fuse_reply_attr(req, &st, TTL_SECONDS); return; } @@ -259,6 +620,7 @@ void mo2_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* /*fi*/) fuse_reply_err(req, ENOENT); return; } + samplePathStat(ctx, "getattr", path); const auto snap = snapshotForPath(ctx, path); if (!snap.found) { @@ -273,14 +635,23 @@ void mo2_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* /*fi*/) fillStatForFile(&st, ino, ctx->uid, ctx->gid, snap.size, snap.mtime); } + { + std::scoped_lock lock(ctx->attr_cache_mutex); + auto& c = ctx->attr_cache[ino]; + c.st = st; + c.expires_at = std::chrono::steady_clock::now() + + std::chrono::milliseconds( + static_cast<int>(ATTR_CACHE_SECONDS * 1000.0)); + c.valid = true; + } + fuse_reply_attr(req, &st, TTL_SECONDS); } -void mo2_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, - struct fuse_file_info* /*fi*/) +void mo2_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) { Mo2FsContext* ctx = getContext(req); - if (ctx == nullptr || off < 0) { + if (ctx == nullptr || fi == nullptr) { fuse_reply_err(req, EINVAL); return; } @@ -293,43 +664,95 @@ void mo2_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, } bool listOk = false; - auto children = listChildrenSnapshot(ctx, path, &listOk); - if (!listOk) { + auto entries = getOrBuildDirEntries(ctx, path, ino, &listOk); + if (!listOk || !entries) { fuse_reply_err(req, ENOTDIR); return; } + samplePathStat(ctx, "readdir", path); - struct Entry + const uint64_t dh = ctx->next_dh.fetch_add(1, std::memory_order_relaxed); { - fuse_ino_t ino; - std::string name; - bool isDir; - }; + std::scoped_lock lock(ctx->open_dirs_mutex); + Mo2FsContext::OpenDir od; + od.path = path; + od.entries = std::move(entries); + od.readdir_blob = getOrBuildReaddirBlob(ctx, req, path, od.entries); + od.readdirplus_blob.reset(); + ctx->open_dirs[dh] = std::move(od); + } - std::vector<Entry> entries; - entries.reserve(children.size() + 2); - entries.push_back({ino, ".", true}); - entries.push_back({1, "..", true}); + fi->fh = dh; + fi->cache_readdir = 1; + fuse_reply_open(req, fi); +} - { - std::scoped_lock lock(ctx->inode_mutex); - for (const auto& [name, isDir] : children) { - const std::string childPath = joinPath(path, name); - entries.push_back({ctx->inodes->getOrCreate(childPath), name, isDir}); +void mo2_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info* fi) +{ + Mo2FsContext* ctx = getContext(req); + if (ctx == nullptr || off < 0) { + fuse_reply_err(req, EINVAL); + return; + } + ctx->readdir_count.fetch_add(1, std::memory_order_relaxed); + maybeLogCounters(ctx); + + std::shared_ptr<std::vector<Mo2FsContext::DirEntry>> entries; + std::shared_ptr<std::vector<char>> readdirBlob; + std::string path; + bool haveCached = false; + if (fi != nullptr) { + std::scoped_lock lock(ctx->open_dirs_mutex); + auto it = ctx->open_dirs.find(fi->fh); + if (it != ctx->open_dirs.end()) { + path = it->second.path; + entries = it->second.entries; + readdirBlob = it->second.readdir_blob; + haveCached = true; + } + } + + if (readdirBlob != nullptr) { + samplePathStat(ctx, "readdir", path); + const size_t start = static_cast<size_t>(off); + if (start >= readdirBlob->size()) { + fuse_reply_buf(req, nullptr, 0); + return; + } + const size_t n = std::min<size_t>(size, readdirBlob->size() - start); + fuse_reply_buf(req, readdirBlob->data() + start, n); + return; + } + + if (!haveCached) { + bool ok = false; + path = inodeToPath(ctx, ino, &ok); + if (!ok) { + fuse_reply_err(req, ENOENT); + return; + } + + bool listOk = false; + entries = getOrBuildDirEntries(ctx, path, ino, &listOk); + if (!listOk || !entries) { + fuse_reply_err(req, ENOTDIR); + return; } } + samplePathStat(ctx, "readdir", path); std::vector<char> buf(size); size_t used = 0; - for (size_t i = static_cast<size_t>(off); i < entries.size(); ++i) { + for (size_t i = static_cast<size_t>(off); i < entries->size(); ++i) { struct stat st; std::memset(&st, 0, sizeof(st)); - st.st_ino = entries[i].ino; - st.st_mode = entries[i].isDir ? (S_IFDIR | 0755) : (S_IFREG | 0644); + st.st_ino = (*entries)[i].ino; + st.st_mode = (*entries)[i].is_dir ? (S_IFDIR | 0755) : (S_IFREG | 0644); const size_t ent = fuse_add_direntry(req, buf.data() + used, size - used, - entries[i].name.c_str(), &st, + (*entries)[i].name.c_str(), &st, static_cast<off_t>(i + 1)); if (ent > size - used) { break; @@ -340,6 +763,104 @@ void mo2_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, fuse_reply_buf(req, buf.data(), used); } +void mo2_readdirplus(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info* fi) +{ + Mo2FsContext* ctx = getContext(req); + if (ctx == nullptr || off < 0) { + fuse_reply_err(req, EINVAL); + return; + } + ctx->readdir_count.fetch_add(1, std::memory_order_relaxed); + maybeLogCounters(ctx); + + std::shared_ptr<std::vector<Mo2FsContext::DirEntry>> entries; + std::shared_ptr<std::vector<char>> readdirPlusBlob; + std::string path; + bool haveCached = false; + if (fi != nullptr) { + std::scoped_lock lock(ctx->open_dirs_mutex); + auto it = ctx->open_dirs.find(fi->fh); + if (it != ctx->open_dirs.end()) { + path = it->second.path; + entries = it->second.entries; + readdirPlusBlob = it->second.readdirplus_blob; + haveCached = true; + } + } + + if (readdirPlusBlob == nullptr && haveCached && entries != nullptr) { + auto built = getOrBuildReaddirPlusBlob(ctx, req, path, entries); + std::scoped_lock lock(ctx->open_dirs_mutex); + auto it = ctx->open_dirs.find(fi->fh); + if (it != ctx->open_dirs.end()) { + if (it->second.readdirplus_blob == nullptr) { + it->second.readdirplus_blob = built; + } + readdirPlusBlob = it->second.readdirplus_blob; + } else { + readdirPlusBlob = std::move(built); + } + } + + if (readdirPlusBlob != nullptr) { + samplePathStat(ctx, "readdir", path); + const size_t start = static_cast<size_t>(off); + if (start >= readdirPlusBlob->size()) { + fuse_reply_buf(req, nullptr, 0); + return; + } + const size_t n = std::min<size_t>(size, readdirPlusBlob->size() - start); + fuse_reply_buf(req, readdirPlusBlob->data() + start, n); + return; + } + + if (!haveCached) { + bool ok = false; + path = inodeToPath(ctx, ino, &ok); + if (!ok) { + fuse_reply_err(req, ENOENT); + return; + } + + bool listOk = false; + entries = getOrBuildDirEntries(ctx, path, ino, &listOk); + if (!listOk || !entries) { + fuse_reply_err(req, ENOTDIR); + return; + } + } + samplePathStat(ctx, "readdir", path); + + std::vector<char> buf(size); + size_t used = 0; + + for (size_t i = static_cast<size_t>(off); i < entries->size(); ++i) { + struct fuse_entry_param e; + std::memset(&e, 0, sizeof(e)); + e.ino = (*entries)[i].ino; + e.attr_timeout = TTL_SECONDS; + e.entry_timeout = TTL_SECONDS; + + if ((*entries)[i].is_dir) { + fillStatForDir(&e.attr, (*entries)[i].ino, ctx->uid, ctx->gid); + } else { + fillStatForFile(&e.attr, (*entries)[i].ino, ctx->uid, ctx->gid, + (*entries)[i].size, (*entries)[i].mtime); + } + + const size_t ent = fuse_add_direntry_plus( + req, buf.data() + used, size - used, (*entries)[i].name.c_str(), &e, + static_cast<off_t>(i + 1)); + if (ent > size - used) { + break; + } + used += ent; + } + + fuse_reply_buf(req, buf.data(), used); +} + void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) { Mo2FsContext* ctx = getContext(req); @@ -347,6 +868,8 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) fuse_reply_err(req, EINVAL); return; } + ctx->open_count.fetch_add(1, std::memory_order_relaxed); + maybeLogCounters(ctx); bool ok = false; const std::string path = inodeToPath(ctx, ino, &ok); @@ -380,10 +903,25 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) } } + int fd = -1; + if (writable) { + const int openFlags = O_RDWR; + if (isBacking && ctx->backing_dir_fd >= 0) { + fd = openat(ctx->backing_dir_fd, realPath.c_str(), openFlags); + } else { + fd = open(realPath.c_str(), openFlags); + } + if (fd < 0) { + fuse_reply_err(req, EIO); + return; + } + } + const uint64_t fh = ctx->next_fh.fetch_add(1, std::memory_order_relaxed); { std::scoped_lock lock(ctx->open_files_mutex); Mo2FsContext::OpenFile of; + of.fd = fd; of.real_path = realPath; of.writable = writable; of.is_backing = isBacking; @@ -404,35 +942,44 @@ void mo2_read(fuse_req_t req, fuse_ino_t /*ino*/, size_t size, off_t off, fuse_reply_err(req, EINVAL); return; } + ctx->read_count.fetch_add(1, std::memory_order_relaxed); + maybeLogCounters(ctx); + int fd = -1; std::string realPath; bool isBacking = false; { - std::scoped_lock lock(ctx->open_files_mutex); + std::shared_lock lock(ctx->open_files_mutex); auto it = ctx->open_files.find(fi->fh); if (it == ctx->open_files.end()) { fuse_reply_err(req, EBADF); return; } - realPath = it->second.real_path; - isBacking = it->second.is_backing; - } - - int fd = -1; - if (isBacking && ctx->backing_dir_fd >= 0) { - fd = openat(ctx->backing_dir_fd, realPath.c_str(), O_RDONLY); - } else { - fd = open(realPath.c_str(), O_RDONLY); + fd = it->second.fd; + realPath = it->second.real_path; + isBacking = it->second.is_backing; } - if (fd < 0) { - fuse_reply_err(req, EIO); - return; + int localFd = fd; + bool openedTempFd = false; + if (localFd < 0) { + if (isBacking && ctx->backing_dir_fd >= 0) { + localFd = openat(ctx->backing_dir_fd, realPath.c_str(), O_RDONLY); + } else { + localFd = open(realPath.c_str(), O_RDONLY); + } + if (localFd < 0) { + fuse_reply_err(req, EIO); + return; + } + openedTempFd = true; } std::vector<char> out(size); - const ssize_t n = pread(fd, out.data(), size, off); - close(fd); + const ssize_t n = pread(localFd, out.data(), size, off); + if (openedTempFd) { + close(localFd); + } if (n < 0) { fuse_reply_err(req, EIO); @@ -451,44 +998,41 @@ void mo2_write(fuse_req_t req, fuse_ino_t /*ino*/, const char* buf, size_t size, return; } - Mo2FsContext::OpenFile open; + int fd = -1; + std::string relativePath; + std::string realPath; + bool writable = false; { - std::scoped_lock lock(ctx->open_files_mutex); + std::shared_lock lock(ctx->open_files_mutex); auto it = ctx->open_files.find(fi->fh); if (it == ctx->open_files.end()) { fuse_reply_err(req, EBADF); return; } - open = it->second; + fd = it->second.fd; + writable = it->second.writable; + relativePath = it->second.relative_path; + realPath = it->second.real_path; } - if (!open.writable) { + if (!writable) { fuse_reply_err(req, EACCES); return; } - std::fstream io(open.real_path, std::ios::binary | std::ios::in | std::ios::out); - if (!io) { - io.open(open.real_path, std::ios::binary | std::ios::out); - io.close(); - io.open(open.real_path, std::ios::binary | std::ios::in | std::ios::out); - } - - if (!io) { - fuse_reply_err(req, EIO); + if (fd < 0) { + fuse_reply_err(req, EBADF); return; } - io.seekp(off, std::ios::beg); - io.write(buf, static_cast<std::streamsize>(size)); - io.flush(); - if (!io) { + const ssize_t written = pwrite(fd, buf, size, off); + if (written < 0) { fuse_reply_err(req, EIO); return; } - updateFileNode(ctx, open.relative_path, open.real_path, "Staging"); - fuse_reply_write(req, size); + updateFileNode(ctx, relativePath, realPath, "Staging"); + fuse_reply_write(req, static_cast<size_t>(written)); } void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t /*mode*/, @@ -518,6 +1062,7 @@ void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t /*mo } updateFileNode(ctx, relative, realPath, "Staging"); + invalidateDirCache(ctx, parentPath); { std::unique_lock lock(ctx->tree_mutex); ++ctx->tree->file_count; @@ -525,7 +1070,7 @@ void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t /*mo fuse_ino_t newIno; { - std::scoped_lock lock(ctx->inode_mutex); + std::unique_lock lock(ctx->inode_mutex); newIno = ctx->inodes->getOrCreate(relative); } @@ -536,9 +1081,16 @@ void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t /*mo } const uint64_t fh = ctx->next_fh.fetch_add(1, std::memory_order_relaxed); + const int fd = open(realPath.c_str(), O_RDWR); + if (fd < 0) { + fuse_reply_err(req, EIO); + return; + } + { std::scoped_lock lock(ctx->open_files_mutex); Mo2FsContext::OpenFile of; + of.fd = fd; of.real_path = realPath; of.writable = true; of.is_backing = false; @@ -607,9 +1159,13 @@ void mo2_rename(fuse_req_t req, fuse_ino_t parent, const char* name, } { - std::scoped_lock lock(ctx->inode_mutex); + std::unique_lock lock(ctx->inode_mutex); ctx->inodes->rename(oldRelative, newRelative); } + invalidateDirCache(ctx, parentPath); + if (newParentPath != parentPath) { + invalidateDirCache(ctx, newParentPath); + } fuse_reply_err(req, 0); } @@ -644,7 +1200,7 @@ void mo2_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set, if (fi != nullptr) { fh = fi->fh; - std::scoped_lock lock(ctx->open_files_mutex); + std::shared_lock lock(ctx->open_files_mutex); auto it = ctx->open_files.find(fh); if (it != ctx->open_files.end()) { target = it->second.real_path; @@ -680,17 +1236,42 @@ void mo2_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set, std::scoped_lock lock(ctx->open_files_mutex); auto it = ctx->open_files.find(fh); if (it != ctx->open_files.end()) { + const int newFd = open(target.c_str(), O_RDWR); + if (newFd < 0) { + fuse_reply_err(req, EIO); + return; + } + if (it->second.fd >= 0) { + close(it->second.fd); + } + it->second.fd = newFd; it->second.real_path = target; it->second.writable = true; + it->second.is_backing = false; } } } - std::error_code ec; - fs::resize_file(target, static_cast<uint64_t>(attr->st_size), ec); - if (ec) { - fuse_reply_err(req, EIO); - return; + bool resized = false; + if (fi != nullptr) { + std::scoped_lock lock(ctx->open_files_mutex); + auto it = ctx->open_files.find(fh); + if (it != ctx->open_files.end() && it->second.fd >= 0) { + if (ftruncate(it->second.fd, static_cast<off_t>(attr->st_size)) != 0) { + fuse_reply_err(req, EIO); + return; + } + resized = true; + } + } + + if (!resized) { + std::error_code ec; + fs::resize_file(target, static_cast<uint64_t>(attr->st_size), ec); + if (ec) { + fuse_reply_err(req, EIO); + return; + } } updateFileNode(ctx, path, target, "Staging"); @@ -804,6 +1385,7 @@ void mo2_unlink(fuse_req_t req, fuse_ino_t parent, const char* name) ctx->tree->file_count = ctx->tree->file_count > 0 ? ctx->tree->file_count - 1 : 0; } } + invalidateDirCache(ctx, parentPath); fuse_reply_err(req, 0); } @@ -834,10 +1416,11 @@ void mo2_mkdir(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t /*mod ctx->tree->root.insertDirectory(splitPath(relative)); ++ctx->tree->dir_count; } + invalidateDirCache(ctx, parentPath); fuse_ino_t dirIno; { - std::scoped_lock lock(ctx->inode_mutex); + std::unique_lock lock(ctx->inode_mutex); dirIno = ctx->inodes->getOrCreate(relative); } @@ -860,8 +1443,74 @@ void mo2_release(fuse_req_t req, fuse_ino_t /*ino*/, struct fuse_file_info* fi) { std::scoped_lock lock(ctx->open_files_mutex); - ctx->open_files.erase(fi->fh); + auto it = ctx->open_files.find(fi->fh); + if (it != ctx->open_files.end()) { + if (it->second.fd >= 0) { + close(it->second.fd); + } + ctx->open_files.erase(it); + } } fuse_reply_err(req, 0); } + +void mo2_releasedir(fuse_req_t req, fuse_ino_t /*ino*/, struct fuse_file_info* fi) +{ + Mo2FsContext* ctx = getContext(req); + if (ctx == nullptr || fi == nullptr) { + fuse_reply_err(req, EINVAL); + return; + } + + { + std::scoped_lock lock(ctx->open_dirs_mutex); + ctx->open_dirs.erase(fi->fh); + } + + fuse_reply_err(req, 0); +} + +#if FUSE_USE_VERSION < 35 +void mo2_ioctl(fuse_req_t req, fuse_ino_t /*ino*/, int cmd, void* /*arg*/, + struct fuse_file_info* /*fi*/, unsigned /*flags*/, + const void* /*in_buf*/, size_t /*in_bufsz*/, size_t out_bufsz) +#else +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 +{ + static std::atomic<uint64_t> ioctlSeen{0}; + static std::atomic<unsigned int> lastCmd{0}; + + Mo2FsContext* ctx = getContext(req); + if (ctx != nullptr) { + ctx->ioctl_count.fetch_add(1, std::memory_order_relaxed); + maybeLogCounters(ctx); + } + + const unsigned int ucmd = static_cast<unsigned int>(cmd); + lastCmd.store(ucmd, std::memory_order_relaxed); + const uint64_t seen = ioctlSeen.fetch_add(1, std::memory_order_relaxed) + 1; + if ((seen % 500000) == 0) { + std::fprintf(stderr, "[VFS] ioctl hotloop cmd=0x%x out=%zu\n", + lastCmd.load(std::memory_order_relaxed), out_bufsz); + } + +#ifdef VFAT_IOCTL_READDIR_BOTH + if (ucmd == static_cast<unsigned int>(VFAT_IOCTL_READDIR_BOTH)) { + // Force Wine fallback path for vfat-specific ioctl probes. + fuse_reply_err(req, ENOTTY); + return; + } +#endif + + if (ucmd == static_cast<unsigned int>(FS_IOC_GETFLAGS)) { + // Some callers probe this heavily; ENOTTY avoids fake-success loops. + fuse_reply_err(req, ENOTTY); + return; + } + + fuse_reply_err(req, ENOTTY); +} diff --git a/src/src/vfs/mo2filesystem.h b/src/src/vfs/mo2filesystem.h index a66d08d..7ab0206 100644 --- a/src/src/vfs/mo2filesystem.h +++ b/src/src/vfs/mo2filesystem.h @@ -13,6 +13,7 @@ #include <shared_mutex> #include <string> #include <unordered_map> +#include <vector> struct Mo2FsContext { @@ -20,7 +21,7 @@ struct Mo2FsContext mutable std::shared_mutex tree_mutex; std::unique_ptr<InodeTable> inodes; - mutable std::mutex inode_mutex; + mutable std::shared_mutex inode_mutex; std::unique_ptr<OverwriteManager> overwrite; @@ -28,6 +29,7 @@ struct Mo2FsContext struct OpenFile { + int fd = -1; std::string real_path; bool writable = false; bool is_backing = false; @@ -35,17 +37,65 @@ struct Mo2FsContext }; std::unordered_map<uint64_t, OpenFile> open_files; - mutable std::mutex open_files_mutex; + mutable std::shared_mutex open_files_mutex; std::atomic<uint64_t> next_fh{1}; + struct DirEntry + { + fuse_ino_t ino = 0; + std::string name; + bool is_dir = false; + uint64_t size = 0; + std::chrono::system_clock::time_point mtime{}; + }; + struct OpenDir + { + std::string path; + std::shared_ptr<std::vector<DirEntry>> entries; + std::shared_ptr<std::vector<char>> readdir_blob; + std::shared_ptr<std::vector<char>> readdirplus_blob; + }; + std::unordered_map<uint64_t, OpenDir> open_dirs; + mutable std::mutex open_dirs_mutex; + std::unordered_map<std::string, std::shared_ptr<std::vector<DirEntry>>> dir_cache; + std::unordered_map<std::string, std::shared_ptr<std::vector<char>>> readdir_blob_cache; + std::unordered_map<std::string, std::shared_ptr<std::vector<char>>> readdirplus_blob_cache; + mutable std::mutex dir_cache_mutex; + struct CachedAttr + { + struct stat st {}; + std::chrono::steady_clock::time_point expires_at{}; + bool valid = false; + }; + std::unordered_map<fuse_ino_t, CachedAttr> attr_cache; + mutable std::mutex attr_cache_mutex; + std::atomic<uint64_t> next_dh{1}; + std::atomic<uint64_t> lookup_count{0}; + std::atomic<uint64_t> getattr_count{0}; + 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> ioctl_count{0}; + std::atomic<uint64_t> op_tick{0}; + std::unordered_map<std::string, uint64_t> lookup_hit_paths; + std::unordered_map<std::string, uint64_t> lookup_miss_paths; + std::unordered_map<std::string, uint64_t> getattr_paths; + std::unordered_map<std::string, uint64_t> readdir_paths; + mutable std::mutex path_stats_mutex; + std::atomic<uint64_t> path_sample_tick{0}; + uid_t uid = 0; gid_t gid = 0; }; +void mo2_init(void* userdata, struct fuse_conn_info* conn); void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name); void mo2_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi); +void mo2_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi); void mo2_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, struct fuse_file_info* fi); +void mo2_readdirplus(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info* fi); void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi); void mo2_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, struct fuse_file_info* fi); @@ -60,5 +110,15 @@ void mo2_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set, void mo2_unlink(fuse_req_t req, fuse_ino_t parent, const char* name); void mo2_mkdir(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode); void mo2_release(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi); +void mo2_releasedir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi); +#if FUSE_USE_VERSION < 35 +void mo2_ioctl(fuse_req_t req, fuse_ino_t ino, int cmd, void* arg, + struct fuse_file_info* fi, unsigned flags, const void* in_buf, + size_t in_bufsz, size_t out_bufsz); +#else +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 #endif |
