From fd3b9e8573625234eeeed5a184d7ead96622562a Mon Sep 17 00:00:00 2001 From: pepper <3782201+rohvani@users.noreply.github.com> Date: Wed, 1 Oct 2025 12:22:19 -0700 Subject: [PATCH 01/12] Initial changes for async inventory --- indra/newview/app_settings/settings.xml | 11 + indra/newview/llinventorymodel.cpp | 308 ++++++-- indra/newview/llinventorymodel.h | 15 +- indra/newview/lllogininstance.cpp | 59 +- indra/newview/lllogininstance.h | 9 + indra/newview/llstartup.cpp | 740 +++++++++++++++++- .../newview/skins/default/xui/en/strings.xml | 1 + 7 files changed, 1073 insertions(+), 70 deletions(-) diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 43270eb3eaa..10c39f08df9 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -3258,6 +3258,17 @@ 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 + ForceLoginURL Comment diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp index 2dfc3b014f5..6469ed41d32 100644 --- a/indra/newview/llinventorymodel.cpp +++ b/indra/newview/llinventorymodel.cpp @@ -489,6 +489,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 +1437,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 +1667,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 +1710,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 +1721,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 +2069,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); @@ -2685,11 +2706,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 +2756,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,10 +3021,135 @@ 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; + for (auto& cat : categories) + { + if (!cat) + { + continue; + } + + rememberCachedCategoryVersion(cat->getUUID(), cat->getVersion()); + cat->setVersion(NO_VERSION); + addCategory(cat); + ++child_counts[cat->getParentUUID()]; + ++cached_category_count; + } + + cat_map_t::iterator unparented = mCategoryMap.end(); + size_t cached_item_count = 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; + } + } + + 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(LOG_INV) << "Loaded cache-only skeleton for " << owner_id + << " with " << cached_category_count << " categories and " + << cached_item_count << " items." << 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() +void LLInventoryModel::buildParentChildMap(bool run_validation) { LL_INFOS(LOG_INV) << "LLInventoryModel::buildParentChildMap()" << LL_ENDL; @@ -3188,61 +3351,109 @@ void LLInventoryModel::buildParentChildMap() } } - const LLUUID &agent_inv_root_id = gInventory.getRootFolderID(); - if (agent_inv_root_id.notNull()) + 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; - } - validation_info->mInitialized = true; - mValidationInfo = validation_info; + 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; + } + 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. + // notifyObservers() has been moved to + // llstartup/idle_startup() after this func completes. + // Allows some system categories to be created before + // observers start firing. + } } } + 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; + } + + mAllowAsyncInventoryUpdates = in_progress; + 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 @@ -5101,4 +5312,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..1a2be7f8386 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,8 @@ 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. + bool mAllowAsyncInventoryUpdates{false}; + 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 +731,3 @@ class LLInventoryModel extern LLInventoryModel gInventory; #endif // LL_LLINVENTORYMODEL_H - 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..73d2d78f8cf 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -78,6 +78,13 @@ #include "llstring.h" #include "lluserrelations.h" #include "llversioninfo.h" +#include "llframetimer.h" +#include "llaisapi.h" +#include "llviewerregion.h" + +#include +#include + #include "llviewercontrol.h" #include "llviewerhelp.h" #include "llxorcipher.h" // saved password, MAC address @@ -186,6 +193,7 @@ #include "llagentlanguage.h" #include "llwearable.h" #include "llinventorybridge.h" +#include "llfoldertype.h" #include "llappearancemgr.h" #include "llavatariconctrl.h" #include "llvoicechannel.h" @@ -206,11 +214,587 @@ #include "threadpool.h" #include "llperfstats.h" +#include +#include +#include + #if LL_WINDOWS #include "lldxhardware.h" #endif +namespace +{ +bool isEssentialFolderType(LLFolderType::EType folder_type) +{ + if (folder_type == LLFolderType::FT_NONE) + { + return false; + } + + if (folder_type == LLFolderType::FT_ROOT_INVENTORY) + { + return true; + } + + return LLFolderType::lookupIsSingletonType(folder_type) + || LLFolderType::lookupIsEnsembleType(folder_type); +} + +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); + AISAPI::ITEM_TYPE requestType(bool is_library) const; + void markEssentialReady(); + void markComplete(); + void markFailed(const std::string& reason); + + 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; + + LLFrameTimer mCapsTimer; + LLFrameTimer mFetchTimer; + LLFrameTimer mTotalTimer; + LLFrameTimer mEssentialTimer; + + F32 mCapsTimeoutSec = 45.f; + F32 mFetchTimeoutSec = 180.f; + F32 mEssentialTimeoutSec = 90.f; + + boost::signals2::connection mCapsConnection; + bool mIdleRegistered = false; + std::string mFailureReason; +}; + +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(); +} + +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(); + + 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_is_library = request.mIsLibrary + || (child->getOwnerID() == gInventory.getLibraryOwnerID()); + + bool child_essential = false; + if (child->getUUID() == LLAppearanceMgr::instance().getCOF()) + { + child_essential = true; + } + else if (isEssentialFolderType(child->getPreferredType())) + { + child_essential = true; + } + + if (child_essential) + { + mEssentialPending.insert(child_id); + } + + if ((child_changed || force_changed_scan || child_essential) + && mQueuedCategories.count(child_id) == 0) + { + enqueueFetch(child_id, child_is_library, child_essential, cached_child_version); + } + } +} + +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; + } + + LLViewerInventoryCategory* cat = gInventory.getCategory(cat_id); + bool is_library = false; + if (cat) + { + is_library = (cat->getOwnerID() == gInventory.getLibraryOwnerID()); + } + + if (mFetchedCategories.count(cat_id) == 0 && mQueuedCategories.count(cat_id) == 0 && mActiveFetches.count(cat_id) == 0) + { + enqueueFetch(cat_id, is_library, true, gInventory.getCachedCategoryVersion(cat_id)); + mEssentialPending.insert(cat_id); + } + } + + 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) + { + enqueueFetch(cof_id, false, true, gInventory.getCachedCategoryVersion(cof_id)); + mEssentialPending.insert(cof_id); + } +} + +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; +} + +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(); + + if (!mEssentialReady && mEssentialPending.empty()) + { + 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(); + } +} + +LLAsyncInventorySkeletonLoader gAsyncInventorySkeletonLoader; +bool gAsyncAgentCacheHydrated = false; +bool gAsyncLibraryCacheHydrated = false; +bool gAsyncParentChildMapPrimed = false; +} + // // exported globals // @@ -1861,30 +2445,153 @@ bool idle_startup() { LL_PROFILE_ZONE_NAMED("State inventory load skeleton") - LLSD response = LLLoginInstance::getInstance()->getResponse(); + LLLoginInstance* login_instance = LLLoginInstance::getInstance(); + LLSD response = login_instance->getResponse(); LLSD inv_skel_lib = response["inventory-skel-lib"]; - if (inv_skel_lib.isDefined() && gInventory.getLibraryOwnerID().notNull()) + 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; + + 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() && gInventory.getLibraryOwnerID().notNull()) + { + LL_PROFILE_ZONE_NAMED("load library inv") + if (!gInventory.loadSkeleton(inv_skel_lib, gInventory.getLibraryOwnerID())) + { + LL_WARNS("AppInit") << "Problem loading inventory-skel-lib" << LL_ENDL; + } + } + do_startup_frame(); + + if (inv_skeleton.isDefined()) + { + LL_PROFILE_ZONE_NAMED("load personal inv") + if (!gInventory.loadSkeleton(inv_skeleton, gAgent.getID())) + { + LL_WARNS("AppInit") << "Problem loading inventory-skel-targets" << LL_ENDL; + } + } + do_startup_frame(); + LLStartUp::setStartupState(STATE_INVENTORY_SEND2); + do_startup_frame(); + return false; } - do_startup_frame(); - LLSD inv_skeleton = response["inventory-skeleton"]; - if (inv_skeleton.isDefined()) + if (!gInventory.isAsyncInventoryLoading()) + { + gInventory.setAsyncInventoryLoading(true); + } + + if (!gAsyncLibraryCacheHydrated && gInventory.getLibraryOwnerID().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, gInventory.getLibraryOwnerID(), 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(), gAgent.getID(), true)) + { + LL_WARNS("AppInit") << "Problem hydrating cached agent inventory skeleton" << LL_ENDL; + } + gAsyncAgentCacheHydrated = true; + } + + if (!gAsyncParentChildMapPrimed && gInventory.getRootFolderID().notNull()) + { + LL_PROFILE_ZONE_NAMED("prime async inv map") + 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(); + + 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.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 +2695,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 +4683,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]. From 0e5bc2ed5b3d0c2bea1ff026de65b92f7b2f56a9 Mon Sep 17 00:00:00 2001 From: pepper <3782201+rohvani@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:43:15 -0700 Subject: [PATCH 02/12] Add support for AsyncInventoryMaxConcurrentFetches --- indra/newview/app_settings/settings.xml | 11 ++++++ indra/newview/llstartup.cpp | 52 ++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 10c39f08df9..bf554a10a1e 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -3269,6 +3269,17 @@ Value 0 + AsyncInventoryMaxConcurrentFetches + + Comment + Maximum number of concurrent AIS fetches used during async inventory skeleton loading. + Persist + 1 + Type + U32 + Value + 2 + ForceLoginURL Comment diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index 73d2d78f8cf..8b605053ced 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" @@ -290,6 +291,7 @@ class LLAsyncInventorySkeletonLoader void markEssentialReady(); void markComplete(); void markFailed(const std::string& reason); + bool hasFetchedCurrentOutfit() const; Phase mPhase = Phase::Idle; bool mForceAsync = false; @@ -302,6 +304,7 @@ class LLAsyncInventorySkeletonLoader std::set mEssentialPending; U32 mMaxConcurrentFetches = 4; + bool mSawCurrentOutfitFolder = false; LLFrameTimer mCapsTimer; LLFrameTimer mFetchTimer; @@ -334,6 +337,10 @@ void LLAsyncInventorySkeletonLoader::reset() mFetchTimer.stop(); mTotalTimer.stop(); mEssentialTimer.stop(); + + const U32 requested = gSavedSettings.getU32("AsyncInventoryMaxConcurrentFetches"); + mMaxConcurrentFetches = std::clamp(requested, 1U, 8U); + mSawCurrentOutfitFolder = false; } bool LLAsyncInventorySkeletonLoader::isRunning() const @@ -352,6 +359,9 @@ void LLAsyncInventorySkeletonLoader::start(bool force_async) 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; @@ -606,6 +616,11 @@ void LLAsyncInventorySkeletonLoader::evaluateChildren(const FetchRequest& reques 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()) { @@ -648,6 +663,11 @@ void LLAsyncInventorySkeletonLoader::discoverEssentialFolders() continue; } + if (type == LLFolderType::FT_CURRENT_OUTFIT) + { + mSawCurrentOutfitFolder = true; + } + LLViewerInventoryCategory* cat = gInventory.getCategory(cat_id); bool is_library = false; if (cat) @@ -668,6 +688,7 @@ void LLAsyncInventorySkeletonLoader::discoverEssentialFolders() && mQueuedCategories.count(cof_id) == 0 && mActiveFetches.count(cof_id) == 0) { + mSawCurrentOutfitFolder = true; enqueueFetch(cof_id, false, true, gInventory.getCachedCategoryVersion(cof_id)); mEssentialPending.insert(cof_id); } @@ -742,6 +763,33 @@ void LLAsyncInventorySkeletonLoader::markFailed(const std::string& reason) 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) @@ -766,7 +814,9 @@ void LLAsyncInventorySkeletonLoader::update() processQueue(); - if (!mEssentialReady && mEssentialPending.empty()) + const bool current_outfit_ready = hasFetchedCurrentOutfit(); + + if (!mEssentialReady && mEssentialPending.empty() && current_outfit_ready) { markEssentialReady(); } From 24b833f39dc74859d846d52efea7813432323bb1 Mon Sep 17 00:00:00 2001 From: pepper <3782201+rohvani@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:43:40 -0700 Subject: [PATCH 03/12] General improvements to async inventory --- indra/newview/app_settings/settings.xml | 11 ++++++ indra/newview/llinventorymodel.cpp | 48 +++++++++++++++++++++++++ indra/newview/llinventorymodel.h | 3 ++ 3 files changed, 62 insertions(+) diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index bf554a10a1e..ea803a73373 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -3280,6 +3280,17 @@ Value 2 + AsyncInventoryNotifyMinInterval + + Comment + Minimum seconds between inventory observer notifications while async inventory loading is active. + Persist + 1 + Type + F32 + Value + 0.05 + ForceLoginURL Comment diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp index 6469ed41d32..9b94b522710 100644 --- a/indra/newview/llinventorymodel.cpp +++ b/indra/newview/llinventorymodel.cpp @@ -432,6 +432,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(), @@ -2209,6 +2213,18 @@ void LLInventoryModel::notifyObservers() return; } + if (mAllowAsyncInventoryUpdates) + { + if (mAsyncNotifyTimer.getElapsedTimeF32() < mAsyncNotifyIntervalSec) + { + mAsyncNotifyPending = true; + return; + } + + mAsyncNotifyTimer.reset(); + mAsyncNotifyPending = false; + } + mIsNotifyObservers = true; for (observer_list_t::iterator iter = mObservers.begin(); iter != mObservers.end(); ) @@ -3153,6 +3169,23 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) { LL_INFOS(LOG_INV) << "LLInventoryModel::buildParentChildMap()" << LL_ENDL; + // Rebuild from scratch so we do not accumulate duplicate children + // across consecutive calls triggered during async skeleton hydration. + std::for_each( + mParentChildCategoryTree.begin(), + mParentChildCategoryTree.end(), + DeletePairedPointer()); + mParentChildCategoryTree.clear(); + + 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 @@ -3423,6 +3456,21 @@ void LLInventoryModel::setAsyncInventoryLoading(bool in_progress) } 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; } diff --git a/indra/newview/llinventorymodel.h b/indra/newview/llinventorymodel.h index 1a2be7f8386..a8d2ed99b04 100644 --- a/indra/newview/llinventorymodel.h +++ b/indra/newview/llinventorymodel.h @@ -225,6 +225,9 @@ class LLInventoryModel 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. bool mAllowAsyncInventoryUpdates{false}; + bool mAsyncNotifyPending{false}; + LLFrameTimer mAsyncNotifyTimer; + F32 mAsyncNotifyIntervalSec{0.05f}; std::map mCachedCategoryVersions; // For internal use only bool hasBacklinkInfo(const LLUUID& link_id, const LLUUID& target_id) const; From 61778b46edce4c4f682344aba65f59dce409e0b9 Mon Sep 17 00:00:00 2001 From: pepper <3782201+rohvani@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:09:25 -0700 Subject: [PATCH 04/12] Allow us to use cat versions from cache --- indra/newview/llinventorymodel.cpp | 8 +++- indra/newview/llstartup.cpp | 72 +++++++++++++++++++++++++++--- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp index 9b94b522710..ea68efb9d63 100644 --- a/indra/newview/llinventorymodel.cpp +++ b/indra/newview/llinventorymodel.cpp @@ -3104,8 +3104,12 @@ bool LLInventoryModel::loadSkeletonFromCacheOnly(const LLUUID& owner_id) continue; } - rememberCachedCategoryVersion(cat->getUUID(), cat->getVersion()); - cat->setVersion(NO_VERSION); + 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); addCategory(cat); ++child_counts[cat->getParentUUID()]; ++cached_category_count; diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index 8b605053ced..93035c58b20 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -287,6 +287,7 @@ class LLAsyncInventorySkeletonLoader 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(); @@ -612,6 +613,7 @@ void LLAsyncInventorySkeletonLoader::evaluateChildren(const FetchRequest& reques 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()); @@ -631,16 +633,33 @@ void LLAsyncInventorySkeletonLoader::evaluateChildren(const FetchRequest& reques child_essential = true; } + bool should_fetch = child_changed || force_changed_scan; if (child_essential) { - mEssentialPending.insert(child_id); + if (!should_fetch && child_cache_valid) + { + mFetchedCategories.insert(child_id); + continue; + } + + if (!child_cache_valid) + { + should_fetch = true; + } } - if ((child_changed || force_changed_scan || child_essential) - && mQueuedCategories.count(child_id) == 0) + 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); } + else if (child_essential && child_cache_valid) + { + mFetchedCategories.insert(child_id); + } } } @@ -675,9 +694,16 @@ void LLAsyncInventorySkeletonLoader::discoverEssentialFolders() is_library = (cat->getOwnerID() == gInventory.getLibraryOwnerID()); } + const S32 cached_version = gInventory.getCachedCategoryVersion(cat_id); + if (cat && isCategoryUpToDate(cat, cached_version)) + { + mFetchedCategories.insert(cat_id); + continue; + } + if (mFetchedCategories.count(cat_id) == 0 && mQueuedCategories.count(cat_id) == 0 && mActiveFetches.count(cat_id) == 0) { - enqueueFetch(cat_id, is_library, true, gInventory.getCachedCategoryVersion(cat_id)); + enqueueFetch(cat_id, is_library, true, cached_version); mEssentialPending.insert(cat_id); } } @@ -689,8 +715,17 @@ void LLAsyncInventorySkeletonLoader::discoverEssentialFolders() && mActiveFetches.count(cof_id) == 0) { mSawCurrentOutfitFolder = true; - enqueueFetch(cof_id, false, true, gInventory.getCachedCategoryVersion(cof_id)); - mEssentialPending.insert(cof_id); + LLViewerInventoryCategory* cof = gInventory.getCategory(cof_id); + const S32 cached_version = gInventory.getCachedCategoryVersion(cof_id); + if (isCategoryUpToDate(cof, cached_version)) + { + mFetchedCategories.insert(cof_id); + } + else + { + enqueueFetch(cof_id, false, true, cached_version); + mEssentialPending.insert(cof_id); + } } } @@ -3261,6 +3296,31 @@ bool idle_startup() return true; } +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; +} + // // local function definition // From 07665b7cff412f13d530b46358fb13221670551c Mon Sep 17 00:00:00 2001 From: pepper <3782201+rohvani@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:09:46 -0700 Subject: [PATCH 05/12] Fix cache dehydration --- indra/newview/llinventorymodel.cpp | 165 ++++++++++++++++++++++++++--- indra/newview/llstartup.cpp | 43 ++++++++ 2 files changed, 191 insertions(+), 17 deletions(-) diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp index ea68efb9d63..3e12037e3c7 100644 --- a/indra/newview/llinventorymodel.cpp +++ b/indra/newview/llinventorymodel.cpp @@ -28,6 +28,7 @@ #include #include +#include #include "llinventorymodel.h" @@ -125,16 +126,27 @@ bool LLCanCache::operator()(LLInventoryCategory* cat, LLInventoryItem* item) { // HACK: downcast LLViewerInventoryCategory* c = (LLViewerInventoryCategory*)cat; - if(c->getVersion() != LLViewerInventoryCategory::VERSION_UNKNOWN) + + bool descendents_match = true; + 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(); + descendents_match = (descendents_server == descendents_actual); + + if (!descendents_match) { - 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 + << LL_ENDL; } } + + mCachedCatIDs.insert(c->getUUID()); + rv = true; } return rv; } @@ -2411,6 +2423,68 @@ void LLInventoryModel::cache( items, INCLUDE_TRASH, can_cache); + + 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()); + } + } + + // Fallback pass: ensure every known category/item under the root is persisted + // even if the parent/child map failed to enumerate it. + 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; + } + 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(); @@ -3097,6 +3171,8 @@ bool LLInventoryModel::loadSkeletonFromCacheOnly(const LLUUID& owner_id) 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) @@ -3110,6 +3186,14 @@ bool LLInventoryModel::loadSkeletonFromCacheOnly(const LLUUID& owner_id) 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; @@ -3117,6 +3201,7 @@ bool LLInventoryModel::loadSkeletonFromCacheOnly(const LLUUID& owner_id) 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(); @@ -3132,8 +3217,14 @@ bool LLInventoryModel::loadSkeletonFromCacheOnly(const LLUUID& owner_id) ++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); @@ -3159,9 +3250,14 @@ bool LLInventoryModel::loadSkeletonFromCacheOnly(const LLUUID& owner_id) categories.clear(); - LL_INFOS(LOG_INV) << "Loaded cache-only skeleton for " << owner_id - << " with " << cached_category_count << " categories and " - << cached_item_count << " items." << LL_ENDL; + 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; } @@ -3236,6 +3332,7 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) S32 i; S32 lost = 0; cat_array_t lost_cats; + size_t tree_links_inserted = 0; for (auto& cat : cats) { catsp = getUnlockedCatArray(cat->getParentUUID()); @@ -3246,6 +3343,7 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) cat->getPreferredType() == LLFolderType::FT_ROOT_INVENTORY )) { catsp->push_back(cat); + ++tree_links_inserted; } else { @@ -3261,6 +3359,7 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) lost_cats.push_back(cat); } } + const S32 lost_categories = lost; if(lost) { LL_WARNS(LOG_INV) << "Found " << lost << " lost categories." << LL_ENDL; @@ -3299,6 +3398,7 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) if(catsp) { catsp->push_back(cat); + ++tree_links_inserted; } else { @@ -3314,6 +3414,7 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) // have to do is iterate over the items and put them in the right // place. item_array_t items; + size_t item_links_inserted = 0; if(!mItemMap.empty()) { LLPointer item; @@ -3331,6 +3432,7 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) if(itemsp) { itemsp->push_back(item); + ++item_links_inserted; } else { @@ -3348,6 +3450,7 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) if(itemsp) { itemsp->push_back(item); + ++item_links_inserted; } else { @@ -3388,6 +3491,21 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) } } + const size_t category_tree_nodes = mParentChildCategoryTree.size(); + const size_t item_tree_nodes = mParentChildItemTree.size(); + const S32 lost_items = lost; + + LL_INFOS("AsyncInventory") << "ParentChildMap summary" + << " category_map_size=" << mCategoryMap.size() + << " category_tree_nodes=" << category_tree_nodes + << " category_links=" << tree_links_inserted + << " item_map_size=" << mItemMap.size() + << " item_tree_nodes=" << item_tree_nodes + << " item_links=" << item_links_inserted + << " lost_categories=" << lost_categories + << " lost_items=" << lost_items + << LL_ENDL; + if (run_validation) { const LLUUID& agent_inv_root_id = gInventory.getRootFolderID(); @@ -3427,7 +3545,7 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) { // 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! " + 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; @@ -3783,14 +3901,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; } } @@ -3806,14 +3934,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 (...) { diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index 93035c58b20..d8244ad5610 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -638,6 +638,13 @@ void LLAsyncInventorySkeletonLoader::evaluateChildren(const FetchRequest& reques { 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; } @@ -655,9 +662,23 @@ void LLAsyncInventorySkeletonLoader::evaluateChildren(const FetchRequest& reques 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); } } @@ -698,6 +719,13 @@ void LLAsyncInventorySkeletonLoader::discoverEssentialFolders() 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; } @@ -705,6 +733,11 @@ void LLAsyncInventorySkeletonLoader::discoverEssentialFolders() { 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; } } @@ -720,11 +753,21 @@ void LLAsyncInventorySkeletonLoader::discoverEssentialFolders() 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; } } } From 55931a2af736099d37bd710c00559f3cb2124704 Mon Sep 17 00:00:00 2001 From: pepper <3782201+rohvani@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:26:30 -0700 Subject: [PATCH 06/12] Some cleanup changes --- indra/llinventory/llfoldertype.cpp | 18 +++++ indra/llinventory/llfoldertype.h | 2 + indra/newview/app_settings/settings.xml | 33 +++++++++ indra/newview/llinventorymodel.cpp | 98 +++++++++++++++---------- indra/newview/llinventorymodel.h | 16 +++- indra/newview/llstartup.cpp | 43 ++++++----- 6 files changed, 149 insertions(+), 61 deletions(-) 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/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index ea803a73373..212c740cd71 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -3291,6 +3291,39 @@ 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/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp index 3e12037e3c7..acdb4ddbfc0 100644 --- a/indra/newview/llinventorymodel.cpp +++ b/indra/newview/llinventorymodel.cpp @@ -127,26 +127,30 @@ bool LLCanCache::operator()(LLInventoryCategory* cat, LLInventoryItem* item) // HACK: downcast LLViewerInventoryCategory* c = (LLViewerInventoryCategory*)cat; - bool descendents_match = true; + // 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) { const S32 descendents_server = c->getDescendentCount(); const S32 descendents_actual = c->getViewerDescendentCount(); - descendents_match = (descendents_server == descendents_actual); - if (!descendents_match) + if (descendents_server != descendents_actual) { 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; + mCachedCatIDs.insert(c->getUUID()); + rv = true; + } } return rv; } @@ -2192,6 +2196,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)) @@ -2229,6 +2247,8 @@ void LLInventoryModel::notifyObservers() { 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; } @@ -2424,6 +2444,13 @@ void LLInventoryModel::cache( 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) @@ -2441,8 +2468,6 @@ void LLInventoryModel::cache( } } - // Fallback pass: ensure every known category/item under the root is persisted - // even if the parent/child map failed to enumerate it. for (auto& entry : mCategoryMap) { LLViewerInventoryCategory* cat = entry.second; @@ -2482,9 +2507,13 @@ void LLInventoryModel::cache( { continue; } - items.push_back(item); - cached_item_ids.insert(item_id); + 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(); @@ -3269,19 +3298,26 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) { LL_INFOS(LOG_INV) << "LLInventoryModel::buildParentChildMap()" << LL_ENDL; - // Rebuild from scratch so we do not accumulate duplicate children - // across consecutive calls triggered during async skeleton hydration. - std::for_each( - mParentChildCategoryTree.begin(), - mParentChildCategoryTree.end(), - DeletePairedPointer()); - mParentChildCategoryTree.clear(); + // 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(); + } - std::for_each( - mParentChildItemTree.begin(), - mParentChildItemTree.end(), - DeletePairedPointer()); - mParentChildItemTree.clear(); + if (!mParentChildItemTree.empty()) + { + std::for_each( + mParentChildItemTree.begin(), + mParentChildItemTree.end(), + DeletePairedPointer()); + mParentChildItemTree.clear(); + } mCategoryLock.clear(); mItemLock.clear(); @@ -3332,7 +3368,6 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) S32 i; S32 lost = 0; cat_array_t lost_cats; - size_t tree_links_inserted = 0; for (auto& cat : cats) { catsp = getUnlockedCatArray(cat->getParentUUID()); @@ -3343,7 +3378,6 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) cat->getPreferredType() == LLFolderType::FT_ROOT_INVENTORY )) { catsp->push_back(cat); - ++tree_links_inserted; } else { @@ -3398,7 +3432,6 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) if(catsp) { catsp->push_back(cat); - ++tree_links_inserted; } else { @@ -3414,7 +3447,6 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) // have to do is iterate over the items and put them in the right // place. item_array_t items; - size_t item_links_inserted = 0; if(!mItemMap.empty()) { LLPointer item; @@ -3432,7 +3464,6 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) if(itemsp) { itemsp->push_back(item); - ++item_links_inserted; } else { @@ -3450,7 +3481,6 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) if(itemsp) { itemsp->push_back(item); - ++item_links_inserted; } else { @@ -3491,19 +3521,13 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) } } - const size_t category_tree_nodes = mParentChildCategoryTree.size(); - const size_t item_tree_nodes = mParentChildItemTree.size(); - const S32 lost_items = lost; - LL_INFOS("AsyncInventory") << "ParentChildMap summary" << " category_map_size=" << mCategoryMap.size() - << " category_tree_nodes=" << category_tree_nodes - << " category_links=" << tree_links_inserted + << " category_tree_nodes=" << mParentChildCategoryTree.size() << " item_map_size=" << mItemMap.size() - << " item_tree_nodes=" << item_tree_nodes - << " item_links=" << item_links_inserted + << " item_tree_nodes=" << mParentChildItemTree.size() << " lost_categories=" << lost_categories - << " lost_items=" << lost_items + << " lost_items=" << lost << LL_ENDL; if (run_validation) diff --git a/indra/newview/llinventorymodel.h b/indra/newview/llinventorymodel.h index a8d2ed99b04..2a20da38bf3 100644 --- a/indra/newview/llinventorymodel.h +++ b/indra/newview/llinventorymodel.h @@ -224,10 +224,18 @@ 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. - bool mAllowAsyncInventoryUpdates{false}; - bool mAsyncNotifyPending{false}; - LLFrameTimer mAsyncNotifyTimer; - F32 mAsyncNotifyIntervalSec{0.05f}; + + // 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; diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index d8244ad5610..e2791b22bea 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -226,22 +226,6 @@ namespace { -bool isEssentialFolderType(LLFolderType::EType folder_type) -{ - if (folder_type == LLFolderType::FT_NONE) - { - return false; - } - - if (folder_type == LLFolderType::FT_ROOT_INVENTORY) - { - return true; - } - - return LLFolderType::lookupIsSingletonType(folder_type) - || LLFolderType::lookupIsEnsembleType(folder_type); -} - class LLAsyncInventorySkeletonLoader { public: @@ -312,9 +296,9 @@ class LLAsyncInventorySkeletonLoader LLFrameTimer mTotalTimer; LLFrameTimer mEssentialTimer; - F32 mCapsTimeoutSec = 45.f; - F32 mFetchTimeoutSec = 180.f; - F32 mEssentialTimeoutSec = 90.f; + F32 mCapsTimeoutSec; + F32 mFetchTimeoutSec; + F32 mEssentialTimeoutSec; boost::signals2::connection mCapsConnection; bool mIdleRegistered = false; @@ -341,6 +325,12 @@ void LLAsyncInventorySkeletonLoader::reset() const U32 requested = gSavedSettings.getU32("AsyncInventoryMaxConcurrentFetches"); mMaxConcurrentFetches = std::clamp(requested, 1U, 8U); + + // Load timeout settings + mCapsTimeoutSec = gSavedSettings.getF32("AsyncInventoryCapsTimeout"); + mFetchTimeoutSec = gSavedSettings.getF32("AsyncInventoryFetchTimeout"); + mEssentialTimeoutSec = gSavedSettings.getF32("AsyncInventoryEssentialTimeout"); + mSawCurrentOutfitFolder = false; } @@ -628,7 +618,7 @@ void LLAsyncInventorySkeletonLoader::evaluateChildren(const FetchRequest& reques { child_essential = true; } - else if (isEssentialFolderType(child->getPreferredType())) + else if (LLFolderType::lookupIsEssentialType(child->getPreferredType())) { child_essential = true; } @@ -2673,6 +2663,19 @@ bool idle_startup() 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()) From 41811839f32fc5b8f8e6f57fd09a6642f6dfde6b Mon Sep 17 00:00:00 2001 From: pepper <3782201+rohvani@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:49:27 -0700 Subject: [PATCH 07/12] Performance optimizations around UI - various improvements - lazily creates the UI wigets for categories when their parent is expanded --- indra/llcommon/llsingleton.h | 17 ++-- indra/llui/llpanel.cpp | 42 +++++---- indra/llui/llview.cpp | 23 +++++ indra/llui/llview.h | 3 + indra/newview/llinventorymodel.cpp | 136 ++++++++++++++++++++++------- indra/newview/llinventorypanel.cpp | 44 ++++++---- indra/newview/llstartup.cpp | 32 ++++--- 7 files changed, 213 insertions(+), 84 deletions(-) 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/llui/llpanel.cpp b/indra/llui/llpanel.cpp index db314cae0f5..6fef76266e2 100644 --- a/indra/llui/llpanel.cpp +++ b/indra/llui/llpanel.cpp @@ -489,58 +489,70 @@ bool LLPanel::initPanelXML(LLXMLNodePtr node, LLView *parent, LLXMLNodePtr outpu LL_RECORD_BLOCK_TIME(FTM_PANEL_SETUP); LLXMLNodePtr referenced_xml; - std::string xml_filename = mXMLFilename; + const std::string& xml_filename = mXMLFilename; // if the panel didn't provide a filename, check the node if (xml_filename.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 +564,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..134383d5503 100644 --- a/indra/llui/llview.cpp +++ b/indra/llui/llview.cpp @@ -305,6 +305,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->getName().empty()) + { + mChildNameCache[child->getName()] = child; + } + // add to tab order list if (tab_group != 0) { @@ -344,6 +350,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->getName().empty()) + { + mChildNameCache.erase(child->getName()); + } + child->mParentView = NULL; child_tab_order_t::iterator found = mTabOrder.find(child); if (found != mTabOrder.end()) @@ -1649,15 +1662,25 @@ 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) + auto cache_it = mChildNameCache.find(name); + if (cache_it != mChildNameCache.end()) + { + return cache_it->second; + } + // Look for direct children *first* for (LLView* childp : mChildList) { llassert(childp); if (childp->getName() == name) { + // Cache the result for next lookup + mChildNameCache[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..932fbdc7d91 100644 --- a/indra/llui/llview.h +++ b/indra/llui/llview.h @@ -589,6 +589,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/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp index acdb4ddbfc0..d9508920bb9 100644 --- a/indra/newview/llinventorymodel.cpp +++ b/indra/newview/llinventorymodel.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include "llinventorymodel.h" @@ -3292,8 +3293,8 @@ bool LLInventoryModel::loadSkeletonFromCacheOnly(const LLUUID& owner_id) } // 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. +// 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; @@ -3330,51 +3331,61 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) // 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); @@ -3401,6 +3412,10 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) // 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) { @@ -3419,7 +3434,7 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) 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. @@ -3428,7 +3443,22 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) // 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); @@ -3445,10 +3475,11 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) // 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) { @@ -3458,9 +3489,27 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) } 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); @@ -3472,12 +3521,24 @@ void LLInventoryModel::buildParentChildMap(bool run_validation) ++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); @@ -3832,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) @@ -3850,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) @@ -3857,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()); } 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/llstartup.cpp b/indra/newview/llstartup.cpp index e2791b22bea..7467e577637 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -2563,17 +2563,22 @@ bool idle_startup() { LL_PROFILE_ZONE_NAMED("State inventory load skeleton") + // Cache frequently accessed values to reduce function call overhead LLLoginInstance* login_instance = LLLoginInstance::getInstance(); - LLSD response = login_instance->getResponse(); + const LLSD& response = login_instance->getResponse(); - LLSD inv_skel_lib = response["inventory-skel-lib"]; - LLSD inv_skeleton = response["inventory-skeleton"]; + 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) { gInventory.setAsyncInventoryLoading(false); @@ -2586,27 +2591,26 @@ bool idle_startup() gAsyncLibraryCacheHydrated = false; gAsyncParentChildMapPrimed = false; - if (inv_skel_lib.isDefined() && gInventory.getLibraryOwnerID().notNull()) + if (inv_skel_lib.isDefined() && library_owner_id.notNull()) { LL_PROFILE_ZONE_NAMED("load library inv") - if (!gInventory.loadSkeleton(inv_skel_lib, gInventory.getLibraryOwnerID())) + if (!gInventory.loadSkeleton(inv_skel_lib, library_owner_id)) { LL_WARNS("AppInit") << "Problem loading inventory-skel-lib" << LL_ENDL; } } - do_startup_frame(); + // 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, gAgent.getID())) + if (!gInventory.loadSkeleton(inv_skeleton, agent_id)) { LL_WARNS("AppInit") << "Problem loading inventory-skel-targets" << LL_ENDL; } } - do_startup_frame(); + do_startup_frame(); // Single frame update after both skeleton loads LLStartUp::setStartupState(STATE_INVENTORY_SEND2); - do_startup_frame(); return false; } @@ -2615,12 +2619,12 @@ bool idle_startup() gInventory.setAsyncInventoryLoading(true); } - if (!gAsyncLibraryCacheHydrated && gInventory.getLibraryOwnerID().notNull()) + if (!gAsyncLibraryCacheHydrated && library_owner_id.notNull()) { 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, gInventory.getLibraryOwnerID(), hydrate_from_cache)) + if (!gInventory.loadSkeleton(library_payload, library_owner_id, hydrate_from_cache)) { LL_WARNS("AppInit") << "Problem loading library inventory skeleton in async mode" << LL_ENDL; } @@ -2631,16 +2635,18 @@ bool idle_startup() if (!gAsyncAgentCacheHydrated) { LL_PROFILE_ZONE_NAMED("hydrate personal inv cache async") - if (!gInventory.loadSkeleton(LLSD(), gAgent.getID(), true)) + if (!gInventory.loadSkeleton(LLSD(), agent_id, true)) { LL_WARNS("AppInit") << "Problem hydrating cached agent inventory skeleton" << LL_ENDL; } gAsyncAgentCacheHydrated = true; } - if (!gAsyncParentChildMapPrimed && gInventory.getRootFolderID().notNull()) + 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=" From a136f866141704b023d9326f0e95798c09874d3d Mon Sep 17 00:00:00 2001 From: Signal Linden Date: Thu, 2 Oct 2025 12:42:02 -0700 Subject: [PATCH 08/12] Add workflow_dispatch trigger to build workflow (#4774) Allow builds to be manually triggered --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1d6a23dc4d7..5a5628ede2b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,6 +1,7 @@ name: Build on: + workflow_dispatch: pull_request: push: branches: ["main", "release/*", "project/*"] From 0743423ee1cbbde7efeeb03a72299bf69299a7fa Mon Sep 17 00:00:00 2001 From: Andrey Lihatskiy <118752495+marchcat@users.noreply.github.com> Date: Fri, 24 Oct 2025 07:22:59 +0300 Subject: [PATCH 09/12] Fix lingering inventory after async skeleton failures (#4894) --- indra/newview/llstartup.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index 7467e577637..bb107e30588 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -2695,6 +2695,7 @@ bool idle_startup() gAsyncAgentCacheHydrated = false; gAsyncLibraryCacheHydrated = false; gAsyncParentChildMapPrimed = false; + gInventory.cleanupInventory(); gInventory.setAsyncInventoryLoading(false); show_connect_box = true; From 72788955ad15abb222e4f68e57454fe38fc1892f Mon Sep 17 00:00:00 2001 From: Andrey Lihatskiy Date: Wed, 29 Oct 2025 01:44:51 +0200 Subject: [PATCH 10/12] #4893 Drop unused llpanel XML filename reference --- indra/llui/llpanel.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/indra/llui/llpanel.cpp b/indra/llui/llpanel.cpp index 6fef76266e2..138e637ede2 100644 --- a/indra/llui/llpanel.cpp +++ b/indra/llui/llpanel.cpp @@ -489,10 +489,9 @@ bool LLPanel::initPanelXML(LLXMLNodePtr node, LLView *parent, LLXMLNodePtr outpu LL_RECORD_BLOCK_TIME(FTM_PANEL_SETUP); LLXMLNodePtr referenced_xml; - const std::string& xml_filename = mXMLFilename; // if the panel didn't provide a filename, check the node - if (xml_filename.empty()) + if (mXMLFilename.empty()) { std::string temp_filename; node->getAttributeString("filename", temp_filename); From 456eddd883ef6be1853489b47a03f45291004111 Mon Sep 17 00:00:00 2001 From: Andrey Lihatskiy Date: Wed, 29 Oct 2025 02:04:36 +0200 Subject: [PATCH 11/12] #4893 Extract async inventory skeleton loader --- indra/newview/CMakeLists.txt | 2 + .../llasyncinventoryskeletonloader.cpp | 672 ++++++++++++++++ .../newview/llasyncinventoryskeletonloader.h | 129 ++++ indra/newview/llstartup.cpp | 722 +----------------- 4 files changed, 804 insertions(+), 721 deletions(-) create mode 100644 indra/newview/llasyncinventoryskeletonloader.cpp create mode 100644 indra/newview/llasyncinventoryskeletonloader.h 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/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/llstartup.cpp b/indra/newview/llstartup.cpp index bb107e30588..90d01a52793 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -80,10 +80,8 @@ #include "lluserrelations.h" #include "llversioninfo.h" #include "llframetimer.h" -#include "llaisapi.h" #include "llviewerregion.h" -#include #include #include "llviewercontrol.h" @@ -196,6 +194,7 @@ #include "llinventorybridge.h" #include "llfoldertype.h" #include "llappearancemgr.h" +#include "llasyncinventoryskeletonloader.h" #include "llavatariconctrl.h" #include "llvoicechannel.h" #include "llpathfindingmanager.h" @@ -215,704 +214,10 @@ #include "threadpool.h" #include "llperfstats.h" -#include -#include -#include - - #if LL_WINDOWS #include "lldxhardware.h" #endif -namespace -{ -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; - F32 mFetchTimeoutSec; - F32 mEssentialTimeoutSec; - - boost::signals2::connection mCapsConnection; - bool mIdleRegistered = false; - std::string mFailureReason; -}; - -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); - - // Load timeout settings - 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(); - } -} - -LLAsyncInventorySkeletonLoader gAsyncInventorySkeletonLoader; -bool gAsyncAgentCacheHydrated = false; -bool gAsyncLibraryCacheHydrated = false; -bool gAsyncParentChildMapPrimed = false; -} - // // exported globals // @@ -3349,31 +2654,6 @@ bool idle_startup() return true; } -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; -} - // // local function definition // From b0a94df25d8967aa37944910878b6857c221d6ce Mon Sep 17 00:00:00 2001 From: Andrey Lihatskiy Date: Wed, 29 Oct 2025 02:34:35 +0200 Subject: [PATCH 12/12] #4893 Tighten LLView child cache - store cache keys as std::string to avoid dangling references - refresh parent entry on rename and skip unnamed views --- indra/llui/llview.cpp | 51 +++++++++++++++++++++++++++++++++++++------ indra/llui/llview.h | 6 +++-- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/indra/llui/llview.cpp b/indra/llui/llview.cpp index 134383d5503..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 @@ -306,7 +328,7 @@ bool LLView::addChild(LLView* child, S32 tab_group) mChildList.push_front(child); // Add to name cache for fast lookup - if (!child->getName().empty()) + if (child->hasName()) { mChildNameCache[child->getName()] = child; } @@ -352,7 +374,7 @@ void LLView::removeChild(LLView* child) mChildList.remove( child ); // Remove from name cache - if (!child->getName().empty()) + if (child->hasName()) { mChildNameCache.erase(child->getName()); } @@ -1663,20 +1685,35 @@ 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) - auto cache_it = mChildNameCache.find(name); - if (cache_it != mChildNameCache.end()) + if (!mChildNameCache.empty()) { - return cache_it->second; + 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[name] = childp; + mChildNameCache[child_name] = childp; return childp; } } diff --git a/indra/llui/llview.h b/indra/llui/llview.h index 932fbdc7d91..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; @@ -590,7 +592,7 @@ class LLView child_list_t mChildList; // Cache for fast child lookup by name - O(1) instead of O(n) - mutable std::unordered_map mChildNameCache; + mutable std::unordered_map mChildNameCache; // location in pixels, relative to surrounding structure, bottom,left=0,0 bool mVisible;