diff options
| -rw-r--r-- | indra/llcommon/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | indra/llcommon/fsyspath.h | 79 | ||||
| -rwxr-xr-x | indra/llcommon/hexdump.h | 106 | ||||
| -rw-r--r-- | indra/llcommon/llsdjson.cpp | 15 | ||||
| -rw-r--r-- | indra/llrender/llglslshader.cpp | 108 | ||||
| -rw-r--r-- | indra/llrender/llglslshader.h | 10 | ||||
| -rw-r--r-- | indra/newview/llappviewer.cpp | 4 | ||||
| -rw-r--r-- | indra/newview/llfeaturemanager.cpp | 2 | ||||
| -rw-r--r-- | indra/newview/llglsandbox.cpp | 2 | ||||
| -rw-r--r-- | indra/newview/llviewerdisplay.cpp | 144 | ||||
| -rw-r--r-- | scripts/perf/logsdir.py | 46 | ||||
| -rw-r--r-- | scripts/perf/profile_cmp.py | 104 | ||||
| -rw-r--r-- | scripts/perf/profile_csv.py | 60 | ||||
| -rw-r--r-- | scripts/perf/profile_pretty.py | 40 | 
14 files changed, 644 insertions, 77 deletions
| diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 60549d9d11..a504e71340 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -119,6 +119,7 @@ set(llcommon_HEADER_FILES      commoncontrol.h      ctype_workaround.h      fix_macros.h +    fsyspath.h      function_types.h      indra_constants.h      lazyeventapi.h diff --git a/indra/llcommon/fsyspath.h b/indra/llcommon/fsyspath.h new file mode 100644 index 0000000000..1b4aec09b4 --- /dev/null +++ b/indra/llcommon/fsyspath.h @@ -0,0 +1,79 @@ +/** + * @file   fsyspath.h + * @author Nat Goodspeed + * @date   2024-04-03 + * @brief  Adapt our UTF-8 std::strings for std::filesystem::path + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_FSYSPATH_H) +#define LL_FSYSPATH_H + +#include <filesystem> + +// While std::filesystem::path can be directly constructed from std::string on +// both Posix and Windows, that's not what we want on Windows. Per +// https://en.cppreference.com/w/cpp/filesystem/path/path: + +// ... the method of conversion to the native character set depends on the +// character type used by source. +// +// * If the source character type is char, the encoding of the source is +//   assumed to be the native narrow encoding (so no conversion takes place on +//   POSIX systems). +// * If the source character type is char8_t, conversion from UTF-8 to native +//   filesystem encoding is used. (since C++20) +// * If the source character type is wchar_t, the input is assumed to be the +//   native wide encoding (so no conversion takes places on Windows). + +// The trouble is that on Windows, from std::string ("source character type is +// char"), the "native narrow encoding" isn't UTF-8, so file paths containing +// non-ASCII characters get mangled. +// +// Once we're building with C++20, we could pass a UTF-8 std::string through a +// vector<char8_t> to engage std::filesystem::path's own UTF-8 conversion. But +// sigh, as of 2024-04-03 we're not yet there. +// +// Anyway, encapsulating the important UTF-8 conversions in our own subclass +// allows us to migrate forward to C++20 conventions without changing +// referencing code. + +class fsyspath: public std::filesystem::path +{ +    using super = std::filesystem::path; + +public: +    // default +    fsyspath() {} +    // construct from UTF-8 encoded std::string +    fsyspath(const std::string& path): super(std::filesystem::u8path(path)) {} +    // construct from UTF-8 encoded const char* +    fsyspath(const char* path): super(std::filesystem::u8path(path)) {} +    // construct from existing path +    fsyspath(const super& path): super(path) {} + +    fsyspath& operator=(const super& p) { super::operator=(p); return *this; } +    fsyspath& operator=(const std::string& p) +    { +        super::operator=(std::filesystem::u8path(p)); +        return *this; +    } +    fsyspath& operator=(const char* p) +    { +        super::operator=(std::filesystem::u8path(p)); +        return *this; +    } + +    // shadow base-class string() method with UTF-8 aware method +    std::string string() const { return super::u8string(); } +    // On Posix systems, where value_type is already char, this operator +    // std::string() method shadows the base class operator string_type() +    // method. But on Windows, where value_type is wchar_t, the base class +    // doesn't have operator std::string(). Provide it. +    operator std::string() const { return string(); } +}; + +#endif /* ! defined(LL_FSYSPATH_H) */ diff --git a/indra/llcommon/hexdump.h b/indra/llcommon/hexdump.h new file mode 100755 index 0000000000..ab5ba2b16d --- /dev/null +++ b/indra/llcommon/hexdump.h @@ -0,0 +1,106 @@ +/** + * @file   hexdump.h + * @author Nat Goodspeed + * @date   2023-10-03 + * @brief  iostream manipulators to stream hex, or string with nonprinting chars + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Copyright (c) 2023, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_HEXDUMP_H) +#define LL_HEXDUMP_H + +#include <cctype> +#include <iomanip> +#include <iostream> +#include <string_view> + +namespace LL +{ + +// Format a given byte string as 2-digit hex values, no separators +// Usage: std::cout << hexdump(somestring) << ... +class hexdump +{ +public: +    hexdump(const std::string_view& data): +        hexdump(data.data(), data.length()) +    {} + +    hexdump(const char* data, size_t len): +        hexdump(reinterpret_cast<const unsigned char*>(data), len) +    {} + +    hexdump(const std::vector<unsigned char>& data): +        hexdump(data.data(), data.size()) +    {} + +    hexdump(const unsigned char* data, size_t len): +        mData(data, data + len) +    {} + +    friend std::ostream& operator<<(std::ostream& out, const hexdump& self) +    { +        auto oldfmt{ out.flags() }; +        auto oldfill{ out.fill() }; +        out.setf(std::ios_base::hex, std::ios_base::basefield); +        out.fill('0'); +        for (auto c : self.mData) +        { +            out << std::setw(2) << unsigned(c); +        } +        out.setf(oldfmt, std::ios_base::basefield); +        out.fill(oldfill); +        return out; +    } + +private: +    std::vector<unsigned char> mData; +}; + +// Format a given byte string as a mix of printable characters and, for each +// non-printable character, "\xnn" +// Usage: std::cout << hexmix(somestring) << ... +class hexmix +{ +public: +    hexmix(const std::string_view& data): +        mData(data) +    {} + +    hexmix(const char* data, size_t len): +        mData(data, len) +    {} + +    friend std::ostream& operator<<(std::ostream& out, const hexmix& self) +    { +        auto oldfmt{ out.flags() }; +        auto oldfill{ out.fill() }; +        out.setf(std::ios_base::hex, std::ios_base::basefield); +        out.fill('0'); +        for (auto c : self.mData) +        { +            // std::isprint() must be passed an unsigned char! +            if (std::isprint(static_cast<unsigned char>(c))) +            { +                out << c; +            } +            else +            { +                out << "\\x" << std::setw(2) << unsigned(c); +            } +        } +        out.setf(oldfmt, std::ios_base::basefield); +        out.fill(oldfill); +        return out; +    } + +private: +    std::string mData; +}; + +} // namespace LL + +#endif /* ! defined(LL_HEXDUMP_H) */ diff --git a/indra/llcommon/llsdjson.cpp b/indra/llcommon/llsdjson.cpp index 5d38e55686..1df2a8f9eb 100644 --- a/indra/llcommon/llsdjson.cpp +++ b/indra/llcommon/llsdjson.cpp @@ -61,12 +61,20 @@ LLSD LlsdFromJson(const boost::json::value& val)          result = LLSD(val.as_bool());          break;      case boost::json::kind::array: +    {          result = LLSD::emptyArray(); -        for (const auto &element : val.as_array()) +        auto& array = val.as_array(); +        // allocate elements 0 .. (size() - 1) to avoid incremental allocation +        if (! array.empty()) +        { +            result[array.size() - 1] = LLSD(); +        } +        for (const auto &element : array)          {              result.append(LlsdFromJson(element));          }          break; +    }      case boost::json::kind::object:          result = LLSD::emptyMap();          for (const auto& element : val.as_object()) @@ -106,6 +114,7 @@ boost::json::value LlsdToJson(const LLSD &val)      case LLSD::TypeMap:      {          boost::json::object& obj = result.emplace_object(); +        obj.reserve(val.size());          for (const auto& llsd_dat : llsd::inMap(val))          {              obj[llsd_dat.first] = LlsdToJson(llsd_dat.second); @@ -115,6 +124,7 @@ boost::json::value LlsdToJson(const LLSD &val)      case LLSD::TypeArray:      {          boost::json::array& json_array = result.emplace_array(); +        json_array.reserve(val.size());          for (const auto& llsd_dat : llsd::inArray(val))          {              json_array.push_back(LlsdToJson(llsd_dat)); @@ -123,7 +133,8 @@ boost::json::value LlsdToJson(const LLSD &val)      }      case LLSD::TypeBinary:      default: -        LL_ERRS("LlsdToJson") << "Unsupported conversion to JSON from LLSD type (" << val.type() << ")." << LL_ENDL; +        LL_ERRS("LlsdToJson") << "Unsupported conversion to JSON from LLSD type (" +                              << val.type() << ")." << LL_ENDL;          break;      } diff --git a/indra/llrender/llglslshader.cpp b/indra/llrender/llglslshader.cpp index a157bfee21..6ba5463acd 100644 --- a/indra/llrender/llglslshader.cpp +++ b/indra/llrender/llglslshader.cpp @@ -63,6 +63,7 @@ U64 LLGLSLShader::sTotalTimeElapsed = 0;  U32 LLGLSLShader::sTotalTrianglesDrawn = 0;  U64 LLGLSLShader::sTotalSamplesDrawn = 0;  U32 LLGLSLShader::sTotalBinds = 0; +boost::json::value LLGLSLShader::sDefaultStats;  //UI shader -- declared here so llui_libtest will link properly  LLGLSLShader    gUIProgram; @@ -101,9 +102,9 @@ void LLGLSLShader::initProfile()      sTotalSamplesDrawn = 0;      sTotalBinds = 0; -    for (std::set<LLGLSLShader*>::iterator iter = sInstances.begin(); iter != sInstances.end(); ++iter) +    for (auto ptr : sInstances)      { -        (*iter)->clearStats(); +        ptr->clearStats();      }  } @@ -117,45 +118,57 @@ struct LLGLSLShaderCompareTimeElapsed  };  //static -void LLGLSLShader::finishProfile(bool emit_report) +void LLGLSLShader::finishProfile(boost::json::value& statsv)  {      sProfileEnabled = false; -    if (emit_report) +    if (! statsv.is_null())      { -        std::vector<LLGLSLShader*> sorted; - -        for (std::set<LLGLSLShader*>::iterator iter = sInstances.begin(); iter != sInstances.end(); ++iter) -        { -            sorted.push_back(*iter); -        } - +        std::vector<LLGLSLShader*> sorted(sInstances.begin(), sInstances.end());          std::sort(sorted.begin(), sorted.end(), LLGLSLShaderCompareTimeElapsed()); +        auto& stats = statsv.as_object(); +        auto shadersit = stats.emplace("shaders", boost::json::array_kind).first; +        auto& shaders = shadersit->value().as_array();          bool unbound = false; -        for (std::vector<LLGLSLShader*>::iterator iter = sorted.begin(); iter != sorted.end(); ++iter) +        for (auto ptr : sorted)          { -            (*iter)->dumpStats(); -            if ((*iter)->mBinds == 0) +            if (ptr->mBinds == 0)              {                  unbound = true;              } +            else +            { +                auto& shaderit = shaders.emplace_back(boost::json::object_kind); +                ptr->dumpStats(shaderit.as_object()); +            }          } +        constexpr float mega = 1'000'000.f; +        float totalTimeMs = sTotalTimeElapsed / mega;          LL_INFOS() << "-----------------------------------" << LL_ENDL; -        LL_INFOS() << "Total rendering time: " << llformat("%.4f ms", sTotalTimeElapsed / 1000000.f) << LL_ENDL; -        LL_INFOS() << "Total samples drawn: " << llformat("%.4f million", sTotalSamplesDrawn / 1000000.f) << LL_ENDL; -        LL_INFOS() << "Total triangles drawn: " << llformat("%.3f million", sTotalTrianglesDrawn / 1000000.f) << LL_ENDL; +        LL_INFOS() << "Total rendering time: " << llformat("%.4f ms", totalTimeMs) << LL_ENDL; +        LL_INFOS() << "Total samples drawn: " << llformat("%.4f million", sTotalSamplesDrawn / mega) << LL_ENDL; +        LL_INFOS() << "Total triangles drawn: " << llformat("%.3f million", sTotalTrianglesDrawn / mega) << LL_ENDL;          LL_INFOS() << "-----------------------------------" << LL_ENDL; - +        auto totalsit = stats.emplace("totals", boost::json::object_kind).first; +        auto& totals = totalsit->value().as_object(); +        totals.emplace("time", totalTimeMs / 1000.0); +        totals.emplace("binds", sTotalBinds); +        totals.emplace("samples", sTotalSamplesDrawn); +        totals.emplace("triangles", sTotalTrianglesDrawn); + +        auto unusedit = stats.emplace("unused", boost::json::array_kind).first; +        auto& unused = unusedit->value().as_array();          if (unbound)          {              LL_INFOS() << "The following shaders were unused: " << LL_ENDL; -            for (std::vector<LLGLSLShader*>::iterator iter = sorted.begin(); iter != sorted.end(); ++iter) +            for (auto ptr : sorted)              { -                if ((*iter)->mBinds == 0) +                if (ptr->mBinds == 0)                  { -                    LL_INFOS() << (*iter)->mName << LL_ENDL; +                    LL_INFOS() << ptr->mName << LL_ENDL; +                    unused.emplace_back(ptr->mName);                  }              }          } @@ -170,36 +183,43 @@ void LLGLSLShader::clearStats()      mBinds = 0;  } -void LLGLSLShader::dumpStats() +void LLGLSLShader::dumpStats(boost::json::object& stats)  { -    if (mBinds > 0) +    stats.emplace("name", mName); +    auto filesit = stats.emplace("files", boost::json::array_kind).first; +    auto& files = filesit->value().as_array(); +    LL_INFOS() << "=============================================" << LL_ENDL; +    LL_INFOS() << mName << LL_ENDL; +    for (U32 i = 0; i < mShaderFiles.size(); ++i)      { -        LL_INFOS() << "=============================================" << LL_ENDL; -        LL_INFOS() << mName << LL_ENDL; -        for (U32 i = 0; i < mShaderFiles.size(); ++i) -        { -            LL_INFOS() << mShaderFiles[i].first << LL_ENDL; -        } -        LL_INFOS() << "=============================================" << LL_ENDL; +        LL_INFOS() << mShaderFiles[i].first << LL_ENDL; +        files.emplace_back(mShaderFiles[i].first); +    } +    LL_INFOS() << "=============================================" << LL_ENDL; -        F32 ms = mTimeElapsed / 1000000.f; -        F32 seconds = ms / 1000.f; +    constexpr float  mega = 1'000'000.f; +    constexpr double giga = 1'000'000'000.0; +    F32 ms = mTimeElapsed / mega; +    F32 seconds = ms / 1000.f; -        F32 pct_tris = (F32)mTrianglesDrawn / (F32)sTotalTrianglesDrawn * 100.f; -        F32 tris_sec = (F32)(mTrianglesDrawn / 1000000.0); -        tris_sec /= seconds; +    F32 pct_tris = (F32)mTrianglesDrawn / (F32)sTotalTrianglesDrawn * 100.f; +    F32 tris_sec = (F32)(mTrianglesDrawn / mega); +    tris_sec /= seconds; -        F32 pct_samples = (F32)((F64)mSamplesDrawn / (F64)sTotalSamplesDrawn) * 100.f; -        F32 samples_sec = (F32)(mSamplesDrawn / 1000000000.0); -        samples_sec /= seconds; +    F32 pct_samples = (F32)((F64)mSamplesDrawn / (F64)sTotalSamplesDrawn) * 100.f; +    F32 samples_sec = (F32)(mSamplesDrawn / giga); +    samples_sec /= seconds; -        F32 pct_binds = (F32)mBinds / (F32)sTotalBinds * 100.f; +    F32 pct_binds = (F32)mBinds / (F32)sTotalBinds * 100.f; -        LL_INFOS() << "Triangles Drawn: " << mTrianglesDrawn << " " << llformat("(%.2f pct of total, %.3f million/sec)", pct_tris, tris_sec) << LL_ENDL; -        LL_INFOS() << "Binds: " << mBinds << " " << llformat("(%.2f pct of total)", pct_binds) << LL_ENDL; -        LL_INFOS() << "SamplesDrawn: " << mSamplesDrawn << " " << llformat("(%.2f pct of total, %.3f billion/sec)", pct_samples, samples_sec) << LL_ENDL; -        LL_INFOS() << "Time Elapsed: " << mTimeElapsed << " " << llformat("(%.2f pct of total, %.5f ms)\n", (F32)((F64)mTimeElapsed / (F64)sTotalTimeElapsed) * 100.f, ms) << LL_ENDL; -    } +    LL_INFOS() << "Triangles Drawn: " << mTrianglesDrawn << " " << llformat("(%.2f pct of total, %.3f million/sec)", pct_tris, tris_sec) << LL_ENDL; +    LL_INFOS() << "Binds: " << mBinds << " " << llformat("(%.2f pct of total)", pct_binds) << LL_ENDL; +    LL_INFOS() << "SamplesDrawn: " << mSamplesDrawn << " " << llformat("(%.2f pct of total, %.3f billion/sec)", pct_samples, samples_sec) << LL_ENDL; +    LL_INFOS() << "Time Elapsed: " << mTimeElapsed << " " << llformat("(%.2f pct of total, %.5f ms)\n", (F32)((F64)mTimeElapsed / (F64)sTotalTimeElapsed) * 100.f, ms) << LL_ENDL; +    stats.emplace("time", seconds); +    stats.emplace("binds", mBinds); +    stats.emplace("samples", mSamplesDrawn); +    stats.emplace("triangles", mTrianglesDrawn);  }  //static diff --git a/indra/llrender/llglslshader.h b/indra/llrender/llglslshader.h index 27c8f0b7d0..2d669c70a9 100644 --- a/indra/llrender/llglslshader.h +++ b/indra/llrender/llglslshader.h @@ -30,6 +30,7 @@  #include "llgl.h"  #include "llrender.h"  #include "llstaticstringtable.h" +#include <boost/json.hpp>  #include <unordered_map>  class LLShaderFeatures @@ -169,14 +170,14 @@ public:      static U32 sMaxGLTFNodes;      static void initProfile(); -    static void finishProfile(bool emit_report = true); +    static void finishProfile(boost::json::value& stats=sDefaultStats);      static void startProfile();      static void stopProfile();      void unload();      void clearStats(); -    void dumpStats(); +    void dumpStats(boost::json::object& stats);      // place query objects for profiling if profiling is enabled      // if for_runtime is true, will place timer query only whether or not profiling is enabled @@ -363,6 +364,11 @@ public:  private:      void unloadInternal(); +    // This must be static because finishProfile() is called at least once +    // within a __try block. If we default its stats parameter to a temporary +    // json::value, that temporary must be destroyed when the stack is +    // unwound, which __try forbids. +    static boost::json::value sDefaultStats;  };  //UI shader (declared here so llui_libtest will link properly) diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 764e52accb..093314a9f1 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -3285,10 +3285,10 @@ LLSD LLAppViewer::getViewerInfo() const          LLVector3d pos = gAgent.getPositionGlobal();          info["POSITION"] = ll_sd_from_vector3d(pos);          info["POSITION_LOCAL"] = ll_sd_from_vector3(gAgent.getPosAgentFromGlobal(pos)); -        info["REGION"] = gAgent.getRegion()->getName(); +        info["REGION"] = region->getName();          boost::regex regex("\\.(secondlife|lindenlab)\\..*"); -        info["HOSTNAME"] = boost::regex_replace(gAgent.getRegion()->getSimHostName(), regex, ""); +        info["HOSTNAME"] = boost::regex_replace(region->getSimHostName(), regex, "");          info["SERVER_VERSION"] = gLastVersionChannel;          LLSLURL slurl;          LLAgentUI::buildSLURL(slurl); diff --git a/indra/newview/llfeaturemanager.cpp b/indra/newview/llfeaturemanager.cpp index 3259ea249b..b5d8f70c2e 100644 --- a/indra/newview/llfeaturemanager.cpp +++ b/indra/newview/llfeaturemanager.cpp @@ -393,7 +393,7 @@ F32 logExceptionBenchmark()      __except (msc_exception_filter(GetExceptionCode(), GetExceptionInformation()))      {          // HACK - ensure that profiling is disabled -        LLGLSLShader::finishProfile(false); +        LLGLSLShader::finishProfile();          // convert to C++ styled exception          char integer_string[32]; diff --git a/indra/newview/llglsandbox.cpp b/indra/newview/llglsandbox.cpp index af025d5879..112008172e 100644 --- a/indra/newview/llglsandbox.cpp +++ b/indra/newview/llglsandbox.cpp @@ -838,7 +838,7 @@ struct ShaderProfileHelper      }      ~ShaderProfileHelper()      { -        LLGLSLShader::finishProfile(false); +        LLGLSLShader::finishProfile();      }  }; diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 301ea5c5f6..f722d0bd1d 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -28,58 +28,70 @@  #include "llviewerdisplay.h" -#include "llgl.h" -#include "llrender.h" -#include "llglheaders.h" -#include "llgltfmateriallist.h" +#include "fsyspath.h" +#include "hexdump.h"  #include "llagent.h"  #include "llagentcamera.h" -#include "llviewercontrol.h" +#include "llappviewer.h"  #include "llcoord.h"  #include "llcriticaldamp.h" +#include "llcubemap.h"  #include "lldir.h" -#include "lldynamictexture.h"  #include "lldrawpoolalpha.h" +#include "lldrawpoolbump.h" +#include "lldrawpoolwater.h" +#include "lldynamictexture.h" +#include "llenvironment.h" +#include "llfasttimer.h"  #include "llfeaturemanager.h" -//#include "llfirstuse.h" +#include "llfloatertools.h" +#include "llfocusmgr.h" +#include "llgl.h" +#include "llglheaders.h" +#include "llgltfmateriallist.h"  #include "llhudmanager.h"  #include "llimagepng.h" +#include "llmachineid.h"  #include "llmemory.h" +#include "llparcel.h" +#include "llperfstats.h" +#include "llpostprocess.h" +#include "llrender.h" +#include "llscenemonitor.h" +#include "llsdjson.h"  #include "llselectmgr.h"  #include "llsky.h" +#include "llspatialpartition.h"  #include "llstartup.h" +#include "llstartup.h" +#include "lltooldraganddrop.h"  #include "lltoolfocus.h"  #include "lltoolmgr.h" -#include "lltooldraganddrop.h"  #include "lltoolpie.h"  #include "lltracker.h"  #include "lltrans.h"  #include "llui.h" +#include "lluuid.h" +#include "llversioninfo.h"  #include "llviewercamera.h" +#include "llviewercontrol.h" +#include "llviewernetwork.h"  #include "llviewerobjectlist.h"  #include "llviewerparcelmgr.h" +#include "llviewerregion.h" +#include "llviewershadermgr.h" +#include "llviewertexturelist.h"  #include "llviewerwindow.h"  #include "llvoavatarself.h"  #include "llvograss.h"  #include "llworld.h"  #include "pipeline.h" -#include "llspatialpartition.h" -#include "llappviewer.h" -#include "llstartup.h" -#include "llviewershadermgr.h" -#include "llfasttimer.h" -#include "llfloatertools.h" -#include "llviewertexturelist.h" -#include "llfocusmgr.h" -#include "llcubemap.h" -#include "llviewerregion.h" -#include "lldrawpoolwater.h" -#include "lldrawpoolbump.h" -#include "llpostprocess.h" -#include "llscenemonitor.h" -#include "llenvironment.h" -#include "llperfstats.h" +#include <boost/json.hpp> + +#include <filesystem> +#include <iomanip> +#include <sstream>  #include <glm/glm.hpp>  #include <glm/gtc/matrix_transform.hpp> @@ -127,6 +139,9 @@ void render_ui_3d();  void render_ui_2d();  void render_disconnected_background(); +void getProfileStatsContext(boost::json::object& stats); +std::string getProfileStatsFilename(); +  void display_startup()  {      if (   !gViewerWindow @@ -1027,8 +1042,87 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot)      if (gShaderProfileFrame)      {          gShaderProfileFrame = false; -        LLGLSLShader::finishProfile(); +        boost::json::value stats{ boost::json::object_kind }; +        getProfileStatsContext(stats.as_object()); +        LLGLSLShader::finishProfile(stats); + +        auto report_name = getProfileStatsFilename(); +        std::ofstream outf(report_name); +        if (! outf) +        { +            LL_WARNS() << "Couldn't write to " << std::quoted(report_name) << LL_ENDL; +        } +        else +        { +            outf << stats; +            LL_INFOS() << "(also dumped to " << std::quoted(report_name) << ")" << LL_ENDL; +        } +    } +} + +void getProfileStatsContext(boost::json::object& stats) +{ +    // populate the context with info from LLFloaterAbout +    auto contextit = stats.emplace("context", +                                   LlsdToJson(LLAppViewer::instance()->getViewerInfo())).first; +    auto& context = contextit->value().as_object(); + +    // then add a few more things +    unsigned char unique_id[MAC_ADDRESS_BYTES]{}; +    LLMachineID::getUniqueID(unique_id, sizeof(unique_id)); +    context.emplace("machine", stringize(LL::hexdump(unique_id, sizeof(unique_id)))); +    context.emplace("grid", LLGridManager::instance().getGrid()); +    LLViewerRegion* region = gAgent.getRegion(); +    if (region) +    { +        context.emplace("regionid", stringize(region->getRegionID())); +    } +    LLParcel* parcel = LLViewerParcelMgr::instance().getAgentParcel(); +    if (parcel) +    { +        context.emplace("parcel", parcel->getName()); +        context.emplace("parcelid", parcel->getLocalID());      } +    context.emplace("time", LLDate::now().toHTTPDateString("%Y-%m-%dT%H:%M:%S")); +} + +std::string getProfileStatsFilename() +{ +    std::ostringstream basebuff; +    // viewer build +    basebuff << "profile.v" << LLVersionInfo::instance().getBuild(); +    // machine ID: zero-initialize unique_id in case LLMachineID fails +    unsigned char unique_id[MAC_ADDRESS_BYTES]{}; +    LLMachineID::getUniqueID(unique_id, sizeof(unique_id)); +    basebuff << ".m" << LL::hexdump(unique_id, sizeof(unique_id)); +    // region ID +    LLViewerRegion *region = gAgent.getRegion(); +    basebuff << ".r" << (region? region->getRegionID() : LLUUID()); +    // local parcel ID +    LLParcel* parcel = LLViewerParcelMgr::instance().getAgentParcel(); +    basebuff << ".p" << (parcel? parcel->getLocalID() : 0); +    // date/time -- omit seconds for now +    auto now = LLDate::now(); +    basebuff << ".t" << LLDate::now().toHTTPDateString("%Y-%m-%dT%H-%M-"); +    // put this candidate file in our logs directory +    auto base = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, basebuff.str()); +    S32 sec; +    now.split(nullptr, nullptr, nullptr, nullptr, nullptr, &sec); +    // Loop over finished filename, incrementing sec until we find one that +    // doesn't yet exist. Should rarely loop (only if successive calls within +    // same second), may produce (e.g.) sec==61, but avoids collisions and +    // preserves chronological filename sort order. +    std::string name; +    std::error_code ec; +    do +    { +        // base + missing 2-digit seconds, append ".json" +        // post-increment sec in case we have to try again +        name = stringize(base, std::setw(2), std::setfill('0'), sec++, ".json"); +    } while (std::filesystem::exists(fsyspath(name), ec)); +    // Ignoring ec means we might potentially return a name that does already +    // exist -- but if we can't check its existence, what more can we do? +    return name;  }  // WIP simplified copy of display() that does minimal work diff --git a/scripts/perf/logsdir.py b/scripts/perf/logsdir.py new file mode 100644 index 0000000000..5ab45a28b6 --- /dev/null +++ b/scripts/perf/logsdir.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""\ +@file   logsdir.py +@author Nat Goodspeed +@date   2024-09-12 +@brief  Locate the Second Life logs directory for the current user on the +        current platform. + +$LicenseInfo:firstyear=2024&license=viewerlgpl$ +Copyright (c) 2024, Linden Research, Inc. +$/LicenseInfo$ +""" + +import os +from pathlib import Path +import platform + +class Error(Exception): +    pass + +# logic used by SLVersionChecker +def logsdir(): +    app = 'SecondLife' +    system = platform.system() +    if (system == 'Darwin'): +        base_dir = os.path.join(os.path.expanduser('~'), +                                'Library','Application Support',app) +    elif (system == 'Linux'): +        base_dir = os.path.join(os.path.expanduser('~'), +                                '.' + app.lower()) +    elif (system == 'Windows'): +        appdata = os.getenv('APPDATA') +        base_dir = os.path.join(appdata, app) +    else: +        raise ValueError("Unsupported platform '%s'" % system) + +    return os.path.join(base_dir, 'logs') + +def latest_file(dirpath, pattern): +    files = Path(dirpath).glob(pattern) +    sort = [(p.stat().st_mtime, p) for p in files if p.is_file()] +    sort.sort(reverse=True) +    try: +        return sort[0][1] +    except IndexError: +        raise Error(f'No {pattern} files in {dirpath}') diff --git a/scripts/perf/profile_cmp.py b/scripts/perf/profile_cmp.py new file mode 100644 index 0000000000..9dbfa3145b --- /dev/null +++ b/scripts/perf/profile_cmp.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""\ +@file   profile_cmp.py +@author Nat Goodspeed +@date   2024-09-13 +@brief  Compare a frame profile stats file with a similar baseline file. + +$LicenseInfo:firstyear=2024&license=viewerlgpl$ +Copyright (c) 2024, Linden Research, Inc. +$/LicenseInfo$ +""" + +from datetime import datetime +import json +from logsdir import Error, latest_file, logsdir +from pathlib import Path +import sys + +# variance that's ignorable +DEFAULT_EPSILON = 0.03          # 3% + +def compare(baseline, test, epsilon=DEFAULT_EPSILON): +    if Path(baseline).samefile(test): +        print(f'{baseline} same as\n{test}\nAnalysis moot.') +        return + +    with open(baseline) as inf: +        bdata = json.load(inf) +    with open(test) as inf: +        tdata = json.load(inf) +    print(f'baseline {baseline}\ntestfile {test}') + +    for k, tv in tdata['context'].items(): +        bv = bdata['context'].get(k) +        if bv != tv: +            print(f'baseline {k}={bv} vs.\ntestfile {k}={tv}') + +    btime = bdata['context'].get('time') +    ttime = tdata['context'].get('time') +    if btime and ttime: +        print('testfile newer by', +              datetime.fromisoformat(ttime) - datetime.fromisoformat(btime)) + +    # The following ignores totals and unused shaders, except to the extent +    # that some shaders were used in the baseline but not in the recent test +    # or vice-versa. While the viewer considers that a shader has been used if +    # 'binds' is nonzero, we exclude any whose 'time' is zero to avoid zero +    # division. +    bshaders = {s['name']: s for s in bdata['shaders'] if s['time'] and s['samples']} +    tshaders = {s['name']: s for s in tdata['shaders'] if s['time']} + +    bothshaders = set(bshaders).intersection(tshaders) +    deltas = [] +    for shader in bothshaders: +        bshader = bshaders[shader] +        tshader = tshaders[shader] +        bthruput = bshader['samples']/bshader['time'] +        tthruput = tshader['samples']/tshader['time'] +        delta = (tthruput - bthruput)/bthruput +        if abs(delta) > epsilon: +            deltas.append((delta, shader, bthruput, tthruput)) + +    # descending order of performance gain +    deltas.sort(reverse=True) +    print(f'{len(deltas)} shaders showed nontrivial performance differences ' +          '(millon samples/sec):') +    namelen = max(len(s[1]) for s in deltas) if deltas else 0 +    for delta, shader, bthruput, tthruput in deltas: +        print(f'  {shader.rjust(namelen)} {delta*100:6.1f}% ' +              f'{bthruput/1000000:8.2f} -> {tthruput/1000000:8.2f}') + +    tunused = set(bshaders).difference(tshaders) +    print(f'{len(tunused)} baseline shaders not used in test:') +    for s in tunused: +        print(f'  {s}') +    bunused = set(tshaders).difference(bshaders) +    print(f'{len(bunused)} shaders newly used in test:') +    for s in bunused: +        print(f'  {s}') + +def main(*raw_args): +    from argparse import ArgumentParser +    parser = ArgumentParser(description=""" +%(prog)s compares a baseline JSON file from Develop -> Render Tests -> Frame +Profile to another such file from a more recent test. It identifies shaders +that have gained and lost in throughput. +""") +    parser.add_argument('-e', '--epsilon', type=float, default=int(DEFAULT_EPSILON*100), +                        help="""percent variance considered ignorable (default %(default)s%%)""") +    parser.add_argument('baseline', +                        help="""baseline profile filename to compare against""") +    parser.add_argument('test', nargs='?', +                        help="""test profile filename to compare +                        (default is most recent)""") +    args = parser.parse_args(raw_args) +    compare(args.baseline, +            args.test or latest_file(logsdir(), 'profile.*.json'), +            epsilon=(args.epsilon / 100.)) + +if __name__ == "__main__": +    try: +        sys.exit(main(*sys.argv[1:])) +    except (Error, OSError, json.JSONDecodeError) as err: +        sys.exit(str(err)) diff --git a/scripts/perf/profile_csv.py b/scripts/perf/profile_csv.py new file mode 100644 index 0000000000..7a6b2b338e --- /dev/null +++ b/scripts/perf/profile_csv.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""\ +@file   profile_csv.py +@author Nat Goodspeed +@date   2024-09-12 +@brief  Convert a JSON file from Develop -> Render Tests -> Frame Profile to CSV + +$LicenseInfo:firstyear=2024&license=viewerlgpl$ +Copyright (c) 2024, Linden Research, Inc. +$/LicenseInfo$ +""" + +import json +from logsdir import Error, latest_file, logsdir +import sys + +def convert(path, totals=True, unused=True, file=sys.stdout): +    with open(path) as inf: +        data = json.load(inf) +    # print path to sys.stderr in case user is redirecting stdout +    print(path, file=sys.stderr) + +    print('"name", "file1", "file2", "time", "binds", "samples", "triangles"', file=file) + +    if totals: +        t = data['totals'] +        print(f'"totals", "", "", {t["time"]}, {t["binds"]}, {t["samples"]}, {t["triangles"]}', +              file=file) + +    for sh in data['shaders']: +        print(f'"{sh["name"]}", "{sh["files"][0]}", "{sh["files"][1]}", ' +              f'{sh["time"]}, {sh["binds"]}, {sh["samples"]}, {sh["triangles"]}', file=file) + +    if unused: +        for u in data['unused']: +            print(f'"{u}", "", "", 0, 0, 0, 0', file=file) + +def main(*raw_args): +    from argparse import ArgumentParser +    parser = ArgumentParser(description=""" +%(prog)s converts a JSON file from Develop -> Render Tests -> Frame Profile to +a more-or-less equivalent CSV file. It expands the totals stats and unused +shaders list to full shaders lines. +""") +    parser.add_argument('-t', '--totals', action='store_false', default=True, +                        help="""omit totals from CSV file""") +    parser.add_argument('-u', '--unused', action='store_false', default=True, +                        help="""omit unused shaders from CSV file""") +    parser.add_argument('path', nargs='?', +                        help="""profile filename to convert (default is most recent)""") + +    args = parser.parse_args(raw_args) +    convert(args.path or latest_file(logsdir(), 'profile.*.json'), +            totals=args.totals, unused=args.unused) + +if __name__ == "__main__": +    try: +        sys.exit(main(*sys.argv[1:])) +    except (Error, OSError, json.JSONDecodeError) as err: +        sys.exit(str(err)) diff --git a/scripts/perf/profile_pretty.py b/scripts/perf/profile_pretty.py new file mode 100644 index 0000000000..405b14b373 --- /dev/null +++ b/scripts/perf/profile_pretty.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""\ +@file   profile_pretty.py +@author Nat Goodspeed +@date   2024-09-12 +@brief  Pretty-print a JSON file from Develop -> Render Tests -> Frame Profile + +$LicenseInfo:firstyear=2024&license=viewerlgpl$ +Copyright (c) 2024, Linden Research, Inc. +$/LicenseInfo$ +""" + +import json +from logsdir import Error, latest_file, logsdir +import sys + +def pretty(path): +    with open(path) as inf: +        data = json.load(inf) +    # print path to sys.stderr in case user is redirecting stdout +    print(path, file=sys.stderr) +    json.dump(data, sys.stdout, indent=4) + +def main(*raw_args): +    from argparse import ArgumentParser +    parser = ArgumentParser(description=""" +%(prog)s pretty-prints a JSON file from Develop -> Render Tests -> Frame Profile. +The file produced by the viewer is a single dense line of JSON. +""") +    parser.add_argument('path', nargs='?', +                        help="""profile filename to pretty-print (default is most recent)""") + +    args = parser.parse_args(raw_args) +    pretty(args.path or latest_file(logsdir(), 'profile.*.json')) + +if __name__ == "__main__": +    try: +        sys.exit(main(*sys.argv[1:])) +    except (Error, OSError, json.JSONDecodeError) as err: +        sys.exit(str(err)) | 
