阅读前的注意事项
本文发布的所有代码的目的是帮助您理解,而不是指导您遵循。很多环境问题没有详细解释,代码也不全面,无法达到后续的效果。建议直接阅读全文。我会在最后给出详细的代码地址。对源码细节比较感兴趣的同学可以下载参考。
性能测试:使用日志
C++ 中的性能测试是一件令人头疼的事情,我们经常需要在成千上万条日志中分析性能瓶颈——找到最耗时的部分。而这部分工作极其无聊:
首先,我们需要准备一个计算时间的工具类。幸运的是,我们有 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!你在干什么!英雄!
当然,在这个例子中,我不能再进一步了,毕竟我不知道英雄如何更快地拯救世界,优化是不可能的,但是从这个糟糕的例子中,我们在最少知道 Logging 可以帮助我们进行性能测试,以了解哪些步骤需要更多时间。
实际情况远比这复杂。我的意思是,这种级别的性能测试根本无法解决实际需求。在真实的项目环境中,程序输出的日志可能有上千条。在实际运行过程中你几乎看不到日志的时间戳,在日志文件中寻找你需要的条目——怎么说呢,这个挑战对我来说很不愉快。我完全不想在我一天的工作中插入这样的过程,太痛苦了,更何况是并发环境下的日志,你甚至无法确定它们的顺序!
可视化很无聊!
可视化是个好主意,我喜欢可视化,尤其是当文字让我眼花缭乱的时候,可视化比从该死的日志中扣除我想要的条目更亲密,如果有图表显示在我面前,那就更好了!
什么?开发可视化工具?
啊,这是一个很大的目标,我还需要分析日志吗?分析的数据应该如何呈现? c++ 对可视化有好处吗?取决于! 是否可以使用正则表达式?
该死!我不想这样做!
全文
谷歌浏览器追踪!
全文还没完呢!世界还没有毁灭!
是的!你想到的大部分东西都会有现成的实现。如果您有谷歌浏览器,您可以尝试在地址栏中输入以下地址:
chrome://跟踪
这个网页接受一个Json文件,然后根据Json文件的内容生成一个图表。我有一份来自 Internet 的 Json 示例的副本。您可以将其保存为 .json 文件,然后单击网页上的加载按钮,选择您的文件。
[
{"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文件,这让我很不舒服。
还好有大佬已经把核心网页代码提取出来了!我不能确定我正在阅读的文章是否是原创的,所以我只是按名称搜索并从几个我认为是原作者的网站中挑选了一个 URL:
(CSDN盗版文章太多了!)
根据作者的说法,在本文中,作者提供了一个 html 文件并使其在线可用
通过chrome://tracing使用Tracer Viewer还是不方便,不利于传播。虽然谷歌在弹射器中提供了trace2html,但是文件很多,使用起来还是有点麻烦,所以我参考了go trace的源码,将相关文件上传到CDN,然后在一个html中引用文件,因此只需要一个文件。
顺便说一句,这里就不贴具体的html文件了,有点长,我也不会照原样使用,所以贴出来没什么意义。有兴趣的同学可以访问作者的文章网址,也可以算是正版引流(如果有的话)。
不得不说,作者的想法很好,但是我觉得用CDN有点吃力,而且我对这个领域不熟悉,所以我会采取另一种方法。
一种基于chrome追踪的可视化方案
我的计划是:
提供插入流程起点、插入流程终点、保存json文件进行性能测试并生成结果的方式。提供一个loader,可以临时搭建一个web server,loader读取json文件,自动打开浏览器访问服务URL,从而呈现结果。
计划确定,开始实施!
追踪工具
目标1首先,提供插入流程起点、插入流程终点、保存json文件进行性能测试并生成结果的方式。
在具体实现之前,我们需要了解一下tracing json的格式。一个tracing json文件可以包含很多’events’,’events’的类型也很多,不同的events最终的视觉展示效果是不一样的。 ,我们的性能测试场景只需要给流程一个可视化的展示,所以用到的事件并不多。
其他闲置时间,有兴趣的同学可以访问网站:地址在墙外。
我们用一个事件来表示一个过程的开始,一个事件来表示过程的结束,所有的测试点都可以用开始和结束来描述。
我们需要使用的事件在上面的例子中没有出现,这里我将详细介绍我们需要了解的字段。
好了,我们理解到此为止,接下来,我将实现一些方法/类来帮助我们将事件插入到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追踪所需要的时间戳是从0开始的相对时间,所以我们不能简单的插入时间戳,而是计算一个测试开始时间和当前时间的差值,这样就可以正常绘制了,所以我们写了一个很简单的纯实用程序类。
class TracingTool {
public:
static int64_t currentDurationTs();
private:
static int64_t start_time_;
};
在这种情况下,我们只需要调用 currentDurationTs 就可以得到一个合理的时间戳。
接下来,我们需要抽象事件并提取一个基类。
class TracingEvent {
public:
template
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();
};
值得注意的是,我在参数中写了原始流程的概念作为任务,这是为了提醒用户,不必拘泥于此,并非所有测试点都必须使用同一个进程名,我们我们的程序可以分为很多任务,这些任务可能由单个线程完成,也可能由多个线程完成。这种基于任务的划分在图上具有更好的表现力。当然,这也是作者个人的感受和意见。
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()
这组宏只是简单地创建对象并调用 start 和 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}]
让我们把他放到谷歌追踪看看吧!
效果还不错~,但是手动选择文件还是有点麻烦。
跟踪加载器
是的,借助前任老板提供的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()) { // 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功能,只能单独写,还是用系统命令,有点难度。
还记得之前的 html 文件吗?在之前的html文件中,通过链接和传递参数来选择json文件。由于我们现在手动让用户通过命令行加载 josn 文件,因此无需传递参数。因此,我直接将html中的参数解析部分替换为固定位置。文件被读取,所以可以看到上面的代码中有一个复制文件的操作。 html里面的细节我就不描述了,对团队里的每个人都帮不上什么忙。我也是门外汉,不想误导。
代码写完后,我们可以尝试加载一个json文件。这个命令行的用法是:
tracing_loader -f xxxx.json
在自己的项目中,我测试过(windows测试,所以是)
❯ .tracingloader.exe -f .json_resultresult.json
the tracing file = .json_resultresult.json
exec = start http://localhost:8081/tracingtool.html
然后自动打开浏览器访问上面的网址,
总结
使用日志执行性能测试既乏味又乏味。可视化方法使我们能够更轻松地分析性能问题。借助chrome跟踪工具,我们可以轻松进行代码的可视化性能测试!本文提供了简单的测试方法和可视化方法,希望对大家有所帮助。
仓库地址:
注意:本文提交时,gitee正在开源应用中,可能无法访问。它将在不久的将来解锁。
(项目使用xmake作为构建系统,xmake效果很好!)
暂无评论内容