重要升級!btrace 2.0 技術(shù)原理大揭秘

背景介紹
在一年多前,我們對外正式開源了 btrace(AKA RheaTrace),它是基于 Systrace 的高性能 Trace 工具,目前字節(jié)跳動已經(jīng)有接近 10+ 產(chǎn)品團(tuán)隊(duì)使用 btrace 做日常性能優(yōu)化工作。在這一年期間,我們收到很多社區(qū)以及公司內(nèi)部反饋,包括使用體驗(yàn)、性能體驗(yàn)、監(jiān)控?cái)?shù)據(jù)等上都收到眾多反饋,我們匯總了大家反饋的內(nèi)容,主要包括以下三類:
- 使用體驗(yàn):Windows 有著大量用戶群體,但 btrace 1.0 未支持;桌面腳本依賴 Systrace 和 Python 2.7 環(huán)境,導(dǎo)致環(huán)境搭建十分復(fù)雜,此外手機(jī)端還依賴外部存儲訪問權(quán)限,在初次使用時很容易導(dǎo)致打斷。同時產(chǎn)物體積龐大,網(wǎng)頁打開速度很慢。
 - 性能體驗(yàn):大型應(yīng)用插樁數(shù)量達(dá)到百萬級別,性能損耗接近 100%,對性能優(yōu)化工作產(chǎn)生一定困擾。
 - 監(jiān)控?cái)?shù)據(jù):在 Trace 分析過程中,有些信息是缺失的,并不知道耗時原因,比如目前 Trace 中僅包含 synchronized 鎖信息,缺少 ReentrantLock 等其他鎖信息,同時渲染監(jiān)控只有部分系統(tǒng)關(guān)鍵路徑信息,缺少業(yè)務(wù)層信息。
 
同時,隨著 Android 系統(tǒng)的不斷發(fā)展,Google 逐漸廢棄了 Systrace 工具,并開始大力推廣 Perfetto 工具。此外,由于系統(tǒng)的 sdcard 權(quán)限限制變得更加嚴(yán)格,btrace 在高版本 Android 系統(tǒng)中已經(jīng)出現(xiàn)兼容性問題。
在此背景下,我們決定大幅改造 btrace,解決用戶反饋?zhàn)疃唷⒆罴械膯栴},同時適應(yīng) Google 發(fā)布的新特性,并修復(fù)兼容性問題,以便更好地滿足開發(fā)者的需求,目前 btrace 2.0 在使用體驗(yàn)、性能體驗(yàn)、監(jiān)控?cái)?shù)據(jù)等方面均做出大量改進(jìn),重點(diǎn)改進(jìn)如下。
- 使用體驗(yàn): 支持 Windows 啦!此外將腳本實(shí)現(xiàn)從 Python 切至 Java 并去除各種權(quán)限要求,因腳本工具可用性問題引起的用戶使用打斷次數(shù)幾乎降為 0,同時還將 Trace 產(chǎn)物切至 PB 協(xié)議,產(chǎn)物體積減小 70%,網(wǎng)頁打開速度提升 7 倍!
 - 性能體驗(yàn): 通過大規(guī)模改造方法 Trace 邏輯,將 App 方法 Trace 底層結(jié)構(gòu)由字符串切換為整數(shù),實(shí)現(xiàn)內(nèi)存占用減少 80%,存儲改為 mmap 方式、優(yōu)化無鎖隊(duì)列邏輯、提供精準(zhǔn)插樁策略等,全插樁場景下性能損耗進(jìn)一步降低至 15%!
 - 監(jiān)控?cái)?shù)據(jù): 新增 4 項(xiàng)數(shù)據(jù)監(jiān)控能力,重磅推出渲染詳情采集能力!同時還新增 Binder、線程啟動、Wait/Notify/Park/Unpark 等詳情數(shù)據(jù)!
 
接下來,我們將詳細(xì)介紹上述三個改進(jìn)方向的具體機(jī)制與實(shí)現(xiàn)原理,以幫助您深入了解 btrace 2.0 的重要升級。
原理揭秘
Perfetto 簡介
Perfetto 和 Systrace 都是用于 Android 系統(tǒng)的性能分析和調(diào)試的工具,但它們有所不同:
Systrace:是 Android SDK 中的一個工具,可用于捕獲和分析不同系統(tǒng)進(jìn)程的時序事件,并提供了用于分析系統(tǒng)性能瓶頸的圖形界面。Systrace 能夠捕獲的事件包括 CPU、內(nèi)存、網(wǎng)絡(luò)、磁盤I/O、渲染等等。Systrace 的工作原理是在內(nèi)核和用戶空間捕獲和解析時序事件,并將其記錄到 HTML 文件中,開發(fā)者可以使用 Chrome 瀏覽器來分析這些事件。Systrace 能夠很好地幫助開發(fā)者找出系統(tǒng)瓶頸,但它在性能方面的表現(xiàn)并不理想,尤其是在處理大量數(shù)據(jù)時。
Perfetto:是一個全新、低開銷的 Trace 采集工具,旨在優(yōu)化 Systrace 的性能表現(xiàn)。Perfetto 的目標(biāo)是提供比 Systrace 更快、更細(xì)粒度的 Trace 采集,并支持與其他跨平臺工具集成。Perfetto 采用二進(jìn)制格式記錄 Trace 數(shù)據(jù),并使用基于 ProtoBuf 的數(shù)據(jù)交換格式進(jìn)行數(shù)據(jù)導(dǎo)出,可與 Grafana、SQLite、BigQuery 等其他分析和可視化工具集成。Perfetto 采集的數(shù)據(jù)種類非常廣泛,包括 CPU 使用情況、網(wǎng)絡(luò)字節(jié)流、觸摸輸入、渲染等等。與 Systrace 相比,Perfetto 在性能和可定制性方面更為出色。
因此,可以看出 Perfetto 是 Systrace 的一種更為先進(jìn)和優(yōu)秀的替代工具,它提供了更強(qiáng)大的數(shù)據(jù)采集和分析功能,更好的性能以及更好的可定制性,為開發(fā)人員提供更全面和深入的性能分析和調(diào)試工具。
整體流程
首先我們了解下 btrace 采集的整體流程:

整個流程分為以下三個階段:
App 編譯時: 在應(yīng)用程序編譯階段,我們提供了兩種插樁模式:方法數(shù)字標(biāo)識插樁和方法字符串標(biāo)識插樁。方法 數(shù)字標(biāo)識插樁適用于只需要記錄方法名稱的場景,而方法字符串標(biāo)識插樁可以同時記錄方法參數(shù)的值。此外,我們還支持精準(zhǔn)插樁引擎,自動識別可疑耗時代碼并進(jìn)行插樁。
App 運(yùn)行時: 在應(yīng)用程序運(yùn)行期間,主要工作是采集應(yīng)用的 apptrace 信息,對于方法數(shù)字標(biāo)識類型的信息,通過 mmap 無鎖隊(duì)列方式采集;對于字符串標(biāo)識類型的信息,直接通過系統(tǒng)函數(shù)寫入 atrace,同時代理 atrace 寫入邏輯,將其替換為 LFRB 高性能寫入方案。
桌面腳本: 桌面腳本主要用于控制應(yīng)用程序的運(yùn)行和開啟/關(guān)閉 Trace 采集功能。此外,桌面腳本還負(fù)責(zé)對采集到的 apptrace 與 atrace 數(shù)據(jù)進(jìn)行編碼,并將它們與 ftrace 進(jìn)行合并。
技術(shù)揭秘
1. 使用體驗(yàn)
使用體驗(yàn)問題在用戶反饋中最多,分析下來基本是存儲權(quán)限、Systrace 環(huán)境、Python 環(huán)境、Trace 產(chǎn)物體積過大、Perffetto 網(wǎng)頁打開過慢等問題,這些體驗(yàn)問題我們完成了針對性的優(yōu)化:
權(quán)限優(yōu)化
為進(jìn)行數(shù)據(jù)處理,桌面腳本需要訪問到 App 數(shù)據(jù)。在 App 層面,最方便的方式是將數(shù)據(jù)存儲到公共 SDCard 中。但從 Android Q 開始,Google 收緊對外置存儲完全訪問權(quán)限。盡管 requestLegacyExternalStorage 可以臨時解決這個問題,但從長遠(yuǎn)來看,SDCard 將無法完全訪問。
為解決此問題,我們搭建 Http Server 來通過端口對外訪問數(shù)據(jù),但訪問該 Server 仍需要確定服務(wù)地址,為此,我們使用 adb forward 功能,它可以建立一個轉(zhuǎn)發(fā),將 PC 端數(shù)據(jù)轉(zhuǎn)發(fā)到手機(jī)端口,并且可以獲取從手機(jī)端口返回的數(shù)據(jù)。這樣,我們就可以使用 localhost 訪問數(shù)據(jù)。
以上解決了腳本讀取 App 數(shù)據(jù)的問題,我們還面臨 App 讀取腳本參數(shù)問題,比如 maxAppTraceBufferSize、 mainThreadOnly,在 btrace 1.0 支持運(yùn)行時通過 push 配置文件到指定目錄進(jìn)行動態(tài)調(diào)整,但這也需要 SDCard 訪問權(quán)限,為徹底去除權(quán)限依賴,我們需要引入新方案。
首先想到的是 adb forward 反向方案:adb reverse,它可以將手機(jī)端口數(shù)據(jù)轉(zhuǎn)發(fā)給 PC,實(shí)現(xiàn)了從手機(jī)到 PC 的訪問,同樣我們可以在腳本啟動 HttpServer 來實(shí)現(xiàn)數(shù)據(jù)接收。但是,因?yàn)槭蔷W(wǎng)絡(luò)請求意味著 App 讀取參數(shù)只能在子線程進(jìn)行,會有一定的不便,尤其在需要參數(shù)實(shí)時生效時。
我們又研究了新方案,在桌面腳本通過 adb setprop 給手機(jī)設(shè)置參數(shù),App 通過 __system_property_get 來讀取參數(shù),只要是參數(shù) property 名稱以 debug. 開頭,就無需任何權(quán)限。
// 桌面腳本設(shè)置參數(shù)
Adb.call("shell", "setprop", "debug.rhea.startWhenAppLaunch", "1");
// 手機(jī)運(yùn)行時讀取參數(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 有強(qiáng)依賴,而 Python 2.7 已被官方廢棄,同時大多數(shù) Android 工程師對 Python 不太熟悉,浪費(fèi)了大量時間解決環(huán)境問題。對此,我們計(jì)劃將 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)容太大、加載速度過慢、甚至需要單獨(dú)搭建服務(wù)來支持 Trace 顯示的問題。Perfetto 是 Google 新推出的性能分析平臺,支持多種數(shù)據(jù)格式解析,Systrace 格式是其中一種,同時 Perfetto 還支持 Protocol Buffer 格式,pb 是一種輕量級、高效的數(shù)據(jù)序列化格式,用于結(jié)構(gòu)化數(shù)據(jù)存儲和傳輸。Perfetto 使用 pb 作為其事件記錄格式,保證記錄系統(tǒng)事件數(shù)據(jù)的同時,保持?jǐn)?shù)據(jù)的高效性和可伸縮性。pb 因?yàn)槠浣Y(jié)構(gòu)化數(shù)據(jù)存儲可以實(shí)現(xiàn)更小體積占用與更快解析速度。因此,btrace 2.0 也將數(shù)據(jù)格式由 HTML 切換到 pb,在減小產(chǎn)物文件體積的同時,還大幅提升 Trace 在網(wǎng)頁上的加載速度。
我們先簡單介紹下 Perfetto 的 pb 數(shù)據(jù)格式,然后再介紹如何將采集到的 apptrace 與 atrace 編碼為 pb 格式,以及如何將其與系統(tǒng) ftrace 進(jìn)行融合。
Perfetto pb 是由一系列 TracePacket 組成,官方文檔可以參考:https://perfetto.dev/docs/reference/trace-packet-proto,這里將介紹 btrace 使用到的一種 TracePacket:FtraceEventBundle:
FtraceEventBundle 是 Android 用于收集系統(tǒng) Trace 數(shù)據(jù)的一種機(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\n 這里 pid 是實(shí)際
                                        // 進(jìn)程 ID,`\n` 是必須項(xiàng)
                                        .setBuf("B|10010|someEvent\n"))) 
        .addEvent(
                FtraceEvent.newBuilder()
                        .setPid(threadId)
                        .setTimestamp(System.nanoTime() + TimeUnit.SECONDS.toNanos(2))
                        .setPrint(
                                Ftrace.PrintFtraceEvent.newBuilder()
                                        .setBuf("E|10010|\n")))
        .setCpu(0);
Trace trace = Trace.newBuilder()
        .addPacket(
                TracePacketOuterClass.TracePacket.newBuilder()
                        .setFtraceEvents(bundle)).build();
try (FileOutputStream out = new FileOutputStream("demo.pb")) {
    trace.writeTo(out);
}上面示例將得到下面這個 Trace:

下面再介紹如何將運(yùn)行時采集到的 apptrace 信息轉(zhuǎn)換成 pb 格式的,這部分操作在桌面腳本進(jìn)行。
首先腳本通過 adb http 方式獲取到手機(jī)上 mmap 映射文件,然后再解析文件內(nèi)容:
// 讀取 mapping,我們將 mapping 內(nèi)置到了 apk 的 assets 目錄
Map<Integer, String> mapping = 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);
    // 記錄相應(yīng)的開始與結(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 進(jìn)行編碼即可,這里不再展開。atrace 的處理方式也是類似的,也不再闡述。
再介紹下如何將采集到的 apptrace、atrace 與系統(tǒng) ftrace 進(jìn)行合并。
前文介紹過,Perfetto pb 是由一系列 TracePacket 組成,一般而言,我們只要將業(yè)務(wù)采集到的 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è)務(wù)采集的 apptrace / atrace 時間戳與系統(tǒng) frace 時間戳一致。實(shí)際上,根據(jù)實(shí)際測試的結(jié)果,不同設(shè)備 ftrace 時間戳可能會采用不同時間,可能是 BOOTTIME,也可能是 MONOTONIC TIME。這導(dǎo)致業(yè)務(wù)層無論使用哪種時間戳都只能兼容部分設(shè)備。為解決此問題,我們在開始記錄 trace 信息時,先記錄一份 BOOTTIME 和 MONOTONIC TIME 初始時間,之后再記錄時間戳?xí)r,都統(tǒng)一使用 MONOTONIC 時間。
最后在腳本中解析 ftrace 時間戳進(jìn)行判斷,如果與 MONOTONIC 接近,就采用 MONOTONIC;如果與 BOOTTIME 接近,就采用 BOOTTIME。雖然我們沒有單獨(dú)記錄每個函數(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àn)
運(yùn)行時優(yōu)化
btrace 1.0 在插樁上是嚴(yán)格依賴 Systrace 模式,通過在方法開始與結(jié)束插入 Trace.beginSection 與 endSection。但是 beginSection 參數(shù)是字符串,百萬級別方法插樁會造成數(shù)百萬字符串額外內(nèi)存占用,對內(nèi)存造成巨大壓力,并且也導(dǎo)致數(shù)據(jù) IO 持久化壓力巨大。此外,字符串?dāng)?shù)據(jù)大小不固定,只能通過有鎖或者 LFRB 方式來記錄數(shù)據(jù),無法做到數(shù)據(jù)高效并發(fā)寫入,只能將數(shù)據(jù)緩存在 buffer 中,但是過小 buffer 容易導(dǎo)致數(shù)據(jù)丟失,過大則會造成內(nèi)存浪費(fèi)。
btrace 2.0 版本通過將方法 ID 數(shù)字化,將方法執(zhí)行信息記錄到一個 mmap 映射文件中。由于方法 ID 大小是固定的,可以使用 atomic 原子操作計(jì)算存儲數(shù)據(jù)位置,從而實(shí)現(xiàn)無鎖并發(fā)寫入,同時方法數(shù)字存儲占用內(nèi)存更小,IO 持久化壓力也更小。
同時,我們發(fā)現(xiàn) Trace.beginSection 和 endSection 方式,需要記錄每個方法的開始與結(jié)束時間、線程 ID 與方法 ID,這里面的線程 ID 和方法 ID 會重復(fù)記錄,也導(dǎo)致了內(nèi)存浪費(fèi)。于是專門進(jìn)行了優(yōu)化,在一條記錄中同時記錄開始時間、方法耗時、線程 ID 和方法 ID 信息,合計(jì)占用 2 個 long,可以充分利用內(nèi)存。
具體插樁邏輯可以參考偽代碼:
// 業(yè)務(wù)代碼
public void appLogic() {
    long begin = nativeTraceBegin();
    // 業(yè)務(wù)邏輯
    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 層實(shí)現(xiàn)。這會導(dǎo)致高頻 JNI 調(diào)用,當(dāng)一個非 JNI 方法調(diào)用常規(guī) JNI 方法,以及從常規(guī) JNI 方法返回時,需要做線程狀態(tài)切換,線程狀態(tài)切換就會涉及到 GC 鎖操作,會有較大性能開銷。
熟悉 Android 系統(tǒng)的同學(xué)可能了解系統(tǒng)專門為高頻 JNI 調(diào)用做的性能優(yōu)化,通過 @CriticalNative 注解與 @FastNative 注解方式來實(shí)現(xiàn),@FastNative 可以使原生方法的性能提升高達(dá) 2 倍,@CriticalNative 則可以提升高達(dá) 4 倍。
我們也參考系統(tǒng)的方式,給方法添加 @CriticalNative 注解實(shí)現(xiàn)方法調(diào)用加速。但是 @CriticalNative 注解是隱藏 API無法直接使用,可以通過構(gòu)建一個定義 CriticalNative 注解的 jar 包,在項(xiàng)目中通過 compileOnly 方式依賴,來達(dá)到使用 @CriticalNative 注解的目的。相關(guān)注解定義參考自源碼:
// 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ù)類型必須是基本類型
@CriticalNative
public 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 以前的設(shè)備,也可以通過在方法簽名中加入!方式來開啟 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)化的相關(guān)內(nèi)容,前文產(chǎn)物優(yōu)化專題已經(jīng)介紹具體的數(shù)據(jù)解碼和 mapping 映射的方案,這里不再贅述。
雖然方法數(shù)字 ID 采集具有性能與內(nèi)存的優(yōu)勢,但它也有一些限制,因?yàn)樗荒苡涗浽诰幾g階段準(zhǔn)備的通過 ID 映射的內(nèi)容,無法記錄 App 運(yùn)行時動態(tài)生成的內(nèi)容。因此,除了方法 ID 的存儲外,我們還支持字符串類型的數(shù)據(jù)存儲,主要用于記錄 btrace 細(xì)粒度監(jiān)控?cái)?shù)據(jù)和方法參數(shù)值。這一方案與 btrace 1.0 中的 LFRB 方案相似,這里就不再詳細(xì)闡述。由于大部分 trace 數(shù)據(jù)都是方法 ID,已經(jīng)被 mmap 分擔(dān)了壓力,因此 LFRB 的壓力相比 btrace 1.0 小得多,我們可以適當(dāng)減小 buffer 大小。
精準(zhǔn)插樁
另一個性能優(yōu)化是插樁優(yōu)化。隨著應(yīng)用中方法數(shù)量越來越多,插樁方法數(shù)量也隨之增多,久而久之插樁對應(yīng)用性能損耗也會越大。btrace 1.0 通過提供 traceFilterFilePath 配置讓用戶來選擇對哪些方法插樁,哪些不插樁,靈活配置的同時也把最終性能與插樁權(quán)衡的困擾轉(zhuǎn)移給了用戶。
在 2.0 中,我們希望建立一套智能規(guī)則,可以精準(zhǔn)識別用戶關(guān)心的高耗時方法,同時將不耗時方法精準(zhǔn)的排除在插樁規(guī)則以外,可以實(shí)現(xiàn)智能精準(zhǔn)插樁體驗(yàn)。
Android App 項(xiàng)目源碼最終會編譯為字節(jié)碼,雖然 Android 虛擬機(jī)支持 200 多條字節(jié)碼指令,但可能導(dǎo)致性能瓶頸的指令往往是比較少且易于枚舉的,如 IO 讀取、synchronized 字節(jié)碼、反射、Gson 解析等函數(shù)調(diào)用等。我們在編譯過程中將調(diào)用相關(guān)指令的方法視為疑似耗時方法,而剩余的非耗時函數(shù)則不進(jìn)行插樁,因?yàn)樗鼈儾粫?dǎo)致性能問題,從而大大縮小了插樁范圍。

以上述耗時特征為基礎(chǔ),我們設(shè)計(jì)了一條精細(xì)化插樁方案,方便用戶可以根據(jù)具體的情況選擇需要的插樁方法。支持的配置方案如下所示:
# 對鎖相關(guān)的方法插樁
-tracesynchronize
# 對Native方法的調(diào)用點(diǎn)插樁
-tracenative
# 對Aidl方法插樁
-traceaidl
# 對包含循環(huán)的方法插樁
-traceloop
# 關(guān)閉默認(rèn)耗時方法的調(diào)用插樁
-disabledefaultpreciseinject
# 開啟大方法插樁,方法調(diào)用數(shù)超過40
-tracelargemethod 40
# 該方法的調(diào)用方需要進(jìn)行插樁
-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)過我們的精細(xì)化插樁后,抖音插樁量減少 94%,在保留較完整的 Trace 數(shù)據(jù)的同時,性能有了顯著的提升。
總之,我們通過權(quán)衡耗時函數(shù)插樁的優(yōu)點(diǎn)和缺點(diǎn),這樣可以幫助我們盡可能的獲取到足夠的耗時函數(shù)信息,同時避免過度插樁導(dǎo)致不必要的性能損耗。
3. 監(jiān)控?cái)?shù)據(jù)
監(jiān)控?cái)?shù)據(jù)是 Trace 的核心,關(guān)系到 Trace 能否給用戶帶來實(shí)際價值,除了常規(guī)方法執(zhí)行 Trace 以外,本次 2.0 還帶來了渲染監(jiān)控、Binder 監(jiān)控、阻塞監(jiān)控、線程創(chuàng)建監(jiān)控等四大能力,下面將介紹相關(guān)背景與實(shí)現(xiàn)原理。
渲染監(jiān)控
Android 系統(tǒng)提供提供 RenderThread 關(guān)鍵執(zhí)行邏輯的跟蹤埋點(diǎn),但其提供的信息不夠充分,無法直觀分析是具體影響渲染問題的業(yè)務(wù)代碼,下圖是 atrace 中渲染線程 Trace 示例:

為此,我們針對這部分信息進(jìn)行更精細(xì)化拓展展示,新增記錄渲染關(guān)鍵 View 節(jié)點(diǎn),下圖是優(yōu)化后效果:

渲染監(jiān)控核心原理如下圖:

- 代理 LayoutInflater 獲取到 inflate 時 View 所屬的布局信息,再通過 View 的 RenderNode 與 native 層 RenderNode關(guān)系,將 View 所屬布局信息綁定到 RenderNode 的 name 字段上。
 - Hook 渲染階段的關(guān)鍵節(jié)點(diǎn),比如 SyncFrameState 階段的 
RenderNode::prepareTreeImpl方法和 RenderPipeline 階段的RenderNodeDrawable::forceDraw方法,將 RenderNode 所屬 View 的布局信息記錄到 Trace 中。 
Binder 監(jiān)控
Binder 是 Android 跨進(jìn)程通信的一個非常重要手段,然而我們在做性能分析時,會偶爾發(fā)現(xiàn) Binder 過程比較耗時,雖然 Android 系統(tǒng) atrace 提供 Binder 耗時監(jiān)控信息,但其并未提供是何種類型的 Binder 調(diào)用,如下圖。

btrace 的 Binder 增強(qiáng)目標(biāo)是將 Binder 調(diào)用的接口名稱與方法名稱進(jìn)行解析與展示,實(shí)現(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 關(guān)聯(lián)信息,隨后再在 IPCThreadState::transact 中通過查詢獲取 interfaceName.
status_t Parcel::writeInterfaceToken(const char* interface) {
    // 記錄 this Parcel 與 interface 名稱的關(guān)聯(lián)
}
status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data,
                                  Parcel* reply, uint32_t flags) {
    // 查詢 Parcel data 對應(yīng)的 interface
    // 記錄 Trace
    RHEA_ATRACE("binder transact[%s:%d]", name.c_str(), code);
}這就記錄了以下信息,包含了 interfaceName 與 code:
binder transact[android.content.pm.IPackageManager:5]此外,還需要將 code 解析到對應(yīng) Binder 調(diào)用方法。在 AIDL 中,interfaceName$Stub 類靜態(tài)字段中記錄了每個 code 與對應(yīng) Binder 調(diào)用名稱??梢栽谧ト?trace 結(jié)束時,通過反射獲取 code 與名稱映射關(guān)系,將他保存到 Trace 產(chǎn)物中。
#android.os.IHintManager
TRANSACTION_createHintSession:1
TRANSACTION_getHintSessionPreferredRate:2
#miui.security.ISecurityManager
TRANSACTION_activityResume:27
TRANSACTION_addAccessControlPass:6最后通過桌面腳本進(jìn)行處理,將運(yùn)行時記錄的 Trace 中 code 進(jìn)行替換,替換為真實(shí)的方法名稱即可。
阻塞監(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 的底層實(shí)現(xiàn)是利用 park 和 unpark。btrace 阻塞監(jiān)控就是提供這部分阻塞信息,下面是 wait/notify 關(guān)聯(lián)示例,通過檢索鎖 obj 信息可以得知當(dāng)前線程 wait 匹配的 notify 的位置:


wait 與 park 等待原理類似,這里以大家更熟悉的 wait/notify 組合進(jìn)行說明。
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();在對應(yīng)的 hook 方法中,通過 Trace 記錄他們的執(zhí)行與對應(yīng)的 this(也就是鎖對象)的 identityHashCode,這樣可以通過 identityHashCode 建立起映射關(guān)系。
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)控?cái)?shù)據(jù),核心原理是對 pthread_create 進(jìn)行代理,記錄線程創(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.h
struct 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;
}最終實(shí)現(xiàn)效果如圖,比如發(fā)現(xiàn) Thread 16125 是新創(chuàng)建的線程,需要分析其創(chuàng)建位置,替換為線程池實(shí)現(xiàn)。

只需要檢索 pthread_create tid=16125就能找到對應(yīng)的創(chuàng)建堆棧。

總結(jié)展望
以上介紹了 btrace 2.0 的主要優(yōu)化點(diǎn),更多優(yōu)化還需要在日常使用中去體會。2.0 不是終點(diǎn),是新征程的起點(diǎn),我們還將圍繞下面幾點(diǎn)持續(xù)優(yōu)化,將 btrace 優(yōu)化到極致:
使用體驗(yàn): 深入優(yōu)化使用體驗(yàn),比如支持不定長時間 Trace 采集,優(yōu)化采集耗時。
性能體驗(yàn): 持續(xù)探索性能優(yōu)化,正面與側(cè)面優(yōu)化雙結(jié)合,提供更加極致性能體驗(yàn)。
監(jiān)控?cái)?shù)據(jù): 在 Java 與 ART 虛擬機(jī)基礎(chǔ)之上,建設(shè)包括內(nèi)存、C/C++、JavaScript 等更多更全的監(jiān)控能力。
使用場景: 提供線上場景接入與使用方案,幫助解決線上疑難問題。
生態(tài)建設(shè): 圍繞 btrace 2.0 建設(shè)完善生態(tài),通過性能診斷與性能防劣化,自動發(fā)現(xiàn)存量與增量性能問題。
最后,歡迎大家深入討論與交流,一起協(xié)作構(gòu)建極致 btrace 工具!
加入我們
抖音 Android 基礎(chǔ)技術(shù)團(tuán)隊(duì)是一個深度追求極致的團(tuán)隊(duì),我們專注于性能、架構(gòu)、包大小、穩(wěn)定性、基礎(chǔ)庫、編譯構(gòu)建等方向的深耕,保障超大規(guī)模團(tuán)隊(duì)的研發(fā)效率和數(shù)億用戶的使用體驗(yàn)。目前北京、上海、深圳都有人才需要,歡迎有志之士與我們共同建設(shè)億級用戶全球化 APP!
你可以進(jìn)入字節(jié)跳動招聘官網(wǎng)查詢「抖音基礎(chǔ)技術(shù) Android」相關(guān)職位,也可以郵件聯(lián)系:chenjiawei.kisson@bytedance.com 咨詢相關(guān)信息或者直接發(fā)送簡歷內(nèi)推!















 
 
 




 
 
 
 