2012年2月1日 星期三

Aprof : Android Profiler - A profiling tool for android native code


目前 Android 在 profiling tool 這邊大致上有 traceview 及 oprofile 可用, 但 traceview 是針對於一般的 DVM 程式, 而 oprofile 又相對的較複雜, 且缺乏類似 gprof 的簡易工具來觀測效能

而事實上已經有人開始注意到此事並提出方法, 最直接的方式是實作 mcount 函數並透過 gcc -pg 選項來使用, 如此一來變不須更動太多部份,例如 android-ndk-profiler 便是採用此方式, Aprof 也將尋此方式來進行擴充, 但我們所採用的方式並非單純將 gprof 功能實作, 而是進一步擴充並且為 Android 系統量身打造.

Aprof 簡介


有鑑於目前 android 平台上對於 native code 的 profiler 較於缺乏, 因此我們提出實作一個為 Android 量身訂作的 Profiler, 取名為 aprof, 主要功能與 gprof 相仿, 提供 Call Graph 與 Time Sample Info.

在 aprof 設計時的幾個主要目標:

  1. 採用舊有 gprof 的 mcount 界面, 避免修改 toolchain
  2. 能夠支援 Shared Library
  3. 可以對 JNI 的 Shared Library 進行 Profiling

在 aprof 中主要也是透過 mcount 函數及 gcc -pg 選項來使用, 但在這邊我們所採取的方式是與 gprof 輸出格式不相容, 主要原因在於 gprof 輸出格式無法支援 shared library 的 profiling, 若沿用原本格式將造成功能上的限制.

Aprof 使用方法


若要使用 Aprof 來 profile 執行檔的話只要在 Android.mk 加入以下的選項開啟, 對於一般的 static library 也是採用相同方式即可

LOCAL_ENABLE_APROF := true


但要注意的是若是要使用 Aprof 來 profile Shared Library 的話主要分為兩種情況使用

針對一般 Non-JNI 使用的 Shared Library 採用與一般執行檔的開啟方式即可, 但要執行檔也開啟 Aprof 時才會啟動 profiling

若是對於 JNI 使用的 Shared Library 則在 Android.mk 加入以下的選項, 其 profiling 資訊將會於 Activity 於 Life Cycle 中的 stop 時寫入檔案, 值得注意的是若程式已經開啟 Aprof 則會以 Non-JNI aprof mode 來進行 profiling

LOCAL_ENABLE_APROF_JNI := true


在開啟 Aprof 並且重新建置程式後, 便可開始準備 profiling, 開始的方式則是執行一次該程式, 在開啟 profiling 的情況下會比未開啟的情況下慢上許多, 在執行完成程式後變可以到 /sdcard/ 裡面檢查 profiling 的輸出, 預設檔名是 $(progname).aprof

接著 profiling file 撈回 Host 端, 也就是使用 adb 去下載, 方式可參考以下指令:

adb pull /sdcard/$(progname).aprof
 
# 例如進行 Profiling 的程式是 foo
adb pull /sdcard/foo.aprof

然後使用 aprof 來讀取 profiling file 的資訊, 使用方式如下:


# aprof $(prog_file) $(prof_file)
aprof foo foo.aprof

下面則是範例輸出:

  %      cumulative     self                 self       total
 time     time          time      calls    ms/call    ms/call   name
 99.52       2170       2140    2178309          0          0   fib
  0.00       2170          0          1          0        217   main
  0.48          0         30          0          0          0   <libc.so>
 
Call graph (explanation follows)
 
-------------------------------------------------------------
Image           : foo
Cumulative time : 2170 ms
Self time       : 2140 ms
  Function  % time  cumulative        self       Count  Call by
 fib                      2170        2140
            100.00        2170           0           1  main
            100.00        2170        2140     2178308  fib
 main                     2170           0
            100.00        2170           0           1  <libc.so>


上半部份主要是顯示各個函數所佔用的時間與百分比, cumulative time 表示包含子函數的時間, 而 self time 則表示該函數所有消耗時間, 例如 a() call b(), a() 花了 1 sec, b() 花了 2 sec, 則 a() 的 cumulative time 為 3 sec, self time 為 2 sec, calls 代表該函數總共被呼叫的次數, self ms/call 及 total ms/call 分別代表每次呼叫約花費多少時間.

但在上面的例子中可以發現有 的這個符號, 主要原因是 aprof 沒有載入該 shared library 的符號資訊, 我們可以透過 -L 來指定 Shared Library 的搜索路徑, 一般搜索路徑設置在 Android 的目錄下的 out/target/product/panda/symbols/system/lib (以 panda board 為例)即可.

# aprof $(prog_file) $(prof_file) -L$(lib_path)
aprof foo foo.aprof -L out/target/product/panda/symbols/system/lib

一般而言在載入了符號後資訊會較為詳細, 但仍有些不足的地方, 例如在 calls 的部份若該 Shared Library 建置時並無開啟 aprof 則無法得到此部份的資訊, 也因此 cumulative time 部份會無法計算, 不過 Self time 的部份在有了 symbol 資訊後則可正確定位.

  %      cumulative     self                 self       total
 time     time          time      calls    ms/call    ms/call   name
 99.52       2170       2140    2178309          0          0   fib
  0.00       2170          0          1          0        217   main
  0.32          0         20          0          0          0   write
  0.16          0         10          0          0          0   memcpy
 
 
Call graph (explanation follows)
 
-------------------------------------------------------------
Image           : foo
Cumulative time : 2170 ms
Self time       : 2140 ms
  Function  % time  cumulative        self       Count  Call by
 fib                      2170        2140
            100.00        2170           0           1  main
            100.00        2170        2140     2178308  fib
 main                     2170           0
            100.00        2170           0           1  __libc_init
-------------------------------------------------------------
Image           : foo
Cumulative time : 2170 ms
Self time       : 30 ms
 write                       0          20
 memcpy                      0          10

接下來的部份則是介紹對於將結果視覺化呈現的方式, aprof 支援 dot 格式的輸出, 可透過 -d 來輸出 dot 格式, 可透過 pipeline 到 dot (通常包含於 graphviz 套件)直接將其結果輸出成圖檔, 使用方式參考下列指令

# aprof $(prog_file) $(prof_file) -L$(lib_path)
aprof foo foo.aprof -L out/target/product/panda/symbols/system/lib -d | dot -Tpng -o foo.aprof.png

在這邊附上一個 busybox 的 md5sum 的 aprof 輸出圖檔供參考:

圖 1. md5sum 的 Call Graph 輸出


Aprof 的使用方式及輸出資訊大致介紹到這個部份.

Aprof 實作


Aprof 為了能夠支援較完整的 Shared Library Profiling 機制, 所以有一大部分是實作在 linker 中, 這樣的作法主要是參考自 sprof 的實作方式, 而在這邊為何要重新打造一個 Profiler 主要有兩個原因:

  1. sprof 實作於 glibc 的 dynamic linker, 其授權是採用 GPL, 因此無法直接採用
  2. 整合 gprof 與 sprof, 並為 Android 系統的需求設計

為何分 JNI 及 Non-JNI mode ?

對於 Shared Library , aprof 提供兩種模式可進行 Profiling, 兩者之間的行為差別在於 JNI mode 下會各個 Shared Library 會分別 Profiling , 分別輸出檔案, 並且最重要的是執行檔不需要開啟 Aprof 也可進行 Profiling.

non-JNI mode 則是必須執行檔開啟 Aprof 時才會進行 Profiling, 這樣的設計主要是因為避免其他 Link 到該 Shared Library 的執行檔皆受到無差別的效能攻擊.

JNI mode 主要存在的意義就在於如其命名一樣, 針對 JNI 的 Shared Library 來使用, 在作為 JNI 的 Shared Library 通常是被 DVM (app_process / zygote)所開啟, 而要等到其執行結束則會有點困難, 故在這邊的設計是採用於 Activity 於 Life Cycle 中的 stop 時輸出 Profiling File.

另外需要注意的是若用在 JNI 上的話, 必須確保該應用程式有寫入的權限, 也就是記得在 AndroidManifest.xml 中加入 android.permission.WRITE_EXTERNAL_STORAG


AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.example.android.simplejni">
    <application android:label="Simple JNI">
        <activity android:name="SimpleJNI">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

目前狀態

若使用 AOSP JB 或 ICS 的話 Aprof 僅需要再使用以下 patch 即可
Aprof 也支援及 Gingerbread, 但如果你使用的是 Linaro, CyanogenMod (包含其延伸) 或 Gingerbread, 則需要額外多加入這幾個 patch 才可正常運作: