環(huán)球要聞:重要升級(jí)!btrace 2.0 技術(shù)原理大揭秘
2023-06-27 10:15:30 來源:技術(shù)聯(lián)盟
抖音性能平臺(tái)團(tuán)隊(duì) 字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì) 2023-06-26 12:01 發(fā)表于北京
干貨不迷路
【資料圖】
項(xiàng)目 GitHub 地址 :https://github.com/bytedance/btrace
背景介紹
在一年多前,我們對(duì)外正式開源了 btrace(AKA RheaTrace),它是基于 Systrace 的高性能 Trace 工具,目前字節(jié)跳動(dòng)已經(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ī)端還依賴外部存儲(chǔ)訪問權(quán)限,在初次使用時(shí)很容易導(dǎo)致打斷。同時(shí)產(chǎn)物體積龐大,網(wǎng)頁打開速度很慢。 性能體驗(yàn) :大型應(yīng)用插樁數(shù)量達(dá)到百萬級(jí)別,性能損耗接近 100%,對(duì)性能優(yōu)化工作產(chǎn)生一定困擾。 監(jiān)控?cái)?shù)據(jù) :在 Trace 分析過程中,有些信息是缺失的,并不知道耗時(shí)原因,比如目前 Trace 中僅包含 synchronized 鎖信息,缺少 ReentrantLock 等其他鎖信息,同時(shí)渲染監(jiān)控只有部分系統(tǒng)關(guān)鍵路徑信息,缺少業(yè)務(wù)層信息。同時(shí),隨著 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)疃?、最集中的問題,同時(shí)適應(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,同時(shí)還將 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%,存儲(chǔ)改為 mmap 方式、優(yōu)化無鎖隊(duì)列邏輯、提供精準(zhǔn)插樁策略等,全插樁場(chǎng)景下性能損耗進(jìn)一步降低至 15%! 監(jiān)控?cái)?shù)據(jù): 新增 4 項(xiàng)數(shù)據(jù)監(jiān)控能力,重磅推出渲染詳情采集能力!同時(shí)還新增 Binder、線程啟動(dòng)、Wait/Notify/Park/Unpark 等詳情數(shù)據(jù)!接下來,我們將詳細(xì)介紹上述三個(gè)改進(jìn)方向的具體機(jī)制與實(shí)現(xiàn)原理,以幫助您深入了解 btrace 2.0 的重要升級(jí)。
原理揭秘
Perfetto 簡(jiǎn)介
Perfetto 和 Systrace 都是用于 Android 系統(tǒng)的性能分析和調(diào)試的工具,但它們有所不同:
Systrace :是 Android SDK 中的一個(gè)工具,可用于捕獲和分析不同系統(tǒng)進(jìn)程的時(shí)序事件,并提供了用于分析系統(tǒng)性能瓶頸的圖形界面。Systrace 能夠捕獲的事件包括 CPU、內(nèi)存、網(wǎng)絡(luò)、磁盤I/O、渲染等等。Systrace 的工作原理是在內(nèi)核和用戶空間捕獲和解析時(shí)序事件,并將其記錄到 HTML 文件中,開發(fā)者可以使用 Chrome 瀏覽器來分析這些事件。Systrace 能夠很好地幫助開發(fā)者找出系統(tǒng)瓶頸,但它在性能方面的表現(xiàn)并不理想,尤其是在處理大量數(shù)據(jù)時(shí)。
Perfetto :是一個(gè)全新、低開銷的 Trace 采集工具,旨在優(yōu)化 Systrace 的性能表現(xiàn)。Perfetto 的目標(biāo)是提供比 Systrace 更快、更細(xì)粒度的 Trace 采集,并支持與其他跨平臺(tái)工具集成。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 采集的整體流程:
整個(gè)流程分為以下三個(gè)階段:
App 編譯時(shí): 在應(yīng)用程序編譯階段,我們提供了兩種插樁模式:方法數(shù)字標(biāo)識(shí)插樁和方法字符串標(biāo)識(shí)插樁。方法 數(shù)字標(biāo)識(shí)插樁適用于只需要記錄方法名稱的場(chǎng)景,而方法字符串標(biāo)識(shí)插樁可以同時(shí)記錄方法參數(shù)的值。此外,我們還支持精準(zhǔn)插樁引擎,自動(dòng)識(shí)別可疑耗時(shí)代碼并進(jìn)行插樁。
App 運(yùn)行時(shí): 在應(yīng)用程序運(yùn)行期間,主要工作是采集應(yīng)用的 apptrace 信息,對(duì)于方法數(shù)字標(biāo)識(shí)類型的信息,通過 mmap 無鎖隊(duì)列方式采集;對(duì)于字符串標(biāo)識(shí)類型的信息,直接通過系統(tǒng)函數(shù)寫入 atrace,同時(shí)代理 atrace 寫入邏輯,將其替換為 LFRB 高性能寫入方案。
桌面腳本: 桌面腳本主要用于控制應(yīng)用程序的運(yùn)行和開啟/關(guān)閉 Trace 采集功能。此外,桌面腳本還負(fù)責(zé)對(duì)采集到的 apptrace 與 atrace 數(shù)據(jù)進(jìn)行編碼,并將它們與 ftrace 進(jìn)行合并。
技術(shù)揭秘
1. 使用體驗(yàn)
使用體驗(yàn)問題在用戶反饋中最多,分析下來基本是存儲(chǔ)權(quán)限、Systrace 環(huán)境、Python 環(huán)境、Trace 產(chǎn)物體積過大、Perffetto 網(wǎng)頁打開過慢等問題,這些體驗(yàn)問題我們完成了針對(duì)性的優(yōu)化:
權(quán)限優(yōu)化
為進(jìn)行數(shù)據(jù)處理,桌面腳本需要訪問到 App 數(shù)據(jù)。在 App 層面,最方便的方式是將數(shù)據(jù)存儲(chǔ)到公共 SDCard 中。但從 Android Q 開始,Google 收緊對(duì)外置存儲(chǔ)完全訪問權(quán)限。盡管 requestLegacyExternalStorage 可以臨時(shí)解決這個(gè)問題,但從長(zhǎng)遠(yuǎn)來看,SDCard 將無法完全訪問。
為解決此問題,我們搭建 Http Server 來通過端口對(duì)外訪問數(shù)據(jù),但訪問該 Server 仍需要確定服務(wù)地址,為此,我們使用 adb forward 功能,它可以建立一個(gè)轉(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)行時(shí)通過 push 配置文件到指定目錄進(jìn)行動(dòng)態(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 的訪問,同樣我們可以在腳本啟動(dòng) HttpServer 來實(shí)現(xiàn)數(shù)據(jù)接收。但是,因?yàn)槭蔷W(wǎng)絡(luò)請(qǐng)求意味著 App 讀取參數(shù)只能在子線程進(jìn)行,會(huì)有一定的不便,尤其在需要參數(shù)實(shí)時(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í)讀取參數(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ā),對(duì) Python 2.7 有強(qiáng)依賴,而 Python 2.7 已被官方廢棄,同時(shí)大多數(shù) Android 工程師對(duì) Python 不太熟悉,浪費(fèi)了大量時(shí)間解決環(huán)境問題。對(duì)此,我們計(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 新推出的性能分析平臺(tái),支持多種數(shù)據(jù)格式解析,Systrace 格式是其中一種,同時(shí) Perfetto 還支持 Protocol Buffer 格式,pb 是一種輕量級(jí)、高效的數(shù)據(jù)序列化格式,用于結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)和傳輸。Perfetto 使用 pb 作為其事件記錄格式,保證記錄系統(tǒng)事件數(shù)據(jù)的同時(shí),保持?jǐn)?shù)據(jù)的高效性和可伸縮性。pb 因?yàn)槠浣Y(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)可以實(shí)現(xiàn)更小體積占用與更快解析速度。因此,btrace 2.0 也將數(shù)據(jù)格式由 HTML 切換到 pb,在減小產(chǎn)物文件體積的同時(shí),還大幅提升 Trace 在網(wǎng)頁上的加載速度。
我們先簡(jiǎn)單介紹下 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 信息,具體使用方式可以參考下面簡(jiǎn)單示例:
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 是實(shí)際 // 進(jìn)程 ID,`
` 是必須項(xiàng) .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);}
上面示例將得到下面這個(gè) Trace:
下面再介紹如何將運(yùn)行時(shí)采集到的 apptrace 信息轉(zhuǎn)換成 pb 格式的,這部分操作在桌面腳本進(jìn)行。
首先腳本通過 adb http 方式獲取到手機(jī)上 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); // 記錄相應(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 對(duì) 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());
然而,這里有一個(gè)前提,就是業(yè)務(wù)采集的 apptrace / atrace 時(shí)間戳與系統(tǒng) frace 時(shí)間戳一致。實(shí)際上,根據(jù)實(shí)際測(cè)試的結(jié)果,不同設(shè)備 ftrace 時(shí)間戳可能會(huì)采用不同時(shí)間,可能是 BOOTTIME,也可能是 MONOTONIC TIME。這導(dǎo)致業(yè)務(wù)層無論使用哪種時(shí)間戳都只能兼容部分設(shè)備。為解決此問題,我們?cè)陂_始記錄 trace 信息時(shí),先記錄一份 BOOTTIME 和 MONOTONIC TIME 初始時(shí)間,之后再記錄時(shí)間戳?xí)r,都統(tǒng)一使用 MONOTONIC 時(shí)間。
最后在腳本中解析 ftrace 時(shí)間戳進(jìn)行判斷,如果與 MONOTONIC 接近,就采用 MONOTONIC;如果與 BOOTTIME 接近,就采用 BOOTTIME。雖然我們沒有單獨(dú)記錄每個(gè)函數(shù)的 BOOTTIME,但是可以通過 MONOTONIC 與初始時(shí)間差異折算。
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)行時(shí)優(yōu)化
btrace 1.0 在插樁上是嚴(yán)格依賴 Systrace 模式,通過在方法開始與結(jié)束插入 Trace.beginSection 與 endSection。但是 beginSection 參數(shù)是字符串,百萬級(jí)別方法插樁會(huì)造成數(shù)百萬字符串額外內(nèi)存占用,對(duì)內(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ù)丟失,過大則會(huì)造成內(nèi)存浪費(fèi)。
btrace 2.0 版本通過將方法 ID 數(shù)字化,將方法執(zhí)行信息記錄到一個(gè) mmap 映射文件中。由于方法 ID 大小是固定的,可以使用 atomic 原子操作計(jì)算存儲(chǔ)數(shù)據(jù)位置,從而實(shí)現(xiàn)無鎖并發(fā)寫入,同時(shí)方法數(shù)字存儲(chǔ)占用內(nèi)存更小,IO 持久化壓力也更小。
同時(shí),我們發(fā)現(xiàn) Trace.beginSection 和 endSection 方式,需要記錄每個(gè)方法的開始與結(jié)束時(shí)間、線程 ID 與方法 ID,這里面的線程 ID 和方法 ID 會(huì)重復(fù)記錄,也導(dǎo)致了內(nèi)存浪費(fèi)。于是專門進(jìn)行了優(yōu)化,在一條記錄中同時(shí)記錄開始時(shí)間、方法耗時(shí)、線程 ID 和方法 ID 信息,合計(jì)占用 2 個(gè) 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í)數(shù)據(jù)記錄格式示例:
方法插樁在 Java 層,但是數(shù)據(jù)采集基于 mmap 方式在 native 層實(shí)現(xiàn)。這會(huì)導(dǎo)致高頻 JNI 調(diào)用,當(dāng)一個(gè)非 JNI 方法調(diào)用常規(guī) JNI 方法,以及從常規(guī) JNI 方法返回時(shí),需要做線程狀態(tài)切換,線程狀態(tài)切換就會(huì)涉及到 GC 鎖操作,會(huì)有較大性能開銷。
熟悉 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)建一個(gè)定義 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ù)類型必須是基本類型@CriticalNativepublic static long nativeTraceBegin();// Critical JNI 方法,不再需要聲明 JNIEnv 與 jclass 參數(shù)static jlong Binary_nativeTraceBegin() { ...}// 動(dòng)態(tài)綁定JNINativeMethod t = {\"nativeTraceBegin\", \"(I)J\", (void *) JNI_CriticalTraceBegin};env->RegisterNatives(clazz, &t, 1);
@CriticalNative/@FastNative 是 8.0 及以后才支持的特性,對(duì)于 8.0 以前的設(shè)備,也可以通過在方法簽名中加入 ! 方式來開啟 FastNative:
// Fast JNI 方法,和普通 JNI 方法一樣需要 JNIEnv 參數(shù)與 jclass 參數(shù)static jlong Binary_nativeTraceBegin(JNIEnv *, jclass) { ...}// 動(dòng)態(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)勢(shì),但它也有一些限制,因?yàn)樗荒苡涗浽诰幾g階段準(zhǔn)備的通過 ID 映射的內(nèi)容,無法記錄 App 運(yùn)行時(shí)動(dòng)態(tài)生成的內(nèi)容。因此,除了方法 ID 的存儲(chǔ)外,我們還支持字符串類型的數(shù)據(jù)存儲(chǔ),主要用于記錄 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)插樁
另一個(gè)性能優(yōu)化是插樁優(yōu)化。隨著應(yīng)用中方法數(shù)量越來越多,插樁方法數(shù)量也隨之增多,久而久之插樁對(duì)應(yīng)用性能損耗也會(huì)越大。btrace 1.0 通過提供 traceFilterFilePath 配置讓用戶來選擇對(duì)哪些方法插樁,哪些不插樁,靈活配置的同時(shí)也把最終性能與插樁權(quán)衡的困擾轉(zhuǎn)移給了用戶。
在 2.0 中,我們希望建立一套智能規(guī)則,可以精準(zhǔn)識(shí)別用戶關(guān)心的高耗時(shí)方法,同時(shí)將不耗時(shí)方法精準(zhǔn)的排除在插樁規(guī)則以外,可以實(shí)現(xiàn)智能精準(zhǔn)插樁體驗(yàn)。
Android App 項(xiàng)目源碼最終會(huì)編譯為字節(jié)碼,雖然 Android 虛擬機(jī)支持 200 多條字節(jié)碼指令,但可能導(dǎo)致性能瓶頸的指令往往是比較少且易于枚舉的,如 IO 讀取、synchronized 字節(jié)碼、反射、Gson 解析等函數(shù)調(diào)用等。我們?cè)诰幾g過程中將調(diào)用相關(guān)指令的方法視為疑似耗時(shí)方法,而剩余的非耗時(shí)函數(shù)則不進(jìn)行插樁,因?yàn)樗鼈儾粫?huì)導(dǎo)致性能問題,從而大大縮小了插樁范圍。
以上述耗時(shí)特征為基礎(chǔ),我們?cè)O(shè)計(jì)了一條精細(xì)化插樁方案,方便用戶可以根據(jù)具體的情況選擇需要的插樁方法。支持的配置方案如下所示:
# 對(duì)鎖相關(guān)的方法插樁-tracesynchronize# 對(duì)Native方法的調(diào)用點(diǎn)插樁-tracenative# 對(duì)Aidl方法插樁-traceaidl# 對(duì)包含循環(huán)的方法插樁-traceloop# 關(guān)閉默認(rèn)耗時(shí)方法的調(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的所有方法均會(huì)被插樁-traceclass io.reactivex.internal.observers.LambdaObserver# 該方法的參數(shù)信息會(huì)在Trace中保留-allowclassmethodswithparametervalues rhea.sample.android.app.RheaApplication { printApplicationName(*java.lang.String);}
經(jīng)過我們的精細(xì)化插樁后,抖音插樁量減少 94%,在保留較完整的 Trace 數(shù)據(jù)的同時(shí),性能有了顯著的提升。
總之,我們通過權(quán)衡耗時(shí)函數(shù)插樁的優(yōu)點(diǎn)和缺點(diǎn),這樣可以幫助我們盡可能的獲取到足夠的耗時(shí)函數(shù)信息,同時(shí)避免過度插樁導(dǎo)致不必要的性能損耗。
3. 監(jiān)控?cái)?shù)據(jù)
監(jiān)控?cái)?shù)據(jù)是 Trace 的核心,關(guān)系到 Trace 能否給用戶帶來實(shí)際價(jià)值,除了常規(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 示例:
為此,我們針對(duì)這部分信息進(jìn)行更精細(xì)化拓展展示,新增記錄渲染關(guān)鍵 View 節(jié)點(diǎn),下圖是優(yōu)化后效果:
渲染監(jiān)控核心原理如下圖:
代理 LayoutInflater 獲取到 inflate 時(shí) 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)程通信的一個(gè)非常重要手段,然而我們?cè)谧鲂阅芊治鰰r(shí),會(huì)偶爾發(fā)現(xiàn) Binder 過程比較耗時(shí),雖然 Android 系統(tǒng) atrace 提供 Binder 耗時(shí)監(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 對(duì)應(yīng)的 interface // 記錄 Trace RHEA_ATRACE(\"binder transact[%s:%d]\", name.c_str(), code);}
這就記錄了以下信息,包含了 interfaceName 與 code:
binder transact[android.content.pm.IPackageManager:5]
此外,還需要將 code 解析到對(duì)應(yīng) Binder 調(diào)用方法。在 AIDL 中,interfaceName$Stub 類靜態(tài)字段中記錄了每個(gè) code 與對(duì)應(yīng) Binder 調(diào)用名稱。可以在抓取 trace 結(jié)束時(shí),通過反射獲取 code 與名稱映射關(guān)系,將他保存到 Trace 產(chǎn)物中。
#android.os.IHintManagerTRANSACTION_createHintSession:1TRANSACTION_getHintSessionPreferredRate:2#miui.security.ISecurityManagerTRANSACTION_activityResume:27TRANSACTION_addAccessControlPass:6
最后通過桌面腳本進(jìn)行處理,將運(yùn)行時(shí)記錄的 Trace 中 code 進(jìn)行替換,替換為真實(shí)的方法名稱即可。
阻塞監(jiān)控
鎖監(jiān)控是性能監(jiān)控中非常重要的一個(gè)監(jiān)控環(huán)節(jié),在 Android 系統(tǒng) atrace 中提供 synchronized 鎖沖突 Trace 信息,比如通過下圖可以得知主線程在獲取鎖時(shí)與 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();
在對(duì)應(yīng)的 hook 方法中,通過 Trace 記錄他們的執(zhí)行與對(duì)應(yīng)的 this(也就是鎖對(duì)象)的 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 時(shí)可能會(huì)遇到一些異常的線程,這時(shí)候往往需要分析線程在什么地方被創(chuàng)建,但是在傳統(tǒng) Trace 中缺少這部分信息。于是 btrace 加入了線程創(chuàng)建監(jiān)控?cái)?shù)據(jù),核心原理是對(duì) pthread_create 進(jìn)行代理,記錄線程創(chuàng)建的同時(shí),還記錄被創(chuàng)建線程的 tid。但是在 pthread_create 調(diào)用完成時(shí)是無法得知被創(chuàng)建線程 ID 的,通過分析系統(tǒng)源碼,發(fā)現(xiàn) pthread_t 本質(zhì)上是一個(gè) 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;}
最終實(shí)現(xiàn)效果如圖,比如發(fā)現(xiàn) Thread 16125 是新創(chuàng)建的線程,需要分析其創(chuàng)建位置,替換為線程池實(shí)現(xiàn)。
只需要檢索 pthread_create tid=16125 就能找到對(duì)應(yīng)的創(chuàng)建堆棧。
總結(jié)展望
以上介紹了 btrace 2.0 的主要優(yōu)化點(diǎn),更多優(yōu)化還需要在日常使用中去體會(huì)。2.0 不是終點(diǎn),是新征程的起點(diǎn),我們還將圍繞下面幾點(diǎn)持續(xù)優(yōu)化,將 btrace 優(yōu)化到極致:
使用體驗(yàn): 深入優(yōu)化使用體驗(yàn),比如支持不定長(zhǎng)時(shí)間 Trace 采集,優(yōu)化采集耗時(shí)。
性能體驗(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)控能力。
使用場(chǎng)景: 提供線上場(chǎng)景接入與使用方案,幫助解決線上疑難問題。
生態(tài)建設(shè): 圍繞 btrace 2.0 建設(shè)完善生態(tài),通過性能診斷與性能防劣化,自動(dòng)發(fā)現(xiàn)存量與增量性能問題。
最后,歡迎大家深入討論與交流,一起協(xié)作構(gòu)建極致 btrace 工具!
加入我們
抖音 Android 基礎(chǔ)技術(shù)團(tuán)隊(duì)是一個(gè)深度追求極致的團(tuán)隊(duì),我們專注于性能、架構(gòu)、包大小、穩(wěn)定性、基礎(chǔ)庫、編譯構(gòu)建等方向的深耕,保障超大規(guī)模團(tuán)隊(duì)的研發(fā)效率和數(shù)億用戶的使用體驗(yàn)。目前北京、上海、深圳都有人才需要,歡迎有志之士與我們共同建設(shè)億級(jí)用戶全球化 APP!
你可以進(jìn)入字節(jié)跳動(dòng)招聘官網(wǎng)查詢「抖音基礎(chǔ)技術(shù) Android」相關(guān)職位,也可以郵件聯(lián)系:chenjiawei.kisson@bytedance.com 咨詢相關(guān)信息或者直接發(fā)送簡(jiǎn)歷內(nèi)推!
關(guān)鍵詞:
相關(guān)閱讀
- (2023-06-27)環(huán)球要聞:重要升級(jí)!btrace 2.0 技術(shù)原理大揭秘
- (2023-06-27)今日看點(diǎn):小米12 spro 和小米12pro 的區(qū)別
- (2023-06-27)要聞速遞:途觀1 8T發(fā)動(dòng)機(jī)多少錢_途觀1 4t
- (2023-06-27)福建農(nóng)信兩大項(xiàng)目獲評(píng)“2022年福建省十大金融創(chuàng)新項(xiàng)目”
- (2023-06-27)上海一些機(jī)構(gòu)“假招工真賣課” 部分求職者陷入連環(huán)套_熱文
- (2023-06-27)青海湖水域面積達(dá)到近十年來最大值 湟魚洄游迎來高峰期
- (2023-06-27)熱議:讓利于民,多只基金宣布降費(fèi)
- (2023-06-27)寧夏回族自治區(qū)同心縣發(fā)布大風(fēng)藍(lán)色預(yù)警
- (2023-06-27)比亞迪全新混動(dòng)系統(tǒng) DM-o 曝光:方程豹車型首發(fā),續(xù)航可達(dá)1200公里!_全球滾動(dòng)
- (2023-06-27)視焦點(diǎn)訊!從5米高的大橋上縱身一躍!凌晨?jī)牲c(diǎn),寧夏輔警馬世豪勇救輕生男子
- (2023-06-27)天天通訊!男孩遭家長(zhǎng)棍打后跳下5樓,致雙腿骨折,律師:家長(zhǎng)或構(gòu)成過失致人重傷罪
- (2023-06-27)《偷偷藏不住》導(dǎo)演李青蓉:桑稚不好演,趙露思很用功
- (2023-06-27)高溫再度“上線” 如何用好防暑“利器”? 環(huán)球微速訊
- (2023-06-27)肉芽組織變?yōu)轳:劢M織時(shí)所見到的變化是(肉芽組織) 焦點(diǎn)速遞
- (2023-06-27)世界快報(bào):顯卡測(cè)試軟件哪個(gè)好 顯卡測(cè)試軟件叫什么
- (2023-06-27)秘密花園主要內(nèi)容概括(秘密花園主要內(nèi)容)_全球快消息
- (2023-06-27)如何提高英語詞匯 如何提高英語
- (2023-06-27)諸暨寶石鑒定機(jī)構(gòu)談?wù)劄槭裁促F族喜歡高定珠寶 熱文
- (2023-06-27)黃子佼岳母說心疼女婿 樂意每天為女婿煮湯
- (2023-06-27)【調(diào)研快報(bào)】通裕重工接待中信建投等多家機(jī)構(gòu)調(diào)研|全球速看料
- (2023-06-27)如果能夠穿越回2020年,新希望生豬養(yǎng)殖還會(huì)激進(jìn)擴(kuò)張?
- (2023-06-27)V觀財(cái)報(bào)|躍嶺股份年報(bào)被問詢:利用信披迎合熱點(diǎn)炒作?配合實(shí)控人減持?
- (2023-06-27)如何降低糖尿病發(fā)生風(fēng)險(xiǎn),聽聽院士怎么說-世界快資訊
- (2023-06-27)2023年浙江臺(tái)州市區(qū)中考普通高中錄取分?jǐn)?shù)線
- (2023-06-27)最新:當(dāng)古詩詞朗誦遇上即興民樂演奏
- (2023-06-27)安徽省全椒縣召開校園食品安全培訓(xùn)_世界消息
- (2023-06-27)廣州酒家董事長(zhǎng)徐偉兵計(jì)劃減持不超過19.7萬股_世界熱點(diǎn)
- (2023-06-27)曝三星Galaxy S24系列內(nèi)部代號(hào)“繆斯”,大杯S24+不會(huì)被砍-環(huán)球速遞
- (2023-06-27)全球最大航空公司遭遇供應(yīng)鏈攻擊,大量飛行員敏感數(shù)據(jù)泄露
- (2023-06-27)消息稱阿里巴巴最快11月分拆盒馬上市_天天快看點(diǎn)