火山引擎 MARS-APMPlus 專欄—— iOS Heimdallr 卡死卡頓監控方案與優化之路

本文主要介紹Heimdallr卡死、卡頓異常的監控原理,並結合長時間的業務沉澱發現的問題進行不斷迭代和優化,逐步實現全面、穩定、可靠的歷程。

作者:字節跳動終端技術——白崑崙

前言

卡死、卡頓作爲目前iOS App的重要性能指標,不僅影響着用戶體驗,更關係到用戶留存、DAU等重要產品數據。本文主要介紹Heimdallr卡死、卡頓異常的監控原理,並結合長時間的業務沉澱發現的問題進行不斷迭代和優化,逐步實現全面、穩定、可靠的歷程。

一、什麼是卡死/卡頓?

卡頓,顧名思義就是在使用過程中出現了一段時間的阻塞,使得用戶在這一段時間內無法進行操作,屏幕上的內容也沒有任何的變化。Heimdallr在監控指標上,根據阻塞時間的長短進行了3個等級的劃分。

                    

1、流暢性與丟幀:動畫、滑動列表不流暢,一般爲十幾至幾十毫秒的級別

2、卡頓:短時間操作無反應,恢復後能繼續使用,從幾百毫秒至幾秒

3、卡死:長時間無反應,直至被系統殺死,通過線上收集數據,最少爲5s

可以看到,根據嚴重性由小至大可將卡頓問題劃分爲流暢性與丟幀、卡頓、卡死三個不同的等級。卡死的嚴重程度與Crash是相當的,甚至更爲嚴重。因爲卡死不僅僅造成了類似於崩潰的閃退,更使得用戶被迫等待了相當長的一段時間,更加損害用戶的體驗。由於監控方案上的差異,本文主要面向的是後兩者卡頓和卡死的監控。

二、卡死/卡頓的原因

iOS開發中,由於UIKit是非線程安全的,因此一切與UI相關的操作都必須放在主線程執行,系統會每16ms(1/60幀)將UI的變化重新繪製,渲染至屏幕上。如果UI刷新的間隔能小於16ms,那麼用戶是不會感到卡頓的。但是如果在主線程進行了一些耗時的操作,阻礙了UI的刷新,那麼就會產生卡頓,甚至是卡死。主線程對於任務的處理是基於Runloop機制,如下圖所示。Runloop支持外部註冊通知回調,提供了

1、RunloopEntry

2、RunloopBeforeTimers

3、RunloopBeforeSources

4、RunloopBeforeWaiting

5、RunloopAfterWaiting

6、RunloopExit

6個時機的事件回調,其流轉關係如下圖所示。Runloop在沒有任務需要處理的時候就會進入至休眠狀態,直至有信號將其喚醒,其又會去處理新的任務。

在日常編碼中,UIEvent事件、Timer事件、dispatch主線程任務都是在Runloop的循環機制的驅動下完成的。一旦我們在主線程中的任何一個環節進行了一個耗時的操作,或者因爲鎖的使用不當造成了與其它線程的死鎖,主線程就會因爲無法執行Core - Animation的回調而造成界面無法刷新。而用戶的交互又依賴於UIEvent的傳遞和響應,該流程也必須在主線程中完成。所以說主線程的阻塞會導致UI和交互的雙雙阻塞,這也是導致卡死、卡頓的根本原因。

三、監控方案

既然問題的根本在於主線程Runloop的阻塞,那麼我們就要通過技術手段監測主線程Runloop的運行狀態。爲了能夠實時獲取主線Runloop狀態,首先對主線程註冊上面提到的幾個事件回調,在觸發事件回調時,利用signal機制將其運行狀態傳遞給另一個正在監聽的子線程(後面稱之爲監聽線程)。監聽線程對於信號的處理可以是多樣的,它可以設置等待signal的超時時間,如果超過了設定的閾值,這說明主線程可能正在經歷阻塞。通過監聽線程,我們可以完整地瞭解到主線Runloop循環的週期,目前處於哪個階段,耗時了多久等等。根據這些必要的信息,就可以採取對應的策略進行異常的捕獲和處理,後面會單獨就卡頓、卡死分別進行說明。

目前大多數APM工具都是採用監聽Runloop的方式進行卡頓的捕獲,這也是性能、準確性表現最好的一種方案。由於RunloopBeforeTimers的和RunloopBeforeSources是緊鄰的兩個事件回調,Heimdallr爲了降低Runloop頻繁事件回調造成的性能損失,去除了對RunloopBeforeTimers的監聽。

1. 卡頓(ANR)

卡頓監控的特點在於主線程的阻塞是暫時的、能夠恢復的,因此我們要獲取卡頓持續的時間,用來評估卡頓問題的嚴重性。我們預先設定一個卡頓時間的閾值T,當主線程阻塞的時間超過該閾值,則會觸發全線程的抓棧,獲取卡頓場景的堆棧信息。此後監聽線程繼續等待主線程直至主線程恢復,並計算卡頓的總時間,整合之前獲取的堆棧信息,上報卡頓異常。

需要說明的是,如果在抓棧之後主線程無法恢復,那麼該異常不是卡頓,應交由卡死模塊處理。

2. 卡死(WatchDog)

與卡頓不同,卡死的阻塞是更長的,而且是無法恢復的。iOS系統會對App的主線程進行類似的監控,一旦發現了阻塞的情況,持續時間大於當前系統內允許的閾值(不同iOS版本和機型不同),就會強制殺死當前App進程,這個操作是沒有任何通知的。因此我們需要做的就是在系統發現卡死並強殺之前,獲取堆棧,並儘可能的評估出卡死持續的時間。

預先設定一個卡死的閾值T(默認是8s),這個閾值可以是相對保守的,並不是說超過了這個閾值就一定會被判定爲卡死。在超過卡死閾值T的時候,獲取全線程的堆棧,並保存至本地文件中。之後每隔一段時間(採樣間隔,默認是1s),會進行一次採樣。採樣的目的不是爲了獲取新的堆棧,而是爲了更新卡死持續的時間,將該信息保存至本地文件中。因此,採樣的間隔越小逼近真實卡死時間按越精確。直至到某一個時間節點,系統把App殺死。當App下一次啓動時,卡死模塊會根據上一次啓動中保留的本地文件信息,還原出卡死的堆棧、持續時間等信息,並上報卡死異常。

需要說明的是,很多人認爲卡死一定是因爲死鎖、死循環這樣的場景,導致程序永遠也無法完成導致的。其實不然,在很多場景下,一個或多個耗時的操作,只要其耗時超過了系統的允許閾值,都會觸發卡死。當應用啓動過程中,沒有在限定時間內完成初始化工作也會被系統殺死。所以,某些卡死可能是多個場景的不合理一起導致的,這也給卡死的問題定位提出了更高的要求。

四、問題與優化

理想是豐滿的,現實是骨感的。看似”無懈可擊“的監控方案,在線上卻暴露出不同程度的問題。

1. 卡頓監控優化

在卡頓監控中,我們認爲超過了卡頓閾值時獲取的堆棧一定是一個卡頓的場景,其實不然。在一些時候,獲取的堆棧可能是他人的”背鍋俠“。我們來看下面這個case。導致主線程卡頓的是4這個耗時操作,但是當我們設定閾值超時時,獲取的堆棧卻是沒有任何性能問題的5。因此如果使用這種方式來進行卡頓的監控,一定會存在誤報。而根據概率來講,雖然上報的5是一個誤報,但就線上的上報量來講,4的數量一定是要大於5的。因此上報量級大的堆棧才應該是真正的耗時操作,是需要我們專注去解決的,而那些量級較小的堆棧則可能是誤報。 

那麼是否能夠通過一些技術手段,在控制性能開銷的情況下,對卡頓場景捕捉的更加準確呢?一個比較好的思路就是採樣策略。如下圖,我們在原有的”常規模式“的基礎上增加了”採樣模式“。需要額外定義採樣間隔、採樣閾值。我們把卡頓閾值的等待過程,劃分爲以採樣間隔爲單位的粒度更細的時間節點。在每個時間節點進行主線程採樣,對主線程進行堆棧的提取。由於僅對主線程進行堆棧提取,所以耗時較全線程抓棧要小很多。

獲取了主線程堆棧後,通過提取頂層第一個自身調用來進行堆棧的聚合。如果某一個相同堆棧持續的時間超過了設定的採樣閾值,例如圖中的4,重複了3次,那麼就會判定該場景一定是一個卡頓場景。那麼此時就會進行全線程抓棧,而後面的卡頓閾值觸發時則不再抓棧。

結合主線程採樣,我們可以更加精準的以函數級別監控卡頓場景,但是也需要付出採樣帶來的額外性能開銷。爲了將採樣的開銷降至最低,避免線上對低端設備造成二次性能劣化,卡頓監控支持採樣功能的退火策略。當某一個卡頓場景被多次捕獲時,爲了避免再次將其捕獲,造成不必要的性能浪費,會逐步增加採樣間隔,直至將”採樣模式“退化成”常規模式“。

在Slardar平臺配置並開啓採樣功能後,可以通過sample_flag來過濾通過採樣超時獲取的卡頓異常。通過此方式獲取的堆棧,大概率爲卡頓場景,可以更加有針對性的去分析和解決。

2. 卡死監控優化

相比卡頓,卡死的誤報大多發生在後臺(目前Heimdallr提供後臺卡死過濾,如果對後臺卡死不關心的業務方可以自行打開)。因爲後臺場景的限制,當前App的線程優先級更低,而且隨時存在被系統掛起的可能,這給我們進行卡死時間的判定帶來了很多問題。

上面的Case描述的是一個卡死的誤報場景,因爲在後臺的原因線程的優先級較低,因此1、2、3任務執行的時間要比前臺更久,更加容易超過我們的卡死閾值。而後,因爲iOS系統的策略問題,後臺應用被掛起(suspend),直至某一個時間點因爲內存緊張,將整個應用殺死。但請注意,這個流程屬於App正常的生命週期範疇,並不是WatchDog。而按照我們之前的策略,這將會被判定爲卡死。由於我們無法監聽到suspend事件,所以這種場景目前還無法排除誤報。

還有一種誤觸發卡死的case是,suspend發生在8s閾值前,在長時間的掛起後,應用被resume,此時8s的超時被觸發。但是實際上,我們的App只有在8s中的很少一部分時間在running,大部分時間都是被掛起,所以不應該觸發卡死判定。歸根結底是卡死計時的準確性問題。

爲了解決上面的問題,對計時策略進行了改進。相比於直接進行8s的等待,我們將時間細分爲8個1s。如果在這段時間內App被掛起,等到恢復時也不會直接超過8s的閾值,而僅僅會造成最多1s的誤差。

此外,上面也提到過,卡死有的時候可能是多個耗時場景累計導致的。爲了能夠跟蹤主線程的變化,在抓棧之後的採樣階段,對主線程進行堆棧採樣,並將其一起上報。結合採樣中獲取的主線程堆棧,我們可以得到一個主線程堆棧變化的時間線,能夠更加準確的幫助定位問題所在。(時間線功能在Heimdallr 0.7.15之後支持)

         

最後,我們發現部分卡死場景是由於OC Runtime Lock導致的(大概率是dyldOC Runtime Lock造成的死鎖)。一旦發生這種類型的卡死,其它所有線程的OC代碼都會因此而阻塞,當然也包括監聽線程,卡死監控此時就無法捕獲這個異常。爲了能夠覆蓋所有場景,我們把卡死、卡頓模塊的所有邏輯進行了C/C++重構,解除了對OC調用的依賴,並且性能相比與OC實現進一步得到提升。

結語

HeimdallrANRWatchDog模塊經過一段時間的迭代與優化,達到了一個全面、穩定、可靠的狀態。這期間的一些優化思路借鑑了一些開源的APM框架,並結合使用方的實際需求進行不斷改進。感謝所有使用方的反饋,幫助我們不斷完善我們的功能與體驗。後續我們會繼續針對Watchdog場景增加防卡死功能,幫助接入方能夠在無侵入式的情況下,解決通用場景的卡死問題。

 


 

🔥 火山引擎 APMPlus 應用性能監控是火山引擎應用開發套件 MARS 下的性能監控產品。我們通過先進的數據採集與監控技術,爲企業提供全鏈路的應用性能監控服務,助力企業提升異常問題排查與解決的效率。

目前我們面向中小企業特別推出「APMPlus 應用性能監控企業助力行動」,爲中小企業提供應用性能監控免費資源包。現在申請,有機會獲得60天免費性能監控服務,最高可享6000萬條事件量。

👉 點擊這裏,立即申請

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