c++可视化性能测试

阅读前注意

本文所有代码贴出来的目的是帮助大家理解,并非是要引导大家跟写,许多环境问题文件问题没有详细说明,代码也并不全面,达不到跟做的效果。建议直接阅读全文即可,我在最后会给出详细代码地址,对源代码细节更感兴趣的同学可以下载参考。

性能测试:使用日志

在c++中进行性能测试是令人头疼的问题,我们往往需要在数以千计的log中分析出性能瓶颈————找出最耗时的部分。而这部分工作是极其枯燥的:

首先,我们需要准备好一个计算时间的工具类,好在我们拥有std::chrono,有了它我们就可计算出过程经历的时间。聪明的你或许会搞出这样一个东西:

//时间计量工具最简单的样子 class TimeTool { public:     //desp 表示输出的日志 日志字符串中可能会用一些文本替换的方式输出时间     //例如 $ST 表示开始时间  $ET 表示结束时间 %DT 表示他们的差     //它很可能是这样的 “xxx cost time $DT, st = %ST  et = $ET”     TimeTool(const std::string& desp);     //在析构时自动输出日志     ~TimeTool(); } 

哦!我觉得他已经足够好了,或许还可以改进,不过现在它能够完成最基本的任务了!

完了吗?当然没有,还有更多的工作要做,接下来最重要的是……

我们不得不在我们富有美感的代码中插入这些令人糟心的“探针”,说不定还会加上一连串的{},让本来漂亮的代码变得层层深入,令人头大不已!

我手头正好有一份代码:

void saveTheWorld() {     Hero h = makeHero("smalldy");     WorldList& wlist = findBadWorld();     World target;     int rank = 0;     for(auto & w : wlist) {         if(w.rank() > rank) {             target = w;             rank = w.rank();         }      }      hero.save(target); } 

哇,很好的故事不是吗?(并不,你只关心性能测试,却没发现英雄已经挂了!)

现在,我们要对此代码片段进行性能测试:

void saveTheWorld() {     TimeTool save_function_cost("函数saveTheWorld耗时 $DT");          {         TimeTool make_hero_cost("makeHero耗时 $DT");         Hero h = makeHero("smalldy");     }     {         TimeTool find_world("findBadWorld耗时 $DT");         WorldList& wlist = findBadWorld();     }     World target;     int rank = 0;     {         TimeTool find_rank("查询最危险的世界耗时 $DT");         for(auto & w : wlist) {             if(w.rank() > rank) {                 target = w;                 rank = w.rank();             }          }     }     {         TimeTool hero_save("英雄耗时 $DT");         hero.save(target);     } } 

天哪!这简直糟糕透了!它甚至不能正确的运行,因为局部变量将在作用域结束后销毁,英雄还没上场,就已经魂归高天了。或许我们可以对TimeTool类加以改动,让他提供主动的计时结束函数,这样,我们就可以去掉该死的{},然后手动设置开始点和结束点了,当然,这样的话,就要书写更多的“探针”代码了。

好吧,假设我们已经完成了这样工作,我想聪明的你一定不想让我再贴一遍这些无意义的代码了,你一定能想象到新的时间工具会长成什么样子了。我们把它跑起来,就会得到一小串日志啦!

TimeTool make_hero_cost("makeHero耗时 200ms"); TimeTool find_world("findBadWorld耗时 200ms"); TimeTool find_rank("查询最危险的世界耗时 100ms"); TimeTool hero_save("英雄耗时 1500ms"); 函数saveTheWorld耗时 2000ms 

我们清楚的看到性能瓶颈所——这个英雄似乎不太给力,他居然耗费了1500ms!你在干什么!Hero!

当然,在这个例子中,我无法再继续深究下去,毕竟我也不知道英雄如何更加快速的拯救世界,优化也就无从谈起了,但是从这个糟糕的例子中,我们至少知道了通过日志记录可以帮助我们进行性能测试,从而观察到哪些步骤耗费了更多的时间。

实际情况可要比这个复杂多了,我是说,这种级别的性能测试,完全不能解决实际的需求,在真实的项目环境下,程序输出的日志可能有成千上万条,你几乎不能再实际运行的过程中去认真阅读日志的时间戳,而在log文件中,寻找你需要的条目——怎么说呢,这个挑战对我来说是十分不愉快的。我完全不想在我一天的工作中,插入这样的流程,这太折磨人了,更别提并发环境下的日志了,你甚至不能确定他们的顺序!

可视化可太烦啦!

可视化是个不错的点子,我喜欢可视化,尤其是在文本让我眼花缭乱的情况下,可视化更加让我感到亲切,比起从该死的日志中扣出我想要的条目,如果有一张图表展现在我的面前,那就更好不过了!

什么?开发一个可视化工具?

啊,这个目标着实有些大,我还要分析日志吗?分析得到的数据该如何呈现呐?c++好做可视化的东西吗?靠!?难不成还要上正则表达式吗?

可恶!不想干啦!

全文完

Google Chrome Tracing!

全文还没完!世界还没毁灭呢!

是的!你想到的东西大部分都会有现成的实现,如果你有谷歌浏览器的话,你可以尝试在地址栏输入以下地址:

chrome://tracing

img

此网页可接受一个Json文件,然后根据Json文件的内容,生成图表,我这里有一份从网上拷贝Json示例,你可以将其保存在.json文件中,然后点击网页上的Load按钮,选择你的文件。

 [     {"name": "休息", "cat": "测试", "ph": "X", "ts": 0, "pid": 0, "tid": 1, "dur": 28800000000, "args": {"duration_hour": 8, "start_hour": 0}},       {"name": "学习", "cat": "测试", "ph": "X", "ts": 28800000000, "pid": 0, "tid": 1, "dur":3600000000 , "args": {"duration_hour": 1, "start_hour": 8}},       {"name": "休息", "cat": "测试", "ph": "X", "ts": 0, "pid": 0, "tid": 2, "dur": 21600000000} ,       {"name": "process_name", "ph": "M", "pid": 0, "args": {"name": "一周时间管理"}},     {"name": "thread_name", "ph": "M", "pid": 0, "tid": 1, "args": {"name": "第一天"}},     {"name": "thread_name", "ph": "M", "pid": 0, "tid": 2, "args": {"name": "第二天"}}  ] 

不方便测试的同学也没关系,结果是这样的:
img

点击对应的条目,下方还会出现json中一些字段的数据,这些我不再进行展示。

回到正题,如果我们性能测试的结果以这种方式进行展示的话,那可就清晰多了!它足够简单,也足够清晰了,甚至不用我写一行关于可视化的代码,简直是我的完美选择。唯一的不足点是,它非常依赖谷歌浏览器,而且还要手动的选择json文件,这让我非常不爽。

幸运的是,已经有大佬将核心网页代码提取出来了!我无法确定我阅读的文章是否为原创,因此,只能按照名称搜索,从若干网站中选出了一个我认为是原作者的网址:

https://2010-2021.limboy.me/2020/03/21/chrome-trace-viewer/

(CSDN盗版文章太多了!)

在这篇文章中,作者给出了一个html文件,并让其可以在线使用,按作者的说法来讲

通过 chrome://tracing 的方式来使用 Tracer Viewer 还是不太方便,也不利于传播,Google 虽然在 catapult 里提供了 trace2html,但包含的文件很多,使用起来还是有点麻烦,于是参考了 go trace 的源码,把相关文件上传到了 CDN,然后在一个 html 文件里引用,这样只需一个文件即可。

题外话,具体的html文件我不在这里贴了,有点长,而且我也不会原封不动的使用,所以贴上来没有什么意义,感兴趣的同学可以访问下作者的文章网址,也算是给正版引流(如果有的话)了罢。

不得不说,作者的想法非常好,不过我认为,使用CDN什么还是有点大费周章了,并且我也并不熟悉这个领域,因此我将采用其它办法。

基于chrome tracing的可视化方案

我的方案是:

  1. 提供一种方法,可插入过程开始点,插入过程结束点,保存json文件,用于进行性能测试并生成结果。
  2. 提供一个加载程序,该程序可以临时搭建一个网页服务端,加载程序读取json文件,并自动打开浏览器访问服务网址,从而呈现出结果。

方案确定,开始实施!

Tracing Tool

首先是目标1,提供一种方法,可插入过程开始点,插入过程结束点,保存json文件,用于进行性能测试并生成结果。

在具体实施之前,我们有必要了解下tracing json的格式,一个 tracing json文件内可包含甚多‘事件’,‘事件’的种类很多,不同的事件最终可视化的显示效果也不近相同,我们的性能测试场景只需要给出一段段过程的可视化显示,所以用到的事件并不多。

关于其他未使用到的时间,感兴趣的同学可以访问网站:https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit 地址在墙外。

我们用一个事件表示一个过程的开始,一个事件表示过程的结束,有开始和结束就能描述所有测试点了。

我们需要使用的事件在上边的例子中并没有出现,在这里我详细介绍一下我们需要了解的字段。

  • name 条形图上显示的名字
  • cat 分类
  • ph 图表种类 B 表示开始点 E表示结束点
  • ts 时间戳
  • pid 进程名 显示
  • tid 线程名 显示
  • args 一段json文本 部分事件需要特定的参数(本文不会用到)

好了,我们了解这么多就够了,接下来,我将会实现一些方法/类,来辅助我们在json中插入事件。

我们需要一个json工具,我比较懒,不想手写json,因此我们选择了nlohman json作为我们的json写入工具,get_json_writer可以获得json对象,从而支持写入数据,gen_json顾名思义,就是生成json文件,将json对象写入到磁盘文件中。

namespace cpp_visual { namespace json_tool { nlohmann::json &get_json_writer(); std::string gen_json(const std::string &json_path); } // namespace json_tool 

由于chrome tracing需要的时间戳都是从0开始的相对时间,因此我们不能简单的插入时间戳,而是要计算一个测试开始到当前时间的差值,这样才能正常的进行绘制,所以我们写一个非常简单的纯工具类。

class TracingTool { public:   static int64_t currentDurationTs(); private:   static int64_t start_time_; }; 

这样的话我们只需调用currentDurationTs就可以获得合理的时间戳了。

接下来,我们需要对事件进行抽象,提取出一个基类。

class TracingEvent { public:   template <typename FieldType>   void setEventField(const std::string &name, const FieldType &value) {     event_json_[name] = value;   }   void commitEvent();  private:   nlohmann::json event_json_; }; 

TracingEvent,它将成为所有事件的基类,即便目前我们并没有这么多事件,但是设计上还是要认真做。它内含一个json对象,它描述一个事件,此对象将会存储所有必须的字段,这个对象将会作为片段插入最终的json文件中。

调用setEventField可以添加字段,调用commitEvent可以将添加好的字段写入到json对象中。

现在我们拥有了一个易于扩展的基类,之后我们便可以实现一个更加方便的“过程事件”,他可以帮我们自动填写一些可自动计算的字段——例如时间戳,让用户手动填写那些需要用户才能决定的字段——例如进程名,线程名等等。

class TracingDuration : public TracingEvent { public:   TracingDuration(const std::string &task_name, const std::string &thread_name,                   const std::string &duration_name);   virtual ~TracingDuration() = default;   void begin();   void end(); }; 

值得注意的是,我将原本进程的概念在参数中写为了任务(task),这是为了提示使用者,不必拘泥于此,不需要所有的测试点都使用同一个进程名,我们可以将我们的程序划分为许多任务,这些任务可能是单线程完成的,也可能是多线程完成的,这种基于任务的划分,在图表上有更好的表现力,当然,这也是作者的个人感受和意见。

TracingDuration类强制我们创建此对象是提供任务名,线程名,以及过程名,调用begin可以确定一个开始点,end确定一个结束点,使用起来非常方便,为了免去重复书写的体力劳动,我还提供了两个宏定义,分别用于标记开始和结束:

#define TRACING_VISUAL_B(__TASK__, __THREAD__, __DURATION_NAME__)              /   cpp_visual::TracingDuration __DURATION_NAME__##_BEGIN(                       /       #__TASK__, #__THREAD__, #__DURATION_NAME__);                             /   __DURATION_NAME__##_BEGIN.begin()  #define TRACING_VISUAL_E(__TASK__, __THREAD__, __DURATION_NAME__)              /   cpp_visual::TracingDuration __DURATION_NAME__##_END(#__TASK__, #__THREAD__,  /                                                       #__DURATION_NAME__);     /   __DURATION_NAME__##_END.end()  

这组宏仅仅是简单的创建对象并调用开始和结束函数,并没有什么复杂的操作。为了方便大家理解,我提供了实例:

// 在代码中插入开始点结束点 // 生成tracing json文件 // 使用 tracing loader 进行可视化 int main(int argc, char **argv) {   // 使用宏   {     // 任务名 线程名 过程名 创建开始点     TRACING_VISUAL_B(MAIN, MAIN_THREAD, READY);     std::this_thread::sleep_for(std::chrono::milliseconds(40));   }    // 自己创建   cpp_visual::TracingDuration duration("Main", "main_thread", "hello");   duration.begin();   cout << "hello world!" << endl;   std::this_thread::sleep_for(std::chrono::milliseconds(20));   cpp_visual::TracingDuration duration2("Main", "main_thread", "hello2");   duration2.begin();   std::this_thread::sleep_for(std::chrono::milliseconds(20));   duration2.end();   duration.end();    TRACING_VISUAL_B(MAIN, MAIN_THREAD, WORLD);   std::this_thread::sleep_for(std::chrono::milliseconds(20));   TRACING_VISUAL_E(MAIN, MAIN_THREAD, WORLD);    // 测试开始和结束不在一个作用域也可以   { TRACING_VISUAL_E(MAIN, MAIN_THREAD, READY); } // 创建结束点   // 写入   std::string path = "./json_result/";   std::string file = "result.json";   std::filesystem::create_directories(path);    cpp_visual::json_tool::gen_json(path + file);    return 0; }  

生成的json如下:

[{"name":"READY","ph":"B","pid":"MAIN","tid":"MAIN_THREAD","ts":21},{"name":"hello","ph":"B","pid":"Main","tid":"main_thread","ts":33179},{"name":"hello2","ph":"B","pid":"Main","tid":"main_thread","ts":64416},{"name":"hello2","ph":"E","pid":"Main","tid":"main_thread","ts":95692},{"name":"hello","ph":"E","pid":"Main","tid":"main_thread","ts":95697},{"name":"WORLD","ph":"B","pid":"MAIN","tid":"MAIN_THREAD","ts":95723},{"name":"WORLD","ph":"E","pid":"MAIN","tid":"MAIN_THREAD","ts":126935},{"name":"READY","ph":"E","pid":"MAIN","tid":"MAIN_THREAD","ts":126940}] 

我们将他放到谷歌tracing中看看吧!
img

效果还不错~,不过手动选文件还是有些繁琐。

tracing loader

没错,借助之前大佬提供的html文件,我们有希望做出一个命令行工具,用来加载json文件!

使用cli11库提供命令行解析;使用cpp-httplib创建一个单页面的服务端。有些这些现成的轮子,我们写起来简直无比轻松!

int main(int argc, char **argv) {   CLI::App app("tracing loader command line tool");   // app.add_flag("-h,--help", "print this help")->configurable(false);   std::string file;   app.add_option("-f,--file", file, "the tracing json file to load")       ->capture_default_str()       ->run_callback_for_default()       ->check(CLI::ExistingFile);    CLI11_PARSE(app, argc, argv);    if (app.get_option("--help")           ->as<bool>()) { // NEW: print configuration and exit     std::cout << app.config_to_str(true, false);     return 0;   }    if (!file.empty()) {     cout << "the tracing file = /t" << file << std::endl; #if OS_WINDOWS     system("start http://localhost:8081/tracingtool.html");     cout << "exec = /t"          << "start http://localhost:8081/tracingtool.html" << std::endl; #elif OS_LINUX     system("xdg-open http://localhost:8081/tracingtool.html");     cout << "exec = /t"          << "xdg - open http://localhost:8081/tracingtool.html" << std::endl; #endif     if (std::filesystem::exists("./resource/tracing.json")) {       std::filesystem::remove("./resource/tracing.json");     }     std::filesystem::copy_file(file, "./resource/tracing.json");   }    httplib::Server server;   server.set_mount_point("/", "./resource");   server.listen("0.0.0.0", 8081);    return 0; } 

可以说,除了检查文件存在和复制文件是我自己写的,其他的代码随便抄抄库的示例程序就好了。比较烦人的是开启浏览器,由于手头也没有一个跨平台的openUrl函数,所以只能自己分开来写,而且还是使用的system命令,多少有些难绷。

还记得之前的html文件吗?之前的html文件采用链接传递参数的方式选择json文件,既然我们现在通过命令行手动让用户加载josn文件,其实是没必要传递参数的,因此我将html中的参数解析部分直接换成了固定位置的文件读取,所以你可以看到在上边的代码中出现了一部复制文件的操作。html中的细节我就不描述了,队大家也没有多少帮助,我也是个门外汉,不想说错了产生误导。

代码写完,我们可以尝试加载一个json文件,这个命令行的用法是:

tracing_loader -f xxxx.json 

在我自己的项目中,我测试了一下(windows测试的,所以是/)

❯ ./tracingloader.exe -f  ./json_result/result.json the tracing file =      ./json_result/result.json exec =  start http://localhost:8081/tracingtool.html  

随后自动打开浏览器访问上边的网址,

总结

使用日志进行性能测试繁琐枯燥,可视化方法可以让我们更加轻松的分析性能问题,借用chrome tracing工具,我们可以轻松的对代码进行可视化性能测试!本文提供了简单的测试方法以及可视化方法,希望对各位小有帮助。

仓库地址:https://gitee.com/smalldyy/cpp-visual-tracing
注意:本文提交时,gitee正在进行开源申请,可能无法访问。近日即可解锁。

(项目使用xmake作为构建系统,xmake很好用!)

商匡云商
Logo
注册新帐户
对比商品
  • 合计 (0)
对比
0
购物车