From 80f5b2d07c280da2aa26dd247c0c024086482cd8 Mon Sep 17 00:00:00 2001 From: SimplisticMind <163564577+SimplisticMind@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:38:29 -0700 Subject: [PATCH 1/2] perf: a bunch of optimization --- src/directoryrefresher.cpp | 20 +++-- src/envfs.h | 3 +- src/moapplication.cpp | 17 ++++- src/modinfowithconflictinfo.cpp | 57 +++++++++++--- src/modlistversiondelegate.cpp | 11 +-- src/modlistversiondelegate.h | 4 + src/organizercore.cpp | 95 +++++++++++++++++++---- src/organizercore.h | 14 +++- src/pluginlist.cpp | 131 ++++++++++++++++++++++---------- src/pluginlist.h | 39 +++++++++- src/settings.cpp | 31 ++++++-- src/settings.h | 11 +++ src/settingsutilities.h | 5 +- src/shared/directoryentry.cpp | 47 ++++++++---- src/shared/directoryentry.h | 2 +- src/shared/fileentry.cpp | 27 ++++++- src/shared/fileentry.h | 8 ++ src/shared/fileregister.cpp | 38 +++++++-- src/shared/fileregister.h | 22 +++++- 19 files changed, 462 insertions(+), 120 deletions(-) diff --git a/src/directoryrefresher.cpp b/src/directoryrefresher.cpp index 09defe90c..91b0e8d00 100644 --- a/src/directoryrefresher.cpp +++ b/src/directoryrefresher.cpp @@ -330,9 +330,9 @@ struct ModThread std::wstring path; int prio = -1; std::vector archives; - std::set enabledArchives; - std::vector* loadOrder = nullptr; - DirectoryStats* stats = nullptr; + const std::set* enabledArchives = nullptr; + std::vector* loadOrder = nullptr; + DirectoryStats* stats = nullptr; env::DirectoryWalker walker; std::condition_variable cv; @@ -360,7 +360,7 @@ struct ModThread ds->addFromOrigin(walker, modName, path, prio, *stats); if (Settings::instance().archiveParsing()) { - ds->addFromAllBSAs(modName, path, prio, archives, enabledArchives, *loadOrder, + ds->addFromAllBSAs(modName, path, prio, archives, *enabledArchives, *loadOrder, *stats); } @@ -406,6 +406,13 @@ void DirectoryRefresher::addMultipleModsFilesToStructure( } } + // the enabled-archive set is the same for every mod, so convert it to wstrings + // once here and share it by pointer instead of rebuilding it per mod thread + std::set enabledArchives; + for (auto&& a : m_EnabledArchives) { + enabledArchives.insert(a.toStdWString()); + } + for (std::size_t i = 0; i < entries.size(); ++i) { const auto& e = entries[i]; const int prio = e.priority + 1; @@ -437,10 +444,7 @@ void DirectoryRefresher::addMultipleModsFilesToStructure( mt.archives.push_back(a.toStdWString()); } - mt.enabledArchives.clear(); - for (auto&& a : m_EnabledArchives) { - mt.enabledArchives.insert(a.toStdWString()); - } + mt.enabledArchives = &enabledArchives; mt.loadOrder = &loadOrder; mt.stats = &stats[i]; diff --git a/src/envfs.h b/src/envfs.h index 4ee1fab10..98acf53b6 100644 --- a/src/envfs.h +++ b/src/envfs.h @@ -89,7 +89,8 @@ class ThreadPool } } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + // Constantly yield here - windows timer is too coarse otherwise (~16ms), leads to wasting a LOT of time idling + std::this_thread::yield(); } } diff --git a/src/moapplication.cpp b/src/moapplication.cpp index dd0b897bb..b1992abc7 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -62,7 +62,22 @@ using namespace MOShared; class ProxyStyle : public QProxyStyle { public: - ProxyStyle(QStyle* baseStyle = 0) : QProxyStyle(baseStyle) {} + ProxyStyle(QStyle* baseStyle = 0) : QProxyStyle(baseStyle) + { + // The Windows Vista / modern Windows style calls + // SystemParametersInfo(SPI_GETCLIENTAREAANIMATION) from drawControl() on + // *every* paint to decide whether to run transition animations. Item-view + // painting hits drawControl once per drawn cell. The whole + // animation pathis skipped when the style + // object carries a "_q_no_animation" property, so set it on the base style. + + // Item views don't use these transitions, so nothing visible is lost there; + // the only effect elsewhere is dropping the subtle widget transition + // animations (e.g. button hover fades), which is a worthwhile trade. + if (baseStyle) { + baseStyle->setProperty("_q_no_animation", true); + } + } void drawPrimitive(PrimitiveElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget) const override diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp index af43433d6..9fee78bc1 100644 --- a/src/modinfowithconflictinfo.cpp +++ b/src/modinfowithconflictinfo.cpp @@ -4,6 +4,7 @@ #include "shared/filesorigin.h" #include "utility.h" #include +#include #include "iplugingame.h" #include "moddatachecker.h" @@ -118,10 +119,46 @@ ModInfoWithConflictInfo::Conflicts ModInfoWithConflictInfo::doConflictCheck() co std::vector files = origin.getFiles(); std::set checkedDirs; + // Hidden files/dirs end with ".mohidden". Test the suffix directly on the + // wide string (ASCII, case-insensitive) instead of converting every file and + // directory name to a QString just to call endsWith. + const std::wstring hiddenExt = ToWString(ModInfo::s_HiddenExt); + const auto endsWithHiddenExt = [&hiddenExt](const std::wstring& s) { + if (s.size() < hiddenExt.size()) { + return false; + } + const auto offset = s.size() - hiddenExt.size(); + for (size_t i = 0; i < hiddenExt.size(); ++i) { + wchar_t a = s[offset + i]; + wchar_t b = hiddenExt[i]; + if (a >= L'A' && a <= L'Z') + a = static_cast(a + (L'a' - L'A')); + if (b >= L'A' && b <= L'Z') + b = static_cast(b + (L'a' - L'A')); + if (a != b) { + return false; + } + } + return true; + }; + + // The same alternative origins recur across many files; cache the + // origin-id -> mod-index mapping to avoid repeating the name lookup. + std::map originModIndex; + const auto modIndexForOrigin = [&](int originID) -> unsigned int { + auto it = originModIndex.find(originID); + if (it != originModIndex.end()) { + return it->second; + } + const auto idx = ModInfo::getIndex( + ToQString(m_Core.directoryStructure()->getOriginByID(originID).getName())); + originModIndex.emplace(originID, idx); + return idx; + }; + // for all files in this origin - for (FileEntryPtr file : files) { - if (QString::fromStdWString(file->getName()) - .endsWith(ModInfo::s_HiddenExt, Qt::CaseInsensitive)) { + for (const FileEntryPtr& file : files) { + if (endsWithHiddenExt(file->getName())) { hasHiddenFiles = true; // skip hidden file conflicts continue; @@ -138,8 +175,7 @@ ModInfoWithConflictInfo::Conflicts ModInfoWithConflictInfo::doConflictCheck() co // well break; } else { - if (QString::fromStdWString(parent->getName()) - .endsWith(ModInfo::s_HiddenExt, Qt::CaseInsensitive)) { + if (endsWithHiddenExt(parent->getName())) { hasHiddenFiles = hidden = true; break; } @@ -152,8 +188,8 @@ ModInfoWithConflictInfo::Conflicts ModInfoWithConflictInfo::doConflictCheck() co } } - hasVisibleFiles = true; - auto alternatives = file->getAlternatives(); + hasVisibleFiles = true; + const auto& alternatives = file->getAlternatives(); if ((alternatives.size() == 0) || std::find(dataIDs.begin(), dataIDs.end(), alternatives.back().originID()) != dataIDs.end()) { @@ -175,9 +211,7 @@ ModInfoWithConflictInfo::Conflicts ModInfoWithConflictInfo::doConflictCheck() co // If this is not the origin then determine the correct overwrite if (file->getOrigin() != origin.getID()) { - FilesOrigin& altOrigin = - m_Core.directoryStructure()->getOriginByID(file->getOrigin()); - unsigned int altIndex = ModInfo::getIndex(ToQString(altOrigin.getName())); + unsigned int altIndex = modIndexForOrigin(file->getOrigin()); if (!file->isFromArchive()) { if (!archiveData.isValid()) conflicts.m_OverwrittenList.insert(altIndex); @@ -197,8 +231,7 @@ ModInfoWithConflictInfo::Conflicts ModInfoWithConflictInfo::doConflictCheck() co (altInfo.originID() != origin.getID())) { FilesOrigin& altOrigin = m_Core.directoryStructure()->getOriginByID(altInfo.originID()); - QString altOriginName = ToQString(altOrigin.getName()); - unsigned int altIndex = ModInfo::getIndex(altOriginName); + unsigned int altIndex = modIndexForOrigin(altInfo.originID()); if (!altInfo.isFromArchive()) { if (!archiveData.isValid()) { if (origin.getPriority() > altOrigin.getPriority()) { diff --git a/src/modlistversiondelegate.cpp b/src/modlistversiondelegate.cpp index fbb4f03e6..7b0f89d96 100644 --- a/src/modlistversiondelegate.cpp +++ b/src/modlistversiondelegate.cpp @@ -5,7 +5,8 @@ #include "settings.h" ModListVersionDelegate::ModListVersionDelegate(ModListView* view, Settings& settings) - : QItemDelegate(view), m_view(view), m_settings(settings) + : QItemDelegate(view), m_view(view), m_settings(settings), + m_upgradeIcon(":/MO/gui/update_available"), m_downgradeIcon(":/MO/gui/warning") {} void ModListVersionDelegate::paint(QPainter* painter, @@ -41,8 +42,8 @@ void ModListVersionDelegate::paint(QPainter* painter, opt.decorationAlignment = Qt::AlignCenter; if (upgrade) { - QIcon icon(":/MO/gui/update_available"); - QPixmap pixmap = decoration(opt, icon); + const QIcon& icon = m_upgradeIcon; + QPixmap pixmap = decoration(opt, icon); QSize pm = icon.actualSize(opt.decorationSize); pm.rwidth() += 2 * margin; @@ -54,8 +55,8 @@ void ModListVersionDelegate::paint(QPainter* painter, } if (downgrade) { - QIcon icon(":/MO/gui/warning"); - QPixmap pixmap = decoration(opt, icon); + const QIcon& icon = m_downgradeIcon; + QPixmap pixmap = decoration(opt, icon); QSize pm = icon.actualSize(opt.decorationSize); pm.rwidth() += 2 * margin; diff --git a/src/modlistversiondelegate.h b/src/modlistversiondelegate.h index 53a6e8ba4..107551ccb 100644 --- a/src/modlistversiondelegate.h +++ b/src/modlistversiondelegate.h @@ -1,6 +1,7 @@ #ifndef MODLISTVERSIONDELEGATE_H #define MODLISTVERSIONDELEGATE_H +#include #include class ModListView; @@ -17,6 +18,9 @@ class ModListVersionDelegate : public QItemDelegate private: ModListView* m_view; Settings& m_settings; + + QIcon m_upgradeIcon; + QIcon m_downgradeIcon; }; #endif diff --git a/src/organizercore.cpp b/src/organizercore.cpp index cd859a3ec..a44311c75 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -1292,10 +1292,10 @@ void OrganizerCore::refresh(bool saveChanges) emit refreshTriggered(); } -void OrganizerCore::refreshESPList(bool force) +void OrganizerCore::refreshESPList(bool force, bool lightRefresh) { onNextRefresh( - [this, force] { + [this, force, lightRefresh] { TimeThis tt("OrganizerCore::refreshESPList()"); m_CurrentProfile->writeModlist(); @@ -1303,7 +1303,8 @@ void OrganizerCore::refreshESPList(bool force) // clear list try { m_PluginList.refresh(m_CurrentProfile->name(), *m_DirectoryStructure, - m_CurrentProfile->getLockedOrderFileName(), force); + m_CurrentProfile->getLockedOrderFileName(), force, + lightRefresh); } catch (const std::exception& e) { reportError(tr("Failed to refresh list of esps: %1").arg(e.what())); } @@ -1450,23 +1451,24 @@ void OrganizerCore::updateModsInDirectoryStructure( m_DirectoryRefresher->addMultipleModsFilesToStructure(m_DirectoryStructure, entries); DirectoryRefresher::cleanStructure(m_DirectoryStructure); - // need to refresh plugin list now so we can activate esps - refreshESPList(true); + // need to refresh plugin list now so we can activate esps. A light refresh is + // enough here: it only has to populate the plugin list and the enabled states + // for updateModsActiveState below. The caller (modStatusChanged) does a full + // refreshLists() afterwards, which rebuilds everything the light refresh skipped. + refreshESPList(true, /*lightRefresh=*/true); // activate all esps of the specified mod so the bsas get activated along with // it m_PluginList.blockSignals(true); updateModsActiveState(modInfo.keys(), true); m_PluginList.blockSignals(false); - // now we need to refresh the bsa list and save it so there is no confusion - // about what archives are available and active - refreshBSAList(); - if (m_UserInterface != nullptr) { - m_UserInterface->archivesWriter().writeImmediately(false); - } - std::vector archives = enabledArchives(); - m_DirectoryRefresher->setMods(m_CurrentProfile->getActiveMods(), - std::set(archives.begin(), archives.end())); + // determine which archives are active so the directory refresher knows which + // bsas to load. This used to go through a full refreshBSAList() (which rebuilds + // the BSA tree widget and round-trips the active set through the archives file); + // since the trailing refreshLists() rebuilds the tree and rewrites that file + // anyway, compute the active set directly and skip the redundant UI work here. + const std::set archives = activeArchives(); + m_DirectoryRefresher->setMods(m_CurrentProfile->getActiveMods(), archives); // finally also add files from bsas to the directory structure for (auto idx : modInfo.keys()) { @@ -1587,6 +1589,71 @@ std::vector OrganizerCore::enabledArchives() return result; } +std::set OrganizerCore::activeArchives() +{ + std::set result; + + auto archives = gameFeatures().gameFeature(); + if (archives == nullptr) { + return result; + } + + // default archives are the ones enabled outside MO (see refreshBSAList) + QStringList defaultArchives = archives->archives(m_CurrentProfile.get()); + if (defaultArchives.isEmpty()) { + defaultArchives = archives->vanillaArchives(); + } + const bool forceCore = settings().game().forceEnableCoreFiles(); + + // plugins present in the virtual data directory, keyed by their base name so we + // can match archives like "MyMod - Textures.bsa" to the plugin "MyMod.esp" + const QStringList plugins = + findFiles("", [](const QString& fileName) -> bool { + return fileName.endsWith(".esp", Qt::CaseInsensitive) || + fileName.endsWith(".esm", Qt::CaseInsensitive) || + fileName.endsWith(".esl", Qt::CaseInsensitive); + }); + + QList> pluginNamePairs; + pluginNamePairs.reserve(plugins.size()); + for (const QString& pluginName : plugins) { + QFileInfo pluginInfo(pluginName); + pluginNamePairs.append( + std::make_pair(pluginInfo.completeBaseName(), pluginInfo.fileName())); + } + + auto hasAssociatedPlugin = [&](const QString& bsaName) -> bool { + for (const auto& [completeBaseName, fileName] : pluginNamePairs) { + if (bsaName.startsWith(completeBaseName, Qt::CaseInsensitive) && + (m_PluginList.state(fileName) == IPluginList::STATE_ACTIVE)) { + return true; + } + } + return false; + }; + + // this must stay in sync with the check-state logic in MainWindow::updateBSAList + for (FileEntryPtr current : m_DirectoryStructure->getFiles()) { + if (current.get() == nullptr) { + continue; + } + QFileInfo fileInfo(ToQString(current->getName().c_str())); + const QString suffix = fileInfo.suffix().toLower(); + if (suffix != "bsa" && suffix != "ba2") { + continue; + } + + const QString name = fileInfo.fileName(); + if ((forceCore && defaultArchives.contains(name)) || + (name.compare("update.bsa", Qt::CaseInsensitive) == 0) || + hasAssociatedPlugin(name)) { + result.insert(name); + } + } + + return result; +} + void OrganizerCore::refreshDirectoryStructure() { if (m_DirectoryUpdate) { diff --git a/src/organizercore.h b/src/organizercore.h index d7810e1b6..8225eb268 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -47,6 +47,7 @@ class PluginContainer; class DirectoryRefresher; #include +#include #include namespace MOBase @@ -280,6 +281,12 @@ class OrganizerCore : public QObject, public MOBase::IPluginDiagnose std::vector enabledArchives(); + // computes the set of archives (bsa/ba2) that should be active, using the same + // rules as MainWindow::updateBSAList (core/default archives, update.bsa and + // archives associated with an active plugin) but without touching the UI. Used + // to feed the directory refresher without rebuilding the BSA tree widget. + std::set activeArchives(); + MOBase::Version getVersion() const { return m_Updater.getVersion(); } // return the plugin container @@ -318,7 +325,12 @@ class OrganizerCore : public QObject, public MOBase::IPluginDiagnose ProcessRunner::Results waitForAllUSVFSProcesses(UILocker::Reasons reason = UILocker::PreventExit); - void refreshESPList(bool force = false); + // when lightRefresh is true, only the work required to query and toggle plugin + // activation state is performed (plugin list + enabled states); the expensive + // relationship/master/index/load-order finalization is skipped. This is used + // for the intermediate refresh during a mod state change, where a second, full + // refresh follows anyway. + void refreshESPList(bool force = false, bool lightRefresh = false); void refreshBSAList(); void refreshDirectoryStructure(); diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index 021b5f5bf..8d5c692a1 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -203,7 +203,7 @@ void PluginList::highlightMasters(const QModelIndexList& selectedPluginIndices) void PluginList::refresh(const QString& profileName, const DirectoryEntry& baseDirectory, - const QString& lockedOrderFile, bool force) + const QString& lockedOrderFile, bool force, bool lightRefresh) { TimeThis tt("PluginList::refresh()"); @@ -286,11 +286,14 @@ void PluginList::refresh(const QString& profileName, originName = modInfo->name(); } + const QString pluginPath = ToQString(current->getFullPath()); + const CachedESPData& fileData = + cachedESPData(pluginPath, current->getFileTime(), mediumPluginsAreSupported); + m_ESPs.emplace_back(filename, forceLoaded, forceEnabled, forceDisabled, - originName, ToQString(current->getFullPath()), hasIni, - loadedArchives, lightPluginsAreSupported, - mediumPluginsAreSupported, blueprintPluginsAreSupported, - blueprintPrefix); + originName, pluginPath, hasIni, loadedArchives, + lightPluginsAreSupported, mediumPluginsAreSupported, + blueprintPluginsAreSupported, blueprintPrefix, fileData); m_ESPs.rbegin()->priority = -1; } catch (const std::exception& e) { reportError(tr("failed to update esp info for file %1 (source id: %2), error: %3") @@ -322,6 +325,14 @@ void PluginList::refresh(const QString& profileName, gamePlugins->readPluginLists(m_Organizer.managedGameOrganizer()->pluginList()); } + // at this point m_ESPs/m_ESPsByName and the enabled states are populated, which + // is everything enableESP()/isEnabled() need. A light refresh stops here and + // lets the ChangeBracket close on scope exit; the skipped finalization (and its + // UI notifications) is redone by the full refresh that follows. + if (lightRefresh) { + return; + } + fixPrimaryPlugins(); fixPluginRelationships(); @@ -2124,51 +2135,60 @@ QModelIndex PluginList::parent(const QModelIndex&) const return QModelIndex(); } +const PluginList::CachedESPData& +PluginList::cachedESPData(const QString& fullPath, FILETIME fileTime, + bool mediumSupported) +{ + auto it = m_ESPParseCache.find(fullPath); + if (it != m_ESPParseCache.end() && + CompareFileTime(&it->second.time, &fileTime) == 0) { + // same source path and modification time: the parse result is still valid + return it->second; + } + + CachedESPData data; + data.time = fileTime; + + try { + ESP::File file(ToWString(fullPath)); + data.isMaster = file.isMaster(); + data.isLight = file.isLight(mediumSupported); + data.isMedium = file.isMedium(); + data.isBlueprint = file.isBlueprint(); + data.isDummy = file.isDummy(); + data.formVersion = file.formVersion(); + data.headerVersion = file.headerVersion(); + data.author = file.author(); + data.description = file.description(); + data.masters = file.masters(); + } catch (const std::exception& e) { + // flag the failure so the ESPInfo constructor zeroes the derived fields and do NOT cache it + log::error("failed to parse plugin file {}: {}", fullPath, e.what()); + static const CachedESPData failed = [] { + CachedESPData d; + d.parseFailed = true; + return d; + }(); + return failed; + } + + return m_ESPParseCache.insert_or_assign(fullPath, std::move(data)).first->second; +} + PluginList::ESPInfo::ESPInfo(const QString& name, bool forceLoaded, bool forceEnabled, bool forceDisabled, const QString& originName, const QString& fullPath, bool hasIni, std::set archives, bool lightSupported, bool mediumSupported, bool blueprintSupported, - const QString& blueprintPrefix) + const QString& blueprintPrefix, + const CachedESPData& fileData) : name(name), fullPath(fullPath), enabled(forceLoaded), forceLoaded(forceLoaded), forceEnabled(forceEnabled), forceDisabled(forceDisabled), priority(0), loadOrder(-1), originName(originName), hasIni(hasIni), archives(archives.begin(), archives.end()), modSelected(false), isMasterOfSelectedPlugin(false) { - try { - ESP::File file(ToWString(fullPath)); - auto extension = name.right(3).toLower(); - hasMasterExtension = (extension == "esm"); - hasLightExtension = (extension == "esl"); - isMasterFlagged = file.isMaster(); - isLightFlagged = lightSupported && file.isLight(mediumSupported); - isMediumFlagged = mediumSupported && file.isMedium(); - isBlueprintFlagged = blueprintSupported && - (isMasterFlagged || hasMasterExtension || hasLightExtension) && - file.isBlueprint(); - isBlueprintPrefixed = - blueprintSupported && name.startsWith(blueprintPrefix, Qt::CaseInsensitive); - hasNoRecords = file.isDummy(); - - if (blueprintSupported) { - if ((isBlueprintFlagged || isBlueprintPrefixed) && !this->forceEnabled) { - this->forceDisabled = true; - } - } - - hasNoRecords = file.isDummy(); - - formVersion = file.formVersion(); - headerVersion = file.headerVersion(); - author = QString::fromLatin1(file.author().c_str()); - description = QString::fromLatin1(file.description().c_str()); - - for (auto&& m : file.masters()) { - masters.insert(QString::fromStdString(m)); - } - } catch (const std::exception& e) { - log::error("failed to parse plugin file {}: {}", fullPath, e.what()); + if (fileData.parseFailed) { hasMasterExtension = false; hasLightExtension = false; isMasterFlagged = false; @@ -2176,10 +2196,43 @@ PluginList::ESPInfo::ESPInfo(const QString& name, bool forceLoaded, bool forceEn isMediumFlagged = false; isBlueprintFlagged = false; hasNoRecords = false; + return; + } + + auto extension = name.right(3).toLower(); + hasMasterExtension = (extension == "esm"); + hasLightExtension = (extension == "esl"); + isMasterFlagged = fileData.isMaster; + isLightFlagged = lightSupported && fileData.isLight; + isMediumFlagged = mediumSupported && fileData.isMedium; + isBlueprintFlagged = blueprintSupported && + (isMasterFlagged || hasMasterExtension || hasLightExtension) && + fileData.isBlueprint; + isBlueprintPrefixed = + blueprintSupported && name.startsWith(blueprintPrefix, Qt::CaseInsensitive); + hasNoRecords = fileData.isDummy; + + if (blueprintSupported) { + if ((isBlueprintFlagged || isBlueprintPrefixed) && !this->forceEnabled) { + this->forceDisabled = true; + } + } + + formVersion = fileData.formVersion; + headerVersion = fileData.headerVersion; + author = QString::fromLatin1(fileData.author.c_str()); + description = QString::fromLatin1(fileData.description.c_str()); + + for (auto&& m : fileData.masters) { + masters.insert(QString::fromStdString(m)); } } void PluginList::managedGameChanged(const IPluginGame* gamePlugin) { m_GamePlugin = gamePlugin; + + // the cached parse results bake in game-dependent flags (e.g. medium-plugin + // support affects isLight), so they must not survive a game switch + m_ESPParseCache.clear(); } diff --git a/src/pluginlist.h b/src/pluginlist.h index 478fa85e1..9766b4523 100644 --- a/src/pluginlist.h +++ b/src/pluginlist.h @@ -127,11 +127,17 @@ class PluginList : public QAbstractItemModel * @param baseDirectory the root directory structure representing the virtual data *directory * @param lockedOrderFile list of plugins that shouldn't change load order + * @param force if true, the plugin list is rebuilt from scratch + * @param lightRefresh if true, only the plugin list and enabled states are + * rebuilt; the relationship/master/index/load-order finalization (and the + * associated UI notifications) are skipped. Use this only when a full + * refresh is guaranteed to follow, e.g. the intermediate refresh while + * applying a mod state change. enableESP()/isEnabled() remain valid. * @todo the profile is not used? If it was, we should pass the Profile-object instead **/ void refresh(const QString& profileName, const MOShared::DirectoryEntry& baseDirectory, - const QString& lockedOrderFile, bool refresh); + const QString& lockedOrderFile, bool force, bool lightRefresh = false); /** * @brief enable a plugin based on its name @@ -326,13 +332,37 @@ public slots: void writePluginsList(); private: + // Cached result of parsing a plugin file from disk (ESP::File). The parsed + // fields depend only on the file's content, so they are reused across refreshes + // as long as the source path and modification time are unchanged. + struct CachedESPData + { + FILETIME time = {}; + bool parseFailed = false; + bool isMaster = false; + bool isLight = false; + bool isMedium = false; + bool isBlueprint = false; + bool isDummy = false; + uint16_t formVersion = 0; + float headerVersion = 0.0f; + std::string author; + std::string description; + std::set masters; + }; + + // Returns parsed data for the plugin at fullPath, reading from disk only on a + // cache miss (new path or changed modification time). + const CachedESPData& cachedESPData(const QString& fullPath, FILETIME fileTime, + bool mediumSupported); + struct ESPInfo { ESPInfo(const QString& name, bool forceLoaded, bool forceEnabled, bool forceDisabled, const QString& originName, const QString& fullPath, bool hasIni, std::set archives, bool lightSupported, bool mediumSupported, bool blueprintSupported, - const QString& blueprintPrefix); + const QString& blueprintPrefix, const CachedESPData& fileData); QString name; QString fullPath; @@ -410,6 +440,11 @@ public slots: std::map m_ESPsByName; std::vector m_ESPsByPriority; + // disk-parse cache for plugin files, keyed by full path (see cachedESPData); + // std::map gives stable references so a returned entry stays valid while the + // refresh loop keeps inserting + std::map m_ESPParseCache; + std::map m_LockedOrder; std::map diff --git a/src/settings.cpp b/src/settings.cpp index a7c46458c..a94ef547d 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1979,12 +1979,17 @@ NexusSettings::NexusSettings(Settings& parent, QSettings& settings) bool NexusSettings::endorsementIntegration() const { - return get(m_Settings, "Settings", "endorsement_integration", true); + if (!m_EndorsementIntegration) { + m_EndorsementIntegration = + get(m_Settings, "Settings", "endorsement_integration", true); + } + return *m_EndorsementIntegration; } void NexusSettings::setEndorsementIntegration(bool b) const { set(m_Settings, "Settings", "endorsement_integration", b); + m_EndorsementIntegration = b; } EndorsementState NexusSettings::endorsementState() const @@ -2006,12 +2011,17 @@ void NexusSettings::setEndorsementState(EndorsementState s) bool NexusSettings::trackedIntegration() const { - return get(m_Settings, "Settings", "tracked_integration", true); + if (!m_TrackedIntegration) { + m_TrackedIntegration = + get(m_Settings, "Settings", "tracked_integration", true); + } + return *m_TrackedIntegration; } void NexusSettings::setTrackedIntegration(bool b) const { set(m_Settings, "Settings", "tracked_integration", b); + m_TrackedIntegration = b; } bool NexusSettings::categoryMappings() const @@ -2201,12 +2211,17 @@ void InterfaceSettings::setCollapsibleSeparators(bool ascending, bool descending bool InterfaceSettings::collapsibleSeparatorsHighlightTo() const { - return get(m_Settings, "Settings", "collapsible_separators_conflicts_to", true); + if (!m_CollapsibleSeparatorsHighlightTo) { + m_CollapsibleSeparatorsHighlightTo = + get(m_Settings, "Settings", "collapsible_separators_conflicts_to", true); + } + return *m_CollapsibleSeparatorsHighlightTo; } void InterfaceSettings::setCollapsibleSeparatorsHighlightTo(bool b) { set(m_Settings, "Settings", "collapsible_separators_conflicts_to", b); + m_CollapsibleSeparatorsHighlightTo = b; } bool InterfaceSettings::collapsibleSeparatorsHighlightFrom() const @@ -2222,14 +2237,20 @@ void InterfaceSettings::setCollapsibleSeparatorsHighlightFrom(bool b) bool InterfaceSettings::collapsibleSeparatorsIcons(int column) const { - return get(m_Settings, "Settings", - QString("collapsible_separators_icons_%1").arg(column), true); + auto it = m_CollapsibleSeparatorsIcons.find(column); + if (it == m_CollapsibleSeparatorsIcons.end()) { + const bool v = get(m_Settings, "Settings", + QString("collapsible_separators_icons_%1").arg(column), true); + it = m_CollapsibleSeparatorsIcons.emplace(column, v).first; + } + return it->second; } void InterfaceSettings::setCollapsibleSeparatorsIcons(int column, bool show) { set(m_Settings, "Settings", QString("collapsible_separators_icons_%1").arg(column), show); + m_CollapsibleSeparatorsIcons[column] = show; } bool InterfaceSettings::collapsibleSeparatorsPerProfile() const diff --git a/src/settings.h b/src/settings.h index 5b2c6ee75..ca9c0f35d 100644 --- a/src/settings.h +++ b/src/settings.h @@ -539,6 +539,11 @@ class NexusSettings private: Settings& m_Parent; QSettings& m_Settings; + + // endorsement/tracking integration are read on every getFlags() call (i.e. per + // painted row), but only ever change through the setters below, so cache them + mutable std::optional m_EndorsementIntegration; + mutable std::optional m_TrackedIntegration; }; class SteamSettings @@ -703,6 +708,12 @@ class InterfaceSettings private: QSettings& m_Settings; + + // these are read on every mod list row paint (separator highlight/icons) but + // only change through their setters, so cache them lazily (see NexusSettings + // for the same pattern). The icons cache is keyed by column. + mutable std::optional m_CollapsibleSeparatorsHighlightTo; + mutable std::map m_CollapsibleSeparatorsIcons; }; class DiagnosticsSettings diff --git a/src/settingsutilities.h b/src/settingsutilities.h index cd55464e5..aaf0c808f 100644 --- a/src/settingsutilities.h +++ b/src/settingsutilities.h @@ -90,8 +90,9 @@ template std::optional getOptional(const QSettings& settings, const QString& section, const QString& key, std::optional def = {}) { - if (settings.contains(settingName(section, key))) { - const auto v = settings.value(settingName(section, key)); + const auto name = settingName(section, key); + if (settings.contains(name)) { + const auto v = settings.value(name); if constexpr (std::is_enum_v) { return static_cast(v.value>()); diff --git a/src/shared/directoryentry.cpp b/src/shared/directoryentry.cpp index 67c13e54f..c473522f1 100644 --- a/src/shared/directoryentry.cpp +++ b/src/shared/directoryentry.cpp @@ -168,6 +168,16 @@ void DirectoryEntry::addFromAllBSAs(const std::wstring& originName, const std::vector& loadOrder, DirectoryStats& stats) { + if (archives.empty()) { + return; + } + + std::vector pluginStemsLc; + pluginStemsLc.reserve(loadOrder.size()); + for (const auto& plugin : loadOrder) { + pluginStemsLc.push_back(ToLowerCopy(std::filesystem::path(plugin).stem().native())); + } + for (const auto& archive : archives) { const std::filesystem::path archivePath(archive); const auto filename = archivePath.filename().native(); @@ -180,15 +190,15 @@ void DirectoryEntry::addFromAllBSAs(const std::wstring& originName, int order = -1; - for (auto plugin : loadOrder) { - const auto pluginNameLc = - ToLowerCopy(std::filesystem::path(plugin).stem().native()); + for (std::size_t i = 0; i < pluginStemsLc.size(); ++i) { + const std::wstring& pluginNameLc = pluginStemsLc[i]; + + if (filenameLc.starts_with(pluginNameLc)) { + std::wstring_view rest(filenameLc); + rest.remove_prefix(pluginNameLc.size()); - if (filenameLc.starts_with(pluginNameLc + L" - ") || - filenameLc.starts_with(pluginNameLc + L".")) { - auto itor = std::find(loadOrder.begin(), loadOrder.end(), plugin); - if (itor != loadOrder.end()) { - order = std::distance(loadOrder.begin(), itor); + if (rest.starts_with(L" - ") || (!rest.empty() && rest.front() == L'.')) { + order = static_cast(i); } } } @@ -246,7 +256,11 @@ void DirectoryEntry::propagateOrigin(int origin) { { std::scoped_lock lock(m_OriginsMutex); - m_Origins.insert(origin); + if (!m_Origins.insert(origin).second) { + // The origin was already recorded here. By construction, an origin is only ever added to a directory + // together with all of its ancestors, so every ancestor already has it too - there is nothing left to propagate. + return; + } } if (m_Parent != nullptr) { @@ -557,10 +571,10 @@ FileEntryPtr DirectoryEntry::insert(std::wstring_view fileName, FilesOrigin& ori this, stats); elapsed(stats.addFileTimes, [&] { - addFileToList(std::move(key.value), fe->getIndex()); + addFileToList(std::move(key), fe->getIndex()); }); - // fileNameLower has moved from this point + // key has moved from this point } } @@ -600,7 +614,7 @@ FileEntryPtr DirectoryEntry::insert(env::File& file, FilesOrigin& origin, // file.name has been moved from this point elapsed(stats.addFileTimes, [&] { - addFileToList(std::move(file.lcname), fe->getIndex()); + addFileToList(DirectoryEntryFileKey(std::move(file.lcname)), fe->getIndex()); }); // file.lcname has been moved from this point @@ -890,11 +904,12 @@ void DirectoryEntry::removeFilesFromList(const std::set& indices) } } -void DirectoryEntry::addFileToList(std::wstring fileNameLower, FileIndex index) +void DirectoryEntry::addFileToList(DirectoryEntryFileKey key, FileIndex index) { - m_FilesLookup.emplace(fileNameLower, index); - m_Files.emplace(std::move(fileNameLower), index); - // fileNameLower has been moved from this point + // copy the name into the ordered map, then move the key (with its already + // computed hash) into the lookup map so it isn't re-hashed + m_Files.emplace(key.value, index); + m_FilesLookup.emplace(std::move(key), index); } struct DumpFailed : public std::runtime_error diff --git a/src/shared/directoryentry.h b/src/shared/directoryentry.h index cb8d5c781..ca51a7502 100644 --- a/src/shared/directoryentry.h +++ b/src/shared/directoryentry.h @@ -259,7 +259,7 @@ class DirectoryEntry void addDirectoryToList(DirectoryEntry* e, std::wstring nameLc); void removeDirectoryFromList(SubDirectories::iterator itor); - void addFileToList(std::wstring fileNameLower, FileIndex index); + void addFileToList(DirectoryEntryFileKey key, FileIndex index); void removeFileFromList(FileIndex index); void removeFilesFromList(const std::set& indices); diff --git a/src/shared/fileentry.cpp b/src/shared/fileentry.cpp index 486706170..be11aeeba 100644 --- a/src/shared/fileentry.cpp +++ b/src/shared/fileentry.cpp @@ -1,10 +1,21 @@ #include "fileentry.h" #include "directoryentry.h" +#include "fileregister.h" #include "filesorigin.h" namespace MOShared { +std::shared_lock FileEntry::originsSortReadLock() const +{ + if (m_Parent != nullptr) { + if (const auto reg = m_Parent->getFileRegister()) { + return std::shared_lock(reg->originsSortMutex()); + } + } + return {}; +} + FileEntry::FileEntry() : m_Index(InvalidFileIndex), m_Name(), m_Origin(-1), m_Parent(nullptr), m_FileSize(NoFileSize), m_CompressedFileSize(NoFileSize) @@ -140,9 +151,15 @@ bool FileEntry::removeOrigin(OriginID origin) void FileEntry::sortOrigins() { - std::scoped_lock lock(m_OriginsMutex); + // No per-entry lock here, you MUST have exclusive lock on originsSortReadLock (see FileRegister::sortOrigins) + + // No alternatives, nothing to sort. Let's not do useless work. + if (m_Alternatives.empty()) { + return; + } - m_Alternatives.push_back({m_Origin, m_Archive}); + // m_Archive is reassigned from the sorted result below, so it can be moved in rather than copied + m_Alternatives.push_back({m_Origin, std::move(m_Archive)}); std::sort(m_Alternatives.begin(), m_Alternatives.end(), [&](auto&& LHS, auto&& RHS) { if (!LHS.isFromArchive() && !RHS.isFromArchive()) { @@ -186,6 +203,9 @@ void FileEntry::sortOrigins() bool FileEntry::isFromArchive(std::wstring archiveName) const { + // shared lock first, then the per-entry lock (consistent order, no deadlock); + // excludes the bulk sort, which mutates origin data without the per-entry lock + const auto sortLock = originsSortReadLock(); std::scoped_lock lock(m_OriginsMutex); if (archiveName.length() == 0) { @@ -207,6 +227,9 @@ bool FileEntry::isFromArchive(std::wstring archiveName) const std::wstring FileEntry::getFullPath(OriginID originID) const { + // shared lock first, then the per-entry lock (consistent order, no deadlock); + // excludes the bulk sort, which mutates origin data without the per-entry lock + const auto sortLock = originsSortReadLock(); std::scoped_lock lock(m_OriginsMutex); if (originID == InvalidOriginID) { diff --git a/src/shared/fileentry.h b/src/shared/fileentry.h index 80d8b2924..af4e85ed4 100644 --- a/src/shared/fileentry.h +++ b/src/shared/fileentry.h @@ -3,6 +3,8 @@ #include "fileregisterfwd.h" +#include + namespace MOShared { @@ -84,6 +86,12 @@ class FileEntry mutable std::mutex m_OriginsMutex; bool recurseParents(std::wstring& path, const DirectoryEntry* parent) const; + + // Shared lock on the owning FileRegister's origins-sort mutex. Readers take + // this (in addition to m_OriginsMutex) so they are excluded while + // FileRegister::sortOrigins holds it exclusively. Returns an empty lock if the + // register can't be reached (e.g. a parentless entry). + std::shared_lock originsSortReadLock() const; }; } // namespace MOShared diff --git a/src/shared/fileregister.cpp b/src/shared/fileregister.cpp index f84d49823..4e312f7fc 100644 --- a/src/shared/fileregister.cpp +++ b/src/shared/fileregister.cpp @@ -3,6 +3,7 @@ #include "fileentry.h" #include "filesorigin.h" #include "originconnection.h" +#include #include namespace MOShared @@ -16,7 +17,7 @@ FileRegister::FileRegister(boost::shared_ptr originConnection) bool FileRegister::indexValid(FileIndex index) const { - std::scoped_lock lock(m_Mutex); + std::shared_lock lock(m_Mutex); if (index < m_Files.size()) { return (m_Files[index].get() != nullptr); @@ -29,15 +30,32 @@ FileEntryPtr FileRegister::createFile(std::wstring name, DirectoryEntry* parent, DirectoryStats& stats) { const auto index = generateIndex(); - auto p = FileEntryPtr(new FileEntry(index, std::move(name), parent)); - + auto p = boost::make_shared(index, std::move(name), parent); + + // Fast path: if the slot already exists, assign it under a SHARED lock. This + // is safe because the container only ever grows (so the slot stays valid), the + // index is unique and not yet visible to any reader, and a shared lock still + // excludes the exclusive resize below - so the deque's structure can't change + // underneath us. Concurrent createFile/getFile calls touch distinct elements, + // so they no longer serialize. { - std::scoped_lock lock(m_Mutex); + std::shared_lock lock(m_Mutex); + if (index < m_Files.size()) { + m_Files[index] = p; + return p; + } + } + // Slow path: the deque needs to grow, which is a structural change and must be + // exclusive. Grow in chunks (indices are dense and monotonic) so this runs + // rarely; trailing unused slots stay null and are bounds/null-checked by every + // accessor, and highestCount() reports the true count via m_NextIndex. + { + std::unique_lock lock(m_Mutex); if (index >= m_Files.size()) { - m_Files.resize(index + 1); + constexpr size_t chunk = 4096; + m_Files.resize(((index + chunk) / chunk) * chunk); } - m_Files[index] = p; } @@ -51,7 +69,7 @@ FileIndex FileRegister::generateIndex() FileEntryPtr FileRegister::getFile(FileIndex index) const { - std::scoped_lock lock(m_Mutex); + std::shared_lock lock(m_Mutex); if (index < m_Files.size()) { return m_Files[index]; @@ -152,7 +170,11 @@ void FileRegister::removeOriginMulti(std::set indices, OriginID origi void FileRegister::sortOrigins() { - std::scoped_lock lock(m_Mutex); + // m_Mutex guards the m_Files container during iteration; m_OriginsSortMutex is + // held exclusively for the whole pass so each FileEntry::sortOrigins no longer + // needs to lock its own per-entry mutex. Readers take m_OriginsSortMutex + // shared, so they are correctly excluded for the duration of the sort. + std::scoped_lock lock(m_Mutex, m_OriginsSortMutex); for (auto&& p : m_Files) { if (p) { diff --git a/src/shared/fileregister.h b/src/shared/fileregister.h index aeb2c5b77..2b574fed8 100644 --- a/src/shared/fileregister.h +++ b/src/shared/fileregister.h @@ -4,6 +4,7 @@ #include "fileregisterfwd.h" #include #include +#include namespace MOShared { @@ -26,8 +27,9 @@ class FileRegister size_t highestCount() const { - std::scoped_lock lock(m_Mutex); - return m_Files.size(); + // m_NextIndex is the number of files ever created (dense, monotonic), which + // is the true count even though m_Files may be over-sized by chunk growth in createFile. + return m_NextIndex; } bool removeFile(FileIndex index); @@ -36,10 +38,24 @@ class FileRegister void sortOrigins(); + // Serializes origin-data access (sortOrigins vs. the FileEntry readers + // getFullPath/isFromArchive) so that sortOrigins can run without taking each + // FileEntry's own mutex per entry. sortOrigins takes this exclusively for the + // whole pass; readers take it shared in addition to their per-entry lock. + // This is intentionally separate from m_Mutex (which guards m_Files) and is + // never held while m_Mutex is taken, to avoid lock-order coupling. + std::shared_mutex& originsSortMutex() const { return m_OriginsSortMutex; } + private: using FileMap = std::deque; - mutable std::mutex m_Mutex; + // shared_mutex, not plain mutex: getFile() and createFile()'s element assign + // take it SHARED so the parallel mod-loading threads don't serialize (each + // createFile writes a unique, not-yet-visible index, and the container only + // ever grows). Only the rare deque resize and the remove* paths take it + // EXCLUSIVE. + mutable std::shared_mutex m_Mutex; + mutable std::shared_mutex m_OriginsSortMutex; FileMap m_Files; boost::shared_ptr m_OriginConnection; std::atomic m_NextIndex; From 17b66c5bca13172293b71a49c2e8188072d40a5c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:49:20 +0000 Subject: [PATCH 2/2] [pre-commit.ci] Auto fixes from pre-commit.com hooks. --- src/envfs.h | 3 ++- src/organizercore.cpp | 11 +++++------ src/pluginlist.cpp | 9 +++++---- src/pluginlist.h | 16 ++++++++-------- src/settings.cpp | 5 +++-- src/shared/directoryentry.cpp | 5 +++-- src/shared/fileentry.cpp | 6 ++++-- src/shared/fileregister.cpp | 2 +- src/shared/fileregister.h | 3 ++- 9 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/envfs.h b/src/envfs.h index 98acf53b6..1d00aba47 100644 --- a/src/envfs.h +++ b/src/envfs.h @@ -89,7 +89,8 @@ class ThreadPool } } - // Constantly yield here - windows timer is too coarse otherwise (~16ms), leads to wasting a LOT of time idling + // Constantly yield here - windows timer is too coarse otherwise (~16ms), leads to + // wasting a LOT of time idling std::this_thread::yield(); } } diff --git a/src/organizercore.cpp b/src/organizercore.cpp index a44311c75..111521c03 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -1607,12 +1607,11 @@ std::set OrganizerCore::activeArchives() // plugins present in the virtual data directory, keyed by their base name so we // can match archives like "MyMod - Textures.bsa" to the plugin "MyMod.esp" - const QStringList plugins = - findFiles("", [](const QString& fileName) -> bool { - return fileName.endsWith(".esp", Qt::CaseInsensitive) || - fileName.endsWith(".esm", Qt::CaseInsensitive) || - fileName.endsWith(".esl", Qt::CaseInsensitive); - }); + const QStringList plugins = findFiles("", [](const QString& fileName) -> bool { + return fileName.endsWith(".esp", Qt::CaseInsensitive) || + fileName.endsWith(".esm", Qt::CaseInsensitive) || + fileName.endsWith(".esl", Qt::CaseInsensitive); + }); QList> pluginNamePairs; pluginNamePairs.reserve(plugins.size()); diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index 8d5c692a1..00213c775 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -2135,9 +2135,9 @@ QModelIndex PluginList::parent(const QModelIndex&) const return QModelIndex(); } -const PluginList::CachedESPData& -PluginList::cachedESPData(const QString& fullPath, FILETIME fileTime, - bool mediumSupported) +const PluginList::CachedESPData& PluginList::cachedESPData(const QString& fullPath, + FILETIME fileTime, + bool mediumSupported) { auto it = m_ESPParseCache.find(fullPath); if (it != m_ESPParseCache.end() && @@ -2162,7 +2162,8 @@ PluginList::cachedESPData(const QString& fullPath, FILETIME fileTime, data.description = file.description(); data.masters = file.masters(); } catch (const std::exception& e) { - // flag the failure so the ESPInfo constructor zeroes the derived fields and do NOT cache it + // flag the failure so the ESPInfo constructor zeroes the derived fields and do NOT + // cache it log::error("failed to parse plugin file {}: {}", fullPath, e.what()); static const CachedESPData failed = [] { CachedESPData d; diff --git a/src/pluginlist.h b/src/pluginlist.h index 9766b4523..5357b40be 100644 --- a/src/pluginlist.h +++ b/src/pluginlist.h @@ -337,15 +337,15 @@ public slots: // as long as the source path and modification time are unchanged. struct CachedESPData { - FILETIME time = {}; - bool parseFailed = false; - bool isMaster = false; - bool isLight = false; - bool isMedium = false; - bool isBlueprint = false; - bool isDummy = false; + FILETIME time = {}; + bool parseFailed = false; + bool isMaster = false; + bool isLight = false; + bool isMedium = false; + bool isBlueprint = false; + bool isDummy = false; uint16_t formVersion = 0; - float headerVersion = 0.0f; + float headerVersion = 0.0f; std::string author; std::string description; std::set masters; diff --git a/src/settings.cpp b/src/settings.cpp index a94ef547d..7401324b4 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -2239,8 +2239,9 @@ bool InterfaceSettings::collapsibleSeparatorsIcons(int column) const { auto it = m_CollapsibleSeparatorsIcons.find(column); if (it == m_CollapsibleSeparatorsIcons.end()) { - const bool v = get(m_Settings, "Settings", - QString("collapsible_separators_icons_%1").arg(column), true); + const bool v = + get(m_Settings, "Settings", + QString("collapsible_separators_icons_%1").arg(column), true); it = m_CollapsibleSeparatorsIcons.emplace(column, v).first; } return it->second; diff --git a/src/shared/directoryentry.cpp b/src/shared/directoryentry.cpp index c473522f1..dc009b8e2 100644 --- a/src/shared/directoryentry.cpp +++ b/src/shared/directoryentry.cpp @@ -257,8 +257,9 @@ void DirectoryEntry::propagateOrigin(int origin) { std::scoped_lock lock(m_OriginsMutex); if (!m_Origins.insert(origin).second) { - // The origin was already recorded here. By construction, an origin is only ever added to a directory - // together with all of its ancestors, so every ancestor already has it too - there is nothing left to propagate. + // The origin was already recorded here. By construction, an origin is only ever + // added to a directory together with all of its ancestors, so every ancestor + // already has it too - there is nothing left to propagate. return; } } diff --git a/src/shared/fileentry.cpp b/src/shared/fileentry.cpp index be11aeeba..64ba7d5ed 100644 --- a/src/shared/fileentry.cpp +++ b/src/shared/fileentry.cpp @@ -151,14 +151,16 @@ bool FileEntry::removeOrigin(OriginID origin) void FileEntry::sortOrigins() { - // No per-entry lock here, you MUST have exclusive lock on originsSortReadLock (see FileRegister::sortOrigins) + // No per-entry lock here, you MUST have exclusive lock on originsSortReadLock (see + // FileRegister::sortOrigins) // No alternatives, nothing to sort. Let's not do useless work. if (m_Alternatives.empty()) { return; } - // m_Archive is reassigned from the sorted result below, so it can be moved in rather than copied + // m_Archive is reassigned from the sorted result below, so it can be moved in rather + // than copied m_Alternatives.push_back({m_Origin, std::move(m_Archive)}); std::sort(m_Alternatives.begin(), m_Alternatives.end(), [&](auto&& LHS, auto&& RHS) { diff --git a/src/shared/fileregister.cpp b/src/shared/fileregister.cpp index 4e312f7fc..724772f2e 100644 --- a/src/shared/fileregister.cpp +++ b/src/shared/fileregister.cpp @@ -30,7 +30,7 @@ FileEntryPtr FileRegister::createFile(std::wstring name, DirectoryEntry* parent, DirectoryStats& stats) { const auto index = generateIndex(); - auto p = boost::make_shared(index, std::move(name), parent); + auto p = boost::make_shared(index, std::move(name), parent); // Fast path: if the slot already exists, assign it under a SHARED lock. This // is safe because the container only ever grows (so the slot stays valid), the diff --git a/src/shared/fileregister.h b/src/shared/fileregister.h index 2b574fed8..65ac6fee2 100644 --- a/src/shared/fileregister.h +++ b/src/shared/fileregister.h @@ -28,7 +28,8 @@ class FileRegister size_t highestCount() const { // m_NextIndex is the number of files ever created (dense, monotonic), which - // is the true count even though m_Files may be over-sized by chunk growth in createFile. + // is the true count even though m_Files may be over-sized by chunk growth in + // createFile. return m_NextIndex; }