diff --git a/indra/llcommon/llsingleton.h b/indra/llcommon/llsingleton.h index b5659e053cb..9cde43c8bc3 100644 --- a/indra/llcommon/llsingleton.h +++ b/indra/llcommon/llsingleton.h @@ -522,13 +522,16 @@ class LLSingleton : public LLSingletonBase case INITIALIZING: // here if DERIVED_TYPE::initSingleton() (directly or indirectly) // calls DERIVED_TYPE::getInstance(): go ahead and allow it - case INITIALIZED: - // normal subsequent calls - // record the dependency, if any: check if we got here from another + // record the dependency: check if we got here from another // LLSingleton's constructor or initSingleton() method capture_dependency(lk->mInstance); return lk->mInstance; + case INITIALIZED: + // normal subsequent calls - skip capture_dependency() for performance + // dependencies are only tracked during initialization + return lk->mInstance; + case DELETED: // called after deleteSingleton() logwarns({"Trying to access deleted singleton ", @@ -728,12 +731,14 @@ class LLParamSingleton : public LLSingleton case super::INITIALIZING: // As with LLSingleton, explicitly permit circular calls from - // within initSingleton() - case super::INITIALIZED: - // for any valid call, capture dependencies + // within initSingleton() and capture dependencies super::capture_dependency(lk->mInstance); return lk->mInstance; + case super::INITIALIZED: + // normal subsequent calls - skip capture_dependency() for performance + return lk->mInstance; + case super::DELETED: super::logerrs({"Trying to access deleted param singleton ", super::template classname()}); diff --git a/indra/llinventory/llfoldertype.cpp b/indra/llinventory/llfoldertype.cpp index 670405e9b55..ee554dfde0b 100644 --- a/indra/llinventory/llfoldertype.cpp +++ b/indra/llinventory/llfoldertype.cpp @@ -195,6 +195,24 @@ bool LLFolderType::lookupIsEnsembleType(EType folder_type) folder_type <= FT_ENSEMBLE_END); } +// static +bool LLFolderType::lookupIsEssentialType(EType folder_type) +{ + if (folder_type == FT_NONE) + { + return false; + } + + if (folder_type == FT_ROOT_INVENTORY) + { + return true; + } + + // Essential folders are those needed for basic viewer operation: + // singleton system folders and ensemble (outfit) folders + return lookupIsSingletonType(folder_type) || lookupIsEnsembleType(folder_type); +} + // static LLAssetType::EType LLFolderType::folderTypeToAssetType(LLFolderType::EType folder_type) { diff --git a/indra/llinventory/llfoldertype.h b/indra/llinventory/llfoldertype.h index dd12693f660..8774014f171 100644 --- a/indra/llinventory/llfoldertype.h +++ b/indra/llinventory/llfoldertype.h @@ -109,6 +109,8 @@ class LL_COMMON_API LLFolderType static bool lookupIsAutomaticType(EType folder_type); static bool lookupIsSingletonType(EType folder_type); static bool lookupIsEnsembleType(EType folder_type); + // Returns true if this folder type should be fully loaded during async inventory startup + static bool lookupIsEssentialType(EType folder_type); static LLAssetType::EType folderTypeToAssetType(LLFolderType::EType folder_type); static LLFolderType::EType assetTypeToFolderType(LLAssetType::EType asset_type); diff --git a/indra/llui/llpanel.cpp b/indra/llui/llpanel.cpp index db314cae0f5..138e637ede2 100644 --- a/indra/llui/llpanel.cpp +++ b/indra/llui/llpanel.cpp @@ -489,58 +489,69 @@ bool LLPanel::initPanelXML(LLXMLNodePtr node, LLView *parent, LLXMLNodePtr outpu LL_RECORD_BLOCK_TIME(FTM_PANEL_SETUP); LLXMLNodePtr referenced_xml; - std::string xml_filename = mXMLFilename; // if the panel didn't provide a filename, check the node - if (xml_filename.empty()) + if (mXMLFilename.empty()) { - node->getAttributeString("filename", xml_filename); - setXMLFilename(xml_filename); + std::string temp_filename; + node->getAttributeString("filename", temp_filename); + setXMLFilename(temp_filename); } + // Cache singleton and filename to avoid repeated calls + LLUICtrlFactory* factory = LLUICtrlFactory::getInstance(); + + // Cache node name pointer to avoid repeated dereferencing + const LLStringTableEntry* node_name = node->getName(); + + // Cache registry to avoid repeated singleton access + const child_registry_t& registry = child_registry_t::instance(); + LLXUIParser parser; - if (!xml_filename.empty()) + if (!mXMLFilename.empty()) { if (output_node) { //if we are exporting, we want to export the current xml //not the referenced xml - parser.readXUI(node, params, LLUICtrlFactory::getInstance()->getCurFileName()); + parser.readXUI(node, params, factory->getCurFileName()); Params output_params(params); setupParamsForExport(output_params, parent); - output_node->setName(node->getName()->mString); + output_node->setName(node_name->mString); parser.writeXUI(output_node, output_params, LLInitParam::default_parse_rules(), &default_params); return true; } - LLUICtrlFactory::instance().pushFileName(xml_filename); + factory->pushFileName(mXMLFilename); LL_RECORD_BLOCK_TIME(FTM_EXTERNAL_PANEL_LOAD); - if (!LLUICtrlFactory::getLayeredXMLNode(xml_filename, referenced_xml)) + if (!LLUICtrlFactory::getLayeredXMLNode(mXMLFilename, referenced_xml)) { - LL_WARNS() << "Couldn't parse panel from: " << xml_filename << LL_ENDL; + LL_WARNS() << "Couldn't parse panel from: " << mXMLFilename << LL_ENDL; return false; } - parser.readXUI(referenced_xml, params, LLUICtrlFactory::getInstance()->getCurFileName()); + // Get filename after pushFileName + const std::string& updated_filename = factory->getCurFileName(); + parser.readXUI(referenced_xml, params, updated_filename); // add children using dimensions from referenced xml for consistent layout setShape(params.rect); - LLUICtrlFactory::createChildren(this, referenced_xml, child_registry_t::instance()); + LLUICtrlFactory::createChildren(this, referenced_xml, registry); - LLUICtrlFactory::instance().popFileName(); + factory->popFileName(); } // ask LLUICtrlFactory for filename, since xml_filename might be empty - parser.readXUI(node, params, LLUICtrlFactory::getInstance()->getCurFileName()); + parser.readXUI(node, params, factory->getCurFileName()); if (output_node) { Params output_params(params); setupParamsForExport(output_params, parent); - output_node->setName(node->getName()->mString); + output_node->setName(node_name->mString); parser.writeXUI(output_node, output_params, LLInitParam::default_parse_rules(), &default_params); } @@ -552,7 +563,7 @@ bool LLPanel::initPanelXML(LLXMLNodePtr node, LLView *parent, LLXMLNodePtr outpu } // add children - LLUICtrlFactory::createChildren(this, node, child_registry_t::instance(), output_node); + LLUICtrlFactory::createChildren(this, node, registry, output_node); // Connect to parent after children are built, because tab containers // do a reshape() on their child panels, which requires that the children diff --git a/indra/llui/llview.cpp b/indra/llui/llview.cpp index 7d6c937b857..056dc454829 100644 --- a/indra/llui/llview.cpp +++ b/indra/llui/llview.cpp @@ -253,6 +253,28 @@ const std::string& LLView::getName() const return mName.empty() ? no_name : mName; } +void LLView::setName(const std::string& name) +{ + if (name == mName) + { + return; + } + + LLView* parent = mParentView; + + if (parent && !mName.empty()) + { + parent->mChildNameCache.erase(mName); + } + + mName = name; + + if (parent && !mName.empty()) + { + parent->mChildNameCache[mName] = this; + } +} + void LLView::sendChildToFront(LLView* child) { // llassert_always(sDepth == 0); // Avoid re-ordering while drawing; it can cause subtle iterator bugs @@ -305,6 +327,12 @@ bool LLView::addChild(LLView* child, S32 tab_group) // add to front of child list, as normal mChildList.push_front(child); + // Add to name cache for fast lookup + if (child->hasName()) + { + mChildNameCache[child->getName()] = child; + } + // add to tab order list if (tab_group != 0) { @@ -344,6 +372,13 @@ void LLView::removeChild(LLView* child) // if we are removing an item we are currently iterating over, that would be bad llassert(!child->mInDraw); mChildList.remove( child ); + + // Remove from name cache + if (child->hasName()) + { + mChildNameCache.erase(child->getName()); + } + child->mParentView = NULL; child_tab_order_t::iterator found = mTabOrder.find(child); if (found != mTabOrder.end()) @@ -1649,15 +1684,40 @@ LLView* LLView::findChildView(std::string_view name, bool recurse) const { LL_PROFILE_ZONE_SCOPED_CATEGORY_UI; + // Check cache first for direct children - O(1) lookup instead of O(n) + if (!mChildNameCache.empty()) + { + std::string lookup_key(name); + auto cache_it = mChildNameCache.find(lookup_key); + if (cache_it != mChildNameCache.end()) + { + return cache_it->second; + } + } + // Look for direct children *first* for (LLView* childp : mChildList) { llassert(childp); - if (childp->getName() == name) + const std::string& child_name = childp->getName(); + + if (child_name.empty()) + { + if (name.empty()) + { + return childp; + } + continue; + } + + if (child_name == name) { + // Cache the result for next lookup + mChildNameCache[child_name] = childp; return childp; } } + if (recurse) { // Look inside each child as well. diff --git a/indra/llui/llview.h b/indra/llui/llview.h index 97212a9d2d0..ae86ba32fe6 100644 --- a/indra/llui/llview.h +++ b/indra/llui/llview.h @@ -49,6 +49,7 @@ #include "llfocusmgr.h" #include +#include #include #include @@ -237,7 +238,8 @@ class LLView void setFollowsAll() { mReshapeFlags |= FOLLOWS_ALL; } void setSoundFlags(U8 flags) { mSoundFlags = flags; } - void setName(std::string name) { mName = name; } + void setName(const std::string& name); + bool hasName() const { return !mName.empty(); } void setUseBoundingRect( bool use_bounding_rect ); bool getUseBoundingRect() const; @@ -589,6 +591,9 @@ class LLView LLView* mParentView; child_list_t mChildList; + // Cache for fast child lookup by name - O(1) instead of O(n) + mutable std::unordered_map mChildNameCache; + // location in pixels, relative to surrounding structure, bottom,left=0,0 bool mVisible; LLRect mRect; diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 8869f4b1f60..bc0a59f1cb7 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -105,6 +105,7 @@ set(viewer_SOURCE_FILES llappearancemgr.cpp llappviewer.cpp llappviewerlistener.cpp + llasyncinventoryskeletonloader.cpp llattachmentsmgr.cpp llaudiosourcevo.cpp llautoreplace.cpp @@ -780,6 +781,7 @@ set(viewer_HEADER_FILES llappearancemgr.h llappviewer.h llappviewerlistener.h + llasyncinventoryskeletonloader.h llattachmentsmgr.h llaudiosourcevo.h llautoreplace.h diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index d6a1fbd124f..91f3c18cd89 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -3258,6 +3258,72 @@ Value 255 + ForceAsyncInventorySkeleton + + Comment + Force viewer to skip legacy login inventory skeleton and rely on async AIS fetching (QA only). + Persist + 1 + Type + Boolean + Value + 0 + + AsyncInventoryMaxConcurrentFetches + + Comment + Maximum number of concurrent AIS fetches used during async inventory skeleton loading. + Persist + 1 + Type + U32 + Value + 2 + + AsyncInventoryNotifyMinInterval + + Comment + Minimum seconds between inventory observer notifications while async inventory loading is active. + Persist + 1 + Type + F32 + Value + 0.05 + + AsyncInventoryCapsTimeout + + Comment + Maximum seconds to wait for inventory capabilities during async skeleton loading. + Persist + 1 + Type + F32 + Value + 45.0 + + AsyncInventoryFetchTimeout + + Comment + Maximum seconds for the entire async inventory skeleton fetch process. + Persist + 1 + Type + F32 + Value + 180.0 + + AsyncInventoryEssentialTimeout + + Comment + Maximum seconds to wait for essential inventory folders during async loading. + Persist + 1 + Type + F32 + Value + 90.0 + ForceLoginURL Comment diff --git a/indra/newview/llasyncinventoryskeletonloader.cpp b/indra/newview/llasyncinventoryskeletonloader.cpp new file mode 100644 index 00000000000..0d79023c8be --- /dev/null +++ b/indra/newview/llasyncinventoryskeletonloader.cpp @@ -0,0 +1,672 @@ +/** + * @file llasyncinventoryskeletonloader.cpp + * @brief Async inventory skeleton loading helper implementation. + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llasyncinventoryskeletonloader.h" + +#include "llagent.h" +#include "llappearancemgr.h" +#include "llcallbacklist.h" +#include "llfoldertype.h" +#include "llinventorymodel.h" +#include "llviewercontrol.h" +#include "llviewerregion.h" + +#include + +#include + +LLAsyncInventorySkeletonLoader gAsyncInventorySkeletonLoader; +bool gAsyncAgentCacheHydrated = false; +bool gAsyncLibraryCacheHydrated = false; +bool gAsyncParentChildMapPrimed = false; + +void LLAsyncInventorySkeletonLoader::reset() +{ + disconnectCapsCallback(); + removeIdleCallback(); + mPhase = Phase::Idle; + mForceAsync = false; + mEssentialReady = false; + mFetchQueue.clear(); + mActiveFetches.clear(); + mQueuedCategories.clear(); + mFetchedCategories.clear(); + mEssentialPending.clear(); + mFailureReason.clear(); + mCapsTimer.stop(); + mFetchTimer.stop(); + mTotalTimer.stop(); + mEssentialTimer.stop(); + + const U32 requested = gSavedSettings.getU32("AsyncInventoryMaxConcurrentFetches"); + mMaxConcurrentFetches = std::clamp(requested, 1U, 8U); + + mCapsTimeoutSec = gSavedSettings.getF32("AsyncInventoryCapsTimeout"); + mFetchTimeoutSec = gSavedSettings.getF32("AsyncInventoryFetchTimeout"); + mEssentialTimeoutSec = gSavedSettings.getF32("AsyncInventoryEssentialTimeout"); + + mSawCurrentOutfitFolder = false; +} + +bool LLAsyncInventorySkeletonLoader::isRunning() const +{ + return mPhase == Phase::WaitingForCaps || mPhase == Phase::Fetching; +} + +void LLAsyncInventorySkeletonLoader::start(bool force_async) +{ + reset(); + mForceAsync = force_async; + mPhase = Phase::WaitingForCaps; + mTotalTimer.start(); + mCapsTimer.start(); + + ensureCapsCallback(); + ensureIdleCallback(); + + LL_DEBUGS("AppInit") << "Async skeleton loader concurrency limit set to " + << mMaxConcurrentFetches << LL_ENDL; + + if (AISAPI::isAvailable()) + { + LL_DEBUGS("AppInit") << "Async skeleton loader detected AIS available at start; beginning fetch." << LL_ENDL; + startFetches(); + } + else + { + LL_DEBUGS("AppInit") << "Async skeleton loader awaiting AIS availability." << LL_ENDL; + } +} + +void LLAsyncInventorySkeletonLoader::ensureCapsCallback() +{ + disconnectCapsCallback(); + + LLViewerRegion* regionp = gAgent.getRegion(); + if (regionp) + { + mCapsConnection = regionp->setCapabilitiesReceivedCallback( + boost::bind(&LLAsyncInventorySkeletonLoader::onCapsReceived, this, _1, _2)); + LL_DEBUGS("AppInit") << "Async skeleton loader registered caps callback for region " + << regionp->getRegionID() << LL_ENDL; + } +} + +void LLAsyncInventorySkeletonLoader::disconnectCapsCallback() +{ + if (mCapsConnection.connected()) + { + mCapsConnection.disconnect(); + LL_DEBUGS("AppInit") << "Async skeleton loader disconnected caps callback." << LL_ENDL; + } +} + +void LLAsyncInventorySkeletonLoader::ensureIdleCallback() +{ + if (!mIdleRegistered) + { + gIdleCallbacks.addFunction(&LLAsyncInventorySkeletonLoader::idleCallback, this); + mIdleRegistered = true; + } +} + +void LLAsyncInventorySkeletonLoader::removeIdleCallback() +{ + if (mIdleRegistered) + { + gIdleCallbacks.deleteFunction(&LLAsyncInventorySkeletonLoader::idleCallback, this); + mIdleRegistered = false; + } +} + +void LLAsyncInventorySkeletonLoader::idleCallback(void* userdata) +{ + if (userdata) + { + static_cast(userdata)->update(); + } +} + +void LLAsyncInventorySkeletonLoader::onCapsReceived(const LLUUID&, LLViewerRegion* regionp) +{ + if (regionp && AISAPI::isAvailable()) + { + LL_DEBUGS("AppInit") << "Async skeleton loader received capabilities for region " + << regionp->getRegionID() << ", starting fetch." << LL_ENDL; + startFetches(); + } +} + +void LLAsyncInventorySkeletonLoader::startFetches() +{ + if (mPhase == Phase::Complete || mPhase == Phase::Failed) + { + LL_DEBUGS("AppInit") << "Async skeleton loader received startFetches after terminal state; ignoring." << LL_ENDL; + return; + } + + if (!AISAPI::isAvailable()) + { + LL_DEBUGS("AppInit") << "Async skeleton loader startFetches called but AIS still unavailable." << LL_ENDL; + return; + } + + if (mPhase == Phase::WaitingForCaps) + { + const LLUUID agent_root = gInventory.getRootFolderID(); + const LLUUID library_root = gInventory.getLibraryRootFolderID(); + + LL_INFOS("AppInit") << "Async inventory skeleton loader primed. force_async=" + << (mForceAsync ? "true" : "false") + << " agent_root=" << agent_root + << " library_root=" << library_root + << LL_ENDL; + + mPhase = Phase::Fetching; + mFetchTimer.start(); + scheduleInitialFetches(); + } + + processQueue(); +} + +void LLAsyncInventorySkeletonLoader::scheduleInitialFetches() +{ + const LLUUID agent_root = gInventory.getRootFolderID(); + if (agent_root.notNull()) + { + enqueueFetch(agent_root, false, true, gInventory.getCachedCategoryVersion(agent_root)); + mEssentialPending.insert(agent_root); + } + + const LLUUID library_root = gInventory.getLibraryRootFolderID(); + if (library_root.notNull()) + { + enqueueFetch(library_root, true, true, gInventory.getCachedCategoryVersion(library_root)); + mEssentialPending.insert(library_root); + } + + mEssentialTimer.reset(); + mEssentialTimer.start(); +} + +void LLAsyncInventorySkeletonLoader::processQueue() +{ + if (mPhase != Phase::Fetching) + { + return; + } + + gInventory.handleResponses(false); + + while (!mFetchQueue.empty() && mActiveFetches.size() < mMaxConcurrentFetches) + { + FetchRequest request = mFetchQueue.front(); + mFetchQueue.pop_front(); + + AISAPI::completion_t cb = [this, request](const LLUUID& response_id) + { + handleFetchComplete(request.mCategoryId, response_id); + }; + + LL_DEBUGS("AppInit") << "Async skeleton loader requesting AIS children for " + << request.mCategoryId << " (library=" + << (request.mIsLibrary ? "true" : "false") + << ", essential=" << (request.mEssential ? "true" : "false") + << ", cached_version=" << request.mCachedVersion + << ")" << LL_ENDL; + + AISAPI::FetchCategoryChildren(request.mCategoryId, + requestType(request.mIsLibrary), + false, + cb, + 1); + mActiveFetches.emplace(request.mCategoryId, request); + } +} + +void LLAsyncInventorySkeletonLoader::handleFetchComplete(const LLUUID& request_id, const LLUUID& response_id) +{ + auto active_it = mActiveFetches.find(request_id); + if (active_it == mActiveFetches.end()) + { + LL_WARNS("AppInit") << "Async skeleton loader received unexpected completion for " << request_id << LL_ENDL; + return; + } + + FetchRequest request = active_it->second; + mActiveFetches.erase(active_it); + mQueuedCategories.erase(request_id); + mFetchedCategories.insert(request_id); + + if (request.mEssential) + { + mEssentialPending.erase(request_id); + } + + if (response_id.isNull()) + { + LL_WARNS("AppInit") << "Async inventory skeleton loader failed to fetch " + << request_id << " (library=" + << (request.mIsLibrary ? "true" : "false") << ")" << LL_ENDL; + markFailed("AIS skeleton fetch returned no data for category " + request_id.asString()); + return; + } + + LLViewerInventoryCategory* category = gInventory.getCategory(request_id); + S32 server_version = LLViewerInventoryCategory::VERSION_UNKNOWN; + if (category) + { + server_version = category->getVersion(); + if (server_version != LLViewerInventoryCategory::VERSION_UNKNOWN) + { + gInventory.rememberCachedCategoryVersion(request_id, server_version); + } + } + + const bool version_changed = (server_version == LLViewerInventoryCategory::VERSION_UNKNOWN) + || (request.mCachedVersion == LLViewerInventoryCategory::VERSION_UNKNOWN) + || (server_version != request.mCachedVersion); + + if (request_id == gInventory.getRootFolderID()) + { + discoverEssentialFolders(); + } + + evaluateChildren(request, version_changed); + + processQueue(); +} + +void LLAsyncInventorySkeletonLoader::evaluateChildren(const FetchRequest& request, bool force_changed_scan) +{ + LLInventoryModel::cat_array_t* categories = nullptr; + LLInventoryModel::item_array_t* items = nullptr; + gInventory.getDirectDescendentsOf(request.mCategoryId, categories, items); + + if (!categories) + { + return; + } + + for (const auto& child_ptr : *categories) + { + LLViewerInventoryCategory* child = child_ptr.get(); + if (!child) + { + continue; + } + + const LLUUID& child_id = child->getUUID(); + if (child_id.isNull()) + { + continue; + } + + const bool already_processed = mFetchedCategories.count(child_id) > 0 + || mActiveFetches.count(child_id) > 0; + + if (already_processed) + { + continue; + } + + const S32 cached_child_version = gInventory.getCachedCategoryVersion(child_id); + const S32 current_child_version = child->getVersion(); + const bool child_version_unknown = (current_child_version == LLViewerInventoryCategory::VERSION_UNKNOWN); + const bool child_changed = child_version_unknown + || (cached_child_version == LLViewerInventoryCategory::VERSION_UNKNOWN) + || (current_child_version != cached_child_version); + const bool child_cache_valid = isCategoryUpToDate(child, cached_child_version); + + const bool child_is_library = request.mIsLibrary + || (child->getOwnerID() == gInventory.getLibraryOwnerID()); + + if (child->getPreferredType() == LLFolderType::FT_CURRENT_OUTFIT) + { + mSawCurrentOutfitFolder = true; + } + + bool child_essential = false; + if (child->getUUID() == LLAppearanceMgr::instance().getCOF()) + { + child_essential = true; + } + else if (LLFolderType::lookupIsEssentialType(child->getPreferredType())) + { + child_essential = true; + } + + bool should_fetch = child_changed || force_changed_scan; + if (child_essential) + { + if (!should_fetch && child_cache_valid) + { + LL_INFOS("AsyncInventory") << "Async skeleton loader trusting cached essential folder" + << " cat_id=" << child_id + << " name=\"" << child->getName() << "\"" + << " cached_version=" << cached_child_version + << " current_version=" << current_child_version + << " descendents=" << child->getDescendentCount() + << LL_ENDL; + mFetchedCategories.insert(child_id); + continue; + } + + if (!child_cache_valid) + { + should_fetch = true; + } + } + + if (should_fetch && mQueuedCategories.count(child_id) == 0) + { + if (child_essential) + { + mEssentialPending.insert(child_id); + } + enqueueFetch(child_id, child_is_library, child_essential, cached_child_version); + LL_INFOS("AsyncInventory") << "Async skeleton loader enqueued fetch" + << " cat_id=" << child_id + << " name=\"" << child->getName() << "\"" + << " essential=" << (child_essential ? "true" : "false") + << " cache_valid=" << (child_cache_valid ? "true" : "false") + << " cached_version=" << cached_child_version + << " current_version=" << current_child_version + << LL_ENDL; + } + else if (child_essential && child_cache_valid) + { + LL_INFOS("AsyncInventory") << "Async skeleton loader treating essential folder as fetched" + << " cat_id=" << child_id + << " name=\"" << child->getName() << "\"" + << " cached_version=" << cached_child_version + << " current_version=" << current_child_version + << LL_ENDL; + mFetchedCategories.insert(child_id); + } + } +} + +void LLAsyncInventorySkeletonLoader::discoverEssentialFolders() +{ + static const LLFolderType::EType essential_types[] = { + LLFolderType::FT_CURRENT_OUTFIT, + LLFolderType::FT_MY_OUTFITS, + LLFolderType::FT_LOST_AND_FOUND, + LLFolderType::FT_TRASH, + LLFolderType::FT_INBOX, + LLFolderType::FT_OUTBOX + }; + + for (LLFolderType::EType type : essential_types) + { + LLUUID cat_id = gInventory.findCategoryUUIDForType(type); + if (cat_id.isNull()) + { + continue; + } + + if (type == LLFolderType::FT_CURRENT_OUTFIT) + { + mSawCurrentOutfitFolder = true; + } + + LLViewerInventoryCategory* cat = gInventory.getCategory(cat_id); + bool is_library = false; + if (cat) + { + is_library = (cat->getOwnerID() == gInventory.getLibraryOwnerID()); + } + + const S32 cached_version = gInventory.getCachedCategoryVersion(cat_id); + if (cat && isCategoryUpToDate(cat, cached_version)) + { + mFetchedCategories.insert(cat_id); + LL_INFOS("AsyncInventory") << "Essential folder up to date from cache" + << " cat_id=" << cat_id + << " name=\"" << cat->getName() << "\"" + << " cached_version=" << cached_version + << " current_version=" << cat->getVersion() + << " descendents=" << cat->getDescendentCount() + << LL_ENDL; + continue; + } + + if (mFetchedCategories.count(cat_id) == 0 && mQueuedCategories.count(cat_id) == 0 && mActiveFetches.count(cat_id) == 0) + { + enqueueFetch(cat_id, is_library, true, cached_version); + mEssentialPending.insert(cat_id); + LL_INFOS("AsyncInventory") << "Essential folder queued for fetch" + << " cat_id=" << cat_id + << " cached_version=" << cached_version + << " current_version=" << (cat ? cat->getVersion() : LLViewerInventoryCategory::VERSION_UNKNOWN) + << LL_ENDL; + } + } + + LLUUID cof_id = LLAppearanceMgr::instance().getCOF(); + if (cof_id.notNull() + && mFetchedCategories.count(cof_id) == 0 + && mQueuedCategories.count(cof_id) == 0 + && mActiveFetches.count(cof_id) == 0) + { + mSawCurrentOutfitFolder = true; + LLViewerInventoryCategory* cof = gInventory.getCategory(cof_id); + const S32 cached_version = gInventory.getCachedCategoryVersion(cof_id); + if (isCategoryUpToDate(cof, cached_version)) + { + mFetchedCategories.insert(cof_id); + LL_INFOS("Inventory") << "COF up to date from cache" + << " cat_id=" << cof_id + << " name=\"" << (cof ? cof->getName() : std::string("")) << "\"" + << " cached_version=" << cached_version + << " current_version=" << (cof ? cof->getVersion() : LLViewerInventoryCategory::VERSION_UNKNOWN) + << LL_ENDL; + } + else + { + enqueueFetch(cof_id, false, true, cached_version); + mEssentialPending.insert(cof_id); + LL_INFOS("Inventory") << "COF queued for fetch" + << " cached_version=" << cached_version + << " current_version=" << (cof ? cof->getVersion() : LLViewerInventoryCategory::VERSION_UNKNOWN) + << LL_ENDL; + } + } +} + +void LLAsyncInventorySkeletonLoader::enqueueFetch(const LLUUID& category_id, + bool is_library, + bool essential, + S32 cached_version) +{ + if (category_id.isNull()) + { + return; + } + + if (mQueuedCategories.count(category_id) > 0 || mActiveFetches.count(category_id) > 0) + { + return; + } + + FetchRequest request; + request.mCategoryId = category_id; + request.mIsLibrary = is_library; + request.mEssential = essential; + request.mCachedVersion = cached_version; + + mFetchQueue.push_back(request); + mQueuedCategories.insert(category_id); +} + +AISAPI::ITEM_TYPE LLAsyncInventorySkeletonLoader::requestType(bool is_library) const +{ + return is_library ? AISAPI::LIBRARY : AISAPI::INVENTORY; +} + +void LLAsyncInventorySkeletonLoader::markEssentialReady() +{ + if (mEssentialReady) + { + return; + } + + mEssentialReady = true; + LL_INFOS("AppInit") << "Async inventory skeleton loader has fetched essential folders after " + << mTotalTimer.getElapsedTimeF32() << " seconds." << LL_ENDL; +} + +void LLAsyncInventorySkeletonLoader::markComplete() +{ + if (mPhase == Phase::Complete) + { + return; + } + + disconnectCapsCallback(); + removeIdleCallback(); + mPhase = Phase::Complete; + mFetchTimer.stop(); + mTotalTimer.stop(); + LL_DEBUGS("AppInit") << "Async inventory skeleton loader finished in " + << mTotalTimer.getElapsedTimeF32() << " seconds." << LL_ENDL; +} + +void LLAsyncInventorySkeletonLoader::markFailed(const std::string& reason) +{ + disconnectCapsCallback(); + removeIdleCallback(); + mFailureReason = reason; + mPhase = Phase::Failed; + mFetchTimer.stop(); + mTotalTimer.stop(); + LL_WARNS("AppInit") << "Async inventory skeleton loader failed: " << mFailureReason << LL_ENDL; +} + +bool LLAsyncInventorySkeletonLoader::hasFetchedCurrentOutfit() const +{ + if (!mSawCurrentOutfitFolder) + { + return true; + } + + LLUUID cof_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_CURRENT_OUTFIT); + if (cof_id.isNull()) + { + return false; + } + + if (mFetchedCategories.count(cof_id) == 0) + { + return false; + } + + const LLViewerInventoryCategory* cof = gInventory.getCategory(cof_id); + if (!cof) + { + return false; + } + + return cof->getVersion() != LLViewerInventoryCategory::VERSION_UNKNOWN; +} + +void LLAsyncInventorySkeletonLoader::update() +{ + if (mPhase == Phase::Idle || mPhase == Phase::Complete || mPhase == Phase::Failed) + { + return; + } + + if (mPhase == Phase::WaitingForCaps) + { + if (AISAPI::isAvailable()) + { + startFetches(); + return; + } + + if (mCapsTimer.getElapsedTimeF32() > mCapsTimeoutSec) + { + markFailed("Timed out waiting for inventory capabilities"); + } + return; + } + + processQueue(); + + const bool current_outfit_ready = hasFetchedCurrentOutfit(); + + if (!mEssentialReady && mEssentialPending.empty() && current_outfit_ready) + { + markEssentialReady(); + } + + if (!mEssentialReady && mEssentialTimer.getElapsedTimeF32() > mEssentialTimeoutSec) + { + markFailed("Timed out loading essential inventory folders"); + return; + } + + if (mFetchTimer.getElapsedTimeF32() > mFetchTimeoutSec) + { + markFailed("Timed out while fetching inventory skeleton via AIS"); + return; + } + + if (mFetchQueue.empty() && mActiveFetches.empty()) + { + markComplete(); + } +} + +bool LLAsyncInventorySkeletonLoader::isCategoryUpToDate(const LLViewerInventoryCategory* cat, S32 cached_version) const +{ + if (!cat) + { + return false; + } + + if (cached_version == LLViewerInventoryCategory::VERSION_UNKNOWN) + { + return false; + } + + if (cat->getVersion() == LLViewerInventoryCategory::VERSION_UNKNOWN) + { + return false; + } + + if (cat->getDescendentCount() == LLViewerInventoryCategory::DESCENDENT_COUNT_UNKNOWN) + { + return false; + } + + return cat->getVersion() == cached_version; +} diff --git a/indra/newview/llasyncinventoryskeletonloader.h b/indra/newview/llasyncinventoryskeletonloader.h new file mode 100644 index 00000000000..98946d85f39 --- /dev/null +++ b/indra/newview/llasyncinventoryskeletonloader.h @@ -0,0 +1,129 @@ +/** + * @file llasyncinventoryskeletonloader.h + * @brief Async inventory skeleton loading helper. + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLASYNCINVENTORYSKELETONLOADER_H +#define LL_LLASYNCINVENTORYSKELETONLOADER_H + +#include "llframetimer.h" +#include "lluuid.h" +#include "llaisapi.h" +#include "llinventorymodel.h" + +#include +#include +#include +#include + +#include + +class LLViewerInventoryCategory; +class LLViewerRegion; + +class LLAsyncInventorySkeletonLoader +{ +public: + void start(bool force_async); + void update(); + bool isRunning() const; + bool isComplete() const { return mPhase == Phase::Complete; } + bool hasFailed() const { return mPhase == Phase::Failed; } + bool isEssentialReady() const { return mEssentialReady; } + const std::string& failureReason() const { return mFailureReason; } + F32 elapsedSeconds() const { return mTotalTimer.getElapsedTimeF32(); } + void reset(); + +private: + struct FetchRequest + { + LLUUID mCategoryId; + bool mIsLibrary = false; + bool mEssential = false; + S32 mCachedVersion = LLViewerInventoryCategory::VERSION_UNKNOWN; + }; + + enum class Phase + { + Idle, + WaitingForCaps, + Fetching, + Complete, + Failed + }; + + void ensureCapsCallback(); + void disconnectCapsCallback(); + void onCapsReceived(const LLUUID& region_id, LLViewerRegion* regionp); + void ensureIdleCallback(); + void removeIdleCallback(); + static void idleCallback(void* userdata); + + void startFetches(); + void scheduleInitialFetches(); + void processQueue(); + void handleFetchComplete(const LLUUID& request_id, const LLUUID& response_id); + void evaluateChildren(const FetchRequest& request, bool force_changed_scan); + void discoverEssentialFolders(); + void enqueueFetch(const LLUUID& category_id, bool is_library, bool essential, S32 cached_version); + bool isCategoryUpToDate(const LLViewerInventoryCategory* cat, S32 cached_version) const; + AISAPI::ITEM_TYPE requestType(bool is_library) const; + void markEssentialReady(); + void markComplete(); + void markFailed(const std::string& reason); + bool hasFetchedCurrentOutfit() const; + + Phase mPhase = Phase::Idle; + bool mForceAsync = false; + bool mEssentialReady = false; + + std::deque mFetchQueue; + std::map mActiveFetches; + std::set mQueuedCategories; + std::set mFetchedCategories; + std::set mEssentialPending; + + U32 mMaxConcurrentFetches = 4; + bool mSawCurrentOutfitFolder = false; + + LLFrameTimer mCapsTimer; + LLFrameTimer mFetchTimer; + LLFrameTimer mTotalTimer; + LLFrameTimer mEssentialTimer; + + F32 mCapsTimeoutSec = 0.f; + F32 mFetchTimeoutSec = 0.f; + F32 mEssentialTimeoutSec = 0.f; + + boost::signals2::connection mCapsConnection; + bool mIdleRegistered = false; + std::string mFailureReason; +}; + +extern LLAsyncInventorySkeletonLoader gAsyncInventorySkeletonLoader; +extern bool gAsyncAgentCacheHydrated; +extern bool gAsyncLibraryCacheHydrated; +extern bool gAsyncParentChildMapPrimed; + +#endif // LL_LLASYNCINVENTORYSKELETONLOADER_H diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp index 2dfc3b014f5..d9508920bb9 100644 --- a/indra/newview/llinventorymodel.cpp +++ b/indra/newview/llinventorymodel.cpp @@ -28,6 +28,8 @@ #include #include +#include +#include #include "llinventorymodel.h" @@ -125,15 +127,30 @@ bool LLCanCache::operator()(LLInventoryCategory* cat, LLInventoryItem* item) { // HACK: downcast LLViewerInventoryCategory* c = (LLViewerInventoryCategory*)cat; - if(c->getVersion() != LLViewerInventoryCategory::VERSION_UNKNOWN) + + // Cache the category if it has a valid version number. + // Categories with mismatched descendent counts are still cached so that + // we preserve the folder structure. During async skeleton loading, these + // categories will be marked for refresh (VERSION_UNKNOWN) if their counts + // don't match, ensuring they get fetched from the server. + if (c->getVersion() != LLViewerInventoryCategory::VERSION_UNKNOWN) { - S32 descendents_server = c->getDescendentCount(); - S32 descendents_actual = c->getViewerDescendentCount(); - if(descendents_server == descendents_actual) + const S32 descendents_server = c->getDescendentCount(); + const S32 descendents_actual = c->getViewerDescendentCount(); + + if (descendents_server != descendents_actual) { - mCachedCatIDs.insert(c->getUUID()); - rv = true; + LL_DEBUGS("AsyncInventory") << "Caching category with mismatched descendents" + << " cat_id=" << c->getUUID() + << " name=\"" << c->getName() << "\"" + << " server_descendents=" << descendents_server + << " viewer_descendents=" << descendents_actual + << " (will be marked for refresh on next login)" + << LL_ENDL; } + + mCachedCatIDs.insert(c->getUUID()); + rv = true; } } return rv; @@ -432,6 +449,10 @@ LLInventoryModel gInventory; LLInventoryModel::LLInventoryModel() : // These are now ordered, keep them that way. mBacklinkMMap(), + mAllowAsyncInventoryUpdates(false), + mAsyncNotifyPending(false), + mAsyncNotifyTimer(), + mAsyncNotifyIntervalSec(0.05f), mIsAgentInvUsable(false), mRootFolderID(), mLibraryRootFolderID(), @@ -489,6 +510,8 @@ void LLInventoryModel::cleanupInventory() mHttpRequestFG = NULL; delete mHttpRequestBG; mHttpRequestBG = NULL; + + mCachedCategoryVersions.clear(); } // This is a convenience function to check if one object has a parent @@ -1435,8 +1458,13 @@ U32 LLInventoryModel::updateItem(const LLViewerInventoryItem* item, U32 mask) if(!isInventoryUsable()) { - LL_WARNS(LOG_INV) << "Inventory is broken." << LL_ENDL; - return mask; + if (!mAllowAsyncInventoryUpdates) + { + LL_WARNS(LOG_INV) << "Inventory is broken." << LL_ENDL; + return mask; + } + + LL_DEBUGS(LOG_INV) << "Processing item update while inventory validation pending (async skeleton)." << LL_ENDL; } if (item->getType() == LLAssetType::AT_MESH || @@ -1660,8 +1688,13 @@ void LLInventoryModel::updateCategory(const LLViewerInventoryCategory* cat, U32 if(!isInventoryUsable()) { - LL_WARNS(LOG_INV) << "Inventory is broken." << LL_ENDL; - return; + if (!mAllowAsyncInventoryUpdates) + { + LL_WARNS(LOG_INV) << "Inventory is broken." << LL_ENDL; + return; + } + + LL_DEBUGS(LOG_INV) << "Processing category update while inventory validation pending (async skeleton)." << LL_ENDL; } LLPointer old_cat = getCategory(cat->getUUID()); @@ -1698,6 +1731,10 @@ void LLInventoryModel::updateCategory(const LLViewerInventoryCategory* cat, U32 mask |= LLInventoryObserver::LABEL; } old_cat->copyViewerCategory(cat); + if (old_cat->getVersion() != LLViewerInventoryCategory::VERSION_UNKNOWN) + { + rememberCachedCategoryVersion(old_cat->getUUID(), old_cat->getVersion()); + } addChangedMask(mask, cat->getUUID()); } else @@ -1705,6 +1742,10 @@ void LLInventoryModel::updateCategory(const LLViewerInventoryCategory* cat, U32 // add this category LLPointer new_cat = new LLViewerInventoryCategory(cat->getOwnerID()); new_cat->copyViewerCategory(cat); + if (new_cat->getVersion() != LLViewerInventoryCategory::VERSION_UNKNOWN) + { + rememberCachedCategoryVersion(new_cat->getUUID(), new_cat->getVersion()); + } addCategory(new_cat); // make sure this category is correctly referenced by its parent. @@ -2049,6 +2090,7 @@ void LLInventoryModel::deleteObject(const LLUUID& id, bool fix_broken_links, boo LL_DEBUGS(LOG_INV) << "Deleting inventory object " << id << LL_ENDL; mLastItem = NULL; + forgetCachedCategoryVersion(id); LLUUID parent_id = obj->getParentUUID(); mCategoryMap.erase(id); mItemMap.erase(id); @@ -2155,6 +2197,20 @@ void LLInventoryModel::idleNotifyObservers() // *FIX: Think I want this conditional or moved elsewhere... handleResponses(true); + // Check if we have a pending notification from async inventory throttling + if (mAllowAsyncInventoryUpdates && mAsyncNotifyPending) + { + if (mAsyncNotifyTimer.getElapsedTimeF32() >= mAsyncNotifyIntervalSec) + { + // Timer has expired, force notification + mAsyncNotifyPending = false; + if (mModifyMask != LLInventoryObserver::NONE || (mChangedItemIDs.size() != 0)) + { + notifyObservers(); + } + } + } + if (mLinksRebuildList.size() > 0) { if (mModifyMask != LLInventoryObserver::NONE || (mChangedItemIDs.size() != 0)) @@ -2188,6 +2244,20 @@ void LLInventoryModel::notifyObservers() return; } + if (mAllowAsyncInventoryUpdates) + { + if (mAsyncNotifyTimer.getElapsedTimeF32() < mAsyncNotifyIntervalSec) + { + // Mark that we have a pending notification that will be delivered + // on the next idleNotifyObservers() call after the timer expires + mAsyncNotifyPending = true; + return; + } + + mAsyncNotifyTimer.reset(); + mAsyncNotifyPending = false; + } + mIsNotifyObservers = true; for (observer_list_t::iterator iter = mObservers.begin(); iter != mObservers.end(); ) @@ -2374,6 +2444,77 @@ void LLInventoryModel::cache( items, INCLUDE_TRASH, can_cache); + + // Fallback pass: catch any categories/items that collectDescendentsIf missed. + // This can happen when: + // 1. Categories have VERSION_UNKNOWN (e.g., during async loading) + // 2. Parent-child tree has broken links (orphaned folders) + // 3. Categories with mismatched descendent counts weren't added to mCachedCatIDs + // Without this pass, we'd lose 80k+ folders in deeply nested inventories. + // (I feel like this might be surfacing a bug somewhere...) + std::set cached_category_ids; + std::set cached_item_ids; + for (auto& cat_ptr : categories) + { + if (cat_ptr.notNull()) + { + cached_category_ids.insert(cat_ptr->getUUID()); + } + } + for (auto& item_ptr : items) + { + if (item_ptr.notNull()) + { + cached_item_ids.insert(item_ptr->getUUID()); + } + } + + for (auto& entry : mCategoryMap) + { + LLViewerInventoryCategory* cat = entry.second; + if (!cat) + { + continue; + } + const LLUUID& cat_id = cat->getUUID(); + if (cached_category_ids.count(cat_id) != 0) + { + continue; + } + if (!isObjectDescendentOf(cat_id, parent_folder_id)) + { + continue; + } + if (can_cache(cat, NULL)) + { + categories.push_back(cat); + cached_category_ids.insert(cat_id); + } + } + + for (auto& entry : mItemMap) + { + LLViewerInventoryItem* item = entry.second; + if (!item) + { + continue; + } + const LLUUID& item_id = item->getUUID(); + if (cached_item_ids.count(item_id) != 0) + { + continue; + } + if (!isObjectDescendentOf(item_id, parent_folder_id)) + { + continue; + } + if (can_cache(NULL, item)) + { + items.push_back(item); + cached_item_ids.insert(item_id); + } + } + // Use temporary file to avoid potential conflicts with other // instances (even a 'read only' instance unzips into a file) std::string temp_file = gDirUtilp->getTempFilename(); @@ -2685,11 +2826,24 @@ bool LLInventoryModel::isCategoryComplete(const LLUUID& cat_id) const bool LLInventoryModel::loadSkeleton( const LLSD& options, - const LLUUID& owner_id) + const LLUUID& owner_id, + bool allow_cache_only) { LL_PROFILE_ZONE_SCOPED; LL_DEBUGS(LOG_INV) << "importing inventory skeleton for " << owner_id << LL_ENDL; + if (options.isUndefined() || !options.isArray() || options.size() == 0) + { + if (allow_cache_only) + { + return loadSkeletonFromCacheOnly(owner_id); + } + + LL_DEBUGS(LOG_INV) << "Skipping skeleton import for " << owner_id + << " because no server payload was provided." << LL_ENDL; + return false; + } + LLTimer timer; typedef std::set, InventoryIDPtrLess> cat_set_t; cat_set_t temp_cats; @@ -2722,6 +2876,10 @@ bool LLInventoryModel::loadSkeleton( } cat->setPreferredType(preferred_type); cat->setVersion(version.asInteger()); + if (cat->getVersion() != LLViewerInventoryCategory::VERSION_UNKNOWN) + { + rememberCachedCategoryVersion(cat->getUUID(), cat->getVersion()); + } temp_cats.insert(cat); } else @@ -2983,13 +3141,188 @@ bool LLInventoryModel::loadSkeleton( return rv; } +bool LLInventoryModel::loadSkeletonFromCacheOnly(const LLUUID& owner_id) +{ + LL_PROFILE_ZONE_SCOPED; + LL_DEBUGS(LOG_INV) << "Hydrating inventory skeleton from cache for " << owner_id << LL_ENDL; + + if (owner_id == gAgent.getID()) + { + // Reset cached version tracking for the primary account cache so we can + // compare against fresh AIS data as it arrives. + mCachedCategoryVersions.clear(); + } + + cat_array_t categories; + item_array_t items; + changed_items_t categories_to_update; + bool is_cache_obsolete = false; + std::string inventory_filename = getInvCacheAddres(owner_id); + std::string gzip_filename(inventory_filename); + gzip_filename.append(".gz"); + LLFILE* fp = LLFile::fopen(gzip_filename, "rb"); + bool remove_inventory_file = false; + + if (LLAppViewer::instance()->isSecondInstance()) + { + inventory_filename = gDirUtilp->getTempFilename(); + remove_inventory_file = true; + } + + if (fp) + { + fclose(fp); + fp = NULL; + if (gunzip_file(gzip_filename, inventory_filename)) + { + remove_inventory_file = true; + } + else + { + LL_INFOS(LOG_INV) << "Unable to gunzip " << gzip_filename << LL_ENDL; + } + } + + if (!loadFromFile(inventory_filename, categories, items, categories_to_update, is_cache_obsolete)) + { + if (remove_inventory_file) + { + LLFile::remove(inventory_filename); + } + if (is_cache_obsolete && !LLAppViewer::instance()->isSecondInstance()) + { + LLFile::remove(gzip_filename); + } + LL_WARNS(LOG_INV) << "Failed to load cached inventory skeleton for " << owner_id << LL_ENDL; + return false; + } + + update_map_t child_counts; + const S32 NO_VERSION = LLViewerInventoryCategory::VERSION_UNKNOWN; + + size_t cached_category_count = 0; + size_t cached_category_unknown_version = 0; + size_t cached_category_marked_refresh = 0; + for (auto& cat : categories) + { + if (!cat) + { + continue; + } + + const S32 cached_version = cat->getVersion(); + rememberCachedCategoryVersion(cat->getUUID(), cached_version); + + const bool requires_refresh = (cached_version == NO_VERSION) + || (categories_to_update.find(cat->getUUID()) != categories_to_update.end()); + cat->setVersion(requires_refresh ? NO_VERSION : cached_version); + if (requires_refresh) + { + ++cached_category_marked_refresh; + } + if (cached_version == NO_VERSION) + { + ++cached_category_unknown_version; + } + addCategory(cat); + ++child_counts[cat->getParentUUID()]; + ++cached_category_count; + } + + cat_map_t::iterator unparented = mCategoryMap.end(); + size_t cached_item_count = 0; + size_t items_missing_parent = 0; + for (auto& item_ptr : items) + { + LLViewerInventoryItem* item = item_ptr.get(); + if (!item) + { + continue; + } + + const cat_map_t::iterator cit = mCategoryMap.find(item->getParentUUID()); + if (cit != unparented) + { + addItem(item); + ++child_counts[item->getParentUUID()]; + ++cached_item_count; + } + else + { + ++items_missing_parent; + } + } + + const size_t category_map_size_post_load = mCategoryMap.size(); + + for (auto& entry : child_counts) + { + const cat_map_t::iterator cit = mCategoryMap.find(entry.first); + if (cit != mCategoryMap.end()) + { + LLViewerInventoryCategory* cat = cit->second.get(); + if (cat) + { + cat->setDescendentCount(entry.second.mValue); + } + } + } + + if (remove_inventory_file) + { + LLFile::remove(inventory_filename); + } + if (is_cache_obsolete && !LLAppViewer::instance()->isSecondInstance()) + { + LL_WARNS(LOG_INV) << "Inv cache out of date, removing" << LL_ENDL; + LLFile::remove(gzip_filename); + } + + categories.clear(); + + LL_INFOS("AsyncInventory") << "Loaded cache-only skeleton for " << owner_id + << " categories_from_cache=" << cached_category_count + << " categories_unknown_version=" << cached_category_unknown_version + << " categories_marked_refresh=" << cached_category_marked_refresh + << " items_from_cache=" << cached_item_count + << " items_missing_parent=" << items_missing_parent + << " category_map_size_post_load=" << category_map_size_post_load + << LL_ENDL; + + return cached_category_count > 0; +} + // This is a brute force method to rebuild the entire parent-child -// relations. The overall operation has O(NlogN) performance, which -// should be sufficient for our needs. -void LLInventoryModel::buildParentChildMap() +// relations. The overall operation has O(N) performance achieved by +// using try_emplace to avoid redundant map lookups. +void LLInventoryModel::buildParentChildMap(bool run_validation) { LL_INFOS(LOG_INV) << "LLInventoryModel::buildParentChildMap()" << LL_ENDL; + // Clear existing parent-child maps if they exist to avoid duplicate entries. + // This handles the case where buildParentChildMap is called multiple times + // during async skeleton loading (once without validation, once with). + if (!mParentChildCategoryTree.empty()) + { + std::for_each( + mParentChildCategoryTree.begin(), + mParentChildCategoryTree.end(), + DeletePairedPointer()); + mParentChildCategoryTree.clear(); + } + + if (!mParentChildItemTree.empty()) + { + std::for_each( + mParentChildItemTree.begin(), + mParentChildItemTree.end(), + DeletePairedPointer()); + mParentChildItemTree.clear(); + } + + mCategoryLock.clear(); + mItemLock.clear(); + // *NOTE: I am skipping the logic around folder version // synchronization here because it seems if a folder is lost, we // might actually want to invalidate it at that point - not @@ -2998,51 +3331,61 @@ void LLInventoryModel::buildParentChildMap() // First the categories. We'll copy all of the categories into a // temporary container to iterate over (oh for real iterators.) // While we're at it, we'll allocate the arrays in the trees. + // Use try_emplace to avoid redundant lookups - achieves O(n) overall cat_array_t cats; - cat_array_t* catsp; - item_array_t* itemsp; + cats.reserve(mCategoryMap.size()); for(cat_map_t::iterator cit = mCategoryMap.begin(); cit != mCategoryMap.end(); ++cit) { LLViewerInventoryCategory* cat = cit->second; cats.push_back(cat); - if (mParentChildCategoryTree.count(cat->getUUID()) == 0) - { - llassert_always(!mCategoryLock[cat->getUUID()]); - catsp = new cat_array_t; - mParentChildCategoryTree[cat->getUUID()] = catsp; - } - if (mParentChildItemTree.count(cat->getUUID()) == 0) - { - llassert_always(!mItemLock[cat->getUUID()]); - itemsp = new item_array_t; - mParentChildItemTree[cat->getUUID()] = itemsp; - } + + const LLUUID& cat_uuid = cat->getUUID(); + llassert_always(!mCategoryLock[cat_uuid]); + llassert_always(!mItemLock[cat_uuid]); + + // try_emplace avoids the separate count() call, reducing overhead + mParentChildCategoryTree.try_emplace(cat_uuid, new cat_array_t); + mParentChildItemTree.try_emplace(cat_uuid, new item_array_t); } // Insert a special parent for the root - so that lookups on // LLUUID::null as the parent work correctly. This is kind of a // blatent wastes of space since we allocate a block of memory for // the array, but whatever - it's not that much space. - if (mParentChildCategoryTree.count(LLUUID::null) == 0) - { - catsp = new cat_array_t; - mParentChildCategoryTree[LLUUID::null] = catsp; - } + mParentChildCategoryTree.try_emplace(LLUUID::null, new cat_array_t); // Now we have a structure with all of the categories that we can // iterate over and insert into the correct place in the child - // category tree. + // category tree. Cache parent lookups to avoid repeated map searches. S32 i; S32 lost = 0; cat_array_t lost_cats; + + // Cache to avoid repeated map lookups for same parent - O(1) amortized + std::unordered_map parent_lookup_cache; + for (auto& cat : cats) { - catsp = getUnlockedCatArray(cat->getParentUUID()); + const LLUUID& parent_uuid = cat->getParentUUID(); + cat_array_t* catsp; + + // Check cache first + auto cache_it = parent_lookup_cache.find(parent_uuid); + if (cache_it != parent_lookup_cache.end()) + { + catsp = cache_it->second; + } + else + { + catsp = getUnlockedCatArray(parent_uuid); + parent_lookup_cache[parent_uuid] = catsp; + } + if(catsp && // Only the two root folders should be children of null. // Others should go to lost & found. - (cat->getParentUUID().notNull() || + (parent_uuid.notNull() || cat->getPreferredType() == LLFolderType::FT_ROOT_INVENTORY )) { catsp->push_back(cat); @@ -3061,6 +3404,7 @@ void LLInventoryModel::buildParentChildMap() lost_cats.push_back(cat); } } + const S32 lost_categories = lost; if(lost) { LL_WARNS(LOG_INV) << "Found " << lost << " lost categories." << LL_ENDL; @@ -3068,6 +3412,10 @@ void LLInventoryModel::buildParentChildMap() // Do moves in a separate pass to make sure we've properly filed // the FT_LOST_AND_FOUND category before we try to find its UUID. + // Cache commonly used folder IDs to avoid repeated lookups + const LLUUID lost_and_found_id = findCategoryUUIDForType(LLFolderType::FT_LOST_AND_FOUND); + const LLUUID root_folder_id = gInventory.getRootFolderID(); + for(i = 0; igetPreferredType(); if(LLFolderType::FT_NONE == pref) { - cat->setParent(findCategoryUUIDForType(LLFolderType::FT_LOST_AND_FOUND)); + cat->setParent(lost_and_found_id); } else if(LLFolderType::FT_ROOT_INVENTORY == pref) { @@ -3086,7 +3434,7 @@ void LLInventoryModel::buildParentChildMap() else { // it's a protected folder. - cat->setParent(gInventory.getRootFolderID()); + cat->setParent(root_folder_id); } // FIXME note that updateServer() fails with protected // types, so this will not work as intended in that case. @@ -3095,7 +3443,22 @@ void LLInventoryModel::buildParentChildMap() // MoveInventoryFolder message, intentionally per item cat->updateParentOnServer(false); - catsp = getUnlockedCatArray(cat->getParentUUID()); + + const LLUUID& parent_uuid = cat->getParentUUID(); + cat_array_t* catsp; + + // Use cached lookup + auto cache_it = parent_lookup_cache.find(parent_uuid); + if (cache_it != parent_lookup_cache.end()) + { + catsp = cache_it->second; + } + else + { + catsp = getUnlockedCatArray(parent_uuid); + parent_lookup_cache[parent_uuid] = catsp; + } + if(catsp) { catsp->push_back(cat); @@ -3112,10 +3475,11 @@ void LLInventoryModel::buildParentChildMap() // Now the items. We allocated in the last step, so now all we // have to do is iterate over the items and put them in the right - // place. + // place. Use caching to avoid repeated map lookups. item_array_t items; if(!mItemMap.empty()) { + items.reserve(mItemMap.size()); LLPointer item; for(item_map_t::iterator iit = mItemMap.begin(); iit != mItemMap.end(); ++iit) { @@ -3125,9 +3489,27 @@ void LLInventoryModel::buildParentChildMap() } lost = 0; uuid_vec_t lost_item_ids; + + // Cache for item parent lookups - O(1) amortized + std::unordered_map item_parent_lookup_cache; + for (auto& item : items) { - itemsp = getUnlockedItemArray(item->getParentUUID()); + const LLUUID& parent_uuid = item->getParentUUID(); + item_array_t* itemsp; + + // Check cache first + auto cache_it = item_parent_lookup_cache.find(parent_uuid); + if (cache_it != item_parent_lookup_cache.end()) + { + itemsp = cache_it->second; + } + else + { + itemsp = getUnlockedItemArray(parent_uuid); + item_parent_lookup_cache[parent_uuid] = itemsp; + } + if(itemsp) { itemsp->push_back(item); @@ -3139,12 +3521,24 @@ void LLInventoryModel::buildParentChildMap() ++lost; // plop it into the lost & found. // - item->setParent(findCategoryUUIDForType(LLFolderType::FT_LOST_AND_FOUND)); + item->setParent(lost_and_found_id); // move it later using a special message to move items. If // we update server here, the client might crash. //item->updateServer(); lost_item_ids.push_back(item->getUUID()); - itemsp = getUnlockedItemArray(item->getParentUUID()); + + // Re-lookup after parent change + cache_it = item_parent_lookup_cache.find(lost_and_found_id); + if (cache_it != item_parent_lookup_cache.end()) + { + itemsp = cache_it->second; + } + else + { + itemsp = getUnlockedItemArray(lost_and_found_id); + item_parent_lookup_cache[lost_and_found_id] = itemsp; + } + if(itemsp) { itemsp->push_back(item); @@ -3188,61 +3582,133 @@ void LLInventoryModel::buildParentChildMap() } } - const LLUUID &agent_inv_root_id = gInventory.getRootFolderID(); - if (agent_inv_root_id.notNull()) + LL_INFOS("AsyncInventory") << "ParentChildMap summary" + << " category_map_size=" << mCategoryMap.size() + << " category_tree_nodes=" << mParentChildCategoryTree.size() + << " item_map_size=" << mItemMap.size() + << " item_tree_nodes=" << mParentChildItemTree.size() + << " lost_categories=" << lost_categories + << " lost_items=" << lost + << LL_ENDL; + + if (run_validation) { - cat_array_t* catsp = get_ptr_in_map(mParentChildCategoryTree, agent_inv_root_id); - if(catsp) + const LLUUID& agent_inv_root_id = gInventory.getRootFolderID(); + if (agent_inv_root_id.notNull()) { - // *HACK - fix root inventory folder - // some accounts has pbroken inventory root folders - - std::string name = "My Inventory"; - for (parent_cat_map_t::const_iterator it = mParentChildCategoryTree.begin(), - it_end = mParentChildCategoryTree.end(); it != it_end; ++it) + cat_array_t* catsp = get_ptr_in_map(mParentChildCategoryTree, agent_inv_root_id); + if(catsp) { - cat_array_t* cat_array = it->second; - for (cat_array_t::const_iterator cat_it = cat_array->begin(), - cat_it_end = cat_array->end(); cat_it != cat_it_end; ++cat_it) - { - LLPointer category = *cat_it; + // *HACK - fix root inventory folder + // some accounts has pbroken inventory root folders - if(category && category->getPreferredType() != LLFolderType::FT_ROOT_INVENTORY) - continue; - if ( category && 0 == LLStringUtil::compareInsensitive(name, category->getName()) ) + std::string name = "My Inventory"; + for (parent_cat_map_t::const_iterator it = mParentChildCategoryTree.begin(), + it_end = mParentChildCategoryTree.end(); it != it_end; ++it) + { + cat_array_t* cat_array = it->second; + for (cat_array_t::const_iterator cat_it = cat_array->begin(), + cat_it_end = cat_array->end(); cat_it != cat_it_end; ++cat_it) { - if(category->getUUID()!=mRootFolderID) + LLPointer category = *cat_it; + + if(category && category->getPreferredType() != LLFolderType::FT_ROOT_INVENTORY) + continue; + if ( category && 0 == LLStringUtil::compareInsensitive(name, category->getName()) ) { - LLUUID& new_inv_root_folder_id = const_cast(mRootFolderID); - new_inv_root_folder_id = category->getUUID(); + if(category->getUUID()!=mRootFolderID) + { + LLUUID& new_inv_root_folder_id = const_cast(mRootFolderID); + new_inv_root_folder_id = category->getUUID(); + } } } } - } - LLPointer validation_info = validate(); - if (validation_info->mFatalErrorCount > 0) - { - // Fatal inventory error. Will not be able to engage in many inventory operations. - // This should be followed by an error dialog leading to logout. - LL_WARNS("Inventory") << "Fatal errors were found in validate(): unable to initialize inventory! " - << "Will not be able to do normal inventory operations in this session." - << LL_ENDL; - mIsAgentInvUsable = false; - } - else - { - mIsAgentInvUsable = true; + LLPointer validation_info = validate(); + if (validation_info->mFatalErrorCount > 0) + { + // Fatal inventory error. Will not be able to engage in many inventory operations. + // This should be followed by an error dialog leading to logout. + LL_WARNS("AsyncInventory") << "Fatal errors were found in validate(): unable to initialize inventory! " + << "Will not be able to do normal inventory operations in this session." + << LL_ENDL; + mIsAgentInvUsable = false; + } + else + { + mIsAgentInvUsable = true; + } + validation_info->mInitialized = true; + mValidationInfo = validation_info; + + // notifyObservers() has been moved to + // llstartup/idle_startup() after this func completes. + // Allows some system categories to be created before + // observers start firing. } - validation_info->mInitialized = true; - mValidationInfo = validation_info; + } + } + else + { + LL_DEBUGS(LOG_INV) << "Skipping inventory validation while async skeleton load seeds cache" << LL_ENDL; + } +} + +void LLInventoryModel::setAsyncInventoryLoading(bool in_progress) +{ + if (mAllowAsyncInventoryUpdates == in_progress) + { + return; + } - // notifyObservers() has been moved to - // llstartup/idle_startup() after this func completes. - // Allows some system categories to be created before - // observers start firing. + mAllowAsyncInventoryUpdates = in_progress; + if (mAllowAsyncInventoryUpdates) + { + if (gSavedSettings.controlExists("AsyncInventoryNotifyMinInterval")) + { + mAsyncNotifyIntervalSec = std::clamp(gSavedSettings.getF32("AsyncInventoryNotifyMinInterval"), 0.0f, 0.5f); } + mAsyncNotifyTimer.reset(); + mAsyncNotifyTimer.setAge(mAsyncNotifyIntervalSec); + mAsyncNotifyPending = false; + } + else + { + mAsyncNotifyPending = false; + } + + LL_DEBUGS(LOG_INV) << "Async skeleton loading " << (in_progress ? "enabled" : "disabled") << LL_ENDL; +} + +void LLInventoryModel::rememberCachedCategoryVersion(const LLUUID& id, S32 version) +{ + if (id.isNull()) + { + return; } + + mCachedCategoryVersions[id] = version; +} + +S32 LLInventoryModel::getCachedCategoryVersion(const LLUUID& id) const +{ + auto it = mCachedCategoryVersions.find(id); + if (it != mCachedCategoryVersions.end()) + { + return it->second; + } + return LLViewerInventoryCategory::VERSION_UNKNOWN; +} + +void LLInventoryModel::forgetCachedCategoryVersion(const LLUUID& id) +{ + if (id.isNull()) + { + return; + } + + mCachedCategoryVersions.erase(id); } // Would normally do this at construction but that's too early @@ -3427,9 +3893,13 @@ bool LLInventoryModel::loadFromFile(const std::string& filename, if (!is_cache_obsolete) { + // Cache LLSD references and reserve vector capacity for large inventories const LLSD& llsd_cats = inventory["categories"]; if (llsd_cats.isArray()) { + const size_t cat_count = llsd_cats.size(); + categories.reserve(cat_count); // Pre-allocate to avoid reallocation + LLSD::array_const_iterator iter = llsd_cats.beginArray(); LLSD::array_const_iterator end = llsd_cats.endArray(); for (; iter != end; ++iter) @@ -3445,6 +3915,9 @@ bool LLInventoryModel::loadFromFile(const std::string& filename, const LLSD& llsd_items = inventory["items"]; if (llsd_items.isArray()) { + const size_t item_count = llsd_items.size(); + items.reserve(item_count); // Pre-allocate to avoid reallocation + LLSD::array_const_iterator iter = llsd_items.beginArray(); LLSD::array_const_iterator end = llsd_items.endArray(); for (; iter != end; ++iter) @@ -3452,14 +3925,16 @@ bool LLInventoryModel::loadFromFile(const std::string& filename, LLPointer inv_item = new LLViewerInventoryItem; if (inv_item->fromLLSD(*iter)) { - if (inv_item->getUUID().isNull()) + const LLUUID& item_uuid = inv_item->getUUID(); + if (item_uuid.isNull()) { LL_DEBUGS(LOG_INV) << "Ignoring inventory with null item id: " << inv_item->getName() << LL_ENDL; } else { - if (inv_item->getType() == LLAssetType::AT_UNKNOWN) + const LLAssetType::EType item_type = inv_item->getType(); + if (item_type == LLAssetType::AT_UNKNOWN) { cats_to_update.insert(inv_item->getParentUUID()); } @@ -3520,14 +3995,24 @@ bool LLInventoryModel::saveToFile(const std::string& filename, LLSD& cat_array = inventory["categories"]; S32 cat_count = 0; + S32 cat_unknown_version_count = 0; for (auto& cat : categories) { - if (cat->getVersion() != LLViewerInventoryCategory::VERSION_UNKNOWN) + if (!cat) { - LLSD sd; - cat->exportLLSD(sd); - cat_array.append(sd); - cat_count++; + continue; + } + + // Persist folders even when their version is unknown so warm-cache + // hydrations retain the complete hierarchy; those folders will be + // marked for refresh on the next login. + LLSD sd; + cat->exportLLSD(sd); + cat_array.append(sd); + ++cat_count; + if (cat->getVersion() == LLViewerInventoryCategory::VERSION_UNKNOWN) + { + ++cat_unknown_version_count; } } @@ -3543,14 +4028,17 @@ bool LLInventoryModel::saveToFile(const std::string& filename, fileSD << LLSDOStreamer(inventory) << std::endl; if (fileSD.fail()) { - LL_WARNS(LOG_INV) << "Failed to write cache. Unable to save inventory to: " << filename << LL_ENDL; + LL_WARNS("AsyncInventory") << "Failed to write cache. Unable to save inventory to: " << filename << LL_ENDL; return false; } fileSD.flush(); fileSD.close(); - LL_INFOS(LOG_INV) << "Inventory saved: " << (S32)cat_count << " categories, " << (S32)it_count << " items." << LL_ENDL; + LL_INFOS("AsyncInventory") << "Inventory saved: categories=" << cat_count + << " categories_unknown_version=" << cat_unknown_version_count + << " items=" << (S32)it_count + << LL_ENDL; } catch (...) { @@ -5101,4 +5589,3 @@ void LLInventoryModel::FetchItemHttpHandler::processFailure(const char * const r << LLCoreHttpUtil::responseToString(response) << "]" << LL_ENDL; gInventory.notifyObservers(); } - diff --git a/indra/newview/llinventorymodel.h b/indra/newview/llinventorymodel.h index d28743357e2..2a20da38bf3 100644 --- a/indra/newview/llinventorymodel.h +++ b/indra/newview/llinventorymodel.h @@ -188,15 +188,23 @@ class LLInventoryModel public: // Methods to load up inventory skeleton & meat. These are used // during authentication. Returns true if everything parsed. - bool loadSkeleton(const LLSD& options, const LLUUID& owner_id); - void buildParentChildMap(); // brute force method to rebuild the entire parent-child relations + bool loadSkeleton(const LLSD& options, const LLUUID& owner_id, bool allow_cache_only = false); + void buildParentChildMap(bool run_validation = true); // brute force method to rebuild the entire parent-child relations void createCommonSystemCategories(); + void setAsyncInventoryLoading(bool in_progress); + bool isAsyncInventoryLoading() const { return mAllowAsyncInventoryUpdates; } + + void rememberCachedCategoryVersion(const LLUUID& id, S32 version); + S32 getCachedCategoryVersion(const LLUUID& id) const; + void forgetCachedCategoryVersion(const LLUUID& id); + static std::string getInvCacheAddres(const LLUUID& owner_id); // Call on logout to save a terse representation. void cache(const LLUUID& parent_folder_id, const LLUUID& agent_id); private: + bool loadSkeletonFromCacheOnly(const LLUUID& owner_id); // Information for tracking the actual inventory. We index this // information in a lot of different ways so we can access // the inventory using several different identifiers. @@ -216,6 +224,19 @@ class LLInventoryModel // category pointers here, because broken links are also supported. typedef std::multimap backlink_mmap_t; backlink_mmap_t mBacklinkMMap; // key = target_id: ID of item, values = link_ids: IDs of item or folder links referencing it. + + // Async inventory loading support + bool mAllowAsyncInventoryUpdates{false}; // True when async skeleton loading is active + bool mAsyncNotifyPending{false}; // True when observer notification is throttled + LLFrameTimer mAsyncNotifyTimer; // Timer for throttling observer notifications + F32 mAsyncNotifyIntervalSec{0.05f}; // Minimum interval between notifications (seconds) + + // Tracks category version numbers as they were when loaded from disk cache. + // Used during async skeleton loading to determine if a cached category is still + // up-to-date compared to the server version received via AIS. + // Key: category UUID, Value: version number from cache + // Cleared when async loading completes or when categories are deleted. + std::map mCachedCategoryVersions; // For internal use only bool hasBacklinkInfo(const LLUUID& link_id, const LLUUID& target_id) const; void addBacklinkInfo(const LLUUID& link_id, const LLUUID& target_id); @@ -721,4 +742,3 @@ class LLInventoryModel extern LLInventoryModel gInventory; #endif // LL_LLINVENTORYMODEL_H - diff --git a/indra/newview/llinventorypanel.cpp b/indra/newview/llinventorypanel.cpp index a935ede1860..890727c87fd 100644 --- a/indra/newview/llinventorypanel.cpp +++ b/indra/newview/llinventorypanel.cpp @@ -1044,17 +1044,25 @@ void LLInventoryPanel::initializeViews(F64 max_time) void LLInventoryPanel::initRootContent() { + // Optimization: Only build root folder widget initially, no children. + // Children will be built lazily when folders are expanded/opened. + // This dramatically reduces startup time with large inventories (100k+ items). + // Instead of creating 430k widgets upfront (12 seconds), we create them on-demand. LLUUID root_id = getRootFolderID(); if (root_id.notNull()) { - buildNewViews(getRootFolderID()); + LLInventoryObject const* objectp = mInventory->getObject(root_id); + buildNewViews(root_id, objectp, nullptr, BUILD_NO_CHILDREN); } else { // Default case: always add "My Inventory" root first, "Library" root second - // If we run out of time, this still should create root folders - buildNewViews(gInventory.getRootFolderID()); // My Inventory - buildNewViews(gInventory.getLibraryRootFolderID()); // Library + // Build only root widgets - children will load on-demand when expanded + LLInventoryObject const* my_inv = mInventory->getObject(gInventory.getRootFolderID()); + buildNewViews(gInventory.getRootFolderID(), my_inv, nullptr, BUILD_NO_CHILDREN); + + LLInventoryObject const* library = mInventory->getObject(gInventory.getLibraryRootFolderID()); + buildNewViews(gInventory.getLibraryRootFolderID(), library, nullptr, BUILD_NO_CHILDREN); } } @@ -1335,18 +1343,17 @@ LLFolderViewItem* LLInventoryPanel::buildViewsTree(const LLUUID& id, const LLViewerInventoryCategory* cat = (*cat_iter); if (typedViewsFilter(cat->getUUID(), cat)) { + LLFolderViewItem* view_itemp = nullptr; if (has_folders) { - // This can be optimized: we don't need to call getItemByID() - // each time, especially since content is growing, we can just - // iter over copy of mItemMap in some way - LLFolderViewItem* view_itemp = getItemByID(cat->getUUID()); - buildViewsTree(cat->getUUID(), id, cat, view_itemp, parentp, (mode == BUILD_ONE_FOLDER ? BUILD_NO_CHILDREN : mode), depth); - } - else - { - buildViewsTree(cat->getUUID(), id, cat, NULL, parentp, (mode == BUILD_ONE_FOLDER ? BUILD_NO_CHILDREN : mode), depth); + // Optimized: only lookup if we know folders exist, otherwise nullptr is correct + auto map_it = mItemMap.find(cat->getUUID()); + if (map_it != mItemMap.end()) + { + view_itemp = map_it->second; + } } + buildViewsTree(cat->getUUID(), id, cat, view_itemp, parentp, (mode == BUILD_ONE_FOLDER ? BUILD_NO_CHILDREN : mode), depth); } if (!mBuildChildrenViews @@ -1378,10 +1385,13 @@ LLFolderViewItem* LLInventoryPanel::buildViewsTree(const LLUUID& id, const LLViewerInventoryItem* item = (*item_iter); if (typedViewsFilter(item->getUUID(), item)) { - // This can be optimized: we don't need to call getItemByID() - // each time, especially since content is growing, we can just - // iter over copy of mItemMap in some way - LLFolderViewItem* view_itemp = getItemByID(item->getUUID()); + // Optimized: direct map lookup instead of getItemByID() function call + LLFolderViewItem* view_itemp = nullptr; + auto map_it = mItemMap.find(item->getUUID()); + if (map_it != mItemMap.end()) + { + view_itemp = map_it->second; + } buildViewsTree(item->getUUID(), id, item, view_itemp, parentp, mode, depth); } diff --git a/indra/newview/lllogininstance.cpp b/indra/newview/lllogininstance.cpp index 41cec4f074e..7665566f442 100644 --- a/indra/newview/lllogininstance.cpp +++ b/indra/newview/lllogininstance.cpp @@ -86,7 +86,10 @@ LLLoginInstance::LLLoginInstance() : mSaveMFA(true), mAttemptComplete(false), mTransferRate(0.0f), - mDispatcher("LLLoginInstance", "change") + mDispatcher("LLLoginInstance", "change"), + mAsyncSkeletonSuppressed(false), + mSupportsAsyncSkeleton(false), + mForceAsyncSkeleton(false) { mLoginModule->getEventPump().listen("lllogininstance", boost::bind(&LLLoginInstance::handleLoginEvent, this, _1)); @@ -152,14 +155,60 @@ LLSD LLLoginInstance::getResponse() return mResponseData; } +void LLLoginInstance::recordAsyncInventoryFailure() +{ + if (!mAsyncSkeletonSuppressed) + { + LL_WARNS("LLLogin") << "Async inventory skeleton failed; suppressing async option on next login attempt." << LL_ENDL; + } + mAsyncSkeletonSuppressed = true; +} + +void LLLoginInstance::recordAsyncInventorySuccess() +{ + if (mAsyncSkeletonSuppressed) + { + LL_INFOS("LLLogin") << "Async inventory skeleton completed successfully; re-enabling async option." << LL_ENDL; + } + mAsyncSkeletonSuppressed = false; +} + void LLLoginInstance::constructAuthParams(LLPointer user_credential) { // Set up auth request options. //#define LL_MINIMIAL_REQUESTED_OPTIONS LLSD requested_options; // *Note: this is where gUserAuth used to be created. + mForceAsyncSkeleton = gSavedSettings.getBOOL("ForceAsyncInventorySkeleton"); + if (mForceAsyncSkeleton) + { + mAsyncSkeletonSuppressed = false; + } + mSupportsAsyncSkeleton = !mAsyncSkeletonSuppressed || mForceAsyncSkeleton; + requested_options.append("inventory-root"); - requested_options.append("inventory-skeleton"); + if (!mForceAsyncSkeleton) + { + requested_options.append("inventory-skeleton"); + } + else + { + LL_DEBUGS("LLLogin") << "ForceAsyncInventorySkeleton enabled; skipping legacy inventory skeleton request" << LL_ENDL; + } + if (mSupportsAsyncSkeleton) + { + requested_options.append("inventory-skeleton-async"); + LL_DEBUGS("LLLogin") << "Requesting async inventory skeleton support"; + if (mForceAsyncSkeleton) + { + LL_CONT << " (forced by debug setting)"; + } + LL_CONT << LL_ENDL; + } + else + { + LL_DEBUGS("LLLogin") << "Async inventory skeleton suppressed for this login attempt" << LL_ENDL; + } //requested_options.append("inventory-meat"); //requested_options.append("inventory-skel-targets"); #if (!defined LL_MINIMIAL_REQUESTED_OPTIONS) @@ -167,7 +216,10 @@ void LLLoginInstance::constructAuthParams(LLPointer user_credentia // Not requesting library will trigger mFatalNoLibraryRootFolder requested_options.append("inventory-lib-root"); requested_options.append("inventory-lib-owner"); - requested_options.append("inventory-skel-lib"); + if (!mForceAsyncSkeleton) + { + requested_options.append("inventory-skel-lib"); + } // requested_options.append("inventory-meat-lib"); requested_options.append("initial-outfit"); @@ -652,4 +704,3 @@ std::string construct_start_string() } return start; } - diff --git a/indra/newview/lllogininstance.h b/indra/newview/lllogininstance.h index 941b378b14a..f09aebfb0b0 100644 --- a/indra/newview/lllogininstance.h +++ b/indra/newview/lllogininstance.h @@ -59,6 +59,11 @@ class LLLoginInstance : public LLSingleton LLSD getResponse(const std::string& key) { return getResponse()[key]; } LLSD getResponse(); + bool supportsAsyncInventorySkeleton() const { return mSupportsAsyncSkeleton; } + bool forceAsyncInventorySkeleton() const { return mForceAsyncSkeleton; } + void recordAsyncInventoryFailure(); + void recordAsyncInventorySuccess(); + // Only valid when authSuccess == true. const F64 getLastTransferRateBPS() { return mTransferRate; } void setSerialNumber(const std::string& sn) { mSerialNumber = sn; } @@ -109,6 +114,10 @@ class LLLoginInstance : public LLSingleton std::string mPlatformVersion; std::string mPlatformVersionName; LLEventDispatcher mDispatcher; + + bool mAsyncSkeletonSuppressed; + bool mSupportsAsyncSkeleton; + bool mForceAsyncSkeleton; }; #endif diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index 3786a9b1609..90d01a52793 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -35,6 +35,7 @@ # include // mkdir() #endif #include // std::unique_ptr +#include #include "llviewermedia_streamingaudio.h" #include "llaudioengine.h" @@ -78,6 +79,11 @@ #include "llstring.h" #include "lluserrelations.h" #include "llversioninfo.h" +#include "llframetimer.h" +#include "llviewerregion.h" + +#include + #include "llviewercontrol.h" #include "llviewerhelp.h" #include "llxorcipher.h" // saved password, MAC address @@ -186,7 +192,9 @@ #include "llagentlanguage.h" #include "llwearable.h" #include "llinventorybridge.h" +#include "llfoldertype.h" #include "llappearancemgr.h" +#include "llasyncinventoryskeletonloader.h" #include "llavatariconctrl.h" #include "llvoicechannel.h" #include "llpathfindingmanager.h" @@ -206,7 +214,6 @@ #include "threadpool.h" #include "llperfstats.h" - #if LL_WINDOWS #include "lldxhardware.h" #endif @@ -1861,30 +1868,173 @@ bool idle_startup() { LL_PROFILE_ZONE_NAMED("State inventory load skeleton") - LLSD response = LLLoginInstance::getInstance()->getResponse(); + // Cache frequently accessed values to reduce function call overhead + LLLoginInstance* login_instance = LLLoginInstance::getInstance(); + const LLSD& response = login_instance->getResponse(); - LLSD inv_skel_lib = response["inventory-skel-lib"]; - if (inv_skel_lib.isDefined() && gInventory.getLibraryOwnerID().notNull()) + const LLSD& inv_skel_lib = response["inventory-skel-lib"]; + const LLSD& inv_skeleton = response["inventory-skeleton"]; + + const bool supports_async = login_instance->supportsAsyncInventorySkeleton(); + const bool force_async = login_instance->forceAsyncInventorySkeleton(); + const bool legacy_payload_available = inv_skeleton.isDefined() && !force_async; + const bool use_async_path = supports_async && !legacy_payload_available; + + // Cache UUIDs to avoid repeated function calls + const LLUUID library_owner_id = gInventory.getLibraryOwnerID(); + const LLUUID agent_id = gAgent.getID(); + + if (!use_async_path) { - LL_PROFILE_ZONE_NAMED("load library inv") - if (!gInventory.loadSkeleton(inv_skel_lib, gInventory.getLibraryOwnerID())) + gInventory.setAsyncInventoryLoading(false); + LL_INFOS("AppInit") << "Using legacy inventory skeleton payload" << LL_ENDL; + if (gAsyncInventorySkeletonLoader.isRunning()) { - LL_WARNS("AppInit") << "Problem loading inventory-skel-lib" << LL_ENDL; + gAsyncInventorySkeletonLoader.reset(); } + gAsyncAgentCacheHydrated = false; + gAsyncLibraryCacheHydrated = false; + gAsyncParentChildMapPrimed = false; + + if (inv_skel_lib.isDefined() && library_owner_id.notNull()) + { + LL_PROFILE_ZONE_NAMED("load library inv") + if (!gInventory.loadSkeleton(inv_skel_lib, library_owner_id)) + { + LL_WARNS("AppInit") << "Problem loading inventory-skel-lib" << LL_ENDL; + } + } + // Removed do_startup_frame() -- not needed here, happens after both loads + + if (inv_skeleton.isDefined()) + { + LL_PROFILE_ZONE_NAMED("load personal inv") + if (!gInventory.loadSkeleton(inv_skeleton, agent_id)) + { + LL_WARNS("AppInit") << "Problem loading inventory-skel-targets" << LL_ENDL; + } + } + do_startup_frame(); // Single frame update after both skeleton loads + LLStartUp::setStartupState(STATE_INVENTORY_SEND2); + return false; + } + + if (!gInventory.isAsyncInventoryLoading()) + { + gInventory.setAsyncInventoryLoading(true); } - do_startup_frame(); - LLSD inv_skeleton = response["inventory-skeleton"]; - if (inv_skeleton.isDefined()) + if (!gAsyncLibraryCacheHydrated && library_owner_id.notNull()) { - LL_PROFILE_ZONE_NAMED("load personal inv") - if (!gInventory.loadSkeleton(inv_skeleton, gAgent.getID())) + const bool hydrate_from_cache = !inv_skel_lib.isDefined() || force_async; + LLSD library_payload = force_async ? LLSD() : inv_skel_lib; + LL_PROFILE_ZONE_NAMED("load library inv async") + if (!gInventory.loadSkeleton(library_payload, library_owner_id, hydrate_from_cache)) { - LL_WARNS("AppInit") << "Problem loading inventory-skel-targets" << LL_ENDL; + LL_WARNS("AppInit") << "Problem loading library inventory skeleton in async mode" << LL_ENDL; } + gAsyncLibraryCacheHydrated = true; } do_startup_frame(); - LLStartUp::setStartupState(STATE_INVENTORY_SEND2); + + if (!gAsyncAgentCacheHydrated) + { + LL_PROFILE_ZONE_NAMED("hydrate personal inv cache async") + if (!gInventory.loadSkeleton(LLSD(), agent_id, true)) + { + LL_WARNS("AppInit") << "Problem hydrating cached agent inventory skeleton" << LL_ENDL; + } + gAsyncAgentCacheHydrated = true; + } + + const LLUUID root_folder_id = gInventory.getRootFolderID(); + if (!gAsyncParentChildMapPrimed && root_folder_id.notNull()) + { + LL_PROFILE_ZONE_NAMED("prime async inv map") + // buildParentChildMap optimized from O(n log n) to O(n) with caching + gInventory.buildParentChildMap(false); + gAsyncParentChildMapPrimed = true; + LL_DEBUGS("AppInit") << "Async inventory skeleton primed parent/child map. usable=" + << (gInventory.isInventoryUsable() ? "true" : "false") << LL_ENDL; + } + + if (!gAsyncInventorySkeletonLoader.isRunning() + && !gAsyncInventorySkeletonLoader.isComplete() + && !gAsyncInventorySkeletonLoader.hasFailed()) + { + LL_INFOS("AppInit") << "Starting async inventory skeleton load" << (force_async ? " (forced)" : "") << LL_ENDL; + gAsyncInventorySkeletonLoader.start(force_async); + } + + gAsyncInventorySkeletonLoader.update(); + + if (gAsyncInventorySkeletonLoader.hasFailed()) + { + std::string failure_reason = gAsyncInventorySkeletonLoader.failureReason(); + LL_WARNS("AppInit") << "Async inventory skeleton failed: " << failure_reason << LL_ENDL; + login_instance->recordAsyncInventoryFailure(); + + // Invalidate the cache to prevent corrupt data from being loaded on next login + const LLUUID& agent_id = gAgent.getID(); + if (agent_id.notNull()) + { + std::string cache_filename = LLInventoryModel::getInvCacheAddres(agent_id); + cache_filename.append(".gz"); + if (LLFile::isfile(cache_filename)) + { + LL_WARNS("AppInit") << "Removing potentially corrupt inventory cache: " << cache_filename << LL_ENDL; + LLFile::remove(cache_filename); + } + } + + LLSD args; + std::string localized_message = LLTrans::getString("AsyncInventorySkeletonFailure"); + if (!failure_reason.empty()) + { + localized_message.append("\n\n").append(failure_reason); + } + args["ERROR_MESSAGE"] = localized_message; + LLNotificationsUtil::add("ErrorMessage", args, LLSD(), login_alert_done); + + gAsyncInventorySkeletonLoader.reset(); + gAsyncAgentCacheHydrated = false; + gAsyncLibraryCacheHydrated = false; + gAsyncParentChildMapPrimed = false; + gInventory.cleanupInventory(); + gInventory.setAsyncInventoryLoading(false); + + show_connect_box = true; + transition_back_to_login_panel(localized_message); + return false; + } + + if (gAsyncInventorySkeletonLoader.isEssentialReady()) + { + LL_INFOS("AppInit") << "Async inventory skeleton essentials ready after " + << gAsyncInventorySkeletonLoader.elapsedSeconds() << " seconds" << LL_ENDL; + login_instance->recordAsyncInventorySuccess(); + gAsyncAgentCacheHydrated = false; + gAsyncLibraryCacheHydrated = false; + gAsyncParentChildMapPrimed = false; + LLStartUp::setStartupState(STATE_INVENTORY_SEND2); + do_startup_frame(); + return false; + } + + if (gAsyncInventorySkeletonLoader.isComplete()) + { + LL_INFOS("AppInit") << "Async inventory skeleton ready after " + << gAsyncInventorySkeletonLoader.elapsedSeconds() << " seconds" << LL_ENDL; + login_instance->recordAsyncInventorySuccess(); + gAsyncInventorySkeletonLoader.reset(); + gAsyncAgentCacheHydrated = false; + gAsyncLibraryCacheHydrated = false; + gAsyncParentChildMapPrimed = false; + LLStartUp::setStartupState(STATE_INVENTORY_SEND2); + do_startup_frame(); + return false; + } + do_startup_frame(); return false; } @@ -1988,12 +2138,18 @@ bool idle_startup() // gInventory.mIsAgentInvUsable is set to true in the gInventory.buildParentChildMap. gInventory.buildParentChildMap(); + const bool inventory_usable = gInventory.isInventoryUsable(); + if (inventory_usable && gInventory.isAsyncInventoryLoading()) + { + gInventory.setAsyncInventoryLoading(false); + } + // If buildParentChildMap succeeded, inventory will now be in // a usable state and gInventory.isInventoryUsable() will be // true. // if inventory is unusable, show warning. - if (!gInventory.isInventoryUsable()) + if (!inventory_usable) { LLNotificationsUtil::add("InventoryUnusable"); } @@ -3970,4 +4126,3 @@ void transition_back_to_login_panel(const std::string& emsg) reset_login(); // calls LLStartUp::setStartupState( STATE_LOGIN_SHOW ); gSavedSettings.setBOOL("AutoLogin", false); } - diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml index 99c7d7b7d4c..735a828d44f 100644 --- a/indra/newview/skins/default/xui/en/strings.xml +++ b/indra/newview/skins/default/xui/en/strings.xml @@ -162,6 +162,7 @@ If you feel this is an error, please contact support@secondlife.com. Please contact Second Life support for assistance at http://support.secondlife.com. Data inconsistency found during login. Please contact support@secondlife.com. + We couldn't finish loading your inventory. Please try logging in again. Your account is undergoing minor maintenance. Your account is not accessible until [TIME].