Merge pull request #2610 from secondlife/frame-profile-json
Make Develop->Render Tests->Frame Profile dump JSON to a file too.master
commit
a7d5d4cf84
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) */
|
||||
|
|
@ -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) */
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
F32 ms = mTimeElapsed / 1000000.f;
|
||||
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_samples = (F32)((F64)mSamplesDrawn / (F64)sTotalSamplesDrawn) * 100.f;
|
||||
F32 samples_sec = (F32)(mSamplesDrawn / 1000000000.0);
|
||||
samples_sec /= seconds;
|
||||
|
||||
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() << mShaderFiles[i].first << LL_ENDL;
|
||||
files.emplace_back(mShaderFiles[i].first);
|
||||
}
|
||||
LL_INFOS() << "=============================================" << LL_ENDL;
|
||||
|
||||
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 / mega);
|
||||
tris_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;
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -838,7 +838,7 @@ struct ShaderProfileHelper
|
|||
}
|
||||
~ShaderProfileHelper()
|
||||
{
|
||||
LLGLSLShader::finishProfile(false);
|
||||
LLGLSLShader::finishProfile();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,10 +1042,89 @@ 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
|
||||
void display_cube_face()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}')
|
||||
|
|
@ -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))
|
||||
|
|
@ -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))
|
||||
|
|
@ -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))
|
||||
Loading…
Reference in New Issue