阅读前注意

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

性能测试:使用日志

在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

此网页可接受一个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": "第二天"}} ]

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

点击对应的条目,下方还会出现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中看看吧!

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

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很好用!)

c++可视化性能测试的更多相关文章

  1. JMeter+Grafana+Influxdb搭建可视化性能测试监控平台(待继续完善。。。)

    influxdb下载.安装.配置.启动 InfluxDB是一个当下比较流行的时序数据库,InfluxDB使用 Go 语言编写,无需外部依赖,安装配置非常方便,适合构建大型分布式系统的监控系统. 下载: ...

  2. JMeter+Grafana+Influxdb搭建可视化性能测试监控平台(使用了docker)

    [运行自定义镜像搭建监控平台] 继上一篇的帖子 ,上一篇已经展示了如何自定义docker镜像,大家操作就行 或者 用我已经自定义好了的镜像,直接pull就行 下面我简单介绍pull下来后如何使用 拉取 ...

  3. 【性能测试实战】jmeter + k8s + 微服务 + skywalking + efk,测试都在学的热门技术

    原文持续更新完善:https://www.cnblogs.com/uncleyong/p/15475614.html 前言:当前的热门主流技术是哪些?测开为啥那么火?90%以上的测试对测开认识不准确 ...

  4. docker安装、基本使用、实战(测试必备)

    Docker概念.作用.术语 一张超级形象的图 看到这张图,大家会想到什么? 可以这么理解:大海是操作系统,鲸鱼是Docker,集装箱是在Docker 运行的容器! 概念 百度百科:Docker 是一 ...

  5. JMeter基础之一 一个简单的性能测试

    JMeter基础之一 一个简单的性能测试 上一节中,我们了解了jmeter的一此主要元件,那么这些元件如何使用到性能测试中呢.这一节创建一个简单的测试计划来使用这些元件.该计划对应的测试需求. 1)测 ...

  6. 开源性能测试工具--Jmeter介绍+安装

     一.           Apache JMeter介绍 1.       Apache JMeter是什么Apache JMeter 是Apache组织的开放源代码项目,是一个100%纯Java桌 ...

  7. JMeter性能测试介绍学习一

    上一节中,我们了解了jmeter的一此主要元件,那么这些元件如何使用到性能测试中呢.这一节创建一个简单的测试计划来使用这些元件.该计划对应的测试需求. 1)测试目标网站是fnng.cnblogs.co ...

  8. JavaScript性能优化:度量、监控与可视化1

    HTTP事务所需要的步骤: 接下来,浏览器与远程Web服务器通过TCP三次握手协商来建立一个TCP/IP连接,类似对讲机的Over(完毕) Roger(明白) TCP/IP模型 TCP即传输控制协议( ...

  9. 如何保存JMeter的性能测试数据到ElasticSearch上,并且使用Kibana进行可视化分析(1)

    前言 Jmeter是一款性能测试,压力测试的开源工具,被大量的测试人员拿来测试产品的性能,负载等等. Jmeter除了强大的预置的各种插件,各种可视化图表工具以外,也有些固有的缺陷,例如: 我们往往只 ...

随机推荐

  1. 搭建springboot集成mybatis

    1.new project创建新项目选择spring initializr: 2.选择依赖需要选择web.mybatis.mysql就够了,后续需要其他的直接pom引入依赖就好了: 3.自己在java ...

  2. 内存之旅——如何提升CMA利用率?

    ​(以下内容来自开发者分享,不代表 OpenHarmony 项目群工作委员会观点)​ 宋远征 李佳伟 OpenAtom OpenHarmony(以下简称"OpenHarmony") ...

  3. vue滚动分页加载

    做了一个项目,要求将后台数据滚动加载. 滚动加载必须要求后台传的接口中由pageSize和pageIndex两个参数,来判断每次传数据的条数和数据的页码. 首先要判断滑轮是向上滚动还是向下滚动,可以在 ...

  4. python基础练习题(题目 求输入数字的平方,如果平方运算后小于 50 则退出)

    day32 --------------------------------------------------------------- 实例046:打破循环 题目 求输入数字的平方,如果平方运算后 ...

  5. pdf.js 预览文件中文内容丢失

    问题: 在.netcore中使用pdf.js,pdf中有部分中文无法显示 在浏览器控制台发现有报错 发现在pdf.view.js中url路径异常,没有指向cmaps文件,于是调整了正确的相对路径 再次 ...

  6. Node爬取网站数据

    npm安装cheerio和axios npm isntall cheerio npm install axios 利用cheerio抓取对应网站中的标签根据链接使用axios获取对应页面数据 cons ...

  7. jmeter元件,作用域与优先级

    jmeter元件,作用域与优先级 一.jmeter元件 1.配置元件:优先级最高 1.1 重点使用元件:csv数据文件设置.用户定义变量.计数器 2.取样器:根据不同协议来编写请求脚本的元件 2.1 ...

  8. Bugku CTF练习题---MISC---telnet

    Bugku CTF练习题---MISC---telnet flag:flag{d316759c281bf925d600be698a4973d5} 解题步骤: 1.观察题目,下载附件 2.拿到手以后发现 ...

  9. 如何突破Jenkins瓶颈,实现集中管理、灵活高效的CI/CD

    在过去的几年间,随着DevOps的兴起,持续集成(Continuous Integration)与持续交付(Continuous Delivery)的热度也水涨船高.在本文中,我们将首先带您了解热门的 ...

  10. Idea分享项目到全球最大同x交友网站gayhub居然失败了!我居然没有权限!来看看解决方法吧

    Idea分享项目到全球最大同x交友网站gayhub居然失败了! 事情是这样的,刚写完一个动态网页就想着部署到github上让大家看看(装逼),然而在我share project时,它告诉我: 大概意思 ...