APM-卡頓監控

卡頓緣由

主要是主線程阻塞。在開發過程當中,遇到的形成主線程阻塞的緣由多是:html

  • 主線程在進行大量I/O操做:爲了方便代碼編寫,直接在主線程去寫入大量數據
  • 主線程在進行大量計算:代碼編寫不合理,主線程進行復雜計算
  • 大量UI繪製:界面過於複雜,UI繪製須要大量時間
  • 主線程在等鎖:主線程須要得到鎖A,可是當前某個子線程持有這個鎖A,致使主線程不得不等待子線程完成任務。
  • ......

業界調研

微信團隊(Matrix)

卡頓檢測流程圖

主線程卡頓表現

  1. FPS下降
  2. CPU佔用率很是高
  3. 主線程RunLoop執行時間過長

監控方法

Matrix 卡頓監控在 RunLoop 的起始最開始和結束最末尾位置添加 Observer,從而得到主線程的開始和結束狀態。卡頓監控起一個子線程定時檢查主線程的狀態,當主線程的狀態運行超過必定閾值則認爲主線程卡頓,從而標記爲一個卡頓。ios

採用兩個準則:git

  • 單核CPU 佔用超過了80%
  • 主線程 RunLoop 執行了超過2秒

微信公開使用的卡頓監控中,主程序 Runloop 超時的閾值是 2 秒,子線程的檢查週期是 1 秒。每隔 1 秒,子線程檢查主線程的運行狀態;若是檢查到主線程 Runloop 運行超過 2 秒則認爲是卡頓,並得到當前的線程快照。同時,微信團隊也認爲 CPU 太高也可能致使應用出現卡頓,因此在子線程檢查主線程狀態的同時,若是檢測到 CPU 佔用太高,會捕獲當前的線程快照保存到文件中。目前微信應用中認爲,單核 CPU 的佔用超過了 80%,此時的 CPU 佔用就太高了。github

檢測策略

  • 內存 dump:每1秒檢查一次,若是檢查到主線程卡頓,就將全部線程的函數調用堆棧 dump 到內存中。
  • 文件 dump:若是內存 dump 的堆棧跟上次捕捉到的不同,則 dump 到文件中;不然按照斐波那契數列將檢查時間遞增(1,1,2,3,5,8…)直到沒有遇到卡頓或卡頓堆棧不同。這樣可以避免同一個卡頓寫入多個文件的狀況,也能避免檢測線程圍着同一個卡頓空轉的狀況。

退火算法

爲了下降檢測帶來的性能損耗,爲檢測線程增長了退火算法:算法

  • 每次子線程檢查到主線程卡頓,會先得到主線程的堆棧並保存到內存中(不會直接去得到線程快照保存到文件中);
  • 將得到的主線程堆棧與上次卡頓得到的主線程堆棧進行比對:
    • 若是堆棧不一樣,則得到當前的線程快照並寫入文件中;
    • 若是相同則會跳過,並按照斐波那契數列將檢查時間遞增直到沒有遇到卡頓或者主線程卡頓堆棧不同。 這樣,能夠避免同一個卡頓寫入多個文件的狀況;避免檢測線程遇到主線程卡死的狀況下,不斷寫線程快照文件。

耗時堆棧提取

子線程檢測到主線程 Runloop 時,會得到當前的線程快照當作卡頓文件,但當前的主線程堆棧不必定是最耗時的堆棧,不必定是致使主線程超時的主要緣由。Matrix 卡頓監控經過主線程耗時堆棧提取來解決這個問題。 卡頓監控定時獲取主線程堆棧,並將堆棧保存到內存的一個循環隊列中。以下圖,每間隔時間 t 得到一個堆棧,而後將堆棧保存到一個最大個數爲 3 的循環隊列中。有一個遊標不斷的指向最近的堆棧。 微信的策略是每隔 50 毫秒獲取一次主線程堆棧,保存最近 20 個主線程堆棧。這個會增長 3% 的 CPU 佔用,內存佔用能夠忽略不計。bash

當主線程檢測到卡頓時,經過對保存到循壞隊列中的堆棧進行回溯,獲取最近最耗時堆棧。 以下圖,檢測到卡頓時,內存的循環隊列中記錄了最近的20個主線程堆棧,須要從中找出最近最耗時的堆棧。Matrix 卡頓監控用以下特徵找出最近最耗時堆棧:微信

  • 以棧頂函數爲特徵,認爲棧頂函數相同的即整個堆棧是相同的;
  • 取堆棧的間隔是相同的,堆棧的重複次數近似做爲堆棧的調用耗時,重複越多,耗時越多;
  • 重複次數相同的堆棧可能頗有多個,取最近的一個最耗時堆棧。 得到的最近最耗時堆棧會附帶到卡頓文件中。

卡死檢測流程以下網絡

堆棧分類方法

卡頓監控須要仔細定義本身的分類規則。能夠是從調用堆棧的最外層開始歸類,或者是取中間一部分歸類,或者是取最裏面一部分歸類。各有優缺點:數據結構

  • 最外層歸類:可以將同一入口的卡頓歸類起來。缺點是層數很差定,可能外面十來層都是系統調用,也有可能第一層就是微信的函數了。
  • 中間層歸類:可以根據事先劃分好的「特徵值」來歸類。缺點是「特徵值」很差定,若是要作到自動學習生成的話,對後臺分析系統要求過高了。
  • 最內層歸類:可以將同一緣由的卡頓歸類起來。缺點是同一分類可能包含不一樣的業務。

微信採用了最內層歸類的優化版,亦即進行二級歸類。 第一級按照最內倒數2層歸類,這樣可以將同一緣由的卡頓集中起來; 第二級分類是從第一級點擊進來,而後從最內層倒數4層進行歸類,這樣可以將同一緣由的不一樣業務分散歸類起來。ide

可運營

灰度收集到的結果是用戶平均天天會產生30個 dump 文件,壓縮上傳大約要 300k 流量。預計正式發佈的話會對後臺有比較大的壓力,對用戶也有必定流量損耗。因此必須進行抽樣上報。

  • 抽樣上報:天天抽取不一樣的用戶進行上報,抽樣機率是5%。
  • 文件上傳:被抽中的用戶1天僅上傳前20個堆棧文件,而且每次上報會進行多文件壓縮上傳。
  • 白名單:對於須要跟進問題的用戶,能夠在後臺配置白名單,強制上報。 另外,爲了減小對用戶存儲空間的影響,卡頓文件僅保存最近7天的記錄,過時刪除。

美團Hertz

卡頓檢測方法

很容易想到經過檢測FPS就能夠知道App是否發生了卡頓,也可以經過一段連續的FPS幀數計算丟幀率來衡量當前頁面繪製的質量。然而實踐發現FPS的刷新頻率很是快,而且容易發生抖動,所以直接經過比較經過FPS來偵測卡頓是比較困難的。而檢測主線程消息循環執行的時間就要容易的多了,這也是業內經常使用的一種檢測卡頓的方法。所以,Hertz在實踐中採用的就是檢測主線程每次執行消息循環的時間,當這一時間大於閾值時,就記爲發生一次卡頓。

解決卡頓連續性耗時策略

有的卡頓連續性耗時較長,例如打開新頁面時的卡頓;而有的卡頓連續性耗時相對較短但頻次較快,例如列表滑動時的卡頓。所以,採用了「N次卡頓超過閾值T」的斷定策略,即一個時間段內卡頓的次數累計大於N時才觸發採集和上報:例如卡頓閾值T=2000ms、卡頓次數N=1,能夠斷定爲單次耗時較長的卡頓;而卡頓閾值T=300ms、卡頓次數N=5,能夠斷定爲頻次較快的卡頓。

dump堆棧和運行日誌

第一個問題是堆棧抓取的時機。抓取堆棧的時機必須是在卡頓發生當時,而不是以後,不然不能準確抓到形成卡頓的代碼,所以在子線程中當卡頓尚未結束時抓取堆棧。 第二個問題是堆棧如何歸類,卡頓堆棧的歸類和Crash堆棧不一樣,以最內層代碼歸類顯然是不合適的,由於外層不一樣的業務邏輯代碼在最內層的調用堆棧有多是相同的。以最外層代碼歸類也是不合適的,由於最外層代碼有多是業務邏輯代碼,也有多是系統調用。採用最內層歸類的原則,並匹配一些簡單的規則,以命中規則的類名來歸類。

Bugly

檢查卡頓的依據和上報時機(bugly.qq.com/docs/user-g… 依據是監控主線程 Runloop 的執行,觀察執行耗時是否超過預約閥值(默認閥值爲3000ms) ,若是監控到卡頓時會當即記錄線程堆棧到本地,在App從後臺切換到前臺時,執行上報。

Wedjat

如何監控卡頓

  • FPS 監控:這是最容易想到的一種方案,若是幀率越高意味着界面越流暢,上文也給出了計算 FPS 的實現方式,經過一段連續的 FPS 計算丟幀率來衡量當前頁面繪製的質量。
  • 主線程卡頓監控:這是業內經常使用的一種檢測卡頓的方法,經過開闢一個子線程來監控主線程的 RunLoop,當兩個狀態區域之間的耗時大於閾值時,就記爲發生一次卡頓。 一樣是採用主線程卡頓監控方案。

MTHawkeye

MTHawkeye是美圖開源的卡頓監控,看了源碼設計思想跟微信的Matrix差很少,不過有不少技術的沉澱,應該是爲美圖定製,設計很值得研究。

調研結論

業界大部分的監控方案大同小異,基於監聽RunLoop的通知狀態,開啓常駐子線程定時檢測主線程的RunLoop狀態切換是否存在超時,超時則記爲一次卡頓,當卡頓時長超過設定的閾值dump堆棧,進行相關策略處理以後在合適的時間上報。Matrix爲騰訊最新的開源庫,其堆棧處理策略較好,目前備受歡迎。

方案對比

FPS

FPS(Frames Per Second)表示頁面每秒的幀數,FPS越高代表頁面越流暢,值50~60之間是比較流暢的,反之低於會卡頓。FPS經過藉助CADisplayLink在一個週期的計數間接表示。例外,根據可滑動界面在滑動狀態RunLoop由kCFRunLoopDefaultMode切換成UITrackingRunLoopMode能夠區分頁面不流暢產生的場景是否在滾動過程。

CADisplayLink是以跟IOS設備相同屏幕刷新頻率(每秒60幀)的定時器,經過添加一個target和綁定selector,以NSRunLoopCommonModes模式將該定時器註冊到RunLoop,屏幕收到每一幀的刷屏通知同時調用target綁定的selector計數操做,獲取時間戳大於1秒的計數爲當前頁面的FPS。

Ping-Pong

實現原理:ping經常使用於測試網絡測試數據包可否到達ip地址進而測試網絡應答。固然用來監控卡頓監控,主要的核心思想是開啓子線程維護一個ping定時器,經過固定時間片斷ping主線程(發送一個通知),若是主線程不是繁忙狀態會收到通知並pong迴應(回送一個通知給子線程),不然子線程超過設定的pong定時閾值,沒有收到主線程pong回覆則斷定爲是卡頓了,而後dump堆棧下來。

監聽RunLoop

實現原理:開啓一個子線程,監聽RunLoop的通知狀態,若是在設定的卡頓閾值時間內沒有收到RunLoop的通知狀態,那麼就斷定爲主線程卡頓了,而後dump堆棧,反之沒有卡頓,能夠記錄卡頓的頻次,到達必定的頻次再上報。

Hook objc_msgSend方法

oc每一個方法的調用最終都是轉成objc_msgSend方式通知消息,經過維護一個數據結構統計每一個方法的調用時長進行性能分析,可是這樣很是損耗性能,維護成本比較高,不推薦使用。

最後,決定採起監聽RunLoop的方式,參考Matrix和MTHawkeye。

方案實現

檢測流程圖

設計與實現

設計原理

在 RunLoop 的起始最開始和結束最末尾位置添加 Observer,從而得到主線程的開始和結束狀態的耗時。卡頓監控起一個子線程定時檢查主線程的狀態(默認200ms),當主線程的狀態運行耗時超過必定閾值(默認400ms)則認爲主線程卡頓,從而標記爲一個卡頓。若是卡頓時長超過8秒,則斷定爲卡死。卡頓閾值和檢測線程的週期直接影響卡頓監控的能力和性能損耗。

堆棧快照

系統提供task_threads方法獲取task的全部線程,每個線程的信息能夠經過thread_get_state方法獲取到,信息填充在 _STRUCT_MCONTEXT 類型的參數中,經過這個參數能夠取到當前線程的Stack Pointer和Frame Pointer,而後回溯整個函數的調用棧找到全部函數的地址,經過偏移計算出物理地址,最後再進行符號化取得函數名。

堆棧去重

採用了退火算法一部分過濾連續相同堆棧

ThreadBacktraceSnapshot *mainBacktraceSnapshot = [self generateBacktraceSnapshot:dumpType];
 ThreadBacktraceSnapshot *preSnapshot = self.snapshotsArray.lastObject;
if (preSnapshot) {
     if (![preSnapshot.backtraceDescription isEqualToArray:mainBacktraceSnapshot.backtraceDescription]) {
         mainBacktraceSnapshot.capturedCount = self.annealingCount;
         [self.snapshotsArray addObject:mainBacktraceSnapshot];
         self.annealingCount = 1;
     } else {
         self.annealingCount += 1;
     }
} else {
     self.annealingCount = 1;
     [self.snapshotsArray addObject:mainBacktraceSnapshot];
}

複製代碼

斷定卡死

上述的實現方案能夠記錄到卡住的時間,業務能夠定製卡頓多長時間則斷定爲卡死的時間。

記錄卡住時間

當頁面卡頓時長超過卡死的閾值,在這個閾值的基礎上在計時,直到RunLoop進入下一個狀態計時結束,不然會一直等到觸發Watch

提供的API

/**
 設定卡頓閾值和檢測卡頓線程檢測時間間隔啓動監控
 @param runloopTimeOut 卡頓閾值
 @param checkRunLoopTimeOutThreshold 檢測卡頓線程檢測時間間隔
 */
- (void)startWithRunloopTimeOut:(useconds_t)runloopTimeOut andCheckPeriodTime:(unsigned)checkRunLoopTimeOutThreshold;

/**
 採用默認值啓動,卡頓閾值400ms,每隔200ms檢查一次
 */
- (void)start;
- (void)stop;
複製代碼

上報的數據

性能損耗

CPU波動2%-4% Dog監控閾值將APP殺死,用這種方式即便最終發生了閃退也能夠逼近實際的卡死時間,偏差暫未有結論。

優化

目前卡頓監控只是應用在demo中,沒有在線上使用過,應該會有不少問題,例如性能瓶頸、堆棧過濾、退火算法優化等待。

參考資料

  1. mp.weixin.qq.com/s/M6r7NIk-s…
  2. tech.meituan.com/2016/12/19/…
  3. mrpeak.cn/blog/ios-ha…
  4. github.com/aozhimin/iO…
  5. github.com/meitu/MTHaw…
相關文章
相關標籤/搜索