instrument工具 -- Timer profile & Signpost

之前使用instrument工具進(jìn)行性能檢測(cè)時(shí),總感覺找不到竅門,要浪費(fèi)很多時(shí)間才能定位到有問題的代碼片段。 工作的項(xiàng)目中慢慢重視性能優(yōu)化一塊了,所以想著先將instrument了解一遍,上周是先看了頭號(hào)種子選手 —— Timer profile。



1 性能檢測(cè)工具使用的原因
2 Timer Profile如何使用
2.1 通用技巧
2.2 signpost
3 總結(jié)

1 性能檢測(cè)工具使用的原因

去年在嘗試進(jìn)行性能優(yōu)化的工作時(shí)根本無從下手,只能不斷的從網(wǎng)上找資料,然后跟著資料學(xué)著給所有的陰影加個(gè)shadowPath、更改所有圓角的繪制方法、以及不斷的想怎么減少層級(jí)關(guān)系等等,雖然這些方法都是已經(jīng)總結(jié)有效的處理,但是如同在2018年WWDC關(guān)于如何通往更高效性能中也提到了,性能問題主要可以劃分三大場(chǎng)景:

① 性能衰退 Major Regression

某次回歸測(cè)試的時(shí)候突然出現(xiàn)大幅度的性能問題,這時(shí)候必須回過頭排查新調(diào)整代碼的邏輯,當(dāng)然如果工程中有自動(dòng)化測(cè)試功能那么就能直接細(xì)致的監(jiān)控。

② 偏離目標(biāo) Off Target

目標(biāo)性能為頁面刷新每秒55幀以上,但是實(shí)際卻只有45或者更少的幀率。說明存在待解決的幾個(gè)問題需要進(jìn)行修復(fù)。

③ 拙劣設(shè)計(jì) Poor Design

性能表現(xiàn)的極差,給用戶的已經(jīng)不是體驗(yàn)而是考驗(yàn)了...不再只是解決幾個(gè)問題就能修復(fù)了,需要認(rèn)真的排查架構(gòu)、頁面結(jié)構(gòu)等深一層的問題。


通往高效性能之路

所以真正的性能優(yōu)化首先要知道自己的方向是哪種場(chǎng)景,針對(duì)該場(chǎng)景下采取相對(duì)應(yīng)的手段檢測(cè)存在的問題。雖然別人的經(jīng)驗(yàn)確實(shí)是有用的,但是得想方設(shè)法的找到關(guān)鍵點(diǎn)。

2 Timer Profile如何使用
2.1 通用技巧
Timer Profile界面

打開檢測(cè)工具的界面時(shí),有幾個(gè)我在學(xué)習(xí)時(shí)值得注意的地方:

① Detail View

左下角的詳細(xì)信息中有Weight 和 Self Weight、Symbol Name三列,后面的符號(hào)名稱比較好理解,前面兩個(gè):
Weight表示權(quán)重,從這個(gè)方法開始耗費(fèi)總時(shí)間的權(quán)重
Self Weght表示這個(gè)方法本身耗費(fèi)的時(shí)間權(quán)重,上圖中全是0,表示真正耗時(shí)的在內(nèi)部的調(diào)用方法中

② Extended Detail View

右下角的堆棧追蹤信息,當(dāng)選中左邊的一項(xiàng)時(shí),右邊會(huì)迅速的打開整個(gè)調(diào)用關(guān)系堆棧,而灰色標(biāo)注的是系統(tǒng)方法。

而在尋找耗時(shí)方法們的路上有幾個(gè)挺好的小技巧:

① 快速過濾

過濾系統(tǒng)庫并且將耗時(shí)的方法置頂


快速過濾
② 快速打開層級(jí)關(guān)系

點(diǎn)擊方法的同時(shí)按住option,能快速開啟多層


快速開啟多層
③ 具體耗時(shí)分析

點(diǎn)擊某一項(xiàng)時(shí),會(huì)進(jìn)入具體某一行代碼的占用權(quán)重分析


具體耗時(shí)分析
③ 降低檢測(cè)成本

當(dāng)檢測(cè)的數(shù)據(jù)過多,instrument默認(rèn)開啟的即時(shí)記錄可能會(huì)導(dǎo)致電腦發(fā)熱卡頓。如果想要在發(fā)現(xiàn)性能問題立刻停止調(diào)試的話是需要開啟即時(shí)記錄,而其他情況下延遲或者最后5秒的處理已經(jīng)能滿足需求了


降低檢測(cè)成本

剩下的就是找啊找啊找啊找,這時(shí)候如果是自己寫的代碼邏輯還好,一眼能知道這個(gè)方法占比較多的原因,但是如果是別人的代碼,你知道是怎么一回事嗎?



是時(shí)候引入蘋果在2018年為OSLog家族引入的性能衡量工具Signpost了。
2.2 signpost

即便在Timer Profile中檢測(cè)的時(shí)候能告訴你是否是方法本身占用耗時(shí),但是卻無法判斷出到底是因?yàn)檎{(diào)用了太多次還是本身方法存在一個(gè)超級(jí)耗時(shí)的操作。而signpost的作用就是告訴你這段代碼占用耗時(shí)比如此大的具體信息。


signpost

在代碼中寫入signpost打印的內(nèi)容會(huì)顯示到instrument,寫入的方法十分簡(jiǎn)單:

① signpost先導(dǎo)入 os/signpost.h
① 創(chuàng)建os_signpost_interval_begin/end
    os_log_t log = os_log_create("com.lizhou.WorkIdeaDemo", "third");
    os_signpost_id_t signid = os_signpost_id_generate(log);
    os_signpost_interval_begin(log, signid, "outerTotal");
    for(int i = 0; i < 100000; i++) {
       
        os_signpost_interval_begin(log, signid, "signpost");
        NSLog(@"打印的第%d遍",i);
        os_signpost_interval_end(log, signid, "signpost");
        
    }
    os_signpost_interval_end(log, signid, "outerTotal");

以上的邏輯可以分為三步進(jìn)行了解:

① os_log_t
os_log_t os_log_create(const char *subsystem, const char *category);

subsystem自定義的標(biāo)識(shí)符,可以直接使用項(xiàng)目的bundle id
category是用于分類,主要作用于對(duì)相關(guān)聯(lián)的內(nèi)容操作進(jìn)行分類

② os_signpost_id_t
os_signpost_id_t os_signpost_id_generate(os_log_t log);
③ begin/end

記錄一段代碼邏輯執(zhí)行的情況就是從開始到結(jié)束,

os_signpost_interval_begin(log, interval_id, name, ...) 
os_signpost_interval_end(log, interval_id, name, ...)

begin/end的name一致,保證能識(shí)別出是同一個(gè)邏輯的開始/結(jié)束,后面的…表示能添加額外的參數(shù),在instrument中顯示。

當(dāng)插入以上邏輯后進(jìn)行檢測(cè)會(huì)顯示以下的內(nèi)容: 代碼的執(zhí)行次數(shù)、時(shí)長(zhǎng)以及每次執(zhí)行時(shí)最大、最短和平均時(shí)長(zhǎng)等內(nèi)容


os_signpost打印內(nèi)容

當(dāng)然在實(shí)際開發(fā)中會(huì)更多的需要檢測(cè)異步線程中的性能問題,注意一下上圖中的顯示: 只使用了os_log的category 和 os_signpost的name進(jìn)行分類,并沒有使用interval_id。解釋一下三種分類字段的含義就能清楚的明白:
category 相關(guān)操作
signpost name 某類相同操作
signpost id 同一類型操作下的不同任務(wù)
所以異步其實(shí)就是相同類型操作下創(chuàng)建了多個(gè)不同的任務(wù),只要三個(gè)標(biāo)識(shí)相同的begin/end成對(duì)出現(xiàn),那么在不同的線程中都可以進(jìn)行檢測(cè)。


for(int i = 0; i < 10; i++) {
        os_signpost_id_t signid_1 = os_signpost_id_make_with_pointer(log, (__bridge const void * _Nullable)([NSString stringWithFormat:@"%d",i]));
        os_signpost_interval_begin(log, signid_1, "signpost");
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"打印的第%d遍",i);
            os_signpost_interval_end(log, signid_1, "signpost");
        });
        
    }

如果想要在begin和end間添加一些特定的點(diǎn),比如用戶的點(diǎn)擊事件和圖片在某個(gè)特定時(shí)間點(diǎn)的加載信息,就能使用event事件

    os_log_t log = os_log_create("com.lizhou.WorkIdeaDemo", OS_LOG_CATEGORY_POINTS_OF_INTEREST);
    os_signpost_id_t signid = os_signpost_id_generate(log);
    os_signpost_event_emit(log, signid, "點(diǎn)擊事件","點(diǎn)擊第%ld個(gè)",index);

每個(gè)事件的name后都能添加additional information,比如上面的動(dòng)態(tài)字符串格式
os_signpost如何添加到points of interest區(qū)塊中,請(qǐng)看上圖的os_log_t創(chuàng)建方法中的category



看一下instrument的顯示格式,按照分類邏輯顯示清晰明了:


instrument檢查結(jié)果界面

聊了這么多如何使用signpost,最重要的一件事還沒聊: 什么時(shí)候可以去使用?

① 禁止與開啟log

雖然signpost本身是輕量級(jí)的,而且為編譯時(shí)做了大量的優(yōu)化,把大部分工作推遲到instrument中。但是release包確實(shí)沒有必要開啟,直接為log賦值disable


② 使用版本
API_AVAILABLE(macos(10.12), ios(10.0), watchos(3.0), tvos(10.0))
OS_EXPORT OS_NOTHROW OS_WARN_RESULT OS_OBJECT_RETURNS_RETAINED OS_NONNULL_ALL
os_log_t
os_log_create(const char *subsystem, const char *category);

API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0))
OS_EXPORT OS_NOTHROW OS_WARN_RESULT
os_signpost_id_t
os_signpost_id_generate(os_log_t log);

當(dāng)在低版本中使用這些字段時(shí),直接崩潰打印以下的報(bào)錯(cuò)信息:

lazy symbol binding failed: Symbol not found: _os_signpost_id_generate

學(xué)習(xí)的過程中真的發(fā)現(xiàn)很多有意思的地方,但是也發(fā)現(xiàn)了一些還未解決的部分,通往高性能的路上還需要繼續(xù)學(xué)習(xí)。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。