btrace 開源!基於 Systrace 高效能 Trace 工具

語言: CN / TW / HK

theme: juejin

介紹

btrace(又名 RheaTrace) 是抖音基礎技術團隊自研的一款高效能 Android Trace 工具,它基於 Systrace 實現,並針對 Systrace 不足之處加以改進,核心改進點如下。

  1. 效率提升:編譯期間為 App 方法自動注入自定義事件,並提供高效、靈活配置規則。
  2. 效能提升:改進 Systrace 檔案實時寫 atrace 資料方式,效能提升最大 400 % 以上。
  3. 實用性提升:額外提供更詳細 IO 等資料,大幅提升方法耗時歸因效率;使用獨創方案徹底來解決方法因執行異常引起 trace 資料不閉合問題。

專案地址:

https://github.com/bytedance/btrace

目前位元組跳動已有多款 App 接入,包括抖音、TikTok、今日頭條、幸福裡等均已接入 RheaTrace,併為其體驗優化提供強有力支援。藉助 RheaTrace 將為您的 App 帶來極致流暢體驗,RheaTrace 使用效果如下(因保密原則,每個方法用 ID 表示)。

圖片

Systrace 簡介

如果我們使用過 Systrace 分析應用效能,我們都知道 Systrace 提供 Category 配置讓使用者決定採集哪些系統 atrace 資料,如下命令,從 sched 開始後續是不同類別的 atrace 資料。

python systrace.py -o mynewtrace.html sched freq idle am wm gfx view \     binder_driver hal dalvik camera input res

atrace 的資料型別見下圖:

圖片

其中,使用者空間 atrace 型別包括應用層自定義 Trace 事件、系統層 gfx 渲染相關 Trace、系統層鎖相關 Trace 資訊等,其最終都是通過呼叫 Android SDK 提供 Trace.beginSection 或者 ATRACE_BEGIN 記錄到同一個檔案 /sys/kernel/debug/tracing/trace_marker 中。此節點允許使用者層寫入資料,ftrace 會記錄該寫入操作時間戳。當用戶層發生函式呼叫時,ftrace 可以記錄被跟蹤函式的執行時間。atrace 若需記錄使用者層某一 trace 型別,只需啟用對應 TAG 型別即可。如選擇 gfx,則會啟用 ATRACE_TAG_GRAPHICS,並將渲染事件記錄到 trace_marker 檔案中。

核心空間的資料主要是一些補充分析資料,如 freq、sched、binder 等,常用 CPU 排程相關資訊包括:

  1. CPU 頻率變化情況。
  2. 任務執行情況。
  3. 大小核排程情況。
  4. CPU Boost 排程情況。

圖片

關於圖中一些標籤釋義。

  1. CPU 使用率:右邊柱狀圖越高,表明使用率越高。
  2. CPU 序號:標識 CPU 核心序號,表示該裝置有 8 個核心,編號 0 -7。
  3. CPU 頻率:右邊對應的粉色柱狀圖表示其頻率變化趨勢。
  4. 排程任務:標識在該 CPU 核心上正在執行的任務,點選任務可檢視其 ID、優先順序等資訊。

這些資訊 App 可以直接讀取 /sys/devices/system/cpu 節點下相關資訊獲得,而另外一部分標識執行緒狀態資訊則只能通過系統或者 adb 才能獲取,且這些資訊不是統一節點控制,需要啟用各自對應的事件節點,讓 ftrace 記錄下不同事件的 tracepoint。核心在執行時,根據節點的使能狀態,會往 ftrace 緩衝中記錄事件。

例如,啟用執行緒排程狀態資訊記錄,需要啟用類似如下相關節點。

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

啟用後,則可以獲取到執行緒排程狀態相關的資訊,比如:

  1. Running: 執行緒在正常執行程式碼邏輯。
  2. Runnable: 可執行狀態,等待排程,如果長時間排程不到,說明 CPU 繁忙。
  3. Sleeping: 休眠,一般是在等待事件驅動。
  4. Uninterruptible Sleep: 不可中斷的休眠,需要看 Args 描述來確定當時狀態。
  5. Uninterruptible Sleep - Block I/O: IO 阻塞。

圖片

最終,上述兩大類事件記錄都彙集到核心態同一緩衝中, Systrace 工具是通過指定抓取 trace 類別等引數,然後觸發手機端 /system/bin/atrace 開啟對應檔案節點資訊,接著 atrace 會讀取 ftrace 快取,生成只包含 ftrace 資訊的 atrace_raw 資訊,最終通過指令碼轉換成視覺化 HTML 檔案,大致流程如下。

圖片

RheaTrace 揭祕

本章節將從 RheaTrace 重點優勢一一介紹。

Systrace 原始碼分析

Systrace 提供 Trace#beginSection(String) 和 Trace.endSection() 採集 atrace 資料,首先,我們大致瞭解下 atrace 工作原理,以 android.os.Trace#beginSection 作為分析入口。

public static void beginSection(@NonNull String sectionName) {     if (isTagEnabled(TRACE_TAG_APP)) {         if (sectionName.length() > MAX_SECTION_NAME_LEN) {             throw new IllegalArgumentException("sectionName is too long");         }         nativeTraceBegin(TRACE_TAG_APP, sectionName);     } }

android.os.Trace#beginSection 會呼叫 nativeTraceBegin 方法,該方法實現參考 frameworks/base/core/jni/android_os_Trace.cpp。

static void android_os_Trace_nativeTraceBegin(JNIEnv* env, jclass,         jlong tag, jstring nameStr) {     withString(env, nameStr, [tag](char* str) {         atrace_begin(tag, str);     }); }

atrace_begin 方法實現參考 system/core/libcutils/include/cutils/trace.h。

```

define ATRACE_BEGIN(name) atrace_begin(ATRACE_TAG, name)

static inline void atrace_begin(uint64_t tag, const char name) {     if (CC_UNLIKELY(atrace_is_tag_enabled(tag))) {         void atrace_begin_body(const char);         atrace_begin_body(name);     } } ```

atrace_begin_body 方法實現參考 system/core/libcutils/trace-dev.cpp。

void atrace_begin_body(const char* name) {     WRITE_MSG("B|%d|", "%s", name, ""); }

atrace_begin_body 最終實現在巨集 WRITE_MSG 實現,程式碼如下:

#define WRITE_MSG(format_begin, format_end, name, value) { \     ...     write(atrace_marker_fd, buf, len); \ }

通過 WRITE_MSG 實現,可知,atrace 資料是實時寫入 fd 為 atrace_marker_fd 的檔案中,如果多執行緒同時寫入,則會出現鎖問題,導致效能損耗加大。

RheaTrace 核心優勢

效率提升

RheaTrace 會在 App 編譯期間自動插入 Trace 跟蹤函式,大大提高效率。針對不同 Android Gradle Plugin 版本,我們支援 Proguard 之後插樁,這樣可以減少 App 方法插樁量,同時也過濾 Empty、Set/Get 等簡單方法。

思路基於 matrix-gradle-plugin 大量改造實現。

``` rheaTrace {

compilation {       //為每個方法生成唯一 ID,若為 true,Trace#beginSection(String) 傳入的是方法 ID。       traceWithMethodID = true       //決定哪些包名下的類您不需要做效能跟蹤。       traceFilterFilePath = "${project.rootDir}/rhea-trace/traceFilter.txt"       //一些特定方法保持 ID 值固定不變。       applyMethodMappingFilePath = "${project.rootDir}/rhea-trace/keep-method-id.txt"    }    runtime {         ......    } } ```

  1. 為減少包體積、效能影響,我們也借鑑 matrix 慢函式思路,支援為每個函式生成唯一 ID,traceWithMethodID 為 true,Trace#beginSection(String)傳入的是方法 ID,不再是方法名。有時候我們想某些方法 ID 固定不變,同樣借鑑 matrix 慢函式思路,我們提供 applyMethodMappingFilePath 配置規則檔案路徑。
  2. 為進一步減少 App 方法插樁量,我們提供 traceFilterFilePath 檔案配置讓您決定哪些包、類、方法不做自定義事件跟蹤,關於其用法請參考 RheaTrace Gradle Config。

效能提升

在 Systrace 概述中,我們瞭解到 atrace 資料是實時寫入檔案,且存在多執行緒同時寫入帶來的鎖問題。因此,我們採取策略是拿到 atrace 檔案 fd,在 atrace 資料寫入前,先將其寫至 LockFreeRingBuffer記憶體中,然後再將迴圈讀取記憶體中 atrace 資料,寫入我們定義的檔案中。

首先我們通過 dlopen 獲取 libcutils.so 對應控制代碼,通過對應 symbol 從中找到 atrace_enabled_tags 和 atrace_marker_fd 對應指標,設定 atrace_enabled_tags 用以開啟 atrace,程式碼實現片段如下。

``` int32_t ATrace::InstallAtraceProbe() {   ......   {     std::string lib_name("libcutils.so");     std::string enabled_tags_sym("atrace_enabled_tags");     std::string marker_fd_sym("atrace_marker_fd");

...     ...     atrace_marker_fd_ = reinterpret_cast(         dlsym(handle, marker_fd_sym.c_str()));

if (atrace_marker_fd_ == nullptr) {       ALOGE("'atrace_marker_fd' is not defined");       dlclose(handle);       return INSTALL_ATRACE_FAILED;     }     if (atrace_marker_fd_ == -1) {       atrace_marker_fd_ = kTracerMagicFd;     }   dlclose(handle);   return OK;   } ```

思路參考 profilo#installSystraceSnooper,本文不做過多介紹。

接著,通過 PLT Hook libcutils.so 中 write、write_chk 方法,判定該方法傳入 fd 是否與 atrace_marker_fd 一致,若一致,則將 atrace 資料寫入我們定義的檔案中。

``` ssize_t proxy_write(int fd, const void* buf, size_t count) {   BYTEHOOK_STACK_SCOPE();

if (ATrace::Get().IsATrace(fd, count)) {     ATrace::Get().LogTrace(buf, count);     return count;   }   ...

ATRACE_END();   return ret; } ```

有時候,我們可能僅需要關注主執行緒 atrace 資料,如果能將子執行緒 atrace 資料過濾掉,那麼整體效能將進一步提升。一種很簡單的思路,就是將 Trace#beginSection(String) 包裝一層,如下程式碼片段。

static void t(String methodId) {     if (!isMainProcess) {         return;     }     if (mainThreadOnly) {         if (Thread.currentThread() == sMainThread) {             Trace.beginSection(methodId);         }     } else {         Trace.beginSection(methodId);     } }

該方法僅能過濾我們為 App 方法插樁的 atrace 資料,系統層 atrace 資料無法過濾。為更徹底實現僅採集主執行緒資料,我們通過 PLT Hook 代理atrace_begin_body 和 atrace_end_body 實現,在該方法進入前,判斷當前執行緒 id 是否為主執行緒,如果不是,則不記錄該條資料,程式碼實現片段如下。

``` void proxy_atrace_begin_body(const char *name) {     BYTEHOOK_STACK_SCOPE();     if (gettid() == TraceProvider::Get().GetMainThreadId()) {         BYTEHOOK_CALL_PREV(proxy_atrace_begin_body, name);     } }

void proxy_atrace_end_body() {     BYTEHOOK_STACK_SCOPE();     if (gettid() == TraceProvider::Get().GetMainThreadId()) {         BYTEHOOK_CALL_PREV(proxy_atrace_end_body);     } } ```

針對降低效能損耗,RheaTrace 提供編譯配置供使用者選擇,針對不同使用場景配置合理引數。

``` rheaTrace {     ......

runtime {         mainThreadOnly false         startWhenAppLaunch true         atraceBufferSize "500000"     } } ```

上述配置釋義如下。

  1. mainThreadOnly:為 true 表示僅採集主執行緒 trace 資料。
  2. startWhenAppLaunch:是否 App 啟動開始就採集 trace 資料。
  3. atraceBufferSize:指定記憶體儲存 atrace 資料 ring buffer 的大小,如果其值過小會導致 trace 資料寫入不完整,若您抓取多執行緒 trace 資料,建議將值設為百萬左右量級;最小值為 1 萬,最大值為 5 百萬。

實用性提升

針對已有的 atrace 資料,額外拓展 IO 等資訊;另外為通過 Python 指令碼徹底解決方法因執行異常導致 trace 資料閉合異常問題,保證每個方法 trace 資料的準確性。

目前我們基於 JVMTI 方案,在 Android 8.0 及以上裝置可以獲取類載入以及記憶體訪問相關 trace 資料,目前僅支援編譯型別為 debuggable 的 App,目前處於實驗功能,本文暫先不過多介紹。

IO 資料拓展

背景簡介

在抖音啟動效能優化時,我們曾統計冷啟動的耗時,其中佔比最長的是程序處於 D 狀態(不可中斷睡眠態,Uninterruptible Sleep ,通常我們用 PS 檢視程序狀態顯示 D,因此俗稱 D 狀態)時間。此部分耗時佔總啟動耗時約 40%,程序為什麼會被置於 D 狀態呢?處於 uninterruptible sleep 狀態的程序通常是在等待 IO,比如磁碟 IO,其他外設 IO,正是因為得不到 IO 響應,程序才進入 uninterruptible sleep 狀態,所以要想使程序從 uninterruptible sleep 狀態恢復,就得使程序等待 IO 恢復,類似如下。

圖片

但在使用 Systrace 進行優化時僅能得到如上核心態的呼叫狀態,卻無法得知具體的 IO 操作是什麼。

方案介紹

因此,我們專門設計一套獲取 IO 耗時資訊方案,其包括使用者空間和核心空間兩部分。

一是在使用者空間,為採集所需 IO 耗時資訊,我們通過 Hook IO 操作標準函式簇,包括 open,write,read,fsync,fdatasync 等,插入對應 atrace 埋點用於統計對應的 IO 耗時,以 fsync 為例。

圖片

其對應 hook 程式碼邏輯如下:

``` 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; } ```

二是在核心空間,除 systrace 或 atrace 可直接支援啟用功能外,ftrace 還提供其他功能,幷包含對除錯效能問題至關重要的一些高階功能(需要 root 訪問許可權,通常也可能需要新核心)。我們基於此新增顯示定製 IO 資訊等功能,開啟/sys/kernel/debug/tracing/events/android_fs節點下 ftrace 資訊,用於收集 IO 相關的資訊。核心空間 IO 資訊是通過 python 指令碼開啟,詳見 io_extender.py。

解決方法閉合錯誤問題

背景介紹

RheaTrace 會自動在每個方法入口、出口處分別插入 Trace#beginSection(String) 和 Trace#endSection() ,一個方法有且只有一個入口,但會有多個出口,方法出口對應的結束位元組碼指令有 return 和 throw 等。

``` public static void testCrash() {         try {             testA();         } catch (Exception e) {             e.printStackTrace();         }     }

public static void testA() {         testB();         testC();     }

public static void testB() {         int ret = 2 / 0; // <----- crash event         testD(ret);     }

public static void testC() {         Log.d("btrace", "do some things.");     }

public static void testD(int num) {         Log.d("btrace", "box size: " + num);     } ```

上面的程式碼很簡單,即 testCrash -> testA -> testB,其中 testB 出現異常,最終是在 testCrash 中捕獲。通過本示例可知,testA、testB 方法出口均未正常執行完成,這也就導致 trace 資料不閉合,生成的 trace 資料如下,從中可以看出,B 和 E 數量上並不匹配,且僅從 trace 上看,我們也無法知道 E 屬於哪個方法。

5108949.231989: B|28045| TestCrash:a 5108949.232055: B|28045| TestCrash:b 5108949.232554: B|28045| TestCrash:c 5108949.232580: E|28045

方案介紹

為解決該問題,RheaTrace 做了取巧處理,方法的出口由插入 Trace#endSection()改為 Trace#beginSection(String)。那我們如何知道哪條 trace 是開始,哪條是結束?我們看如下示例。

5108949.231989: B|28045|B:TestCrash:a 5108949.232055: B|28045|B:TestCrash:b 5108949.232554: B|28045|B:TestCrash:c 5108949.232580: B|28045|E:TestCrash:a

如上 trace 資料,每個方法描述前都會加上 B: 或 ETB: 表示方法開始,E 表示方法 retrun 結束,T: 表示方法 throw 結束。然後通過 Python 指令碼處理並還原正常 trace 資料。如此做以後,我們就可以明確知道方法開始和結束,同時針對異常結束方法,我們會做補全處理,處理後的 trace 資料如下。

5108949.231989: B|28045|TestCrash:a 5108949.232055: B|28045|TestCrash:b 5108949.232554: B|28045|TestCrash:c 5108949.232554: E|28045|TestCrash:c 5108949.232554: E|28045|TestCrash:b 5108949.232580: E|28045|TestCrash:a

關於 Python 指令碼的處理過程,本文不做過多介紹,大家可以閱讀相關原始碼即可。

RheaTrace 工作流程

流程概述

RheaTrace 作為線下效能分析利器,我們首先看下其整體工作流程。

圖片

如上文介紹,我們將 Systrace 中 atrace 資料做攔截,將其轉存至我們自定義的檔案中。

Systrace 格式

首先我們 Systrace 生成的 trace.html 中 atrace 資料格式。

<idle>-0     (-----) [001] d.h4 1308823.803921: sched_waking: comm=TimerDispatch pid=704 prio=97 target_cpu=001           <idle>-0     (-----) [001] dnh5 1308823.803929: sched_wakeup: comm=TimerDispatch pid=704 prio=97 target_cpu=001           <idle>-0     (-----) [001] d..2 1308823.803943: sched_switch: prev_comm=swapper/1 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=TimerDispatch next_pid=704 next_prio=97           <idle>-0     (-----) [003] d.s3 1308823.803980: sched_waking: comm=kworker/3:0 pid=11120 prio=120 target_cpu=003           <idle>-0     (-----) [003] d.s4 1308823.803986: sched_blocked_reason: pid=11120 iowait=0 caller=worker_thread+0x4fc/0x804    TimerDispatch-704   (  643) [001] .... 1308823.803988: tracing_mark_write: B|643|TimerIteration #9392           <idle>-0     (-----) [003] dns4 1308823.803988: sched_wakeup: comm=kworker/3:0 pid=11120 prio=120 target_cpu=003    TimerDispatch-704   (  643) [001] .... 1308823.803992: tracing_mark_write: E|643           <idle>-0     (-----) [003] d..2 1308823.803997: sched_switch: prev_comm=swapper/3 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=kworker/3:0 next_pid=11120 next_prio=120    TimerDispatch-704   (  643) [001] .... 1308823.804011: tracing_mark_write: C|643|VSP-mode|0    TimerDispatch-704   (  643) [001] .... 1308823.804014: tracing_mark_write: C|643|VSP-timePoint|405332069786762    TimerDispatch-704   (  643) [001] .... 1308823.804016: tracing_mark_write: C|643|VSP-prediction|405332075389317    TimerDispatch-704   (  643) [001] .... 1308823.804022: tracing_mark_write: B|643|app-alarm in:5602555 for vs:15880333    TimerDispatch-704   (  643) [001] .... 1308823.804024: tracing_mark_write: E|643

文字形式開啟 trace.html,在其底部是填充的 trace 資料 ,如上所示資料片段,帶有 tracing_mark_write 標籤的即包含 atrace 資料。在 trace.html 檔案中有關於 trace 格式介紹,如下資料片段。

```

tracer: nop

entries-in-buffer/entries-written: 178063/178063   #P:8

_-----=> irqs-off

/ _----=> need-resched

| / _---=> hardirq/softirq

|| / _--=> preempt-depth

||| /     delay

TASK-PID    TGID   CPU#  ||||    TIMESTAMP  FUNCTION

| |        |      |   ||||       |         |

-0     (-----) [003] d.s2 1308814.493991: sched_waking: comm=rcu_preempt pid=9 prio=120 target_cpu=003           -0     (-----) [000] d.s2 1308814.493997: sched_waking: comm=rcu_sched pid=10 prio=120 target_cpu=000 ```

在 trace.html 中,一條完整的 atrace 資料為:

.sample.android-19452 (19452) [005] .... 1308823.801863: tracing_mark_write: B|19452|activityStart ...... .sample.android-19452 (19452) [005] .... 1308824.801753: tracing_mark_write: E|19452

在上文介紹 Systrace 時候,我們提到,Trace#beginSection(String) 和 Trace.endSection() 最終是呼叫如下巨集。

#define WRITE_MSG(format_begin, format_end, name, value) { \     ...     write(atrace_marker_fd, buf, len); \ }

其中,write 函式傳入的 trace 資料為:

B|19452|activityStart ...... E|19452

B 表示 Section 進入,E 表示 Section 退出,從以上資料片段可以看出,相較於 trace.html 中 atrace 資料少了很多資訊,缺少的資訊是核心補全。

Systrace 工具中 --from-file 是可以將原始 atrace 資料轉化為視覺化的 html 檔案。因此,針對 atrace 資料我們需要補全缺少的資訊。結合前面介紹的 trace 格式說明及多次驗證,可被 Systrace 工具識別的 atrace 檔案格式滿足如下:

<ThreadName>-<TheadID> [001] ...1 <Timestamp>: trace_mark_write:<B|E>|<ProcessID>|<TAG>

格式說明:

  1. <ThreadName>:執行緒名,若為主執行緒,可指定為包名。
  2. <ThreadID>:執行緒 ID。
  3. <Time seconds>:方法開始或者結束時間戳。
  4. <B|E>:標記該條記錄為方法開始(B)還是結束(E)。
  5. <ProcessID>:所在程序 ID。
  6. <TAG>:方法標記,字元長度不可超過 127。

[001] 和 ...1 對應的資料使用者層是無法獲取,因此硬編碼寫死。

RheaTrace 格式

Systrace 中相關 atrace 資料格式有很多冗餘資訊,冗餘資訊是可以通過指令碼來進行補充,這樣在 atrace 儲存過程中可以減少一定資料量的儲存。

僅採集主執行緒 atrace 資料,其對應格式如下:

1306401.857369: B|16667|VerifyClass com.bytedance.rheatrace.atrace.TraceEnableTagsHelper 1306401.857498: E|16667 1306401.857560: B|16667|VerifyClass com.bytedance.rheatrace.common.ReflectUtil 1306401.857825: E|16667 1306401.857876: B|16667|VerifyClass kotlin.jvm.internal.Intrinsics 1306401.858241: E|16667 1306401.858523: B|16667|VerifyClass com.bytedance.rheatrace.core.RheaNoticeManager 1306401.858633: E|16667

因為 RheaTrace 僅支援採集主程序 trace 資料,因此,程序 ID 資訊無需寫入,另外主執行緒名為包名也無需寫入,主執行緒 ID 與程序 ID 一致也無需寫入,剩餘資訊均為格式模板資訊也無需寫入,唯一需要記錄的是時間戳。

採集所有執行緒 atrace 資料,其對應格式如下。

1306401.859162 16667: B|16667|RheaApplication#onCreate 1306401.859173 16667: E|16667 1306401.859756 16667: E|16667 1306401.859877 16667: B|16667|activityStart 1306401.862738 16680: B|16667|JIT compiling int sun.util.locale.StringTokenIterator.nextDelimiter(int) (baseline=0, osr=0) 1306401.862772 16680: B|16667|Compiling 1306401.863154 16680: B|16667|ScopedCodeCacheWrite 1306401.863172 16680: B|16667|mprotect all 1306401.863207 16680: E|16667

當採集多執行緒資料時,我們需要獲取對應的執行緒 ID,執行緒名我們沒有通過在 App 期間獲取,而是讀取 Systrace 工具生成 trace.html 中讀取。如下資料片段,我們可以獲取程序 ID 為 16667 對應的所有執行緒 ID 及名稱。當然也會存線上程 ID 如下資料片段找不到的情況,我們暫時用 <...>代替。

USER            PID   TID CMD root              1     1 init root              1   548 init root              2     2 kthreadd root              3     3 rcu_gp root              5     5 kworker/0:0H root              7     7 mm_percpu_wq root              8     8 ksoftirqd/0 root              9     9 rcu_preempt root             10    10 rcu_sched root             11    11 rcu_bh ..... u0_a168       16667 16684 FinalizerWatchd u0_a168       16667 16685 Binder:16667_1 u0_a168       16667 16686 Binder:16667_2 u0_a168       16667 16687 Binder:16667_3 u0_a168       16667 16688 Profile Saver u0_a168       16667 16689 async-writer u0_a168       16667 16690 RenderThread u0_a168       16667 16693 HWC release u0_a168       16667 16694 GPU completion

通過 RheaTrace 提供的指令碼,我們就可以將原始 atrace 資料加工為標準 atrace 格式,如下資料片段。

.sample.android-16667 [001] ...1 1306401.857369: tracing_mark_write: B|16667|VerifyClass com.bytedance.rheatrace.atrace.TraceEnableTagsHelper  .sample.android-16667 [001] ...1 1306401.857498: tracing_mark_write: E|16667  .sample.android-16667 [001] ...1 1306401.857560: tracing_mark_write: B|16667|VerifyClass com.bytedance.rheatrace.common.ReflectUtil  .sample.android-16667 [001] ...1 1306401.857825: tracing_mark_write: E|16667  .sample.android-16667 [001] ...1 1306401.857876: tracing_mark_write: B|16667|VerifyClass kotlin.jvm.internal.Intrinsics  .sample.android-16667 [001] ...1 1306401.858241: tracing_mark_write: E|16667  .sample.android-16667 [001] ...1 1306401.858523: tracing_mark_write: B|16667|VerifyClass com.bytedance.rheatrace.core.RheaNoticeManager  .sample.android-16667 [001] ...1 1306401.858633: tracing_mark_write: E|16667

當通過 Systrace 命令獲取 trace.html 後,trace.html 中的 atrace 因為被 RheaTrace 攔截寫入自定義檔案中,因此生成的 trace.html 檔案中是不包含 atrace 資料。

如果不包含 atrace,那麼 trace.html 的作用將非常小,因此,我們需要將 atrace 資料填充進入 trace.html 中,經過驗證 atrace 資料滿足如下格式,能夠被 trace.html 識別。

<ThreadName>-<TheadID> (ProcessID) [001] ...1 <Timestamp>: trace_mark_write:<B|E>|<ProcessID>|<TAG>

相較於標準 atrace 檔案,它多了 (ProcessID)資料。

有了上述介紹,我們介紹下 RheaTrace 生成的 systrace.html 中間會生成哪些檔案。

python rheatrace.py -a rhea.sample.android -t 3 -o ./output/systrace.html

在 systrace.html 同級目錄下會生成 .build 目錄,其中包括上述中間產物檔案。

├── .build │   ├── atrace-standard          //標準 atrace 格式,可直接用 perfetto 開啟。 │   ├── rhea-atrace              //從裝置中拉取的 rhea-atrace.gz 檔案解壓得到,原始 RheaTrace 格式的 atrace 資料。 │   ├── systrace-fs-origin.html  //如果裝置 root,其中會包括核心 IO 事件,前文有提到。 │   └── systrace-origin.html     //通過 Systrace 工具抓取的視覺化 trace 檔案,不包含 atrace 資料。 └── systrace.html                //atrace-standard 與 systrace-fs-origin.html 或 systrace-origin.html 合併得到。

未來規劃

  1. 支援 App 獨立抓取 atrace 資料,無需依賴 Systrace 環境。
  2. 提供穩定、高效的 trace 採集環境,適配更多手機機型。
  3. 支援更多維度的 trace 資訊,比如渲染、記憶體等,更方便定位函式耗時原因。
  4. 進一步降低效能損耗,到達線上使用要求。