環(huán)球要聞:重要升級!btrace 2.0 技術(shù)原理大揭秘
2023-06-27 10:15:30 來源:技術(shù)聯(lián)盟
抖音性能平臺團隊 字節(jié)跳動技術(shù)團隊 2023-06-26 12:01 發(fā)表于北京
干貨不迷路
【資料圖】
項目 GitHub 地址 :https://github.com/bytedance/btrace
背景介紹
在一年多前,我們對外正式開源了 btrace(AKA RheaTrace),它是基于 Systrace 的高性能 Trace 工具,目前字節(jié)跳動已經(jīng)有接近 10+ 產(chǎn)品團隊使用 btrace 做日常性能優(yōu)化工作。在這一年期間,我們收到很多社區(qū)以及公司內(nèi)部反饋,包括使用體驗、性能體驗、監(jiān)控數(shù)據(jù)等上都收到眾多反饋,我們匯總了大家反饋的內(nèi)容,主要包括以下三類:
使用體驗 :Windows 有著大量用戶群體,但 btrace 1.0 未支持;桌面腳本依賴 Systrace 和 Python 2.7 環(huán)境,導致環(huán)境搭建十分復雜,此外手機端還依賴外部存儲訪問權(quán)限,在初次使用時很容易導致打斷。同時產(chǎn)物體積龐大,網(wǎng)頁打開速度很慢。 性能體驗 :大型應用插樁數(shù)量達到百萬級別,性能損耗接近 100%,對性能優(yōu)化工作產(chǎn)生一定困擾。 監(jiān)控數(shù)據(jù) :在 Trace 分析過程中,有些信息是缺失的,并不知道耗時原因,比如目前 Trace 中僅包含 synchronized 鎖信息,缺少 ReentrantLock 等其他鎖信息,同時渲染監(jiān)控只有部分系統(tǒng)關鍵路徑信息,缺少業(yè)務層信息。同時,隨著 Android 系統(tǒng)的不斷發(fā)展,Google 逐漸廢棄了 Systrace 工具,并開始大力推廣 Perfetto 工具。此外,由于系統(tǒng)的 sdcard 權(quán)限限制變得更加嚴格,btrace 在高版本 Android 系統(tǒng)中已經(jīng)出現(xiàn)兼容性問題。
在此背景下,我們決定大幅改造 btrace,解決用戶反饋最多、最集中的問題,同時適應 Google 發(fā)布的新特性,并修復兼容性問題,以便更好地滿足開發(fā)者的需求,目前 btrace 2.0 在使用體驗、性能體驗、監(jiān)控數(shù)據(jù)等方面均做出大量改進,重點改進如下。
使用體驗: 支持 Windows 啦!此外將腳本實現(xiàn)從 Python 切至 Java 并去除各種權(quán)限要求,因腳本工具可用性問題引起的用戶使用打斷次數(shù)幾乎降為 0,同時還將 Trace 產(chǎn)物切至 PB 協(xié)議,產(chǎn)物體積減小 70%,網(wǎng)頁打開速度提升 7 倍! 性能體驗: 通過大規(guī)模改造方法 Trace 邏輯,將 App 方法 Trace 底層結(jié)構(gòu)由字符串切換為整數(shù),實現(xiàn)內(nèi)存占用減少 80%,存儲改為 mmap 方式、優(yōu)化無鎖隊列邏輯、提供精準插樁策略等,全插樁場景下性能損耗進一步降低至 15%! 監(jiān)控數(shù)據(jù): 新增 4 項數(shù)據(jù)監(jiān)控能力,重磅推出渲染詳情采集能力!同時還新增 Binder、線程啟動、Wait/Notify/Park/Unpark 等詳情數(shù)據(jù)!接下來,我們將詳細介紹上述三個改進方向的具體機制與實現(xiàn)原理,以幫助您深入了解 btrace 2.0 的重要升級。
原理揭秘
Perfetto 簡介
Perfetto 和 Systrace 都是用于 Android 系統(tǒng)的性能分析和調(diào)試的工具,但它們有所不同:
Systrace :是 Android SDK 中的一個工具,可用于捕獲和分析不同系統(tǒng)進程的時序事件,并提供了用于分析系統(tǒng)性能瓶頸的圖形界面。Systrace 能夠捕獲的事件包括 CPU、內(nèi)存、網(wǎng)絡、磁盤I/O、渲染等等。Systrace 的工作原理是在內(nèi)核和用戶空間捕獲和解析時序事件,并將其記錄到 HTML 文件中,開發(fā)者可以使用 Chrome 瀏覽器來分析這些事件。Systrace 能夠很好地幫助開發(fā)者找出系統(tǒng)瓶頸,但它在性能方面的表現(xiàn)并不理想,尤其是在處理大量數(shù)據(jù)時。
Perfetto :是一個全新、低開銷的 Trace 采集工具,旨在優(yōu)化 Systrace 的性能表現(xiàn)。Perfetto 的目標是提供比 Systrace 更快、更細粒度的 Trace 采集,并支持與其他跨平臺工具集成。Perfetto 采用二進制格式記錄 Trace 數(shù)據(jù),并使用基于 ProtoBuf 的數(shù)據(jù)交換格式進行數(shù)據(jù)導出,可與 Grafana、SQLite、BigQuery 等其他分析和可視化工具集成。Perfetto 采集的數(shù)據(jù)種類非常廣泛,包括 CPU 使用情況、網(wǎng)絡字節(jié)流、觸摸輸入、渲染等等。與 Systrace 相比,Perfetto 在性能和可定制性方面更為出色。
因此,可以看出 Perfetto 是 Systrace 的一種更為先進和優(yōu)秀的替代工具,它提供了更強大的數(shù)據(jù)采集和分析功能,更好的性能以及更好的可定制性,為開發(fā)人員提供更全面和深入的性能分析和調(diào)試工具。
整體流程
首先我們了解下 btrace 采集的整體流程:
整個流程分為以下三個階段:
App 編譯時: 在應用程序編譯階段,我們提供了兩種插樁模式:方法數(shù)字標識插樁和方法字符串標識插樁。方法 數(shù)字標識插樁適用于只需要記錄方法名稱的場景,而方法字符串標識插樁可以同時記錄方法參數(shù)的值。此外,我們還支持精準插樁引擎,自動識別可疑耗時代碼并進行插樁。
App 運行時: 在應用程序運行期間,主要工作是采集應用的 apptrace 信息,對于方法數(shù)字標識類型的信息,通過 mmap 無鎖隊列方式采集;對于字符串標識類型的信息,直接通過系統(tǒng)函數(shù)寫入 atrace,同時代理 atrace 寫入邏輯,將其替換為 LFRB 高性能寫入方案。
桌面腳本: 桌面腳本主要用于控制應用程序的運行和開啟/關閉 Trace 采集功能。此外,桌面腳本還負責對采集到的 apptrace 與 atrace 數(shù)據(jù)進行編碼,并將它們與 ftrace 進行合并。
技術(shù)揭秘
1. 使用體驗
使用體驗問題在用戶反饋中最多,分析下來基本是存儲權(quán)限、Systrace 環(huán)境、Python 環(huán)境、Trace 產(chǎn)物體積過大、Perffetto 網(wǎng)頁打開過慢等問題,這些體驗問題我們完成了針對性的優(yōu)化:
權(quán)限優(yōu)化
為進行數(shù)據(jù)處理,桌面腳本需要訪問到 App 數(shù)據(jù)。在 App 層面,最方便的方式是將數(shù)據(jù)存儲到公共 SDCard 中。但從 Android Q 開始,Google 收緊對外置存儲完全訪問權(quán)限。盡管 requestLegacyExternalStorage 可以臨時解決這個問題,但從長遠來看,SDCard 將無法完全訪問。
為解決此問題,我們搭建 Http Server 來通過端口對外訪問數(shù)據(jù),但訪問該 Server 仍需要確定服務地址,為此,我們使用 adb forward 功能,它可以建立一個轉(zhuǎn)發(fā),將 PC 端數(shù)據(jù)轉(zhuǎn)發(fā)到手機端口,并且可以獲取從手機端口返回的數(shù)據(jù)。這樣,我們就可以使用 localhost 訪問數(shù)據(jù)。
以上解決了腳本讀取 App 數(shù)據(jù)的問題,我們還面臨 App 讀取腳本參數(shù)問題,比如 maxAppTraceBufferSize、 mainThreadOnly,在 btrace 1.0 支持運行時通過 push 配置文件到指定目錄進行動態(tài)調(diào)整,但這也需要 SDCard 訪問權(quán)限,為徹底去除權(quán)限依賴,我們需要引入新方案。
首先想到的是 adb forward 反向方案:adb reverse,它可以將手機端口數(shù)據(jù)轉(zhuǎn)發(fā)給 PC,實現(xiàn)了從手機到 PC 的訪問,同樣我們可以在腳本啟動 HttpServer 來實現(xiàn)數(shù)據(jù)接收。但是,因為是網(wǎng)絡請求意味著 App 讀取參數(shù)只能在子線程進行,會有一定的不便,尤其在需要參數(shù)實時生效時。
我們又研究了新方案,在桌面腳本通過 adb setprop 給手機設置參數(shù),App 通過 __system_property_get 來讀取參數(shù),只要是參數(shù) property 名稱以 debug. 開頭,就無需任何權(quán)限。
// 桌面腳本設置參數(shù)Adb.call(\"shell\", \"setprop\", \"debug.rhea.startWhenAppLaunch\", \"1\");// 手機運行時讀取參數(shù)static jboolean JNI_startWhenAppLaunch(JNIEnv *env, jobject thiz) { char value[PROP_VALUE_MAX]; __system_property_get(\"debug.rhea.startWhenAppLaunch\", value); return value[0] == "1";}
環(huán)境優(yōu)化
btrace 1.0 基于 Systrace 開發(fā),對 Python 2.7 有強依賴,而 Python 2.7 已被官方廢棄,同時大多數(shù) Android 工程師對 Python 不太熟悉,浪費了大量時間解決環(huán)境問題。對此,我們計劃將 Systrace 切換到 Perfetto ,并選擇 Android 工程師更熟悉的 Java 語言重寫腳本,用戶只需有可用的 Java 和 adb 環(huán)境,即可輕松使用 btrace 2.0。
產(chǎn)物優(yōu)化
btrace 1.0 產(chǎn)物是基于 Systrace 的 HTML 文本數(shù)據(jù),常常遇到文本內(nèi)容太大、加載速度過慢、甚至需要單獨搭建服務來支持 Trace 顯示的問題。Perfetto 是 Google 新推出的性能分析平臺,支持多種數(shù)據(jù)格式解析,Systrace 格式是其中一種,同時 Perfetto 還支持 Protocol Buffer 格式,pb 是一種輕量級、高效的數(shù)據(jù)序列化格式,用于結(jié)構(gòu)化數(shù)據(jù)存儲和傳輸。Perfetto 使用 pb 作為其事件記錄格式,保證記錄系統(tǒng)事件數(shù)據(jù)的同時,保持數(shù)據(jù)的高效性和可伸縮性。pb 因為其結(jié)構(gòu)化數(shù)據(jù)存儲可以實現(xiàn)更小體積占用與更快解析速度。因此,btrace 2.0 也將數(shù)據(jù)格式由 HTML 切換到 pb,在減小產(chǎn)物文件體積的同時,還大幅提升 Trace 在網(wǎng)頁上的加載速度。
我們先簡單介紹下 Perfetto 的 pb 數(shù)據(jù)格式,然后再介紹如何將采集到的 apptrace 與 atrace 編碼為 pb 格式,以及如何將其與系統(tǒng) ftrace 進行融合。
Perfetto pb 是由一系列 TracePacket 組成,官方文檔可以參考:https://perfetto.dev/docs/reference/trace-packet-proto,這里將介紹 btrace 使用到的一種 TracePacket:FtraceEventBundle:
FtraceEventBundle 是 Android 用于收集系統(tǒng) Trace 數(shù)據(jù)的一種機制。它由大量 FtraceEvent 組成,可以被用來記錄各種系統(tǒng)行為,如調(diào)度、中斷、內(nèi)存管理和文件系統(tǒng)等。btrace 主要利用其中 PrintFtraceEvent 來記錄方法 Trace 信息,具體使用方式可以參考下面簡單示例:
int threadId = 10011;FtraceEventBundle.Builder bundle = FtraceEventBundle.newBuilder() .addEvent( FtraceEvent.newBuilder() .setPid(threadId) // 線程內(nèi)核 pid,就是 tid .setTimestamp(System.nanoTime()) .setPrint( Ftrace.PrintFtraceEvent.newBuilder() // buf 格式是 B|$pid|$msg
這里 pid 是實際 // 進程 ID,`
` 是必須項 .setBuf(\"B|10010|someEvent
\"))) .addEvent( FtraceEvent.newBuilder() .setPid(threadId) .setTimestamp(System.nanoTime() + TimeUnit.SECONDS.toNanos(2)) .setPrint( Ftrace.PrintFtraceEvent.newBuilder() .setBuf(\"E|10010|
\"))) .setCpu(0);Trace trace = Trace.newBuilder() .addPacket( TracePacketOuterClass.TracePacket.newBuilder() .setFtraceEvents(bundle)).build();try (FileOutputStream out = new FileOutputStream(\"demo.pb\")) { trace.writeTo(out);}
上面示例將得到下面這個 Trace:
下面再介紹如何將運行時采集到的 apptrace 信息轉(zhuǎn)換成 pb 格式的,這部分操作在桌面腳本進行。
首先腳本通過 adb http 方式獲取到手機上 mmap 映射文件,然后再解析文件內(nèi)容:
// 讀取 mapping,我們將 mapping 內(nèi)置到了 apk 的 assets 目錄Mapmapping = Mapping.get();// 開始解碼并保存解碼后的結(jié)果List<frame>result = new ArrayList<>();byte[] bytes = FileUtils.readFileToByteArray(traceFile);ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);while (buffer.hasRemaining()) { long a = buffer.getLong(); long b = buffer.getLong(); // 分別解析出 startTime / duration / tid / methodId long startTime = a >>>19; long dur0 = a & 0x7FFFF; long dur1 = (b >>>38) & 0x3FFFFFF; long dur = (dur0 << 26) + dur1; int tid = (int) ((b >>>23) & 0x7FFF); int mid = (int) (b & 0x7FFFFF); // 記錄相應的開始與結(jié)束 Trace result.add(new Frame(Frame.B, startTime, dur, pid, tid, mid, mapping)); result.add(new Frame(Frame.E, startTime, dur, pid, tid, mid, mapping));}// 排序result.sort(Comparator.comparingLong(frame ->frame.time));
之后再利用上文介紹的 FtraceEventBundle 對 List 進行編碼即可,這里不再展開。atrace 的處理方式也是類似的,也不再闡述。
再介紹下如何將采集到的 apptrace、atrace 與系統(tǒng) ftrace 進行合并。
前文介紹過,Perfetto pb 是由一系列 TracePacket 組成,一般而言,我們只要將業(yè)務采集到的 Trace 分別封裝成 TracePacket,然后加入到系統(tǒng) TracePacket 集合中就完成了 Trace 的合并。
Trace.Builder systemTrace = Trace.parseFrom(systraceStream).toBuilder();FtraceEventBundle.Builder bundle = ...;for (int i = 0; i < events.size(); i++) { bundle.addEvent(events.get(i).toEvent());}systemTrace.addPacket(TracePacket.newBuilder().setFtraceEvents(bundle).build());
然而,這里有一個前提,就是業(yè)務采集的 apptrace / atrace 時間戳與系統(tǒng) frace 時間戳一致。實際上,根據(jù)實際測試的結(jié)果,不同設備 ftrace 時間戳可能會采用不同時間,可能是 BOOTTIME,也可能是 MONOTONIC TIME。這導致業(yè)務層無論使用哪種時間戳都只能兼容部分設備。為解決此問題,我們在開始記錄 trace 信息時,先記錄一份 BOOTTIME 和 MONOTONIC TIME 初始時間,之后再記錄時間戳時,都統(tǒng)一使用 MONOTONIC 時間。
最后在腳本中解析 ftrace 時間戳進行判斷,如果與 MONOTONIC 接近,就采用 MONOTONIC;如果與 BOOTTIME 接近,就采用 BOOTTIME。雖然我們沒有單獨記錄每個函數(shù)的 BOOTTIME,但是可以通過 MONOTONIC 與初始時間差異折算。
if (Math.abs(systemFtraceTime - monotonicTime) < Math.abs(systemFtraceTime - bootTime)) { Log.d(\"System is monotonic time.\");} else { long diff = bootTime - monotonicTime; Log.d(\"System is BootTime. time diff is \" + diff); for (Event e: events) { e.time += diff; }}
2. 性能體驗
運行時優(yōu)化
btrace 1.0 在插樁上是嚴格依賴 Systrace 模式,通過在方法開始與結(jié)束插入 Trace.beginSection 與 endSection。但是 beginSection 參數(shù)是字符串,百萬級別方法插樁會造成數(shù)百萬字符串額外內(nèi)存占用,對內(nèi)存造成巨大壓力,并且也導致數(shù)據(jù) IO 持久化壓力巨大。此外,字符串數(shù)據(jù)大小不固定,只能通過有鎖或者 LFRB 方式來記錄數(shù)據(jù),無法做到數(shù)據(jù)高效并發(fā)寫入,只能將數(shù)據(jù)緩存在 buffer 中,但是過小 buffer 容易導致數(shù)據(jù)丟失,過大則會造成內(nèi)存浪費。
btrace 2.0 版本通過將方法 ID 數(shù)字化,將方法執(zhí)行信息記錄到一個 mmap 映射文件中。由于方法 ID 大小是固定的,可以使用 atomic 原子操作計算存儲數(shù)據(jù)位置,從而實現(xiàn)無鎖并發(fā)寫入,同時方法數(shù)字存儲占用內(nèi)存更小,IO 持久化壓力也更小。
同時,我們發(fā)現(xiàn) Trace.beginSection 和 endSection 方式,需要記錄每個方法的開始與結(jié)束時間、線程 ID 與方法 ID,這里面的線程 ID 和方法 ID 會重復記錄,也導致了內(nèi)存浪費。于是專門進行了優(yōu)化,在一條記錄中同時記錄開始時間、方法耗時、線程 ID 和方法 ID 信息,合計占用 2 個 long,可以充分利用內(nèi)存。
具體插樁邏輯可以參考偽代碼:
// 業(yè)務代碼public void appLogic() { long begin = nativeTraceBegin(); // 業(yè)務邏輯 nativeTraceEnd(begin, 10010);}// 插樁邏輯long nativeTraceBegin() { return nanoTime();}void nativeTraceEnd(long begin, int mid) { long dur = nanoTime() - begin; int tid = gettid(); write(begin, dur, tid, mid);}
方法耗時數(shù)據(jù)記錄格式示例:
方法插樁在 Java 層,但是數(shù)據(jù)采集基于 mmap 方式在 native 層實現(xiàn)。這會導致高頻 JNI 調(diào)用,當一個非 JNI 方法調(diào)用常規(guī) JNI 方法,以及從常規(guī) JNI 方法返回時,需要做線程狀態(tài)切換,線程狀態(tài)切換就會涉及到 GC 鎖操作,會有較大性能開銷。
熟悉 Android 系統(tǒng)的同學可能了解系統(tǒng)專門為高頻 JNI 調(diào)用做的性能優(yōu)化,通過 @CriticalNative 注解與 @FastNative 注解方式來實現(xiàn),@FastNative 可以使原生方法的性能提升高達 2 倍,@CriticalNative 則可以提升高達 4 倍。
我們也參考系統(tǒng)的方式,給方法添加 @CriticalNative 注解實現(xiàn)方法調(diào)用加速。但是 @CriticalNative 注解是隱藏 API無法直接使用,可以通過構(gòu)建一個定義 CriticalNative 注解的 jar 包,在項目中通過 compileOnly 方式依賴,來達到使用 @CriticalNative 注解的目的。相關注解定義參考自源碼:
// ref: https://cs.android.com/android/platform/superproject/+/master:libcore/dalvik/src/main/java/dalvik/annotation/optimization/CriticalNative.java;l=26?q=criticalnative&sq=@Retention(RetentionPolicy.CLASS) // Save memory, don"t instantiate as an object at runtime.@Target(ElementType.METHOD)public @interface CriticalNative {}
具體使用規(guī)則可以參考下面代碼:
// Java 方法定義,必須是 static,不能用 synchronized,參數(shù)類型必須是基本類型@CriticalNativepublic static long nativeTraceBegin();// Critical JNI 方法,不再需要聲明 JNIEnv 與 jclass 參數(shù)static jlong Binary_nativeTraceBegin() { ...}// 動態(tài)綁定JNINativeMethod t = {\"nativeTraceBegin\", \"(I)J\", (void *) JNI_CriticalTraceBegin};env->RegisterNatives(clazz, &t, 1);
@CriticalNative/@FastNative 是 8.0 及以后才支持的特性,對于 8.0 以前的設備,也可以通過在方法簽名中加入 ! 方式來開啟 FastNative:
// Fast JNI 方法,和普通 JNI 方法一樣需要 JNIEnv 參數(shù)與 jclass 參數(shù)static jlong Binary_nativeTraceBegin(JNIEnv *, jclass) { ...}// 動態(tài)綁定JNINativeMethod t = {\"nativeTraceBegin\", \"!(I)J\", (void *) Binary_nativeTraceBegin};env->RegisterNatives(clazz, &t, 1);
以上方法 ID 數(shù)字化采集優(yōu)化的相關內(nèi)容,前文產(chǎn)物優(yōu)化專題已經(jīng)介紹具體的數(shù)據(jù)解碼和 mapping 映射的方案,這里不再贅述。
雖然方法數(shù)字 ID 采集具有性能與內(nèi)存的優(yōu)勢,但它也有一些限制,因為它只能記錄在編譯階段準備的通過 ID 映射的內(nèi)容,無法記錄 App 運行時動態(tài)生成的內(nèi)容。因此,除了方法 ID 的存儲外,我們還支持字符串類型的數(shù)據(jù)存儲,主要用于記錄 btrace 細粒度監(jiān)控數(shù)據(jù)和方法參數(shù)值。這一方案與 btrace 1.0 中的 LFRB 方案相似,這里就不再詳細闡述。由于大部分 trace 數(shù)據(jù)都是方法 ID,已經(jīng)被 mmap 分擔了壓力,因此 LFRB 的壓力相比 btrace 1.0 小得多,我們可以適當減小 buffer 大小。
精準插樁
另一個性能優(yōu)化是插樁優(yōu)化。隨著應用中方法數(shù)量越來越多,插樁方法數(shù)量也隨之增多,久而久之插樁對應用性能損耗也會越大。btrace 1.0 通過提供 traceFilterFilePath 配置讓用戶來選擇對哪些方法插樁,哪些不插樁,靈活配置的同時也把最終性能與插樁權(quán)衡的困擾轉(zhuǎn)移給了用戶。
在 2.0 中,我們希望建立一套智能規(guī)則,可以精準識別用戶關心的高耗時方法,同時將不耗時方法精準的排除在插樁規(guī)則以外,可以實現(xiàn)智能精準插樁體驗。
Android App 項目源碼最終會編譯為字節(jié)碼,雖然 Android 虛擬機支持 200 多條字節(jié)碼指令,但可能導致性能瓶頸的指令往往是比較少且易于枚舉的,如 IO 讀取、synchronized 字節(jié)碼、反射、Gson 解析等函數(shù)調(diào)用等。我們在編譯過程中將調(diào)用相關指令的方法視為疑似耗時方法,而剩余的非耗時函數(shù)則不進行插樁,因為它們不會導致性能問題,從而大大縮小了插樁范圍。
以上述耗時特征為基礎,我們設計了一條精細化插樁方案,方便用戶可以根據(jù)具體的情況選擇需要的插樁方法。支持的配置方案如下所示:
# 對鎖相關的方法插樁-tracesynchronize# 對Native方法的調(diào)用點插樁-tracenative# 對Aidl方法插樁-traceaidl# 對包含循環(huán)的方法插樁-traceloop# 關閉默認耗時方法的調(diào)用插樁-disabledefaultpreciseinject# 開啟大方法插樁,方法調(diào)用數(shù)超過40-tracelargemethod 40# 該方法的調(diào)用方需要進行插樁-traceclassmethods rhea.sample.android.app.PreciseInjectTest { test}# 被該注解修飾的方法需要被插樁-tracemethodannotation org.greenrobot.eventbus.Subscribe# 該Class的所有方法均會被插樁-traceclass io.reactivex.internal.observers.LambdaObserver# 該方法的參數(shù)信息會在Trace中保留-allowclassmethodswithparametervalues rhea.sample.android.app.RheaApplication { printApplicationName(*java.lang.String);}
經(jīng)過我們的精細化插樁后,抖音插樁量減少 94%,在保留較完整的 Trace 數(shù)據(jù)的同時,性能有了顯著的提升。
總之,我們通過權(quán)衡耗時函數(shù)插樁的優(yōu)點和缺點,這樣可以幫助我們盡可能的獲取到足夠的耗時函數(shù)信息,同時避免過度插樁導致不必要的性能損耗。
3. 監(jiān)控數(shù)據(jù)
監(jiān)控數(shù)據(jù)是 Trace 的核心,關系到 Trace 能否給用戶帶來實際價值,除了常規(guī)方法執(zhí)行 Trace 以外,本次 2.0 還帶來了渲染監(jiān)控、Binder 監(jiān)控、阻塞監(jiān)控、線程創(chuàng)建監(jiān)控等四大能力,下面將介紹相關背景與實現(xiàn)原理。
渲染監(jiān)控
Android 系統(tǒng)提供提供 RenderThread 關鍵執(zhí)行邏輯的跟蹤埋點,但其提供的信息不夠充分,無法直觀分析是具體影響渲染問題的業(yè)務代碼,下圖是 atrace 中渲染線程 Trace 示例:
為此,我們針對這部分信息進行更精細化拓展展示,新增記錄渲染關鍵 View 節(jié)點,下圖是優(yōu)化后效果:
渲染監(jiān)控核心原理如下圖:
代理 LayoutInflater 獲取到 inflate 時 View 所屬的布局信息,再通過 View 的 RenderNode 與 native 層 RenderNode關系,將 View 所屬布局信息綁定到 RenderNode 的 name 字段上。 Hook 渲染階段的關鍵節(jié)點,比如 SyncFrameState 階段的 RenderNode::prepareTreeImpl 方法和 RenderPipeline 階段的 RenderNodeDrawable::forceDraw 方法,將 RenderNode 所屬 View 的布局信息記錄到 Trace 中。Binder 監(jiān)控
Binder 是 Android 跨進程通信的一個非常重要手段,然而我們在做性能分析時,會偶爾發(fā)現(xiàn) Binder 過程比較耗時,雖然 Android 系統(tǒng) atrace 提供 Binder 耗時監(jiān)控信息,但其并未提供是何種類型的 Binder 調(diào)用,如下圖。
btrace 的 Binder 增強目標是將 Binder 調(diào)用的接口名稱與方法名稱進行解析與展示,實現(xiàn)效果如下:
核心原理通過 plt hook IPCThreadState::transact 記錄 binder 調(diào)用的 code 與 Parcel& data 參數(shù)中的 interfaceName.
status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags);
但是 Parcel 結(jié)構(gòu)是非公開,很難從 data 中解析出 interfaceName 信息,于是轉(zhuǎn)變思路,通過 hook Parcel::writeInterfaceToken 來記錄 interfaceName 與 Parcel 關聯(lián)信息,隨后再在 IPCThreadState::transact 中通過查詢獲取 interfaceName.
status_t Parcel::writeInterfaceToken(const char* interface) { // 記錄 this Parcel 與 interface 名稱的關聯(lián)}status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) { // 查詢 Parcel data 對應的 interface // 記錄 Trace RHEA_ATRACE(\"binder transact[%s:%d]\", name.c_str(), code);}
這就記錄了以下信息,包含了 interfaceName 與 code:
binder transact[android.content.pm.IPackageManager:5]
此外,還需要將 code 解析到對應 Binder 調(diào)用方法。在 AIDL 中,interfaceName$Stub 類靜態(tài)字段中記錄了每個 code 與對應 Binder 調(diào)用名稱。可以在抓取 trace 結(jié)束時,通過反射獲取 code 與名稱映射關系,將他保存到 Trace 產(chǎn)物中。
#android.os.IHintManagerTRANSACTION_createHintSession:1TRANSACTION_getHintSessionPreferredRate:2#miui.security.ISecurityManagerTRANSACTION_activityResume:27TRANSACTION_addAccessControlPass:6
最后通過桌面腳本進行處理,將運行時記錄的 Trace 中 code 進行替換,替換為真實的方法名稱即可。
阻塞監(jiān)控
鎖監(jiān)控是性能監(jiān)控中非常重要的一個監(jiān)控環(huán)節(jié),在 Android 系統(tǒng) atrace 中提供 synchronized 鎖沖突 Trace 信息,比如通過下圖可以得知主線程在獲取鎖時與 16105 線程發(fā)生鎖沖突,這給優(yōu)化線程阻塞提供了重要的信息輸入。
但是線程阻塞原因不只有鎖沖突,還包含 wait/park 等原因?qū)е碌木€程等待,比如 ReentrantLock 的底層實現(xiàn)是利用 park 和 unpark。btrace 阻塞監(jiān)控就是提供這部分阻塞信息,下面是 wait/notify 關聯(lián)示例,通過檢索鎖 obj 信息可以得知當前線程 wait 匹配的 notify 的位置:
wait 與 park 等待原理類似,這里以大家更熟悉的 wait/notify 組合進行說明。
wait/notify 都是 Object 直接定義的方法,本質(zhì)上都是 JNI 方法,可以通過 JNI hook 方式記錄他們的調(diào)用。
public final native void wait(long timeoutMillis, int nanos) throws InterruptedException;public final native void notify();
在對應的 hook 方法中,通過 Trace 記錄他們的執(zhí)行與對應的 this(也就是鎖對象)的 identityHashCode,這樣可以通過 identityHashCode 建立起映射關系。
static void Object_waitJI(JNIEnv *env, jobject java_this, jlong ms, jint ns) { ATRACE_FORMAT(\"Object#wait(obj:0x%x, timeout:%d)\", Object_identityHashCodeNative(env, nullptr, java_this), ms); Origin_waitJI(env, java_this, ms, ns);}static void Object_notify(JNIEnv *env, jobject java_this) { ATRACE_FORMAT(\"Object#notify(obj:0x%x)\", Object_identityHashCodeNative(env, nullptr, java_this)); Origin_notify(env, java_this);}
線程創(chuàng)建監(jiān)控
在分析 Trace 時可能會遇到一些異常的線程,這時候往往需要分析線程在什么地方被創(chuàng)建,但是在傳統(tǒng) Trace 中缺少這部分信息。于是 btrace 加入了線程創(chuàng)建監(jiān)控數(shù)據(jù),核心原理是對 pthread_create 進行代理,記錄線程創(chuàng)建的同時,還記錄被創(chuàng)建線程的 tid。但是在 pthread_create 調(diào)用完成時是無法得知被創(chuàng)建線程 ID 的,通過分析系統(tǒng)源碼,發(fā)現(xiàn) pthread_t 本質(zhì)上是一個 pthread_internal_t 指針,而 pthread_internal_t 則記錄著被創(chuàng)建線程的 ID.
// https://cs.android.com/android/platform/superproject/+/master:bionic/libc/bionic/pthread_internal.hstruct pthread_internal_t { struct pthread_internal_t *next; struct pthread_internal_t *prev; pid_t tid;};int pthread_create_proxy(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) { BYTEHOOK_STACK_SCOPE(); int ret = BYTEHOOK_CALL_PREV(pthread_create_proxy, thread, attr, start_routine, arg); if (ret == 0) { ATRACE_FORMAT(\"pthread_create tid=%lu\", ((pthread_internal_t *) *thread)->tid); } return ret;}
最終實現(xiàn)效果如圖,比如發(fā)現(xiàn) Thread 16125 是新創(chuàng)建的線程,需要分析其創(chuàng)建位置,替換為線程池實現(xiàn)。
只需要檢索 pthread_create tid=16125 就能找到對應的創(chuàng)建堆棧。
總結(jié)展望
以上介紹了 btrace 2.0 的主要優(yōu)化點,更多優(yōu)化還需要在日常使用中去體會。2.0 不是終點,是新征程的起點,我們還將圍繞下面幾點持續(xù)優(yōu)化,將 btrace 優(yōu)化到極致:
使用體驗: 深入優(yōu)化使用體驗,比如支持不定長時間 Trace 采集,優(yōu)化采集耗時。
性能體驗: 持續(xù)探索性能優(yōu)化,正面與側(cè)面優(yōu)化雙結(jié)合,提供更加極致性能體驗。
監(jiān)控數(shù)據(jù): 在 Java 與 ART 虛擬機基礎之上,建設包括內(nèi)存、C/C++、JavaScript 等更多更全的監(jiān)控能力。
使用場景: 提供線上場景接入與使用方案,幫助解決線上疑難問題。
生態(tài)建設: 圍繞 btrace 2.0 建設完善生態(tài),通過性能診斷與性能防劣化,自動發(fā)現(xiàn)存量與增量性能問題。
最后,歡迎大家深入討論與交流,一起協(xié)作構(gòu)建極致 btrace 工具!
加入我們
抖音 Android 基礎技術(shù)團隊是一個深度追求極致的團隊,我們專注于性能、架構(gòu)、包大小、穩(wěn)定性、基礎庫、編譯構(gòu)建等方向的深耕,保障超大規(guī)模團隊的研發(fā)效率和數(shù)億用戶的使用體驗。目前北京、上海、深圳都有人才需要,歡迎有志之士與我們共同建設億級用戶全球化 APP!
你可以進入字節(jié)跳動招聘官網(wǎng)查詢「抖音基礎技術(shù) Android」相關職位,也可以郵件聯(lián)系:chenjiawei.kisson@bytedance.com 咨詢相關信息或者直接發(fā)送簡歷內(nèi)推!
關鍵詞:
相關閱讀
- (2023-06-27)環(huán)球要聞:重要升級!btrace 2.0 技術(shù)原理大揭秘
- (2023-06-27)今日看點:小米12 spro 和小米12pro 的區(qū)別
- (2023-06-27)要聞速遞:途觀1 8T發(fā)動機多少錢_途觀1 4t
- (2023-06-27)福建農(nóng)信兩大項目獲評“2022年福建省十大金融創(chuàng)新項目”
- (2023-06-27)上海一些機構(gòu)“假招工真賣課” 部分求職者陷入連環(huán)套_熱文
- (2023-06-27)青海湖水域面積達到近十年來最大值 湟魚洄游迎來高峰期
- (2023-06-27)熱議:讓利于民,多只基金宣布降費
- (2023-06-27)寧夏回族自治區(qū)同心縣發(fā)布大風藍色預警
- (2023-06-27)比亞迪全新混動系統(tǒng) DM-o 曝光:方程豹車型首發(fā),續(xù)航可達1200公里!_全球滾動
- (2023-06-27)視焦點訊!從5米高的大橋上縱身一躍!凌晨兩點,寧夏輔警馬世豪勇救輕生男子
- (2023-06-27)天天通訊!男孩遭家長棍打后跳下5樓,致雙腿骨折,律師:家長或構(gòu)成過失致人重傷罪
- (2023-06-27)《偷偷藏不住》導演李青蓉:桑稚不好演,趙露思很用功
- (2023-06-27)高溫再度“上線” 如何用好防暑“利器”? 環(huán)球微速訊
- (2023-06-27)肉芽組織變?yōu)轳:劢M織時所見到的變化是(肉芽組織) 焦點速遞
- (2023-06-27)世界快報:顯卡測試軟件哪個好 顯卡測試軟件叫什么
- (2023-06-27)秘密花園主要內(nèi)容概括(秘密花園主要內(nèi)容)_全球快消息
- (2023-06-27)如何提高英語詞匯 如何提高英語
- (2023-06-27)諸暨寶石鑒定機構(gòu)談談為什么貴族喜歡高定珠寶 熱文
- (2023-06-27)黃子佼岳母說心疼女婿 樂意每天為女婿煮湯
- (2023-06-27)【調(diào)研快報】通裕重工接待中信建投等多家機構(gòu)調(diào)研|全球速看料
- (2023-06-27)如果能夠穿越回2020年,新希望生豬養(yǎng)殖還會激進擴張?
- (2023-06-27)V觀財報|躍嶺股份年報被問詢:利用信披迎合熱點炒作?配合實控人減持?
- (2023-06-27)如何降低糖尿病發(fā)生風險,聽聽院士怎么說-世界快資訊
- (2023-06-27)2023年浙江臺州市區(qū)中考普通高中錄取分數(shù)線
- (2023-06-27)最新:當古詩詞朗誦遇上即興民樂演奏
- (2023-06-27)安徽省全椒縣召開校園食品安全培訓_世界消息
- (2023-06-27)廣州酒家董事長徐偉兵計劃減持不超過19.7萬股_世界熱點
- (2023-06-27)曝三星Galaxy S24系列內(nèi)部代號“繆斯”,大杯S24+不會被砍-環(huán)球速遞
- (2023-06-27)全球最大航空公司遭遇供應鏈攻擊,大量飛行員敏感數(shù)據(jù)泄露
- (2023-06-27)消息稱阿里巴巴最快11月分拆盒馬上市_天天快看點