性能深度分析之SystemTrace

前言

App中大多數的性能指標都和時間相關,如啓動速度,列表滑動FPS,頁面打開耗時等等。爲了優化這些指標,我們需要了解時間都消耗在哪裏。

通常我們會打開Time Profiler,通過聚合Call Stack來分析和優化代碼耗時。偶爾會出現優化後Time Profiler已經沒有什麼高耗時的Call Stack,但列表滑動仍然掉幀,這時候應該怎麼辦呢?

不妨試試System Trace~

一個實際例子

用dyld提供的C接口來註冊image加載的回調是一種常見做法,第一次註冊回調的時候會立刻把當前已經加載的image回調,所以如果回調函數裏有耗時操作,我們一般會在子線程註冊。

模擬啓動的時候註冊dyld回調:

在這裏插入圖片描述

接着在真機上運行這段代碼,發現在啓動頁面會卡很久:

爲什麼這裏會卡很久呢?

試試Time Profiler?

就不具體講解Time Profiler的用法了,相關資料網上很多,通過分析我們可以拿到如下的調用棧:

在這裏插入圖片描述
嗯~??不對啊,明明耗時好幾秒,怎麼統計出來的只有堆棧142ms。可以看到Time Profiler提供的信息有限,以下兩條算是比較有用的:

  1. 區間選中有3.17s,但是統計到的調用棧卻只有142ms
  2. iPhone 6一共只有兩個Core,兩個Core在這段時間內,大部分時候都是空閒的。

似乎線程在等待什麼?

System Trace

System Trace一個比較大的優勢是可以看到線程的狀態,分析上面的代碼(如下圖),發現主線程有一段3s左右的時間被block住了,通過右側的調用棧能看到主線程在didFinishLaunching中調用了imageNamed後者觸發了dlopen,dlopen裏在等待一個互斥鎖

在這裏插入圖片描述
切換到Events:Thread State,可以看到線程切換的每一個事件,和狀態切換的發生的原因,切換後再選中主線程被block的這段時間,觀察這個事件的下一個事件,因爲下個事件通常是鎖被釋放,線程重新進入可執行的狀態。

在這裏插入圖片描述
我們發現,主線程因爲線程0x9e1f釋放鎖才進入了可執行的狀態。這時候把線程0x9e1f也pin一下,觀察到主線程被block這段時間,這個線程在不停的sleep 10ms左右然後繼續執行代碼,通過右側的調用棧會發現,sleep的源頭是在_dyld_register_func_for_add_image,到這裏原因找到了:子線程卡住了主線程

在這裏插入圖片描述

細心的朋友會發現,右側的調用棧裏並沒有最上面C函數:add_image_callback,這是因爲add_image_callback內部只調用了usleep,這一層調用完全沒有存在的必要,所以release模式下編譯器做了優化。

小結

通過System Trace分析,我們很容易就找到了這次啓動耗時長的原因:主線程的imageNamed內部觸發了dlopen,子線程在執行執行_dyld_register_func_for_add_image的回調,二者都需要先獲取同一個互斥鎖,所以子線程卡住了主線程

這個例子讓我們見識到了System Trace的威力,那麼System Trace一般可以用來分析哪些問題呢?

  • 鎖的互斥,主要是主線程等子線程釋放鎖
  • 線程優先級,搶佔和高優線程超過CPU核心數量
  • 虛擬內存,Page Fault的代價其實不小
  • 系統調用,瞭解性能瓶系統正在做什麼

Tips: Runtime, dyld存在着很多隱藏的全局互斥鎖,很容易踩到雷。

Time Profiler的原理

這部分回答一個問題:爲什麼剛剛的代碼裏,Time Profiler看不出端倪?

正常Time Profiler會1ms採樣一次,默認只採集所有在運行線程的調用棧,最後以統計學的方式彙總。比如下圖中的5次採樣中,method3都沒有采樣到,所以最後聚合到的棧裏就看不到method3。

在這裏插入圖片描述
Time Profiler中的看到的時間,並不是代碼實際執行的時間,而是棧在採樣統計中出現的時間。另外,Time Profiler還有一些配置是容易忽略的,依次點擊File -> Recording Options:

  • High Frequency,降低採樣的時間間隔
  • Record Kernel Callstacks,記錄內核的調用棧
  • Record Waiting Thread,記錄被block的線程

第三個選項比較有用,比如剛剛的代碼開啓Record Waiting Thread,再運行Time Profiler就會發現主線程很長一段時間在等待一個互斥鎖:

在這裏插入圖片描述

System Trace

用好System Trace的前提是對操作系統的一些核心基礎知識有基本的瞭解,所以這一小節會講解下System Trace的各個模塊的使用方式以及涉及到的基本原理。

Point of Interest

有時候我們只關心某一段小段時間的性能,如何把時間段和System Trace對應起來呢?

可以通過kdebug_signpost相關的接口相關的接口來打一些點,這些點會在Point of Interest區域中顯示,比如剛剛的代碼我們想標記出來didFinishLaunch這個方法,就可以在方法的收尾添加kdebug_interval:

kdebug_signpost_start(10, 0, 0, 0, 0); 
kdebug_signpost_end(10, 0, 0, 0, 0); 

然後重新運行System Trace,即可在Point of Interest觀察到該方法的執行區間

在這裏插入圖片描述

但顯示Code 10明顯是不友好的,可以在File -> Recording Options裏做Code映射,變成可讀的字符串。Color Using Last Argument可以把最後一個code映射到不同顏色,進一步提高辨識度。

> Tips: > - kdebug_signpost_start的幾個參數是用來做多級區分的,但整型本身不夠友好,顯示區間可以也可以用`os_signpost`相關的API > - signpost配合Objective C的Swizzling往往會發揮意想不到的效果

Thread State Trace

System Trace一個比較很重要的特性就是能看到線程不同的狀態,以及狀態之間切換的原因,通常我們會選擇一個時間段,然後彙總觀察結果:

在這裏插入圖片描述
幾個線程狀態說明:

  • Running,線程在CPU上運行
  • Blocked,線程被掛起,原因有很多,比如等待鎖,sleep,File Backed Page In等等。
  • Runnable,線程處於可執行狀態,但此時等CPU有資源的時候,就可以運行
  • Interrupted,被打斷,通常是因爲一些系統事件,一般不需要關注
  • Preempted,被搶佔,優先級更高的線程進入了Runnable狀態

Blocked和Preempted是優化的時候需要比較關注的兩個狀態,分析的時候通常需要知道切換到這兩個狀態的原因,這時候要切換到Events: Thread State模式,然後查看狀態切換的前一個和後一個事件,往往能找到狀態切換的原因。

在這裏插入圖片描述

Tips:在線程上右鍵,可以快速把線程設置爲filter或者pin,方便分析,尤其是大型App的線程非常多的情況下。

除了Thread State Event比較有用,另外一個比較有用的事Narrative,這裏會把所有的事件,包括下文的虛擬內存等按照時間軸的方式彙總:

在這裏插入圖片描述

Virtual Memory Trace

內存分爲物理內存和虛擬內存,二者按照Page的方式進行映射。

可執行文件,也就是Mach-O本質上是通過mmap相關API映射到虛擬內存中的,這時候只分配了虛擬內存,並沒有分配物理內存。如果訪問一個虛擬內存地址,而物理內存中不存在的時候,會怎麼樣呢?會觸發一個File Backed Page In,分配物理內存,並把文件中的內容拷貝到物理內存裏,如果在操作系統的物理內存裏有緩存,則會觸發一個Page Cache Hit,後者是比較快的,這也是熱啓動比冷啓動快的原因之一

這種剛剛讀入沒有被修改的頁都是Clean Page,是可以在多個進程之間共享的。所以像__TEXT段這種只讀的段,映射的都是Clearn Page。

_DATA段是可讀寫的,當_DATA段中的頁沒有被修改的時候,同樣也可以在兩個進程共享。但一個進程要寫入,就會觸發一次Copy On Write,把頁複製一份,重新分配物理內存。這樣被寫入的頁稱爲Dirty Page,無法在進程之間共享。像全局變量這種初始值都是零的,對應的頁在讀入後會觸發一次內存寫入零的操作,稱作Zero Fill

iOS不支持內存Swapping out即把內存交換到磁盤,但卻支持內存壓縮(Compress memory),對應被壓縮的內存訪問的時候就需要解壓縮(Decompress memory),所以在Virtial Memroy Trace裏偶爾能看到內存解壓縮的耗時。

在這裏插入圖片描述

System Load

以10ms爲緯度,統計活躍的高優線程數量和CPU核心數對比,如果高於核心數量會顯示成黃色,小於等於核心數量會是綠色。這個工具是用來幫助調試線程的優先級的:

在這裏插入圖片描述

線程的優先級可以通過QoS來指定,比如GCD在創建Queue的時候指定,NSOperationQueue通過屬性指定:

//GCD
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, -1);
dispatch_queue_t queue = dispatch_queue_create("com.custom.utility.queue", attr);
//NSOperationQueue
operationQueue.qualityOfService = NSQualityOfServiceUtility

選擇合適的優先級,避免優先級反轉,影響線程的執行效率,尤其是別讓後臺線程搶佔主線程的時間。延伸閱讀:libdispatch efficiency tips

System Call & Context Switch

操作系統爲了安全考慮,把文件讀寫(open/close/write/read),鎖(ulock_wait/ulock_wake)等核心操作封裝到了內核裏,用戶態必須調用內核提供的接口才能完成對應的操作,這樣的調用稱作系統調用System Call。System Trace裏提供了系統調用相關的Event:

在這裏插入圖片描述

線程/進程需要輪流到CPU上執行,在切換的時候,必須把線程/進程狀態保存下來,之後才能恢復,這種保存/恢復的過程稱作上下文切換Context Switch,在System Trace裏通常會關注下主線程是否在頻繁的上下文切換:

在這裏插入圖片描述

Thermal State

一個大家不怎麼關注,但其實挺重要的性能指標是發熱狀態,因爲發熱後系統會限制CPU/GPU/IO等使用。System Trace也提供了對應的分析工具

在這裏插入圖片描述
iOS 11之後,可以通過NSProcesssInfo的相關API來獲取當前發熱狀態:

NSProcessInfo.processInfo.thermalState

一共有四種狀態,正常的狀態是Nominal,後面逐級嚴重:

  • Nominal
  • Fair
  • Serious
  • Critical

Xcode也提供了工具來模擬發熱狀態,讓開發者可以測量不同發熱情況下App的體驗,在Xcode中,依次選擇Window -> Device And Simulator,然後按照下圖的方式開啓:

在這裏插入圖片描述

Tips:非遊戲類App一般不會引起發熱,除非有一些邏輯上Bug引起死循環,或者不停發送網絡請求。

總結

當遇到性能瓶頸的時候,優先還是建議Time Profiler,因爲簡單直接,排查效率的問題高。如果遇到Time Profiler裏採樣到的時間和實際消耗的時間差距比較大的時候,或者像優化FPS這種需要精細化的瞭解16ms線程都在做什麼的時候,可以試試System Trace,也許會有意外的收穫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章