<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          抖音 Android 性能優(yōu)化:新一代全能型性能分析工具 Rhea!

          共 18624字,需瀏覽 38分鐘

           ·

          2021-07-08 01:40

           微信改了推動機(jī)制,真愛請星標(biāo)本公號
          公眾號回復(fù)加入BATcoder技術(shù)群BAT

          本文選自「抖音 Android 性能優(yōu)化」系列文章。

          「抖音 Android 性能優(yōu)化」系列文章是由抖音 Android 基礎(chǔ)技術(shù)部門技術(shù)專家傾力打造的技術(shù)干貨內(nèi)容,和大家分享基礎(chǔ)技術(shù)團(tuán)隊在打造極致用戶體驗的抖音的過程中,收獲的性能優(yōu)化方法論、工具和實踐,與各位技術(shù)同學(xué)一起交流成長。

          用戶交互響應(yīng)的耗時,作為 Android 用戶日常感知最深的一項性能指標(biāo),在日常開發(fā)中有著非常重要的意義。而抖音 Android 基礎(chǔ)技術(shù)團(tuán)隊為打造極致的交互響應(yīng)體驗,一直在致力于極致性能的探索,其中就包括如何打造極致的耗時檢測工具。

          概述

          俗話說,工欲善其事,必先利其器,我們要做好性能優(yōu)化,首要是要能夠發(fā)現(xiàn)性能的問題,這就需要有靠譜的工具來幫助我們做性能分析。市面上主流的性能分析工具有:Systrace、TraceView、Android Studio 的 CPU Profiler。相信做性能優(yōu)化的同學(xué)對這些工具應(yīng)該都是非常熟悉了,抖音最早也是用 Systrace 作為主要的分析工具,在優(yōu)化前期也發(fā)揮了比較大的作用。隨著抖音的性能優(yōu)化來到了深水區(qū),我們需要發(fā)現(xiàn)并解決更細(xì)粒度、更多維度的性能問題,我們會關(guān)注幾毫秒的耗時,關(guān)注線上一些低端機(jī)用戶遇到的鎖阻塞和 IO 等待問題。而市面上這些主流的性能分析工具因其使用的局限性和較大的性能損耗,已經(jīng)無法滿足抖音性能優(yōu)化的需求。為了能夠百尺竿頭更進(jìn)一步,我們需要開發(fā)更加靈活、精細(xì)化以及多元的信息和工具來輔助我們進(jìn)行高效的優(yōu)化工作。

          在這樣的背景之下,抖音 Android 基礎(chǔ)技術(shù)團(tuán)隊開發(fā)了 Rhea( [?ri??] 瑞亞,寓意時光女神)跟蹤器(Tracer),其是一種通過靜態(tài)代碼插樁技術(shù)自動添加 Trace,用來分析 APP 運行時耗時的性能分析工具,意思是要做一個功能全面、追求效率、大家都喜歡的女神,也符合我們工具的核心設(shè)計原則。Rhea 跟蹤器獲取 Trace 不僅要性能損耗低,還要能脫離 PC 端工具在 App 側(cè)直接抓取,跟蹤更多常規(guī)函數(shù)耗時的同時還要可以跟蹤系統(tǒng)調(diào)用,如:鎖信息、I/O 耗時、Binder IPC 以及更多其他信息。最后,還提供轉(zhuǎn)換腳本工具,用于將原始跟蹤文件生成可視化報告,便于用戶分析性能問題。

          優(yōu)勢對比

          Rhea 當(dāng)前因其無侵入、高性能、信息全等優(yōu)勢已在字節(jié)多個 APP 上落地使用,效果明顯,已多次幫助大家快速發(fā)現(xiàn)性能問題,其包含的信息包括不限層級的應(yīng)用層函數(shù)、IO、鎖、Binder、CPU 調(diào)度等耗時信息等,其部分效果如下所示:

          相對于其他 Android 性能排查工具,其具體優(yōu)勢表現(xiàn)為:

          當(dāng)前,Systrace 只能監(jiān)控特定系統(tǒng)信息,監(jiān)控應(yīng)用層的耗時則需要手動打點;TraceView 性能跟采樣率關(guān)系密切,采樣過于頻繁性能開銷巨大,采樣過低又難以精準(zhǔn)發(fā)現(xiàn)問題函數(shù);Nanoscope 雖然幾乎沒有性能損耗,但每次都需定制 ROM 刷機(jī),使用成本非常高,并且這些工具都只支持 debugable 的應(yīng)用程序線下分析,這些工具在針對 APP 性能優(yōu)化都有不甚完美之處,而 Rhea 是一個集大成者,融合了各工具優(yōu)勢并彌補(bǔ)了相關(guān)缺陷。

          架構(gòu)演進(jìn)之路

          第一階段:基于 Systrace 補(bǔ)充函數(shù)耗時 Trace

          Systrace 是 Android 性能調(diào)試優(yōu)化的常用工具,它可以收集進(jìn)程的活動信息,如函數(shù)調(diào)用耗時、鎖等;也可以收集內(nèi)核信息,如 CPU 調(diào)度、IO 活動、Binder 調(diào)用信息等;這些信息會統(tǒng)一時間軸,在 Chrome 瀏覽器中顯示出來,方便工程師性能調(diào)試、優(yōu)化卡頓等工作。因此,抖音早期性能優(yōu)化首選 Systrace 作為主要工具,其大致流程如下:

          1.  功能改造

          Systrace 工具只能監(jiān)控特定系統(tǒng)調(diào)用的耗時情況,它不支持應(yīng)用程序代碼的耗時分析,所以在使用時有一些局限性。原生 Systrace 需要開發(fā)者在方法的起止位置手動加入 Trace.beginSection 與 Trace.endSection 方法對,這個過程就變成了開發(fā)者預(yù)判耗時位置,然后在手動加入監(jiān)控函數(shù)對,通過不斷重復(fù)添加監(jiān)控點、打包、運行、采集數(shù)據(jù),從而一步步完成耗時方法定位,這也使得 Systrace 的使用成本變得極高。

          為了提高 Systrace 的易用性,我們開發(fā)了 Rhea 1.0 對 Systrace 功能進(jìn)行了改造,加入了自動插樁機(jī)制:通過字節(jié)碼插樁自動完成  Trace.beginSection 和 Trace.endSection 方法對的插入,并且通過運行時限制方法層級的方式,來有效控制因引入監(jiān)控帶來的性能損耗。

          • 插樁類及樁方法偽代碼:
          class Tracer{
              method_stack = list()
              max_size = 6

              methodIn(method_id, method_name){
                  if(method_stack.size()<=max_size){
                      method_stack.push(method_id)
                      Trace.beginSection(method_name)
                  }
              }

              methodOut(){
                  if(method_stack.size>0){
                      method_stack.pop()
                      Trace.end()
                  }
              }
          }
          • 被插樁方法:
          method1(){
              Tracer.methodIn(1,method1)
               ...
              Tracer.methodOut()
          }

          輸出數(shù)據(jù)如下所示,指定層級內(nèi)所有方法即可按照預(yù)期展示在輸出 html 中:

          2.  方法 Did not finished 問題

          在使用改造后的 systrace 時,我們時常會遇到如下問題:

          分析發(fā)現(xiàn),主要原因在于方法在運行期執(zhí)行中被中斷,例如:方法執(zhí)行過程中發(fā)生異常后,被其調(diào)用者方法捕獲,發(fā)生異常方法的 Systracer.o 方法未被調(diào)用。如圖:test 方法中的 error 方法執(zhí)行時出現(xiàn) arr[2]的數(shù)組越界,導(dǎo)致 test 方法中的插樁方法 SysTracer.o(13L)未調(diào)用,異常被 onCreate 中的 catch 塊捕獲,從而導(dǎo)致 test 的插樁方法沒有被成對調(diào)用,最終導(dǎo)致了 test 外層所有的方法調(diào)用都無法正確閉合。(注意:本小結(jié)提到的樁方法,即 SysTracer 相關(guān)方法,均是通過字節(jié)碼插樁自動插入

          解決辦法,在外層所有異常捕獲的位置,額外插入樁方法,重新這種異常調(diào)用鏈下的樁方法不成對問題。如下圖:


          3.  依然存在的問題

          • 性能問題

          隨著 Rhea 1.0 功能的深入使用,在帶來極大便利的同時,功能本身的不足也逐漸暴露出來。在采集數(shù)據(jù)過程中,其本身的性能損耗會導(dǎo)致在一些實際性能優(yōu)化過程中會帶偏方向。經(jīng)我們嚴(yán)格測試,其性能損耗有 11.5%左右,如下所示:

          在實際使用過程中發(fā)現(xiàn),在開啟 Systrace 之后,對應(yīng) Sleep 耗時占比在極端情況下會超過 40%以上。一方面是 APP 鎖帶來的 Sleep 耗時。例如,在抖音啟動路徑上 SharedPreference 優(yōu)化過程中,在開啟 Rhea 1.0 的 Systrace 功能后,發(fā)現(xiàn) SP 調(diào)用存在明顯的鎖耗時,當(dāng)時針對 SP 進(jìn)行了一番鎖的優(yōu)化后,上線發(fā)現(xiàn)效果并不明顯,后續(xù)經(jīng)過一系列排查,發(fā)現(xiàn)鎖的耗時是由于開啟 Systrace 功能后導(dǎo)致。另一方面是 IO 帶來的 Uninterrupt Sleep 耗時。例如,我們在一次性能優(yōu)化過程中看到了很多__fdget_pos 操作,對_fdget_pos 操作相對 Uninterruptible Sleep 的占比統(tǒng)計了下,至少占了 Uninterruptible Sleep 總耗時的 60%左右。我們花了比較長時間,額外加了很多 IO 的信息,最終定位原因是在開啟 Systrace 后,由于所有線程的 trace 都會寫入同一個文件,所有線程會同步競爭內(nèi)核態(tài)的文件 pos 鎖導(dǎo)致。工具本身的性能問題誤導(dǎo)了我們的排查方向,同時也暴露了在排查這種 IO Wait 問題的時候由于 IO 信息不全導(dǎo)致排查效率不高的問題。

          • 限制層級導(dǎo)致的調(diào)用缺失

          Systrace 的原理決定了當(dāng)我們在應(yīng)用層插入更多函數(shù)插樁以定位應(yīng)用層耗時問題的時候,會導(dǎo)致非常嚴(yán)重的性能問題,所以我們在線下使用該工具會通過限制插樁層級的方式以減少運行時性能損耗。但是層級的限制使得超過既定層級的函數(shù)調(diào)用數(shù)據(jù)缺失,在分析調(diào)用層級較深的函數(shù)耗時的時候,無法定位到準(zhǔn)確的耗時點。

          • 使用場景限制

          由于 Systrace 在采集數(shù)據(jù)的過程中,需要依賴 PC,對于一些需要脫離 PC 采集數(shù)據(jù)的場景,Systrace 就無法滿足需求了。比如我們產(chǎn)品運營同學(xué)經(jīng)常會在線下場景實地測試抖音的使用性能,例如地鐵、餐館、咖啡廳等,這些實際使用場景下的性能數(shù)據(jù),systrace 就無法支持到了。

          • 低端機(jī)無法正常使用

          我們在針對低端機(jī)進(jìn)行耗時優(yōu)化時,發(fā)現(xiàn)諸如三星、oppo 等一些早期的低端機(jī)型,systrace 也不能支持其數(shù)據(jù)抓取。

          針對以上問題,我們對工具進(jìn)行了深入的探索和優(yōu)化。于是,工具的開發(fā)進(jìn)入了第二階段。

          第二階段:高性能全場景的 Trace 抓取工具

          1.  功能升級

          為了彌補(bǔ)第一階段功能短板,進(jìn)一步提高性能,同時能滿足更多使用場景,我們找到了新的解決方案:在 Java 層,通過記錄方法首末位置時間戳、所在線程等信息,過濾出大于指定耗時閾值的函數(shù)后,將數(shù)據(jù)異步記錄到文件。數(shù)據(jù)采集結(jié)束后,將輸出文件轉(zhuǎn)換成指定格式后,便可通過 SDK 提供的 Systrace 工具轉(zhuǎn)化成方便查看的 Html 格式,從而實現(xiàn)和 Systrace 相同的可視化效果。

          2.  實現(xiàn)原理

          Rhea 2.0 如何采集數(shù)據(jù)并生成和 Systrace 相同可視化效果的 html 呢?SDK 中 Systrace 工具的--from-file 命令可將原始的.trace 格式數(shù)據(jù)轉(zhuǎn)成 html 格式,分析.trace 數(shù)據(jù)內(nèi)部格式:

          多次嘗試后得出結(jié)論,可被 SDK Systrace 工具解析的 .trace 文件需滿足如下格式:

          格式說明:

          • <ThreadName>:線程名,若為主線程,可指定為包名。
          • <ThreadID>:線程 ID。
          • <Time seconds>:方法開始或者結(jié)束的時間,單位 s。
          • <B|E>:標(biāo)記該條記錄為方法開始(B)還是結(jié)束(E)。
          • <ProcessID>:所在進(jìn)程 ID。
          • <TAG>:方法標(biāo)記,字符長度不可超過 127。

          由此可知,Mtrace 采集的數(shù)據(jù)至少需要包含以上內(nèi)容。

          以下則是對應(yīng) Trace 格式:

          depth,methodID,inTime,outTime,threadName,threadID

          相較于 Rhea 1.0 的 Systrace,Rhea 2.0 的 Method Trace 性能損耗有了顯著的降低,性能損耗也由 11.5%下降至 3%,效果如下所示:

          3.  最佳實踐

          • 功能使用

          MTrace 相較于 Systrace,提供了更豐富的線下功能,其中包括:解決針對真實用戶點對點的卡頓耗時問題反饋功能,解決產(chǎn)品、運營、QA 同學(xué)外出走查場景的問題反饋功能。

          總之,不管你在哪,性能反饋都一觸即達(dá)!如圖為完整操作流程。


          • 線上案例

          一個真實的案例:抖音灰度版本線上用戶反饋卡頓,通過 MTrace 功能包實現(xiàn)遠(yuǎn)程卡頓問題分析排查!以下則是通過用戶配合回傳的真實卡頓數(shù)據(jù),經(jīng)過解析即可發(fā)現(xiàn)耗時調(diào)用點:


          4.  存在的問題

          由于這個階段采集的只有 Java 方法層的數(shù)據(jù),在抖音啟動 IO 耗時優(yōu)化工作中,Method Trace 無法提供哪些函數(shù)進(jìn)行了 IO 操作,以及 IO 操作讀取/寫入了哪些文件,給優(yōu)化工作帶來了較大的難度。另外在一些復(fù)雜場景中,Method Trace 只記錄函數(shù)執(zhí)行時長,但是不能準(zhǔn)確定位是由于多線程同步等鎖或者系統(tǒng) IO 導(dǎo)致的執(zhí)行時間變長。

          針對上面的問題,我們意識到一套優(yōu)秀的 Trace 工具還需要融合更多的系統(tǒng)事件,于是工具進(jìn)入了第三階段的打磨。

          第三階段:動態(tài)一體化 Trace 工具規(guī)劃

          Rhea 1.0 和 2.0 在抖音早期的性能優(yōu)化工作中成績顯著,但隨著優(yōu)化工作的深入同時也暴漏諸多局限與不便。

          一方面,使用常規(guī)的 Systrace 工具做性能優(yōu)化,本身有諸多局限性。一是 Trace 信息少,在默認(rèn)情況下,只包含系統(tǒng)預(yù)置的耗時打點信息,并不足以支持常規(guī)的耗時分析需要在 App 側(cè)手動調(diào)用  Trace.beginSection 和 Trace.endSection  方法才能獲取更多函數(shù)耗時信息,為避免影響線上包大小,使用完以后又需手動移除,一上一下事倍而功半。二是 Systrace 本身性能損耗大,特別是應(yīng)用通過插樁的方式對業(yè)務(wù)代碼進(jìn)行大量的打點時,極端情況性能損耗會超過 50%。三是 Systrace 完全依賴 PC 端工具抓取,不夠靈活。尤其是需要能夠穩(wěn)定復(fù)現(xiàn)性能問題的場景,對于一些特定區(qū)域或者特定用戶群體才能復(fù)現(xiàn)的問題無法獲直接高效的取到有效信息,依賴研發(fā)或者測試走查,甚至用戶反饋的部分概率問題即使走查也無法通過 Systrace 獲取到對應(yīng)的信息,從而導(dǎo)致優(yōu)化效率低。

          另一方面,通過簡單定制 Trace 的獲取函數(shù)耗時相較于 Systrace,雖然有顯著的性能提升,和更高的靈活性,但數(shù)據(jù)只包含基本的耗時信息,在部分復(fù)雜場景(如持有鎖引起的耗時),數(shù)據(jù)仍存在局限。

          如上工具都均已無法完全滿足抖音的啟動、首刷以及低端機(jī)等核心場景的性能優(yōu)化工作,我們需要重新設(shè)計和規(guī)劃功能更加強(qiáng)大的動態(tài)一體化 Trace 工具來輔助分析性能。

          1. 該工具要非常靈活,可以不依賴 PC 端的抓取腳本,同時支持線上線下,能夠在應(yīng)用任何想要抓取數(shù)據(jù)的時候運行,作為一個平臺性工具,Rhea 還需要支持動態(tài)擴(kuò)展,支持多種場景的配置和動態(tài)開關(guān),可以將任意需要的信息進(jìn)行采集。
          2. 該工具抓取的 Trace 信息要全面,能夠采集和追蹤包括 ATrace 插樁、等鎖信息、I/O 信息以及 Binder 耗時等在內(nèi)的多種信息。
          3. 要支持可視化,統(tǒng)一的格式進(jìn)行輸出和格式化,最終以兼容 Systrace 的結(jié)果進(jìn)行前端展示和使用,盡量不要改變使用者習(xí)慣。
          4. 性能損耗要低,以免帶偏性能優(yōu)化方向。

          因此,我們重新設(shè)計了新一代 Trace 分析工具:

          整體上,App 通過集成 Rhea SDK 在打包時不限層級插入函數(shù)耗時樁方法,在運行時插入 IO、Binder、Lock 等相關(guān) Trace 信息,支持動態(tài)配置,統(tǒng)一 Trace 格式為 atrace,同時支持獲取系統(tǒng)級別的 Linux ftrace、Android Framework atrace 和 App 插入的 atrace 信息,能夠不依賴 PC 抓取,最終提供可視化顯示。具體實現(xiàn)如下:

          一、不依賴 PC 抓取 Trace

          為了實現(xiàn)不依賴 PC 抓取 Trace,我們有必要先了解下 Android atrace 的實現(xiàn)機(jī)制。首先,是 atrace 包括的數(shù)據(jù)源包括:

          其中,用戶空間的數(shù)據(jù)包括了應(yīng)用層的自定義 Trace、系統(tǒng)層的 gfx 渲染相關(guān) Trace、系統(tǒng)層打的鎖相關(guān)的 Trace 信息等,其最終都是通過調(diào)用 Android SDK 提供的Trace.beginSection或者 ATRACE_BEGIN 記錄到同一個文件點/sys/kernel/debug/tracing/trace_marker 中的。此節(jié)點允許用戶層寫入字符串,ftrace 會記錄該寫入操作時的時間戳,當(dāng)用戶在上層調(diào)用不同函數(shù)時,寫入不同的調(diào)用信息,比如函數(shù)進(jìn)入和退出分別寫入,那么 ftrace 就可以記錄跟蹤函數(shù)的運行時間。atrace 在處理用戶層的多種 trace 類別時,只是激活不同的 TAG,如選擇了 Graphics,則激活 ATRACE_TAG_GRAPHICS,將渲染事件記錄到 trace_marker。

          而內(nèi)核空間的數(shù)據(jù)主要是一些補(bǔ)充的分析數(shù)據(jù) freq、sched、binder 等,常用的比如 CPU 調(diào)度的相關(guān)信息包括:

          • CPU 頻率變化情況
          • 任務(wù)執(zhí)行情況
          • 大小核的調(diào)度情況
          • CPU Boost 調(diào)度情況

          這些信息是 App 可以通過直接讀取/sys/devices/system/cpu 節(jié)點下相關(guān)信息獲得,而另外一部分標(biāo)識線程狀態(tài)的信息則只能通過系統(tǒng)或者 adb 才能獲取,且這些信息不是統(tǒng)一的一個節(jié)點控制,其需要激活各自對應(yīng)的事件節(jié)點,讓 ftrace 記錄下不同事件的 tracepoint。內(nèi)核在運行時,根據(jù)節(jié)點的使能狀態(tài),會往 ftrace 緩沖中打點記錄事件。例如,激活線程調(diào)度狀態(tài)信息記錄,需要激活類似如下相關(guān)節(jié)點:

          events/sched/sched_switch/enable
          events/sched/sched_wakeup/enable

          激活后,則可以獲取到線程調(diào)度狀態(tài)相關(guān)的信息,比如:

          • Running: 線程在正常執(zhí)行代碼邏輯
          • Runnable: 可執(zhí)行狀態(tài),等待調(diào)度,如果長時間調(diào)度不到,說明 CPU 繁忙
          • Sleeping: 休眠,一般是在等待事件驅(qū)動
          • Uninterruptible Sleep: 不可中斷的休眠,需要看 Args 的描述來確定當(dāng)時的狀態(tài)
          • Uninterruptible Sleep - Block I/O: IO 阻塞

          最終,上述兩大類事件記錄都匯集到內(nèi)核態(tài)的同一緩沖中,PC 端上 Systrace 工具腳本是通過指定抓取 trace 的類別等參數(shù),然后觸發(fā)手機(jī)端的/system/bin/atrace 開啟對應(yīng)文件節(jié)點的信息,接著 atrace 會讀取 ftrace 的緩存,生成只包含 ftrace 信息的 atrace_raw 信息,最終通過腳本轉(zhuǎn)換成可視化 HTML 文件。大致流程如下:

          因此,我們基于 Android atrace 的實現(xiàn)原理,我們同步參考了 Facebook 的 profilo 用于在 APP 側(cè)直接獲取 atrace 的方案,實現(xiàn)了不依賴 PC 抓取 Trace 的方法。

          我們通過 dlopen 獲取 libcutils.so 對應(yīng)句柄,通過對應(yīng) symbol 從中找到 atrace_enabled_tags 和 atrace_marker_fd 對應(yīng)指針,從而設(shè)置 atrace_enabled_tags 用以打開 atrace 開關(guān),具體實現(xiàn)如下:


            std::string lib_name("libcutils.so");
            std::string enabled_tags_sym("atrace_enabled_tags");
            std::string marker_fd_sym("atrace_marker_fd");

            if (sdk < 18) {
              lib_name = "libutils.so";
              // android::Tracer::sEnabledTags
              enabled_tags_sym = "_ZN7android6Tracer12sEnabledTagsE";
              // android::Tracer::sTraceFD
              marker_fd_sym = "_ZN7android6Tracer8sTraceFDE";
            }

            if (sdk < 21) {
              handle = dlopen(lib_name.c_str(), RTLD_LOCAL);
            } else {
              handle = dlopen(nullptr, RTLD_GLOBAL);
            }
            // safe check the handle
            if (handle == nullptr) {
              ALOGE("atrace_handle is null");
              return false;
            }

            atrace_enabled_tags_ = reinterpret_cast<std::atomic<uint64_t> *>(
                    dlsym(handle, enabled_tags_sym.c_str()));
            if (atrace_enabled_tags_ == nullptr) {
              ALOGE("atrace_enabled_tags not defined");
              goto fail;
            }

            atrace_marker_fd_ = reinterpret_cast<int*>(
                dlsym(handle, marker_fd_sym.c_str()));

          接下來,我們通過 hook libcutils 動態(tài)庫中的 write、write_chk 方法通過判定 atrace_marker_fd 來將對應(yīng) atrace 信息攔截下來轉(zhuǎn)儲到到本地或上傳到云端分析。實現(xiàn)如下所示:

          ssize_t proxy_write_chk(int fd, const void* buf, size_t count, size_t buf_size) {
            BYTEHOOK_STACK_SCOPE();
            if (Atrace::Get().IsAtrace(fd, count)) {
              Atrace::Get().LogTrace(buf, count);
              return count;
            }

            ATRACE_BEGIN_VALUE("__write_chk:", FileInfo(fd, count).c_str());

            size_t ret = BYTEHOOK_CALL_PREV(proxy_write_chk, fd, buf, count, buf_size);

            ATRACE_END();

            return ret;
          }

          二、提供更加全面 Trace 信息

          1.  鎖耗時

          Java 層的鎖,無論是同步方法還是同步塊,最終都會走到虛擬機(jī)的 MonitorEnter 和 MonitorExit,在 MonitorEnter 中實現(xiàn)了多種鎖狀態(tài)的切換,包括從無鎖到輕鎖,輕鎖中的偏向和重入,出現(xiàn)競爭并超過自旋的次數(shù)之后升級成重鎖分配 monitor 對象,其中 art 現(xiàn)在的自旋不是真的自旋,而是用 sched_yield 主動讓出 CPU 等待下次調(diào)度。

          而我們需要首先關(guān)注的就是出現(xiàn)鎖競爭升級成重鎖后的等待耗時信息,這個信息從 Android 6.x 開始會通過 ATrace 的方式輸出到 trace_marker 中。

          但是想要輕鎖的信息還需要做一些額外的工作,因為是否輸出輕鎖的 ATrace 信息除了 ATRACE_ENABLE 條件之外,還有另外一個 systrace_lock_logging 的開關(guān)變量控制,這個變量是虛擬機(jī)中一個全局變量的成員,這個成員變量的值正常情況下是由虛擬機(jī)啟動的時候確定,默認(rèn)是 false,可以通過啟動虛擬機(jī)的時候傳遞-verbose:sys-locks 參數(shù)來打開,但是作為普通應(yīng)用我們沒有辦法通過這種方式來打開,所以需要用非常規(guī)手段在運行時動態(tài)打開:

          1. 首先確認(rèn)從 Android7.x 開始,這個結(jié)構(gòu)的大小、成員順序是否有發(fā)生變化;
          2. 如果沒有變化,則可以自己定義一個相同的結(jié)構(gòu),因為里面都是原始的 bool 類型變量,不會引入其他依賴;
          3. 如果有變化,但是向前兼容,我們想要訪問的成員位置沒有變化,只是往后追加了成員,也同樣可以自己定義相同的結(jié)構(gòu);
          4. 通過 dlsym 找到虛擬機(jī)的全局符號 gLogVerbosity;
          5. 將其類型轉(zhuǎn)換為預(yù)先定義的結(jié)構(gòu)體類型;
          6. 訪問 systrace_lock_logging 成員并賦值為 true;
          7. 輕鎖的 ATrace 信息即可正常輸出;
          std::string lib_name("libart.so");
          // art::gLogVerbosity
          std::string log_verbosity_sym("_ZN3art13gLogVerbosityE");

          void *handle = nullptr;
          handle = npth_dlopen_full(lib_name.c_str());
          if (handle == nullptr) {
            ALOGE("libart handle is null");
            return false;
          }

          log_verbosity_ = reinterpret_cast<LogVerbosity*>(
              npth_dlsym(handle, log_verbosity_sym.c_str()));
          if (log_verbosity_ == nullptr) {
            ALOGE("gLogVerbosity not defined");
            npth_dlclose(handle);
            return false;
          }

          npth_dlclose(handle);

          2.  IO 耗時

          在做抖音在啟動路徑上性能優(yōu)化時,我們統(tǒng)計了冷啟動的耗時,其中占比最長的是進(jìn)程處于 D 狀態(tài)(不可中斷睡眠態(tài),Uninterruptible Sleep ,通常我們用 PS 查看進(jìn)程狀態(tài)顯示 D,因此俗稱 D 狀態(tài))的時間,這部分耗時占比占總啟動耗時的 40%左右,進(jìn)程為什么會被置于 D 狀態(tài)呢?處于 uninterruptible sleep 狀態(tài)的進(jìn)程通常是在等待 IO,比如磁盤 IO,其他外設(shè) IO,正是因為得不到 IO 的響應(yīng),進(jìn)程才進(jìn)入了 uninterruptible sleep 狀態(tài),所以要想使進(jìn)程從 uninterruptible sleep 狀態(tài)恢復(fù),就得使進(jìn)程等待的 IO 恢復(fù)。類似如下:

          但我們在使用 Systrace 進(jìn)行優(yōu)化時僅能得到如上內(nèi)核態(tài)的調(diào)用狀態(tài),卻無法得知具體的 IO 操作是什么。因此,我們專門設(shè)計了一套獲取 IO 耗時信息的方案,其包括用戶空間和內(nèi)核空間兩部分。

          一是在用戶空間,為了采集到需要的 I/O 耗時信息,我們通過 Hook I/O 操作時標(biāo)準(zhǔn)的關(guān)鍵函數(shù)族,包括 open,write,read,fsync,fdatasync 等,插入對應(yīng)的 trace 埋點用于統(tǒng)計對應(yīng)的 IO 耗時。以 fsync 為例:

          int proxy_fsync(int fd) {
            BYTEHOOK_STACK_SCOPE();
            ATRACE_BEGIN_VALUE("fsync:", FileInfo(fd).c_str());

            int ret = BYTEHOOK_CALL_PREV(proxy_fsync, fd);

            ATRACE_END();
            return ret;
          }

          二是在內(nèi)核空間,除了可由 systrace 或 atrace 直接支持啟用的功能之外,ftrace 還提供了其他功能,并且包含一些對調(diào)試性能問題至關(guān)重要的高級功能(這些功能需要 root 訪問權(quán)限,通??赡芤残枰碌膬?nèi)核)。因此,我們基于此添加了顯示定制 IO 信息等功能。在線下模式,我們開啟了/sys/kernel/debug/tracing/events/android_fs 節(jié)點下 ftrace 信息,用于收集 IO 相關(guān)的信息,

          這時候,我們追本溯源,先找到 Systrace 之母,Google Android 和 Chrome 團(tuán)隊的所有開源項目 Catapult 。正是 Catapult 生成了 Systrace 及其解析器的工具,在 Catapult 中,采用 javascript 實現(xiàn)了一個跨平臺的 trace 解析工具,我們在此基礎(chǔ)上開發(fā)了 Rhea 工具腳本將轉(zhuǎn)換成 systrace 可顯示化的格式,用于快速診斷發(fā)現(xiàn) IO 性能瓶頸。

          例如,我們線上監(jiān)控發(fā)現(xiàn)我們某個 View 方法調(diào)用 setText 方法會導(dǎo)致 ANR,線下通過 Systrace 抓取 Trace 如下:

          此時,看到主線程處于 D 狀態(tài),卻束手無策,而通過我們的 Rhea 工具,獲取 Trace 如下:

          我們很容易就定位到此時是由于讀取對應(yīng)字體帶來的 IO 耗時導(dǎo)致的問題。

          3.  Binder 耗時

          在抖音啟動性能性能優(yōu)化過程中,我們通常還會遇到 Sleep 帶來的耗時問題,這部分耗時通常占據(jù)總耗時的 30%左右,處在這種睡眠狀態(tài),進(jìn)程通常是在等鎖或是 Binder 調(diào)用耗時導(dǎo)致,通常在線下,我們可以通過開啟 tracing/events/binder 節(jié)點獲取到,但是在線上由于權(quán)限問題我們很難獲取到這部分信息。因此,我們通過 Hook libbinder.so 對應(yīng)的 android_os_BinderProxy_transact 方法來統(tǒng)計對應(yīng) binder 調(diào)用耗時。

          if (TraceProvider::Get().isEnableBinder()) {
            // static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,jint code, jobject dataObj, jobject replyObj, jint flags)
            bytehook_stub_t stub = bytehook_hook_single(
                "libbinder.so",
                NULL,
                "_ZN7android14IPCThreadState8transactEijRKNS_6ParcelEPS1_j",
                reinterpret_cast<void*>(proxy_transact),
                NULL,
                NULL);
            stubs.push_back(stub);
          }

          之后,統(tǒng)計對應(yīng) binder 耗時,如果耗時超過指定閾值,則將對應(yīng)堆棧打印出來用于輔助分析 Sleep 耗時問題。

          static void log_binder(int64_t start, int64_t end, int64_t flags) {
            JNIEnv *env = context.env;
            env->CallStaticVoidMethod(context.javaRef, context.logBinder, start, end, flags);
          }

          status_t proxy_transact(void *pIPCThreadState, int32_t handle, uint32_t code,
                            const void *data, void *reply, uint32_t flags)
           
          {
            // todo: add more informations
            nsecs_t start = systemTime();
            status_t status = BYTEHOOK_CALL_PREV(proxy_transact, pIPCThreadState, handle, code, data, reply,
                                        flags);
            nsecs_t end = systemTime();
            nsecs_t cost_us = ns2us(end - start);
            if (is_main_thread() && cost_us > 10000) {
              log_binder(ns2us(start), ns2us(end), flags);
              nsecs_t end_ = systemTime();
            }

            return status;
          }

          trace 效果如圖所示:

          4.  支持后續(xù)增加更多數(shù)據(jù)源

          當(dāng)然,僅僅支持上述這些信息不可能完全覆蓋我們性能優(yōu)化過程中未來還可能遇到的其他問題,因此,我們支持了動態(tài)配置的功能,后續(xù)僅需要在現(xiàn)有框架下,簡單添加對應(yīng)配置項及其功能即可快速方便收集到我們所需要的信息。

          enum TraceConfigKey {
            kIO = 0,
            kBinder,
            kThinLock,
            kStopTraceUnhook,
            kLockStack,

            kKeyEnd,
          };

          5.  不限層級插樁獲取函數(shù)耗時

          限制插樁的層級固然可以提升運行時性能,但是限制層級后面臨兩個問題:

          • 函數(shù)調(diào)用數(shù)據(jù)采集不全面;
          • 難以定位深層的耗時調(diào)用;

          因此在用戶態(tài),為了獲取 App 更多的 Trace 信息,便于性能優(yōu)化。我們采用不限制層級的插樁方案。開發(fā)了在編譯階段不限制層級插樁的插件,通過靜態(tài)代碼插樁方式,在 App 調(diào)用方法的起始和結(jié)束位置分別插入  Trace.beginSection 和 Trace.endSection 。效果如下:

          三、優(yōu)化降低性能損耗

          1.  插樁性能優(yōu)化

          在插樁階段, 我們做了如下優(yōu)化:

          • 支持自定義插樁作用域, 減少 Trace 對于其他無關(guān)模塊的運行損耗;
          • 針對 Trace 數(shù)據(jù)出現(xiàn)不閉合的問題, 對 catch 代碼塊進(jìn)行全插樁;
          • 針對高頻調(diào)用函數(shù), 可以選擇性的添加到黑名單中, 提升運行時性能;
          • 為支持生產(chǎn)環(huán)境使用,我們采用在 proguard 后進(jìn)行插樁,由于函數(shù)內(nèi)聯(lián)等優(yōu)化, 相較于混淆前插樁插樁數(shù)量可以減少 2.6%。對于線上模式,直接插入方法 ID,收集 Trace 后需在主機(jī)端或服務(wù)端對方法 id 重新映射成方法名,但又考慮到線下用戶的易用性,在線下模式打包階段直接插入方法名;
          • 在編譯階段通過分析字節(jié)碼信息,過濾掉不耗時函數(shù)的插樁。

          2.  優(yōu)化 App 側(cè)啟停 Trace 性能

          由于 App 側(cè)抓取 Trace 的實現(xiàn)要依賴于 hook,我們參考了 Facebook Profilo 的實現(xiàn),但其實現(xiàn)存在動態(tài)庫過大、啟停 Trace 耗時問題,因此我們進(jìn)一步優(yōu)化了 App 本地獲取 atrace 依賴的動態(tài)庫大小和性能。如下所示:

          3.  優(yōu)化 Trace 寫入性能

          由于在 App 方法中插入大量 Trace 信息,在開啟 atrace 后,所有線程會將所有的 trace 都寫入到 trace_marker 文件,會帶來 IO 損耗劇增,會掩蓋真實性能問題,原因是所有線程都在短時間向 trace_marker 文件進(jìn)行寫入操作,同時競爭內(nèi)核態(tài) pos 鎖,導(dǎo)致獲取到的 trace 文件無法真實反映性能問題,如下圖所示:

          因此,我們將原本直接寫入內(nèi)核態(tài)文件的 Trace 在用戶態(tài)進(jìn)行攔截,緩存起來,再以異步 IO 的方式轉(zhuǎn)儲。既避免了大量用戶態(tài)與內(nèi)核態(tài)切換帶來的上下文損耗,又避免了直接 IO 帶來的 IO 損耗。效果如下所示:

          四、可視化

          由于我們將用戶態(tài) atrace 和內(nèi)核態(tài) ftrace 分別存儲在對應(yīng)空間下的 ringbuffer 中,原生的 systrace 只能分別進(jìn)行可視化,因此我們開發(fā)了統(tǒng)一整合 trace 的腳本工具,將多個 trace 信息將成為單個的 html 文件,當(dāng)瀏覽 trace 信息時,可在 Chrome(chrome://tracing 訪問)中可視化顯示。

          未來規(guī)劃

          目前,Rhea 對 Native 的支持還不夠全;性能優(yōu)化還不夠極致,特別在用于分析卡頓問題時需要定位幾毫秒甚至更細(xì)粒度耗時的情況下,性能損耗仍然會有些偏大,在一定程度上會帶偏優(yōu)化方向;目前 Trace 工具更多的還是在線下使用,由于插樁過多影響了包大小,使得我們線上部分只能對小規(guī)模的用戶群體定向打開,沒法全量上線定位線上大規(guī)模用戶的性能問題。未來我們會重點解決如上問題,將 Trace 工具打造到極致。

          小結(jié)

          目前新一代 Trace 分析工具 Rhea 其主要優(yōu)勢如下:

          1、使用靈活,不依賴 PC 抓取腳本,同時支持線上線下多種模式和配置開關(guān);

          2、支持采集和追蹤包括不限層級 ATrace 函數(shù)耗時插樁、等鎖信息、I/O 信息以及 Binder 耗時等在內(nèi)的多種信息;

          3、兼容性高,支持 API 16~30 全機(jī)型的 trace 抓?。?/p>

          4、零侵入代碼,通過 gradle 完成插件全部配置,無任何代碼直接調(diào)用。



          ·················END·················

          推薦閱讀

          ? 耗時2年,Android進(jìn)階三部曲第三部《Android進(jìn)階指北》出版!

          ? 『BATcoder』做了多年安卓還沒編譯過源碼?一個視頻帶你玩轉(zhuǎn)!

          ? 鴻蒙來了,拜拜了,Powered by Android!

          ? 重生!進(jìn)階三部曲第一部《Android進(jìn)階之光》第2版 出版!

          BATcoder技術(shù)群,讓一部分人先進(jìn)大廠

          大家,我是劉望舒,騰訊TVP,著有三本技術(shù)暢銷書,連續(xù)四年蟬聯(lián)電子工業(yè)出版社年度優(yōu)秀作者,谷歌開發(fā)者社區(qū)特邀講師。

          前華為技術(shù)專家,現(xiàn)大廠技術(shù)負(fù)責(zé)人。

          想要加入 BATcoder技術(shù)群,公號回復(fù)BAT 即可。

          為了防止失聯(lián),歡迎關(guān)注我的小號


            微信改了推送機(jī)制,真愛請星標(biāo)本公號??
          瀏覽 161
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美伊人大香蕉 | 三级无码在线播放 | 怕怕怕视频大全 | 亚洲AT视频| 91亚洲国产成人久久精品网站 |