閱讀前注意
本文所有代碼貼出來的目的是幫助大家理解,并非是要引導大家跟寫,許多環境問題檔案問題沒有詳細說明,代碼也并不全面,達不到跟做的效果,建議直接閱讀全文即可,我在最后會給出詳細代碼地址,對源代碼細節更感興趣的同學可以下載參考,
性能測驗:使用日志
在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的可視化方案
我的方案是:
- 提供一種方法,可插入程序開始點,插入程序結束點,保存json檔案,用于進行性能測驗并生成結果,
- 提供一個加載程式,該程式可以臨時搭建一個網頁服務端,加載程式讀取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很好用!)
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/492407.html
標籤:C++
上一篇:【C++】建構式的分類與呼叫
