diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 04cf9f7140..0256c7c78d 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -1691,6 +1691,15 @@ set(viewer_HEADER_FILES list(APPEND viewer_SOURCE_FILES llperfstats.cpp) list(APPEND viewer_HEADER_FILES llperfstats.h) +if (USE_BUGSPLAT) + list(APPEND viewer_SOURCE_FILES + bugsplatattributes.cpp + ) + list(APPEND viewer_HEADER_FILES + bugsplatattributes.h + ) +endif (USE_BUGSPLAT) + # Flickr / Discord keys and fsversionvalues headers are generated in here include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) diff --git a/indra/newview/bugsplatattributes.cpp b/indra/newview/bugsplatattributes.cpp new file mode 100644 index 0000000000..46febab638 --- /dev/null +++ b/indra/newview/bugsplatattributes.cpp @@ -0,0 +1,132 @@ +#include "bugsplatattributes.h" +#include +#include + +std::wstring BugSplatAttributes::mCrashContextFileName; + +BugSplatAttributes& BugSplatAttributes::instance() +{ + static BugSplatAttributes sInstance; + return sInstance; +} + + +#include +#include + +std::wstring BugSplatAttributes::to_xml_token(const std::wstring& input) +{ + // Example usage: + // std::wstring token = to_xml_token(L"Bandwidth (kbit/s)"); + // The result should be: L"Bandwidth_kbit_per_s" + std::wstring result; + result.reserve(input.size() * 2); // Reserve some space, since we might insert more chars for "/" + + for (wchar_t ch : input) + { + if (ch == L'/') + { + // Replace '/' with "_per_" + result.append(L"_per_"); + } + else if (std::iswalnum(ch) || ch == L'_' || ch == L'-') + { + // Letters, digits, underscore, and hyphen are okay + result.push_back(ch); + } + else if (ch == L' ' || ch == L'(' || ch == L')') + { + // Replace spaces and parentheses with underscore + result.push_back(L'_'); + } + else + { + // Other characters map to underscore + result.push_back(L'_'); + } + } + + // Ensure the first character is a letter or underscore: + // If not, prepend underscore. + if (result.empty() || !(std::iswalpha(result.front()) || result.front() == L'_')) + { + result.insert(result.begin(), L'_'); + } + // trim trailing underscores + while (!result.empty() && result.back() == L'_') + { + result.pop_back(); + } + return result; +} + + + +bool BugSplatAttributes::writeToFile(const std::wstring& file_path) +{ + std::lock_guard lock(mMutex); + + // Write to a temporary file first + std::wstring tmp_file = file_path + L".tmp"; + + { + std::wofstream ofs(tmp_file, std::ios::out | std::ios::trunc); + if (!ofs.good()) + { + return false; + } + + ofs << L"\n"; + ofs << L"\n"; + + // First, write out attributes that have an empty category (top-level) + auto empty_category_it = mAttributes.find(L""); + if (empty_category_it != mAttributes.end()) + { + for (const auto& kv : empty_category_it->second) + { + const std::wstring& key = kv.first; + const std::wstring& val = kv.second; + ofs << L" <" << key << L">" << val << L"\n"; + } + } + + // Write out all other categories + for (const auto& cat_pair : mAttributes) + { + const std::wstring& category = cat_pair.first; + + // Skip empty category since we already handled it above + if (category.empty()) + { + continue; + } + + ofs << L" <" << category << L">\n"; + for (const auto& kv : cat_pair.second) + { + const std::wstring& key = kv.first; + const std::wstring& val = kv.second; + ofs << L" <" << key << L">" << val << L"\n"; + } + ofs << L" \n"; + } + + ofs << L"\n"; + + // Close the file before renaming + ofs.close(); + } + + // Rename the temp file to the final file. If rename fails, leave the old file intact. + std::error_code ec; + std::filesystem::rename(tmp_file, file_path, ec); + if (ec) + { + // Rename failed, remove the temp file and return false + std::filesystem::remove(tmp_file, ec); + return false; + } + + return true; +} diff --git a/indra/newview/bugsplatattributes.h b/indra/newview/bugsplatattributes.h new file mode 100644 index 0000000000..8155225ac3 --- /dev/null +++ b/indra/newview/bugsplatattributes.h @@ -0,0 +1,71 @@ +#ifndef BUGSPLATATTRIBUTES_INCLUDED +#define BUGSPLATATTRIBUTES_INCLUDED +#ifdef LL_BUGSPLAT +// Currently on Windows BugSplat supports the new attributes file, but it is expected to be added to Mac and Linux soon. + +#include +#include +#include + +class BugSplatAttributes +{ +public: + // Obtain the singleton instance + static BugSplatAttributes& instance(); + + template + void setAttribute(const std::wstring& key, const T& value, const std::wstring& category = L"FS") + { + std::lock_guard lock(mMutex); + const auto& xml_key = to_xml_token(key); + if constexpr (std::is_same_v) + { + // Already wide string + mAttributes[category][xml_key] = value; + } + else if constexpr (std::is_same_v || std::is_array_v) + { + // Handle wide string literals and arrays (which includes string literals) + mAttributes[category][xml_key] = std::wstring(value); + } + else if constexpr (std::is_same_v) + { + // Convert narrow to wide + std::wstring wide_value(value.begin(), value.end()); + mAttributes[category][xml_key] = wide_value; + } + else if constexpr (std::is_same_v) + { + // Convert boolean to "true"/"false" + mAttributes[category][xml_key] = value ? L"true" : L"false"; + } + else if constexpr (std::is_arithmetic_v) + { + // Convert arithmetic types (int, float, double, etc.) to wstring + mAttributes[category][xml_key] = std::to_wstring(value); + } + else + { + static_assert(!sizeof(T*), "No known conversion for this type to std::wstring."); + } + } + // Returns true on success, false on failure. + bool writeToFile(const std::wstring& file_path); + const static std::wstring& getCrashContextFileName() { return mCrashContextFileName; } + static void setCrashContextFileName(const std::wstring& file_name) { mCrashContextFileName = file_name; } +private: + BugSplatAttributes() = default; + ~BugSplatAttributes() = default; + BugSplatAttributes(const BugSplatAttributes&) = delete; + BugSplatAttributes& operator=(const BugSplatAttributes&) = delete; + std::wstring to_xml_token(const std::wstring& input); + // Internal structure to hold attributes by category + // For example: + // attributes_["RuntimeProperties"]["CrashGUID"] = L"649F57B7EE6E92A0_0000" + std::unordered_map> mAttributes; + std::mutex mMutex; + static std::wstring mCrashContextFileName; +}; + +#endif // LL_BUGSPLAT +#endif // BUGSPLATATTRIBUTES_INCLUDED \ No newline at end of file diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 2b9d642aa7..522ae59736 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -289,7 +289,7 @@ using namespace LL; #include "fsradar.h" #include "fsassetblacklist.h" - +#include "bugsplatattributes.h" // #include "fstelemetry.h" // Tracy profiler support #if LL_LINUX && LL_GTK @@ -767,6 +767,10 @@ LLAppViewer::LLAppViewer() // MAINT-8917: don't create a dump directory just for the // static_debug_info.log file std::string logdir = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, ""); + // Improve Bugsplat tracking by using attribu + std::wstring wlogdir(logdir.begin(), logdir.end()); + BugSplatAttributes::setCrashContextFileName(wlogdir + L"crash-context.xml"); + // # else // ! LL_BUGSPLAT // write Google Breakpad minidump files to a per-run dump directory to avoid multiple viewer issues. std::string logdir = gDirUtilp->getExpandedFilename(LL_PATH_DUMP, ""); @@ -3762,6 +3766,10 @@ bool LLAppViewer::waitForUpdater() void LLAppViewer::writeDebugInfo(bool isStatic) { #if LL_WINDOWS && LL_BUGSPLAT + // Improve Bugsplat tracking by using attributes for certain static data items. + const LLSD& info = getViewerInfo(); + bugsplatAddStaticAttributes(info); + // // bugsplat does not create dump folder and debug logs are written directly // to logs folder, so it conflicts with main instance if (mSecondInstance) @@ -3970,10 +3978,17 @@ LLSD LLAppViewer::getViewerInfo() const info["OPENGL_VERSION"] = ll_safe_string((const char*)(glGetString(GL_VERSION))); info["LIBCURL_VERSION"] = LLCore::LLHttp::getCURLVersion(); // Settings - - LLRect window_rect = gViewerWindow->getWindowRectRaw(); - info["WINDOW_WIDTH"] = window_rect.getWidth(); - info["WINDOW_HEIGHT"] = window_rect.getHeight(); + // gViewerWindow can be null on shutdown. Crashes if bugsplatt uses the info + // LLRect window_rect = gViewerWindow->getWindowRectRaw(); + // info["WINDOW_WIDTH"] = window_rect.getWidth(); + // info["WINDOW_HEIGHT"] = window_rect.getHeight(); + if(gViewerWindow) + { + LLRect window_rect = gViewerWindow->getWindowRectRaw(); + info["WINDOW_WIDTH"] = window_rect.getWidth(); + info["WINDOW_HEIGHT"] = window_rect.getHeight(); + } + // // Custom sysinfo //info["FONT_SIZE_ADJUSTMENT"] = gSavedSettings.getF32("FontScreenDPI"); @@ -3994,7 +4009,7 @@ LLSD LLAppViewer::getViewerInfo() const info["J2C_VERSION"] = LLImageJ2C::getEngineInfo(); bool want_fullname = true; info["AUDIO_DRIVER_VERSION"] = gAudiop ? LLSD(gAudiop->getDriverName(want_fullname)) : "Undefined"; - if(LLVoiceClient::getInstance()->voiceEnabled()) + if(LLStartUp::getStartupState() == STATE_STARTED && LLVoiceClient::getInstance()->voiceEnabled()) // Calling this too earli leads to a nasty crash loop { LLVoiceVersionInfo version = LLVoiceClient::getInstance()->getVersion(); const std::string build_version = version.mBuildVersion; @@ -4086,7 +4101,13 @@ LLSD LLAppViewer::getViewerInfo() const } // populate field for new local disk cache with some details - info["DISK_CACHE_INFO"] = LLDiskCache::getInstance()->getCacheInfo(); + // only populate if the cache is available + // info["DISK_CACHE_INFO"] = LLDiskCache::getInstance()->getCacheInfo(); + if (auto cache = LLDiskCache::getInstance(); cache) + { + info["DISK_CACHE_INFO"] = cache->getCacheInfo(); + } + // // FIRE-4785: Current render quality setting in sysinfo / about floater switch (gSavedSettings.getU32("RenderQualityPerformance")) diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h index 1183ab7f6c..40b686c094 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -253,6 +253,7 @@ protected: virtual void overrideDetectedHardware(); // Override VRAM (and others in future?) consistently. virtual bool initSLURLHandler(); virtual bool sendURLToOtherInstance(const std::string& url); + virtual void bugsplatAddStaticAttributes(const LLSD& info) {/*empty*/}; // create a NOOP base impl virtual bool initParseCommandLine(LLCommandLineParser& clp) { return true; } // Allow platforms to specify the command line args. diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index 4361107589..643b1ab321 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -75,6 +75,7 @@ // Bugsplat (http://bugsplat.com) crash reporting tool #ifdef LL_BUGSPLAT +#include "bugsplatattributes.h" #include "BugSplat.h" #include "boost/json.hpp" // Boost.Json #include "llagent.h" // for agent location @@ -164,6 +165,7 @@ namespace flavor = "oss"; #endif sBugSplatSender->setDefaultUserEmail( WCSTR(STRINGIZE(LLOSInfo::instance().getOSStringSimple() << " (" << ADDRESS_SIZE << "-bit, flavor " << flavor <<")"))); + // BugSplatAttributes::instance().setAttribute(L"Flavor", flavor); // // Clear out username first, as we get some crashes that has the OS set as username, let's see if this fixes it. Use Crash.Linden as a usr can never have a "Linden" @@ -195,7 +197,6 @@ namespace // LL_ERRS message, when there is one sBugSplatSender->setDefaultUserDescription(WCSTR(LLError::getFatalMessage())); - sBugSplatSender->setAttribute(WCSTR(L"OS"), WCSTR(LLOSInfo::instance().getOSStringSimple())); // In case we ever stop using email for this sBugSplatSender->setAttribute(WCSTR(L"AppState"), WCSTR(LLStartUp::getStartupStateString())); sBugSplatSender->setAttribute(WCSTR(L"GL Vendor"), WCSTR(gGLManager.mGLVendor)); @@ -203,18 +204,41 @@ namespace sBugSplatSender->setAttribute(WCSTR(L"GPU Version"), WCSTR(gGLManager.mDriverVersionVendorString)); sBugSplatSender->setAttribute(WCSTR(L"GL Renderer"), WCSTR(gGLManager.mGLRenderer)); sBugSplatSender->setAttribute(WCSTR(L"VRAM"), WCSTR(STRINGIZE(gGLManager.mVRAM))); - sBugSplatSender->setAttribute(WCSTR(L"RAM"), WCSTR(STRINGIZE(gSysMemory.getPhysicalMemoryKB().value()))); + // sBugSplatSender->setAttribute(WCSTR(L"RAM"), WCSTR(STRINGIZE(gSysMemory.getPhysicalMemoryKB().value()))); + // Improve bugsplpat reporting with attributes + // sBugSplatSender->setDefaultUserDescription(WCSTR(LLError::getFatalMessage())); + // sBugSplatSender->setAttribute(WCSTR(L"AppState"), WCSTR(LLStartUp::getStartupStateString())); + auto fatal_message = LLError::getFatalMessage(); + sBugSplatSender->setDefaultUserDescription(WCSTR(fatal_message)); + BugSplatAttributes::instance().setAttribute(L"FatalMessage", fatal_message); // Store this additionally as an attribute in case user overwrites. + // App state + BugSplatAttributes::instance().setAttribute(L"AppState", LLStartUp::getStartupStateString()); + // Location + // if (gAgent.getRegion()) { // region location, when we have it - LLVector3 loc = gAgent.getPositionAgent(); - sBugSplatSender->resetAppIdentifier( - WCSTR(STRINGIZE(gAgent.getRegion()->getName() + // Improve bugsplat reporting with attributes + // LLVector3 loc = gAgent.getPositionAgent(); + // sBugSplatSender->resetAppIdentifier( + // WCSTR(STRINGIZE(gAgent.getRegion()->getName() + // << '/' << loc.mV[0] + // << '/' << loc.mV[1] + // << '/' << loc.mV[2]))); + const LLVector3 loc = gAgent.getPositionAgent(); + const auto & fullLocation = STRINGIZE(gAgent.getRegion()->getName() << '/' << loc.mV[0] << '/' << loc.mV[1] - << '/' << loc.mV[2]))); + << '/' << loc.mV[2]); + sBugSplatSender->resetAppIdentifier(WCSTR(fullLocation)); + BugSplatAttributes::instance().setAttribute(L"Location", std::string(fullLocation)); + // } + // Improve bugsplat reporting with attributes + LLAppViewer::instance()->writeDebugInfo(); + sBugSplatSender->sendAdditionalFile(WCSTR(BugSplatAttributes::getCrashContextFileName())); // Add the new attributes file + // } // MDSCB_EXCEPTIONCODE return false; @@ -619,6 +643,71 @@ int APIENTRY wWinMain(HINSTANCE hInstance, } } #endif +// Use the Attributes API on Windows to enhance crash metadata +void LLAppViewerWin32::bugsplatAddStaticAttributes(const LLSD& info) +{ +#ifdef LL_BUGSPLAT + static bool write_statics = true; + + auto& bugSplatMap = BugSplatAttributes::instance(); + + if (write_statics) + { + write_statics = false; + auto multipleInstances = gDebugInfo["FoundOtherInstanceAtStartup"].asBoolean(); + bugSplatMap.setAttribute(L"MultipleInstance", multipleInstances); + + bugSplatMap.setAttribute(L"GPU", info["GRAPHICS_CARD"].asString()); + bugSplatMap.setAttribute(L"GPU VRAM (MB)", info["GRAPHICS_CARD_MEMORY"].asInteger()); + bugSplatMap.setAttribute(L"GPU VRAM Detected (MB)", info["GRAPHICS_CARD_MEMORY_DETECTED"].asInteger()); + bugSplatMap.setAttribute(L"GPU VRAM (Budget)", info["VRAM_BUDGET_ENGLISH"].asInteger()); + + bugSplatMap.setAttribute(L"CPU", info["CPU"].asString()); + bugSplatMap.setAttribute(L"Graphics Driver", info["GRAPHICS_DRIVER_VERSION"].asString()); + bugSplatMap.setAttribute(L"CPU MHz", (S32)gSysCPU.getMHz()); // +#ifdef USE_AVX2_OPTIMIZATION + bugSplatMap.setAttribute(L"SIMD", L"AVX2"); +#elif USE_AVX_OPTIMIZATION + bugSplatMap.setAttribute(L"SIMD", L"AVX"); +#else + bugSplatMap.setAttribute(L"SIMD", L"SSE2"); +#endif + // set physical ram integer as a string attribute + bugSplatMap.setAttribute(L"Physical RAM (KB)", LLMemory::getMaxMemKB().value()); + bugSplatMap.setAttribute(L"OpenGL Version", info["OPENGL_VERSION"].asString()); + bugSplatMap.setAttribute(L"libcurl Version", info["LIBCURL_VERSION"].asString()); + bugSplatMap.setAttribute(L"J2C Decoder Version", info["J2C_VERSION"].asString()); + bugSplatMap.setAttribute(L"Audio Driver Version", info["AUDIO_DRIVER_VERSION"].asString()); + // bugSplatMap.setAttribute(L"CEF Info", info["LIBCEF_VERSION"].asString()); + bugSplatMap.setAttribute(L"LibVLC Version", info["LIBVLC_VERSION"].asString()); + bugSplatMap.setAttribute(L"Vivox Version", info["VOICE_VERSION"].asString()); + bugSplatMap.setAttribute(L"RLVa", info["RLV_VERSION"].asString()); + bugSplatMap.setAttribute(L"Mode", info["MODE"].asString()); + bugSplatMap.setAttribute(L"Skin", llformat("%s (%s)", info["SKIN"].asString().c_str(), info["THEME"].asString().c_str())); + #if LL_DARWIN + bugSplatMap.setAttribute(L"HiDPI", info["HIDPI"].asBoolean() ? L"Enabled" : L"Disabled"); + #endif + } + // These attributes are potentially dynamic + bugSplatMap.setAttribute(L"Packets Lost", llformat("%.0f/%.0f (%.1f%%)", info["PACKETS_LOST"].asReal(), info["PACKETS_IN"].asReal(), info["PACKETS_PCT"].asReal())); + bugSplatMap.setAttribute(L"Window Size", llformat("%sx%s px", info["WINDOW_WIDTH"].asString().c_str(), info["WINDOW_HEIGHT"].asString().c_str())); + bugSplatMap.setAttribute(L"Draw Distance (m)", info["DRAW_DISTANCE"].asInteger()); + bugSplatMap.setAttribute(L"Bandwidth (kbit/s)", info["BANDWIDTH"].asInteger()); + bugSplatMap.setAttribute(L"LOD Factor", info["LOD"].asReal()); + bugSplatMap.setAttribute(L"Render quality", info["RENDERQUALITY_FSDATA_ENGLISH"].asString()); + bugSplatMap.setAttribute(L"Disk Cache", info["DISK_CACHE_INFO"].asString()); + + bugSplatMap.setAttribute(L"GridName", gDebugInfo["GridName"].asString()); + bugSplatMap.setAttribute(L"Available RAM (KB)", LLMemory::getAvailableMemKB().value()); + bugSplatMap.setAttribute(L"Allocated RAM (KB)", LLMemory::getAllocatedMemKB().value()); + + if (bugSplatMap.writeToFile(BugSplatAttributes::getCrashContextFileName())) + { + LL_INFOS() << "Crash context saved to " << WCSTR(BugSplatAttributes::getCrashContextFileName()) << LL_ENDL; + } +#endif +} +// void LLAppViewerWin32::disableWinErrorReporting() { diff --git a/indra/newview/llappviewerwin32.h b/indra/newview/llappviewerwin32.h index 1be4c62ee5..90ed468bc1 100644 --- a/indra/newview/llappviewerwin32.h +++ b/indra/newview/llappviewerwin32.h @@ -62,7 +62,7 @@ protected: private: void disableWinErrorReporting(); - + void bugsplatAddStaticAttributes(const LLSD& info) override; // override for windows attributes std::string mCmdLine; bool mIsConsoleAllocated;