本文選自「抖音 Android 性能優化」系列文章。「抖音 Android 性能優化」系列文章是由抖音 Android 基礎技術部門技術專家傾力打造的技術干貨內容,和大家分享基礎技術團隊在打造極致用戶體驗的抖音的過程中,收獲的性能優化方法論、工具和實踐,與各位技術同學一起交流成長。用戶交互響應的耗時,作為 Android 用戶日常感知最深的一項性能指標,在日常開發中有著非常重要的意義。而抖音 Android 基礎技術團隊為打造極致的交互響應體驗,一直在致力于極致性能的探索,其中就包括如何打造極致的耗時檢測工具。
概述俗話說,工欲善其事,必先利其器,我們要做好性能優化,首要是要能夠發現性能的問題,這就需要有靠譜的工具來幫助我們做性能分析。市面上主流的性能分析工具有:Systrace、TraceView、Android Studio 的 CPU Profiler。相信做性能優化的同學對這些工具應該都是非常熟悉了,抖音最早也是用 Systrace 作為主要的分析工具,在優化前期也發揮了比較大的作用。隨著抖音的性能優化來到了深水區,我們需要發現并解決更細粒度、更多維度的性能問題,我們會關注幾毫秒的耗時,關注線上一些低端機用戶遇到的鎖阻塞和 IO 等待問題。而市面上這些主流的性能分析工具因其使用的局限性和較大的性能損耗,已經無法滿足抖音性能優化的需求。為了能夠百尺竿頭更進一步,我們需要開發更加靈活、精細化以及多元的信息和工具來輔助我們進行高效的優化工作。
在這樣的背景之下,抖音 Android 基礎技術團隊開發了 Rhea( [?ri??] 瑞亞,寓意時光女神)跟蹤器(Tracer),其是一種通過靜態代碼插樁技術自動添加 Trace,用來分析 APP 運行時耗時的性能分析工具,意思是要做一個功能全面、追求效率、大家都喜歡的女神,也符合我們工具的核心設計原則。Rhea 跟蹤器獲取 Trace 不僅要性能損耗低,還要能脫離 PC 端工具在 App 側直接抓取,跟蹤更多常規函數耗時的同時還要可以跟蹤系統調用,如:鎖信息、I/O 耗時、Binder IPC 以及更多其他信息。最后,還提供轉換腳本工具,用于將原始跟蹤文件生成可視化報告,便于用戶分析性能問題。
優勢對比Rhea 當前因其無侵入、高性能、信息全等優勢已在字節多個 APP 上落地使用,效果明顯,已多次幫助大家快速發現性能問題,其包含的信息包括不限層級的應用層函數、IO、鎖、Binder、CPU 調度等耗時信息等,其部分效果如下所示:
相對于其他 Android 性能排查工具,其具體優勢表現為:
當前,Systrace 只能監控特定系統信息,監控應用層的耗時則需要手動打點;TraceView 性能跟采樣率關系密切,采樣過于頻繁性能開銷巨大,采樣過低又難以精準發現問題函數;Nanoscope 雖然幾乎沒有性能損耗,但每次都需定制 ROM 刷機,使用成本非常高,并且這些工具都只支持 debugable 的應用程序線下分析,這些工具在針對 APP 性能優化都有不甚完美之處,而 Rhea 是一個集大成者,融合了各工具優勢并彌補了相關缺陷。
架構演進之路第一階段:基于 Systrace 補充函數耗時 TraceSystrace 是 Android 性能調試優化的常用工具,它可以收集進程的活動信息,如函數調用耗時、鎖等;也可以收集內核信息,如 CPU 調度、IO 活動、Binder 調用信息等;這些信息會統一時間軸,在 Chrome 瀏覽器中顯示出來,方便工程師性能調試、優化卡頓等工作。因此,抖音早期性能優化首選 Systrace 作為主要工具,其大致流程如下:
1. 功能改造Systrace 工具只能監控特定系統調用的耗時情況,它不支持應用程序代碼的耗時分析,所以在使用時有一些局限性。原生 Systrace 需要開發者在方法的起止位置手動加入 Trace.beginSection 與 Trace.endSection 方法對,這個過程就變成了開發者預判耗時位置,然后在手動加入監控函數對,通過不斷重復添加監控點、打包、運行、采集數據,從而一步步完成耗時方法定位,這也使得 Systrace 的使用成本變得極高。
為了提高 Systrace 的易用性,我們開發了 Rhea 1.0 對 Systrace 功能進行了改造,加入了自動插樁機制:通過字節碼插樁自動完成 Trace.beginSection 和 Trace.endSection 方法對的插入,并且通過運行時限制方法層級的方式,來有效控制因引入監控帶來的性能損耗。
插樁類及樁方法偽代碼:classTracer{method_stack=list()max_size=6methodIn(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()}
輸出數據如下所示,指定層級內所有方法即可按照預期展示在輸出 html 中:
2. 方法 Did not finished 問題在使用改造后的 systrace 時,我們時常會遇到如下問題:
分析發現,主要原因在于方法在運行期執行中被中斷,例如:方法執行過程中發生異常后,被其調用者方法捕獲,發生異常方法的 Systracer.o 方法未被調用。如圖:test 方法中的 error 方法執行時出現 arr[2]的數組越界,導致 test 方法中的插樁方法 SysTracer.o(13L)未調用,異常被 onCreate 中的 catch 塊捕獲,從而導致 test 的插樁方法沒有被成對調用,最終導致了 test 外層所有的方法調用都無法正確閉合。(注意:本小結提到的樁方法,即 SysTracer 相關方法,均是通過字節碼插樁自動插入)
解決辦法,在外層所有異常捕獲的位置,額外插入樁方法,重新這種異常調用鏈下的樁方法不成對問題。如下圖:
3. 依然存在的問題性能問題隨著 Rhea 1.0 功能的深入使用,在帶來極大便利的同時,功能本身的不足也逐漸暴露出來。在采集數據過程中,其本身的性能損耗會導致在一些實際性能優化過程中會帶偏方向。經我們嚴格測試,其性能損耗有 11.5%左右,如下所示:
在實際使用過程中發現,在開啟 Systrace 之后,對應 Sleep 耗時占比在極端情況下會超過 40%以上。一方面是 APP 鎖帶來的 Sleep 耗時。例如,在抖音啟動路徑上 SharedPreference 優化過程中,在開啟 Rhea 1.0 的 Systrace 功能后,發現 SP 調用存在明顯的鎖耗時,當時針對 SP 進行了一番鎖的優化后,上線發現效果并不明顯,后續經過一系列排查,發現鎖的耗時是由于開啟 Systrace 功能后導致。另一方面是 IO 帶來的 Uninterrupt Sleep 耗時。例如,我們在一次性能優化過程中看到了很多__fdget_pos 操作,對_fdget_pos 操作相對 Uninterruptible Sleep 的占比統計了下,至少占了 Uninterruptible Sleep 總耗時的 60%左右。我們花了比較長時間,額外加了很多 IO 的信息,最終定位原因是在開啟 Systrace 后,由于所有線程的 trace 都會寫入同一個文件,所有線程會同步競爭內核態的文件 pos 鎖導致。工具本身的性能問題誤導了我們的排查方向,同時也暴露了在排查這種 IO Wait 問題的時候由于 IO 信息不全導致排查效率不高的問題。
限制層級導致的調用缺失Systrace 的原理決定了當我們在應用層插入更多函數插樁以定位應用層耗時問題的時候,會導致非常嚴重的性能問題,所以我們在線下使用該工具會通過限制插樁層級的方式以減少運行時性能損耗。但是層級的限制使得超過既定層級的函數調用數據缺失,在分析調用層級較深的函數耗時的時候,無法定位到準確的耗時點。
使用場景限制由于 Systrace 在采集數據的過程中,需要依賴 PC,對于一些需要脫離 PC 采集數據的場景,Systrace 就無法滿足需求了。比如我們產品運營同學經常會在線下場景實地測試抖音的使用性能,例如地鐵、餐館、咖啡廳等,這些實際使用場景下的性能數據,systrace 就無法支持到了。
低端機無法正常使用我們在針對低端機進行耗時優化時,發現諸如三星、oppo 等一些早期的低端機型,systrace 也不能支持其數據抓取。
針對以上問題,我們對工具進行了深入的探索和優化。于是,工具的開發進入了第二階段。
第二階段:高性能全場景的 Trace 抓取工具1. 功能升級為了彌補第一階段功能短板,進一步提高性能,同時能滿足更多使用場景,我們找到了新的解決方案:在 Java 層,通過記錄方法首末位置時間戳、所在線程等信息,過濾出大于指定耗時閾值的函數后,將數據異步記錄到文件。數據采集結束后,將輸出文件轉換成指定格式后,便可通過 SDK 提供的 Systrace 工具轉化成方便查看的 Html 格式,從而實現和 Systrace 相同的可視化效果。
2. 實現原理Rhea 2.0 如何采集數據并生成和 Systrace 相同可視化效果的 html 呢?SDK 中 Systrace 工具的--from-file 命令可將原始的.trace 格式數據轉成 html 格式,分析.trace 數據內部格式:
多次嘗試后得出結論,可被 SDK Systrace 工具解析的 .trace 文件需滿足如下格式:
格式說明:
<ThreadName>:線程名,若為主線程,可指定為包名。<ThreadID>:線程 ID。<Time conds>:方法開始或者結束的時間,單位 s。<B|E>:標記該條記錄為方法開始(B)還是結束(E)。<ProcessID>:所在進程 ID。<TAG>:方法標記,字符長度不可超過 127。由此可知,Mtrace 采集的數據至少需要包含以上內容。
以下則是對應 Trace 格式:
depth,methodID,inTime,outTime,threadName,threadID
相較于 Rhea 1.0 的 Systrace,Rhea 2.0 的 Method Trace 性能損耗有了顯著的降低,性能損耗也由 11.5%下降至 3%,效果如下所示:
3. 最佳實踐功能使用MTrace 相較于 Systrace,提供了更豐富的線下功能,其中包括:解決針對真實用戶點對點的卡頓耗時問題反饋功能,解決產品、運營、QA 同學外出走查場景的問題反饋功能。
總之,不管你在哪,性能反饋都一觸即達!如圖為完整操作流程。
線上案例一個真實的案例:抖音灰度版本線上用戶反饋卡頓,通過 MTrace 功能包實現遠程卡頓問題分析排查!以下則是通過用戶配合回傳的真實卡頓數據,經過解析即可發現耗時調用點:
4. 存在的問題由于這個階段采集的只有 Java 方法層的數據,在抖音啟動 IO 耗時優化工作中,Method Trace 無法提供哪些函數進行了 IO 操作,以及 IO 操作讀取/寫入了哪些文件,給優化工作帶來了較大的難度。另外在一些復雜場景中,Method Trace 只記錄函數執行時長,但是不能準確定位是由于多線程同步等鎖或者系統 IO 導致的執行時間變長。
針對上面的問題,我們意識到一套優秀的 Trace 工具還需要融合更多的系統事件,于是工具進入了第三階段的打磨。
第三階段:動態一體化 Trace 工具規劃Rhea 1.0 和 2.0 在抖音早期的性能優化工作中成績顯著,但隨著優化工作的深入同時也暴漏諸多局限與不便。
一方面,使用常規的 Systrace 工具做性能優化,本身有諸多局限性。一是 Trace 信息少,在默認情況下,只包含系統預置的耗時打點信息,并不足以支持常規的耗時分析需要在 App 側手動調用 Trace.beginSection 和 Trace.endSection 方法才能獲取更多函數耗時信息,為避免影響線上包大小,使用完以后又需手動移除,一上一下事倍而功半。二是 Systrace 本身性能損耗大,特別是應用通過插樁的方式對業務代碼進行大量的打點時,極端情況性能損耗會超過 50%。三是 Systrace 完全依賴 PC 端工具抓取,不夠靈活。尤其是需要能夠穩定復現性能問題的場景,對于一些特定區域或者特定用戶群體才能復現的問題無法獲直接高效的取到有效信息,依賴研發或者測試走查,甚至用戶反饋的部分概率問題即使走查也無法通過 Systrace 獲取到對應的信息,從而導致優化效率低。
另一方面,通過簡單定制 Trace 的獲取函數耗時相較于 Systrace,雖然有顯著的性能提升,和更高的靈活性,但數據只包含基本的耗時信息,在部分復雜場景(如持有鎖引起的耗時),數據仍存在局限。
如上工具都均已無法完全滿足抖音的啟動、首刷以及低端機等核心場景的性能優化工作,我們需要重新設計和規劃功能更加強大的動態一體化 Trace 工具來輔助分析性能。
該工具要非常靈活,可以不依賴 PC 端的抓取腳本,同時支持線上線下,能夠在應用任何想要抓取數據的時候運行,作為一個平臺性工具,Rhea 還需要支持動態擴展,支持多種場景的配置和動態開關,可以將任意需要的信息進行采集。該工具抓取的 Trace 信息要全面,能夠采集和追蹤包括 ATrace 插樁、等鎖信息、I/O 信息以及 Binder 耗時等在內的多種信息。要支持可視化,統一的格式進行輸出和格式化,最終以兼容 Systrace 的結果進行前端展示和使用,盡量不要改變使用者習慣。性能損耗要低,以免帶偏性能優化方向。因此,我們重新設計了新一代 Trace 分析工具:
整體上,App 通過集成 Rhea SDK 在打包時不限層級插入函數耗時樁方法,在運行時插入 IO、Binder、Lock 等相關 Trace 信息,支持動態配置,統一 Trace 格式為 atrace,同時支持獲取系統級別的 Linux ftrace、Android Framework atrace 和 App 插入的 atrace 信息,能夠不依賴 PC 抓取,最終提供可視化顯示。具體實現如下:
一、不依賴 PC 抓取 Trace為了實現不依賴 PC 抓取 Trace,我們有必要先了解下 Android atrace 的實現機制。首先,是 atrace 包括的數據源包括:
其中,用戶空間的數據包括了應用層的自定義 Trace、系統層的 gfx 渲染相關 Trace、系統層打的鎖相關的 Trace 信息等,其最終都是通過調用 Android SDK 提供的Trace.beginSection或者 ATRACE_BEGIN 記錄到同一個文件點/sys/kernel/debug/tracing/trace_marker 中的。此節點允許用戶層寫入字符串,ftrace 會記錄該寫入操作時的時間戳,當用戶在上層調用不同函數時,寫入不同的調用信息,比如函數進入和退出分別寫入,那么 ftrace 就可以記錄跟蹤函數的運行時間。atrace 在處理用戶層的多種 trace 類別時,只是激活不同的 TAG,如選擇了 Graphics,則激活 ATRACE_TAG_GRAPHICS,將渲染事件記錄到 trace_marker。
而內核空間的數據主要是一些補充的分析數據 freq、sched、binder 等,常用的比如 CPU 調度的相關信息包括:
CPU 頻率變化情況任務執行情況大小核的調度情況CPU Boost 調度情況這些信息是 App 可以通過直接讀取/sys/devices/system/cpu 節點下相關信息獲得,而另外一部分標識線程狀態的信息則只能通過系統或者 adb 才能獲取,且這些信息不是統一的一個節點控制,其需要激活各自對應的事件節點,讓 ftrace 記錄下不同事件的 tracepoint。內核在運行時,根據節點的使能狀態,會往 ftrace 緩沖中打點記錄事件。例如,激活線程調度狀態信息記錄,需要激活類似如下相關節點:
events/sched/sched_switch/enableevents/sched/sched_wakeup/enable
激活后,則可以獲取到線程調度狀態相關的信息,比如:
Running: 線程在正常執行代碼邏輯Runnable: 可執行狀態,等待調度,如果長時間調度不到,說明 CPU 繁忙Sleeping: 休眠,一般是在等待事件驅動Uninterruptible Sleep: 不可中斷的休眠,需要看 Args 的描述來確定當時的狀態Uninterruptible Sleep - Block I/O: IO 阻塞最終,上述兩大類事件記錄都匯集到內核態的同一緩沖中,PC 端上 Systrace 工具腳本是通過指定抓取 trace 的類別等參數,然后觸發手機端的/system/bin/atrace 開啟對應文件節點的信息,接著 atrace 會讀取 ftrace 的緩存,生成只包含 ftrace 信息的 atrace_raw 信息,最終通過腳本轉換成可視化 HTML 文件。大致流程如下:
因此,我們基于 Android atrace 的實現原理,我們同步參考了 Facebook 的 profilo 用于在 APP 側直接獲取 atrace 的方案,實現了不依賴 PC 抓取 Trace 的方法。
我們通過 dlopen 獲取 libcutils.so 對應句柄,通過對應 symbol 從中找到 atrace_enabled_tags 和 atrace_marker_fd 對應指針,從而設置 atrace_enabled_tags 用以打開 atrace 開關,具體實現如下:
std::stringlib_name("libcutils.so");std::stringenabled_tags_sym("atrace_enabled_tags");std::stringmarker_fd_sym("atrace_marker_fd");if(sdk<18){lib_name="libutils.so";//android::Tracer::sEnabledTagnabled_tags_sym="_ZN7android6Tracer12sEnabledTagsE";//android::Tracer::sTraceFDmarker_fd_sym="_ZN7android6Tracer8sTraceFDE";}if(sdk<21){handle=dlopen(lib_name.c_str(),RTLD_LOCAL);}el{handle=dlopen(nullptr,RTLD_GLOBAL);}//safecheckthehandleif(handle==nullptr){ALOGE("atrace_handleisnull");returnfal;}atrace_enabled_tags_=reinterpret_cast<std::atomic<uint64_t>*>(dlsym(handle,enabled_tags_sym.c_str()));if(atrace_enabled_tags_==nullptr){ALOGE("atrace_enabled_tagsnotdefined");gotofail;}atrace_marker_fd_=reinterpret_cast<int*>(dlsym(handle,marker_fd_sym.c_str()));
接下來,我們通過 hook libcutils 動態庫中的 write、write_chk 方法通過判定 atrace_marker_fd 來將對應 atrace 信息攔截下來轉儲到到本地或上傳到云端分析。實現如下所示:
ssize_tproxy_write_chk(intfd,constvoid*buf,size_tcount,size_tbuf_size){BYTEHOOK_STACK_SCOPE();if(Atrace::Get().IsAtrace(fd,count)){Atrace::Get().LogTrace(buf,count);returncount;}ATRACE_BEGIN_VALUE("__write_chk:",FileInfo(fd,count).c_str());size_tret=BYTEHOOK_CALL_PREV(proxy_write_chk,fd,buf,count,buf_size);ATRACE_END();returnret;}二、提供更加全面 Trace 信息1. 鎖耗時
Java 層的鎖,無論是同步方法還是同步塊,最終都會走到虛擬機的 MonitorEnter 和 MonitorExit,在 MonitorEnter 中實現了多種鎖狀態的切換,包括從無鎖到輕鎖,輕鎖中的偏向和重入,出現競爭并超過自旋的次數之后升級成重鎖分配 monitor 對象,其中 art 現在的自旋不是真的自旋,而是用 sched_yield 主動讓出 CPU 等待下次調度。
而我們需要首先關注的就是出現鎖競爭升級成重鎖后的等待耗時信息,這個信息從 Android 6.x 開始會通過 ATrace 的方式輸出到 trace_marker 中。
但是想要輕鎖的信息還需要做一些額外的工作,因為是否輸出輕鎖的 ATrace 信息除了 ATRACE_ENABLE 條件之外,還有另外一個 systrace_lock_logging 的開關變量控制,這個變量是虛擬機中一個全局變量的成員,這個成員變量的值正常情況下是由虛擬機啟動的時候確定,默認是 fal,可以通過啟動虛擬機的時候傳遞-verbo:sys-locks 參數來打開,但是作為普通應用我們沒有辦法通過這種方式來打開,所以需要用非常規手段在運行時動態打開:
首先確認從 Android7.x 開始,這個結構的大小、成員順序是否有發生變化;如果沒有變化,則可以自己定義一個相同的結構,因為里面都是原始的 bool 類型變量,不會引入其他依賴;如果有變化,但是向前兼容,我們想要訪問的成員位置沒有變化,只是往后追加了成員,也同樣可以自己定義相同的結構;通過 dlsym 找到虛擬機的全局符號 gLogVerbosity;將其類型轉換為預先定義的結構體類型;訪問 systrace_lock_logging 成員并賦值為 true;輕鎖的 ATrace 信息即可正常輸出;std::stringlib_name("libart.so");//art::gLogVerbositystd::stringlog_verbosity_sym("_ZN3art13gLogVerbosityE");void*handle=nullptr;handle=npth_dlopen_full(lib_name.c_str());if(handle==nullptr){ALOGE("libarthandleisnull");returnfal;}log_verbosity_=reinterpret_cast<LogVerbosity*>(npth_dlsym(handle,log_verbosity_sym.c_str()));if(log_verbosity_==nullptr){ALOGE("gLogVerbositynotdefined");npth_dlclo(handle);returnfal;}npth_dlclo(handle);2. IO 耗時
在做抖音在啟動路徑上性能優化時,我們統計了冷啟動的耗時,其中占比最長的是進程處于 D 狀態(不可中斷睡眠態,Uninterruptible Sleep ,通常我們用 PS 查看進程狀態顯示 D,因此俗稱 D 狀態)的時間,這部分耗時占比占總啟動耗時的 40%左右,進程為什么會被置于 D 狀態呢?處于 uninterruptible sleep 狀態的進程通常是在等待 IO,比如磁盤 IO,其他外設 IO,正是因為得不到 IO 的響應,進程才進入了 uninterruptible sleep 狀態,所以要想使進程從 uninterruptible sleep 狀態恢復,就得使進程等待的 IO 恢復。類似如下:
但我們在使用 Systrace 進行優化時僅能得到如上內核態的調用狀態,卻無法得知具體的 IO 操作是什么。因此,我們專門設計了一套獲取 IO 耗時信息的方案,其包括用戶空間和內核空間兩部分。
一是在用戶空間,為了采集到需要的 I/O 耗時信息,我們通過 Hook I/O 操作時標準的關鍵函數族,包括 open,write,read,fsync,fdatasync 等,插入對應的 trace 埋點用于統計對應的 IO 耗時。以 fsync 為例:
intproxy_fsync(intfd){BYTEHOOK_STACK_SCOPE();ATRACE_BEGIN_VALUE("fsync:",FileInfo(fd).c_str());intret=BYTEHOOK_CALL_PREV(proxy_fsync,fd);ATRACE_END();returnret;}
二是在內核空間,除了可由 systrace 或 atrace 直接支持啟用的功能之外,ftrace 還提供了其他功能,并且包含一些對調試性能問題至關重要的高級功能(這些功能需要 root 訪問權限,通??赡芤残枰碌膬群耍R虼耍覀兓诖颂砑恿孙@示定制 IO 信息等功能。在線下模式,我們開啟了/sys/kernel/debug/tracing/events/android_fs 節點下 ftrace 信息,用于收集 IO 相關的信息,
這時候,我們追本溯源,先找到 Systrace 之母,Google Android 和 Chrome 團隊的所有開源項目 Catapult 。正是 Catapult 生成了 Systrace 及其解析器的工具,在 Catapult 中,采用 javascript 實現了一個跨平臺的 trace 解析工具,我們在此基礎上開發了 Rhea 工具腳本將轉換成 systrace 可顯示化的格式,用于快速診斷發現 IO 性能瓶頸。
例如,我們線上監控發現我們某個 View 方法調用 tText 方法會導致 ANR,線下通過 Systrace 抓取 Trace 如下:
此時,看到主線程處于 D 狀態,卻束手無策,而通過我們的 Rhea 工具,獲取 Trace 如下:
我們很容易就定位到此時是由于讀取對應字體帶來的 IO 耗時導致的問題。
3. Binder 耗時在抖音啟動性能性能優化過程中,我們通常還會遇到 Sleep 帶來的耗時問題,這部分耗時通常占據總耗時的 30%左右,處在這種睡眠狀態,進程通常是在等鎖或是 Binder 調用耗時導致,通常在線下,我們可以通過開啟 tracing/events/binder 節點獲取到,但是在線上由于權限問題我們很難獲取到這部分信息。因此,我們通過 Hook libbinder.so 對應的 android_os_BinderProxy_transact 方法來統計對應 binder 調用耗時。
if(TraceProvider::Get().isEnableBinder()){//staticjbooleanandroid_os_BinderProxy_transact(JNIEnv*env,jobjectobj,jintcode,jobjectdataObj,jobjectreplyObj,jintflags)bytehook_stub_tstub=bytehook_hook_single("libbinder.so",NULL,"_ZN7android14IPCThreadState8transactEijRKNS_6ParcelEPS1_j",reinterpret_cast<void*>(proxy_transact),NULL,NULL);stubs.push_back(stub);}
之后,統計對應 binder 耗時,如果耗時超過指定閾值,則將對應堆棧打印出來用于輔助分析 Sleep 耗時問題。
staticvoidlog_binder(int64_tstart,int64_tend,int64_tflags){JNIEnv*env=context.env;env->CallStaticVoidMethod(context.javaRef,context.logBinder,start,end,flags);}status_tproxy_transact(void*pIPCThreadState,int32_thandle,uint32_tcode,constvoid*data,void*reply,uint32_tflags){//todo:addmoreinformationsncs_tstart=systemTime();status_tstatus=BYTEHOOK_CALL_PREV(proxy_transact,pIPCThreadState,handle,code,data,reply,flags);ncs_tend=systemTime();ncs_tcost_us=ns2us(end-start);if(is_main_thread()&&cost_us>10000){log_binder(ns2us(start),ns2us(end),flags);ncs_tend_=systemTime();}returnstatus;}
trace 效果如圖所示:
4. 支持后續增加更多數據源當然,僅僅支持上述這些信息不可能完全覆蓋我們性能優化過程中未來還可能遇到的其他問題,因此,我們支持了動態配置的功能,后續僅需要在現有框架下,簡單添加對應配置項及其功能即可快速方便收集到我們所需要的信息。
enumTraceConfigKey{kIO=0,kBinder,kThinLock,kStopTraceUnhook,kLockStack,kKeyEnd,};5. 不限層級插樁獲取函數耗時
限制插樁的層級固然可以提升運行時性能,但是限制層級后面臨兩個問題:
函數調用數據采集不全面;難以定位深層的耗時調用;因此在用戶態,為了獲取 App 更多的 Trace 信息,便于性能優化。我們采用不限制層級的插樁方案。開發了在編譯階段不限制層級插樁的插件,通過靜態代碼插樁方式,在 App 調用方法的起始和結束位置分別插入 Trace.beginSection 和 Trace.endSection 。效果如下:
三、優化降低性能損耗1. 插樁性能優化在插樁階段, 我們做了如下優化:
支持自定義插樁作用域, 減少 Trace 對于其他無關模塊的運行損耗;針對 Trace 數據出現不閉合的問題, 對 catch 代碼塊進行全插樁;針對高頻調用函數, 可以選擇性的添加到黑名單中, 提升運行時性能;為支持生產環境使用,我們采用在 proguard 后進行插樁,由于函數內聯等優化, 相較于混淆前插樁插樁數量可以減少 2.6%。對于線上模式,直接插入方法 ID,收集 Trace 后需在主機端或服務端對方法 id 重新映射成方法名,但又考慮到線下用戶的易用性,在線下模式打包階段直接插入方法名;在編譯階段通過分析字節碼信息,過濾掉不耗時函數的插樁。2. 優化 App 側啟停 Trace 性能由于 App 側抓取 Trace 的實現要依賴于 hook,我們參考了 Facebook Profilo 的實現,但其實現存在動態庫過大、啟停 Trace 耗時問題,因此我們進一步優化了 App 本地獲取 atrace 依賴的動態庫大小和性能。如下所示:
3. 優化 Trace 寫入性能由于在 App 方法中插入大量 Trace 信息,在開啟 atrace 后,所有線程會將所有的 trace 都寫入到 trace_marker 文件,會帶來 IO 損耗劇增,會掩蓋真實性能問題,原因是所有線程都在短時間向 trace_marker 文件進行寫入操作,同時競爭內核態 pos 鎖,導致獲取到的 trace 文件無法真實反映性能問題,如下圖所示:
因此,我們將原本直接寫入內核態文件的 Trace 在用戶態進行攔截,緩存起來,再以異步 IO 的方式轉儲。既避免了大量用戶態與內核態切換帶來的上下文損耗,又避免了直接 IO 帶來的 IO 損耗。效果如下所示:
四、可視化由于我們將用戶態 atrace 和內核態 ftrace 分別存儲在對應空間下的 ringbuffer 中,原生的 systrace 只能分別進行可視化,因此我們開發了統一整合 trace 的腳本工具,將多個 trace 信息將成為單個的 html 文件,當瀏覽 trace 信息時,可在 Chrome(chrome://tracing 訪問)中可視化顯示。
未來規劃目前,Rhea 對 Native 的支持還不夠全;性能優化還不夠極致,特別在用于分析卡頓問題時需要定位幾毫秒甚至更細粒度耗時的情況下,性能損耗仍然會有些偏大,在一定程度上會帶偏優化方向;目前 Trace 工具更多的還是在線下使用,由于插樁過多影響了包大小,使得我們線上部分只能對小規模的用戶群體定向打開,沒法全量上線定位線上大規模用戶的性能問題。未來我們會重點解決如上問題,將 Trace 工具打造到極致。
小結目前新一代 Trace 分析工具 Rhea 其主要優勢如下:
1、使用靈活,不依賴 PC 抓取腳本,同時支持線上線下多種模式和配置開關;
2、支持采集和追蹤包括不限層級 ATrace 函數耗時插樁、等鎖信息、I/O 信息以及 Binder 耗時等在內的多種信息;
3、兼容性高,支持 API 16~30 全機型的 trace 抓取;
4、零侵入代碼,通過 gradle 完成插件全部配置,無任何代碼直接調用。
加入我們我們是負責抖音客戶端基礎技術能力研發和前沿技術探索的客戶端團隊,我們專注于性能、架構、穩定性、研發工具、編譯構建等方向的深耕,保障超大規模團隊的研發效率和工程質量,將 6 億人使用的抖音打造成極致用戶體驗的產品。
如果你對技術充滿熱情,歡迎加入抖音基礎技術團隊,讓我們共建億級全球化 App。目前我們在上海、北京、杭州、深圳均有招聘需求,內推可以聯系郵箱: tech@bytedance.com ;郵件標題: 姓名 - 工作年限 - 抖音 - 基礎技術 - Android / iOS 。
歡迎關注「 字節跳動技術團隊 」
簡歷投遞聯系郵箱「 tech@bytedance.com 」
本文發布于:2023-02-28 20:01:00,感謝您對本站的認可!
本文鏈接:http://www.newhan.cn/zhishi/a/167764943774028.html
版權聲明:本站內容均來自互聯網,僅供演示用,請勿用于商業和其他非法用途。如果侵犯了您的權益請與我們聯系,我們將在24小時內刪除。
本文word下載地址:rhea(rhea任務倒計時).doc
本文 PDF 下載地址:rhea(rhea任務倒計時).pdf
| 留言與評論(共有 0 條評論) |