目前 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 設計時的幾個主要目標:
- 採用舊有 gprof 的 mcount 界面, 避免修改 toolchain
- 能夠支援 Shared Library
- 可以對 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 $(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 主要有兩個原因:
- sprof 實作於 glibc 的 dynamic linker, 其授權是採用 GPL, 因此無法直接採用
- 整合 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 才可正常運作: