游戏引擎从零开始(32)-性能数据可视化

前言

上一篇实现了实时的看性能的效果,但是只能看单帧的数据,实际工程中我们需要观察一整段的性能。

实现的思路是,记录一段性能数据,存储成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...

总结

  1. 学习tracing工具的使用,能按照tracing格式拼接数据。

  2. 学习一个宏定义的细节,"##"和"LINE"混用时不能准确展开,通过多层嵌套来解决。

相关推荐
编程之路,妙趣横生19 分钟前
list模拟实现
c++
一只小bit2 小时前
数据结构之栈,队列,树
c语言·开发语言·数据结构·c++
la_vie_est_belle3 小时前
《Cocos Creator游戏实战》非固定摇杆实现原理
游戏·cocos creator·游戏开发·cocos·非固定摇杆
Thomas_YXQ3 小时前
Unity3D Huatuo技术原理剖析详解
unity·unity3d·游戏开发·性能调优·热更新
沐泽Mu4 小时前
嵌入式学习-QT-Day05
开发语言·c++·qt·学习
szuzhan.gy4 小时前
DS查找—二叉树平衡因子
数据结构·c++·算法
火云洞红孩儿5 小时前
基于AI IDE 打造快速化的游戏LUA脚本的生成系统
c++·人工智能·inscode·游戏引擎·lua·游戏开发·脚本系统
FeboReigns6 小时前
C++简明教程(4)(Hello World)
c语言·c++
FeboReigns6 小时前
C++简明教程(10)(初识类)
c语言·开发语言·c++
zh路西法6 小时前
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(二):从FSM开始的2D游戏角色操控底层源码编写
c++·游戏·unity·设计模式·状态模式