前言
上一篇实现了实时的看性能的效果,但是只能看单帧的数据,实际工程中我们需要观察一整段的性能。
实现的思路是,记录一段性能数据,存储成json格式,然后借助第三方工具可视化出来。
tracing/perfetto工具
tracing是google开发的chrome浏览器插件
浏览器输入chrome://tracing 就能打开,默认长这样:
tracing更新了新版本为:
ui.perfetto.dev/
本章节,我们就借助perfetto,实现简单的性能可视化。
性能统计工具(Instrumentor)
需要实现会话(session)、函数(function)、代码块(block)三个级别的统计。
Sandbox/Hazel/src/Hazel/Debug/Instrumentor.h
准备基础的数据结构,将ProfileResult挪过来
c++
namespace Hazel {
struct ProfileResult
{
std::string Name;
long long Start, End;
uint32_t ThreadID;
};
struct InstrumentationSession
{
std::string Name;
};
}
实现Instrumentor,简单起见设计成单例,实际上单例不一定合理,因为可能会有多个session同时存在于不同的线程。
看下面代码,是一个最简单的饿汉单例。设计了三组统计,分别实现了Session、Function、代码块block级别的统计。
c++
class Instrumentor
{
private:
InstrumentationSession* m_CurrentSession;
std::ofstream m_OutputStream;
int m_ProfileCount;
public:
Instrumentor() : m_CurrentSession(nullptr), m_ProfileCount(0)
{
}
void WriteHeader()
{
m_OutputStream << R"({"otherData":{}, "traceEvents":[ )";
m_OutputStream.flush();
}
void WriteFooter()
{
m_OutputStream << "]}";
m_OutputStream.flush();
}
void BeginSession(const std::string& name, const std::string& filepath = "results.json")
{
HZ_CORE_INFO("BeginSession----file = {0}", filepath);
m_OutputStream.open(filepath);
WriteHeader();
m_CurrentSession = new InstrumentationSession{name};
}
void EndSession()
{
HZ_CORE_INFO("EndSession----");
WriteFooter();
m_OutputStream.close();
delete m_CurrentSession;
m_CurrentSession = nullptr;
m_ProfileCount = 0;
}
void WriteProfile(const ProfileResult& result)
{
if (m_ProfileCount++ > 0) {
m_OutputStream << ",";
}
std::string name = result.Name;
std::replace(name.begin(), name.end(), '\"', '\'');
m_OutputStream << "{";
m_OutputStream << R"("cat":"function",)";
m_OutputStream << R"("dur":)" << (result.End - result.Start) << ',';
m_OutputStream << R"("name":")" << name << "\",";
m_OutputStream << R"("ph":"X",)";
m_OutputStream << R"("pid":0,)";
m_OutputStream << R"("tid":)" << result.ThreadID << ",";
m_OutputStream << R"("ts":)" << result.Start;
m_OutputStream << "}";
m_OutputStream.flush();
}
static Instrumentor& Get()
{
static Instrumentor instance;
return instance;
}
};
对Instrumentor的调用封装在InstrumentationTimer中,析构函数中调用Instrumentor的Stop(),记录耗时
c++
class InstrumentationTimer
{
public:
InstrumentationTimer(const char* name) : m_Name(name), m_Stopped(false)
{
m_StartTimePoint = std::chrono::high_resolution_clock::now();
}
~InstrumentationTimer()
{
if(!m_Stopped){
Stop();
}
}
void Stop()
{
auto endTimePoint = std::chrono::high_resolution_clock::now();
long long start = std::chrono::time_point_cast<std::chrono::microseconds>(m_StartTimePoint).time_since_epoch().count();
long long end = std::chrono::time_point_cast<std::chrono::microseconds>(endTimePoint).time_since_epoch().count();
uint32_t threadID = std::hash<std::thread::id>()(std::this_thread::get_id());
Instrumentor::Get().WriteProfile({m_Name, start, end, threadID});
m_Stopped = true;
}
private:
const char* m_Name;
std::chrono::time_point<std::chrono::high_resolution_clock> m_StartTimePoint;
bool m_Stopped;
};
定义一组宏,以简化代码,这里用行号区别每一个block统计,因为编译器对"##"的解析有点问题,用嵌套的宏定义来连接行号。
c++
#define HZ_CONCATENATE_IMPL(x, y) x##y
#define HZ_CONCATENATE(x, y) HZ_CONCATENATE_IMPL(x, y)
#define HZ_PROFILE 1
#if HZ_PROFILE
#define HZ_PROFILE_BEGIN_SESSION(name, filepath) ::Hazel::Instrumentor::Get().BeginSession(name, filepath)
#define HZ_PROFILE_END_SESSION() ::Hazel::Instrumentor::Get().EndSession()
#define HZ_PROFILE_SCOPE(name) ::Hazel::InstrumentationTimer HZ_CONCATENATE(timer, __LINE__) (name)
#define HZ_PROFILE_FUNCTION() HZ_PROFILE_SCOPE(__PRETTY_FUNCTION__)
#else
#define HZ_PROFILE_BEGIN_SESSION(name, filepath)
#define HZ_PROFILE_END_SESSION()
#define HZ_PROFILE_SCOPE(name)
#define HZ_PROFILE_FUNCTION()
#endif
行号的问题,stackflow上有一个合理的解释:
The problem is that when you have a macro replacement, the preprocessor will only expand the macros recursively if neither the stringizing operator # nor the token-pasting operator ## are applied to it. So, you have to use some extra layers of indirection, you can use the token-pasting operator with a recursively expanded argument。
翻译:只有在没有#、##时,才会递归的展开宏定义
原贴链接:
stackoverflow.com/questions/1...
更新Sandbox2D统计
基于Instrumentor,重新实现一遍性能统计。
应用入口增加3个session统计,记录在不同的文件中。
Sandbox/Hazel/src/Hazel/Core/EntryPoint.h
c++
int main(int argc, char** argv) {
Hazel::Log::Init();
HZ_PROFILE_BEGIN_SESSION("Startup", "HazelProfile-Startup.json");
auto app = Hazel::CreateApplication();
HZ_PROFILE_END_SESSION();
HZ_PROFILE_BEGIN_SESSION("Runtime", "HazelProfile-Runtime.json");
app->Run();
HZ_PROFILE_END_SESSION();
HZ_PROFILE_BEGIN_SESSION("Startup", "HazelProfile-Shutdown.json");
delete app;
HZ_PROFILE_END_SESSION();
}
更新Sandbox2D中的耗时统计
Sandbox/src/Sandbox2D.cpp
c++
void Sandbox2D::OnUpdate(Hazel::Timestep ts) {
HZ_PROFILE_FUNCTION();
// Update
{
HZ_PROFILE_SCOPE("Sandbox2D::OnUpdate");
m_CameraController.OnUpdate(ts);
}
// Render
{
HZ_PROFILE_SCOPE("Renderer Prep");
Hazel::RenderCommand::SetClearColor({0.1f, 0.1f, 0.1f, 1.0});
Hazel::RenderCommand::Clear();
}
{
HZ_PROFILE_SCOPE("Renderer Draw");
Hazel::Renderer2D::BeginScene(m_CameraController.GetCamera());
Hazel::Renderer2D::DrawQuad({-1.0f, 0.0f}, {0.8f, 0.8f}, {0.8f, 0.2f, 0.3f, 1.0f});
Hazel::Renderer2D::DrawQuad({0.5f, -0.5f}, {0.5f, 0.5f}, {0.2f, 0.3f, 0.8f, 1.0f});
Hazel::Renderer2D::DrawQuad({0.0f, 0.0f, -0.1f}, {5.f, 5.f}, m_CheckerboardTexture);
Hazel::Renderer2D::EndScene();
}
}
运行正常的话,能看到生成三个json文件
- HazelProfile-Runtime.json
- HazelProfile-Shutdown.json
- HazelProfile-Startup.json
将生成的数据文件,拖到tracing/perfetto工具中,进行可视化分析
关于tracing数据格式参考:
blog.csdn.net/zgcjaxj/art... "chrome://tracing
docs.google.com/document/d/...
全工程加上统计
在整个工程中加上性能统计,都是重复的代码,不一一说明了。修改参考:
github.com/summer-go/H...
整个工程的核心地方都加上统计后,数据就比较丰富了:
代码 & 总结
本次代码修改参考:
性能统计工具:
github.com/summer-go/H...
全工程加上性能统计:
github.com/summer-go/H...
总结
-
学习tracing工具的使用,能按照tracing格式拼接数据。
-
学习一个宏定义的细节,"##"和"LINE"混用时不能准确展开,通过多层嵌套来解决。