背景
卡頓 & ANR 在各 APP 中都是非常影響用戶體驗的問題,關于其的分析和治理一直也是個老生常談的話題。過去調查卡頓 & ANR 問題主要依賴上報的堆棧和 traceInfo 文件,通過這些信息還原問題的現場情況。但是在實踐過程中發現,現有監控機制下堆棧的抓取時機是晚于問題發生的,大部分情況獲取到的是問題發生后某一瞬間的堆棧,隨機性強,是不置信的,無法反映問題的真實現場情況,同一個問題可能聚合到不同堆棧中,無法清晰的歸類和定位問題,這就使得很多開發者清楚原理,但到了具體 case 時無從下手,調查起來缺乏方向甚至因堆棧聚合的不置信而陷入了錯誤的排查方向,效率低下。另一方面,大多能夠準確衡量性能的工具本身會帶來嚴重耗時問題,無法用于線上,而性能問題大多發生于復雜的線上用戶場景。所以,如何對卡頓 & ANR 進行有效的防治就是我們需要考慮的問題。
卡頓 & ANR 治理現狀和痛點
過去幾年,在業務發展的同時積累了大量的卡頓 & ANR 問題,對用戶的使用體驗帶來了極大的負面影響。隨著治理工作的進行,現有的監控機制暴露出一定的問題,堆棧不置信、聚合錯誤、缺乏正確信息、缺乏有效防治策略,這些成了制約治理工作進行的瓶頸。
卡頓 & ANR 現狀
長期以來,新老問題的不斷疊加,同時沒有系統的進行相關防治工作使得數據指標常態高水位,影響的用戶以及發生次數都很不樂觀。
在治理之初,卡頓周均影響用戶比例達到 10‰ 左右,受影響的用戶平均 5 次卡頓,ANR 影響用戶則常態高水位保持在 6‰ 左右,受影響用戶平均 ANR 2 次,這些數據在公司的各大 APP 中都排名很差。
對問題進行篩查發現,問題呈現頭部集中,整體分散的現象,TOP 2 問題占總體的 30%,其余問題零零散散的分布在 60000 個不同的堆棧聚合上,觀察這些不同的堆棧聚合,TOP 2 問題落在了系統的堆棧上,同時很多小量級聚合并非直觀上的耗時點,這些現象給我們初期的治理工作帶來了很大的困擾,占比極大的 TOP 問題優先級最高,但是如何導致,需要如何優化,分散在 60000 個堆棧聚合上的問題應該如何切入。
另外,長期以來缺乏有效的增量問題防治能力。在開發、測試階段沒有專項測試,問題很少暴露,也缺乏持續跟進計劃和問題復現定位能力,在灰度、線上等用戶場景下報警策略單一,只有新增堆棧聚合情況下才會觸發報警,實際運行中發現報警策略很少觸發,大多情況下也無法消費。
卡頓 & ANR 檢測機制及問題
首先,我們來看一下 TOP 2 問題的堆棧表現。
TOP 2 問題聚合到了 nativePollonce 和 nSyncAndDrawframe 系統堆棧上,占比分別達到了 20% 和 10%,nativePollonce 是主線程消息機制下的消息分發函數,nSyncAndDrawframe 是頁面的基礎繪制函數,直觀上看沒有問題,對此我們在初期進行了一系列的常規分析和嘗試。以 TOP 1 的 nativePollonce 為例。首先,常規懷疑該方法本身耗時,分析了方法在 Java 層和 native 層的執行邏輯。
看到在 native 層有 epoll_wait 調用,通過在 C++ 層 hook 相關方法驗證,沒有發現問題。接著我們在 Java 層構造一個一直存在的 idleTask,使得消息隊列空閑時就執行 idle 任務而不休眠,驗證發現問題仍然存在。再看 Java 層邏輯。
這部分是關于同步屏障的處理,異步更新 UI 可能會導致同步屏障出現多線程問題而無法移除,驗證后排除該可能。調查至此,并沒有找到該問題的明確原因,排名第二的 nSyncAndDrawframe 的問題與此類似,經過埋點調查,很多 nSyncAndDrawframe 問題下的調用鏈路并不耗時。
此時,我們回過頭來看一下目前的監控機制。對于卡頓 & ANR 的檢測和分析,長期以來我們依賴于 NPTH 工具提供的能力。對于卡頓的監控,采用攔截消息調度流程,在消息執行前埋點計時,當耗時超過閾值時,則認為是一次卡頓,會進行堆棧抓取和上報工作。ANR 的監控則是通過定時輪詢,在線程中每 500ms 定時和 AMS 進行交互,通過 AMS 的 Error 信息來判斷是否發生 ANR,當確定發生 ANR 時,進行堆棧的抓取和信息的上報。
在實際分析解決問題時,以上不論卡頓還是 ANR,在現有檢測機制下獲取到的堆棧及其他信息都存在一定的缺陷,無法有效解決問題。
對于卡頓,由于是以 Message 為維度進行檢測,當檢測到 Message 超時發生卡頓時,拿到的堆棧是從 Message 開始到當前執行 Method 的堆棧鏈路。實際上 Message 中可能執行了幾千個 Method,耗時點很可能是 Message 中的另外 Method 或者多個 Method 耗時堆積導致 Message 超時,這一點我們無法確認。因此知道 Message 耗時對我們排查問題幫助很小,我們還是無法定位到具體的可消費的耗時點,這就導致當前的卡頓數據無法快速消費。
對于 ANR,抓棧時機是定時輪詢有 ANR 發生才進行的。一方面從發生 ANR 到開始抓棧到抓棧完成都有一定的時間間隔,除了少部分循環等待、鎖等待等卡住場景能夠相對準確抓到,大部分問題抓到堆棧和問題現場不匹配,堆棧會落在耗時點之后的調用鏈路上。另一方面對于那種多次耗時累積導致 ANR 的情況,單點的堆棧也無法定位問題。
在此基礎上,我們接入了調度時序圖,調度時序圖就是主線程 MessageQueue 中的 Message 執行情況,包括已執行 Message、當前 Message 和待執行 Message,可以在 ANR 發生時一起上報。我們借助調度時序圖來看 nativePollonce 聚合下的 case:
可以看到,前邊有 Message 耗時 42s,而上報的堆棧當前 Message 耗時很少,ANR 和當前 Message 沒有關系。
這是一個多次耗時累積導致 ANR 的問題,同樣當前 Message 耗時很少。借助調度時序圖我們可以得出結論,nativePollonce 這類問題很可能和當前堆棧沒有關系,聚合在 nativePollonce 是因為消息調度是執行頻率最高的函數,抓棧時堆棧落到 nativePollonce 的頻率是最高的,這個堆棧信息對于我們解決問題是無用的,那么是否可以借助調度時序圖解決問題呢?
很遺憾,也不可以。調度時序圖展示的是 Message 級的耗時情況,類似卡頓,我們即使知道了哪一個 Message 耗時,但 Message 中執行的 Method 非常多,而且很多都是系統級 Message,我們無法定位具體是哪些 Method 耗時。另外,單點的堆棧也無法定位問題,這種情況無論當前 Message 是否耗時都無法定位問題,因為問題的原因和已執行的耗時 Message 是息息相關的。
經過以上從原理分析和初期的案例調研,我們確認了基于目前的卡頓和 ANR 機制及工具,無法獲取正確的問題堆棧聚合,對于多次耗時導致的問題更是無從下手,無法有效定位問題和解決問題。
增量問題的防治
在優化工作中,新增問題的防治和存量問題的治理同樣重要,只有堵住新增問題,線上的情況才會隨著存量問題解決越來越好。
關于增量問題,之前并無有效的防治手段,僅有的線下測試和灰度/線上新增問題線上報警也收效甚微。線下測試主要是開發測試階段針對功能的測試以及一系列自動化測試,這些測試并非針對卡頓 & ANR 等性能問題設計,對相關問題敏感度和關注度不夠,同時機型和觸達的場景不足使得暴露出的問題很少,而且缺乏必要的分析能力和分析工具,現場可用信息很少。
而灰度/線上新增問題報警策略,準確率和可消費性都不高。只有出現新的堆棧聚合才會觸發報警,而通過對現狀的分析可知,除了個別死鎖、循環等待等 case 外,大部分 case 的堆棧具有很大的隨機性,要么落到 nativePollOnce/nSyncAndDrawframe 等無法消費的系統堆棧,要么分散到各類其他業務堆棧,分析人員拿到的信息大都是不置信的,這樣很可能發生這樣的情況:
總之,抓棧隨機性決定了我們無法定位真實原因,也就無法確定新增問題的有效性,這就使得很多新增問題被帶入線上,我們也在這種低效的惡性循環中不斷重復。
我們的訴求
經過以上的分析和調研,我們的痛點可以歸結為以下三類:
現有的問題現場堆棧對于由于抓棧的不準確,無論對單點問題排查還是多段耗時問題排查都意義不大,無法正確的還原現場信息,這使得我們的問題排查優化進展緩慢,甚至偏離正常的方向?,F有的卡頓及調度時序圖等工具都是以 Message 為統計粒度,無法提供真正可優化的耗時定位,而現有的以 Method 為統計粒度的工具由于性能和穩定性問題都只能運行在線下。為此,我們希望有一套能夠高效運行在線上的 Method trace 工具,用于卡頓及 ANR 的檢測,以 Method 耗時為統計粒度,獲取卡頓/ANR 時用戶當前和之前一段時間內的 Method 執行耗時情況,這樣我們可以完整的呈現問題發生時刻以及之前一段時間的 Method 執行耗時情況,高效清晰的定位問題癥結所在。
針對增量問題的防治,由于現有的能力無法識別問題是否新增,導致在錯誤的方向上耗費太多精力,而真正的問題無法被發現從而帶入線上,為此我們需要搭建增量問題的防治體系,去體系化前置化的完成增量問題的監控、有效信息的提供、問題的分發,前置化預防才能避免問題被帶入線上,體系化才能更高效更全面的最大限度發現問題,同時將增量問題的防治體系建設和問題監控解決能力建設結合起來,建立一個自動化、前置化、發現問題全面、易消費、分發及時的的全鏈路體系。
監控體系建設
在目前的監控體系下,堆棧抓取不準確,堆棧聚合存在問題,大量聚合在了無意義的堆棧上,現有的工具體系下,分析成本極高,大多數問題無法得到有效消費,卡頓和 ANR 指標長期高位,這就要求我們盡快找到破解之法。
誠然,最終導致彈出 ANR 彈窗的誘因很多,但是歸根結底,根本原因都是執行超時,而我們最需要關注的也是那些耗時較高的 Method,當 Method 耗時減少后,相應的觸發 ANR 的幾率也會隨之減少,為此我們就需要找出那些真正耗時卡頓的地方并對其進行優化。
針對以上的痛點和訴求,我們重新梳理了思路,對比了現有方案的優缺點后,取長補短,開發了基于 Method 的高性能線上 trace 工具。在此基礎上,我們針對 ANR、卡頓進行了方案升級和全方位的體系建設。
基于 Sliver 的 ANR 治理方案介紹
針對 ANR,我們希望獲取到發生 ANR 時前一段時間的堆棧記錄,以快速的找出發生耗時的 Method 調用堆棧。
Sliver 采用采樣的方式來定時獲取堆棧,我們在 APP 啟動時打開 Sliver 的監控能力,根據不同機型傳入不同的采樣值,通常在低端機采樣值會大一些,在高端機采樣值會小一些,這樣最大限度降低獲取 trace 本身對性能的影響,Sliver 定時抓取堆棧,并對獲取到的堆棧做 diff 聚合、緩存以區分不同堆棧的關系。同時,通過 NPTH 的接口注冊 ANR 的回調,當發生 ANR 時,回調函數中將緩存的堆棧 dump 到文件,同時將文件隨 ANR 其他信息上報到 Sladar,這樣我們就可以在對 case 的分析中使用精確的 trace 信息問題定位,下圖說明了針對 ANR 的整體工作流程。
我們將這一套流程運行起來,收集了相關 case,在同一個 case 拿到相關信息對比。
以上三個圖是同一個 case 中的不同信息,分別是堆棧、調度時序圖、trace,通過 trace 能清晰看出問題的原因所在。
目前該方案已在線下、灰度、眾測渠道常態開啟,作用明顯,如下:
整體上,該方案的上線,使得我們能夠更清晰準確的定位問題原因,加快問題的流轉解決,促進各類隱藏較深問題的快速解決。
卡頓問題的防治方案
不同于 ANR 問題,卡頓問題的標準是我們自己定義的,卡頓以及多次卡頓的疊加是導致 ANR 以及影響性能的大項,現有的卡頓監控只能拿到單一的堆棧鏈路,無法完整還原當前卡頓產生現場全貌,基于此我們設計了基于 Sliver trace 的卡頓監控體系。
先看整體流程圖:
主要包含兩個方面:
在監控卡頓時,首先需要打開 Sliver 的 trace 記錄能力,Sliver 采樣記錄 trace 執行信息,對抓取到的堆棧進行 diff 聚合和緩存。
同時基于我們的需要設置相應的卡頓閾值,以 Message 的執行耗時為衡量。對主線程消息調度流程進行攔截,在消息開始分發執行時埋點,在消息執行結束時計算消息執行耗時,當消息執行耗時超過閾值,則認為產生了一次卡頓。
當卡頓發生時,我們需要為此次卡頓準備數據,這部分工作是在端上子線程中完成的,主要是 dump trace 到文件以及過濾聚合要上報的堆棧。分為以下幾步:
之后,將 trace 文件和堆棧一同上報,這樣的特征堆棧提取策略保證了堆棧聚合的可靠性和準確性,保證了上報到平臺后堆棧的正確合理聚合,同時提供了進一步分析問題的 trace 文件。
上線后,我們通過和原卡頓體系進行效果對比:
以上三圖分別是,針對高斯模糊問題的原卡頓列表、現在卡頓列表、trace 。在原先的卡頓上報列表中,問題分散到了不同的堆棧中,這是由于發生卡頓時抓棧隨機,而現在的卡頓列表聚合到了單一的堆棧鏈路中,這是由于我們取每一層堆棧中耗時最長的函數組合成特征堆棧,通過trace也可以驗證特征堆棧的有效性,能夠更準確的定位問題原因。同時,trace 詳細的展示了函數調用鏈路,提供了深入分析問題的能力。
經過 trace 和堆棧驗證,該方式輸出的卡頓信息,堆棧聚合更加契合真正的卡頓點,當然一個 Message 中可能有多個大大小小的耗時函數存在,trace 文件的存在能夠更全面的還原現場情況,二者的結合才能更好的解決問題。
目前卡頓檢測體系已經在眾測及線下自動化常態運行,產出數據來看均為線上存在問題。
前置發現能力建設
基于 Sliver 能力的卡頓和 ANR 檢測方案,能夠極大提高解決問題的效率,接下來我們需要考慮如何將這兩種能力常態的運行起來,服務于我們的日常存量問題、增量問題的防和治,尤其是將問題的暴露階段提前,減少對用戶的影響尤為重要。為此,我們進行了以下幾個方向的建設。
目前,測試平臺提供了一些自動化測試 job,這些 job 大多以遍歷方式自動的測試 APP 的功能,對所有功能優先級一樣的觸達,我們將我們的 ANR 檢測能力和卡頓檢測能力進行集成打包,觸發自動化 job,產出相關的卡頓和 ANR case。
分析對比這些 case 后發現,線下上報的 TOP 問題和線上問題差異較大,不符合用戶真實的使用場景。線下檢測出的一些量級較大的 case 在線上場景出現的量級很小,影響的用戶很少,而線上一些影響用戶較多的 case,線下檢測卻上報很少。分析這是由于遍歷式的測試方案不符合真實的用戶行為,這會使我們在推動解決問題中優先級錯誤,無法及時正確辨別那些真正量級高、影響用戶多、優先級高的問題,影響整體的優化節奏。
為此,我們接入了更智能的基于用戶行為的測試策略,產出了更符合用戶真實行為的智能測試 job,基于此 job 進行卡頓和 ANR 數據收集,采樣分析相關數據符合線上數據分布,在量級和影響用戶量級分布上更接近真實的用戶場景,得到正確的問題優先級。
同時利用測試平臺接口,我們構建了完全自動化的測試機制:基于最新 release 分支定時觸發打包平臺打包 -> 配置渠道為性能測試專用渠道 -> 成功后執行自動化測試生成數據。
線下的自動化測試畢竟受機型、場景等條件限制,不易發現一些用戶個性化問題。為此,在線上進行問題檢測顯得尤為重要。beta_version 和灰度渠道都是真實的用戶渠道,能夠覆蓋各種場景,但二者又有所不同,beta_version 用戶較少但活躍度更高。為此我們在 beta_version 渠道集成了卡頓和 ANR 數據的收集方案。同時,灰度渠道由于用戶數多,可以提供更全面的場景和用戶,我們也在灰度渠道集成了 ANR 方案,不過由于卡頓發生的頻率相對較高,考慮到灰度用戶多的特點,我們暫未開啟灰度渠道的卡頓采集。
很多時候需要對線上用戶遇到的問題進行動態調查,相關調查能力雖然完備,但出于包大小的考慮很多時候并不會帶到線上。針對此類問題,需要有一種類似于補丁但又相對輕量的方案,能夠動態的下發能力到用戶的手機上。
為了提高西瓜 Android 客戶端的動態調查能力,將所有的通用能力封裝成一個模塊,通過統一的接口進行調度與事件分發,結合插件化下發加載能力,實現精準下發調查能力到任意手機上。
在實現上,整體流程如下圖:
可以分為宿主、插件、組件三部分來看:
基于此框架,我們可以根據需求以動態下發插件的方式下發攜帶不同能力的插件包,同時利用 Setting 控制宿主執行相應的操作,完成動態的定向下發特定能力到特定手機或某類渠道的能力,這有以下優點:
目前,我們已將多種問題調查能力進行了集成,為線上問題調查和修復提供了支撐。
卡頓數據的消費鏈路建設
以上部分從線上、線下、動態能力角度結合卡頓 & ANR 方案進行了全方位的運行,產出了易消費可消費的數據,接下來我們需要完善消費流程,提高問題的解決效率。
針對產出的數據,我們通過輕服務進行數據處理,根據 apm_open 開放接口,我們可以拿到 job 對應的卡頓& ANR 數據列表,遍歷列表,將每一個 case 的相關信息進行拼接,尤其是卡頓的 trace 文件鏈接,避免了文件下載鏈路較長的弊端,降低優化成本,之后將這些信息分發到對應的跟進群中。同時,在 Sladar 上根據對應的代碼修改人或模塊 owner 指定 owner 跟進。效果如下:
同時,針對需要獲取大量 trace 文件進行分析的場景,我們也開發了本地工具,便捷批量拉取 trace 文件。
總的來說,西瓜從基礎工具的開發到在此之上卡頓 & ANR 方案的優化到線上線下動態前置發現能力建設再到最終的消費鏈路,完成整個卡頓 & ANR 監控體系的閉環,在存量問題解決、增量問題防治、單點問題跟進、整體性能治理上發揮了重要作用。
典型案例介紹
堆棧聚合錯誤案例
對 TOP 1 的 nativePollonce 問題撈取多個 trace 樣本進行分析,堆棧表現如下:
通過 trace 看出其實是主線程在執行數據庫操作,快速推動解決。
通過 trace 看出其實是 ClassLoader. 執行了 20+s,查看源碼,發現是 PluginClassLoader.->....dex2oat....->Runtime.exec 這樣一個調用鏈路。
基于以上的堆棧,我們知道該問題是在加載插件時,驗證 oat 文件不通過而觸發主線程 dex2oat 操作導致。因此我們提前在插件 Plugin 實例初始化時,判斷 oat 文件是否有效,無效的話中斷插件狀態機,置為不可用,同時異步重新生成 dex2oat 產物。
通過 trace 清晰看出是直播插件內部的初始化耗時嚴重導致問題,而非上報的堆棧分析發現,觸發主要發生在插件加載成功的回調中,基于現在的插件框架,插件的加載主要有兩條路徑:
為此,我們從兩個方面進行了優化:
非常規案例
有一類這樣的問題,看堆棧發生在 JSonObject clone = new JSonObject(origin.toString),在其中的浮點類型轉換時。
看到這個堆棧的第一印象是該方法并不耗時,堆棧偏移,然后拿到對應的 trace 可以看到,確實是當前方法非常耗時導致。
看 trace 的最下層,都是重復的位計算,推測是一個超級長的 double 類型數字導致的運算過長,在灰度上收集對應的 json 發現其中無此類數據,推測是 toString 的時候,會把存放的 double 數據轉成 string,然后 new 的時候又把 string 轉成 double,這兩次轉換可能會出現精度問題,造成 double 的值變成了 1.9999999999999999999999 這種很長的數,然后計算耗時很長,導致 ANR。
為此,將上述 JSonObject clone = new JSonObject(origin.toString) 邏輯修改為遍歷 origin 內容復制拷貝,驗證后此問題消失。
一類問題看堆棧報在了 HashMap.remove 方法中。
同樣,看到該堆棧,第一反應是當前方法并不耗時,堆棧偏移導致,然而拿到對應的 trace 后,我們發現確實是當前方法導致的耗時。
從 trace 可以明顯看出,確實在 HashMap.remove 中卡了 40+s,結合 cpu 負載情況看,也并不是得不到調度導致。
深入分析,發現在多線程操作 HashMap 時,若發生擴容,可能會產生循環鏈表,進而觸發死循環,最終采用 ConcurrentHashMap 后解決該問題。參見:https://www.jianshu.com/p/c72af03abba5。
卡頓案例
有一類動畫問題,動畫本身是個簡單的閃光動畫,內部沒有復雜邏輯,關于其耗時的可信程度存疑,但是借助 trace 圖可以看到:
確實動畫存在嚴重的耗時情況,清晰的展示耗時點。此時再看原先卡頓監控和現在卡頓監控的堆棧聚合效果:
上邊兩圖分別表示了原卡頓監控和現在卡頓監控的堆棧聚合效果,可以看到原卡頓監控的堆棧聚合到了多個不同的堆棧鏈路下,這也是因為其抓棧的隨機性,這樣會使我們分散精力,也無法確定問題真實原因,而在我們現有的卡頓體系下,堆棧高度精確聚合到唯一的堆棧鏈路上,借助 trace 信息,也可以驗證堆棧的準確性?;诖耍覀兛梢跃_定位和分析問題。
有一種情況,如果一個 Message 中每一層調用中的函數都非常耗時,那么就會有多個聚合,此時的每一個聚合都是一個真實耗時鏈路,對應的 trace 如下:
可以看到,所有函數的耗時一目了然,這樣可以清晰明確問題之所在,找準優化方向。
以上案例介紹,展示了我們在卡頓和 ANR 方面調查能力的提升,大大提升了我們在問題解決及防治上的能力,解決了長期以來制約我們提升性能的瓶頸,為長期的發展提升提供了支撐。
卡頓 & ANR 后續規劃
過去一段時間的監控建設和治理工作取得了不錯的成果,但是仍然存在許多問題,主要有以下幾類:
基于這些仍然存在的問題,接下來,我們考慮做以下幾方面的工作:
總結
在上面我們整體介紹了過去一段時間西瓜 APP 的體系建設和治理工作,全方位的展示了我們的思考和各項短板的建設,并使之成功用于優化實踐和常態問題防治,80% 以上的卡頓和 ANR 問題能夠準確還原現場信息,線上嚴重影響用戶體驗的問題得到了很大程度的緩解,同時有效遏制了新增問題被帶入線上,為西瓜長期常態性能問題防治提供了參考。
加入我們
歡迎加入字節跳動西瓜視頻客戶端團隊,我們專注于西瓜視頻 App 的開發和基礎技術建設,在客戶端架構、性能、穩定性、編譯構建、研發工具等方向都有投入。如果你也想一起攻克技術難題,迎接更大的技術挑戰,歡迎加入我們!
西瓜視頻客戶端團隊正在熱招 Android、iOS 架構師和研發工程師,最 Nice 的工作氛圍和成長機會,各種福利各種機遇,在北京、杭州、上海、廈門四地均有職位,歡迎投遞簡歷!聯系郵箱:liaojinxing@bytedance.com ;郵件標題:姓名-西瓜-工作年限-工作地點。