拿到性能問題后,首先要做基本排查,
1. 確認是不是kernel perf 版本
2. 確認kernel debug flag
3. 檢查config.zip是否有不該打開的debug feature
4. 打開performance mode之后是否有改善,如果有改善,建議和高通溝通一起優化。
打開方法如下:
adb root
adb shell setenforce 0
adb shell stop perfd
adb shell stop thermal-engine
adb shell 'echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor'
adb shell 'echo performance >/sys/devices/system/cpu/cpu4/cpufreq/scaling_governor'
如果以上配置check都沒問題,
接下來繼續看logcat
通過看Choreographer: Skipped, OpenGLRenderer: Davey! duration , Displayed這些關鍵詞判斷什么變卡頓,
變卡頓時,觀察是否所有apk還是單個或者某些apk變卡頓,如果是單個apk卡頓,使用traceview 和systrace,kill命令還有profiler工具,進一步排查比較方便。
systrace主要看cpu schedule(包含frequency 和 線程調度),celling frequency, 對應進程的frame, vsync信號。
關於systrace 和 traceview 詳細使用后續再更新。
kill -3 pid 命令可以抓取該進程當前狀態下所有線程堆棧信息,即Dalvik Threads信息,以此來check 此進程下的線程是否存在死鎖問題。
如果是所有apk變卡頓,就變成了系統性能問題,接下來主要講分析系統卡頓問題的思路,
遇到整體性能下降的問題,
首先要排查下RAM剩余空間,方法有bugreport, dumpsys meminfo, cat /proc/meminfo, lowmemorykiller log, kswapd cpu占用
還有一個釋放RAM的命令,echo 3 > /proc/sys/vm/drop_caches 反向確認是否是內存不足引起的卡頓。
其次, 排查下emmc的容量和讀寫IO速度,
排查emmc的容量,可以借助benchmark等工具跑分測試,如antutu。還有就是反向debug, 清理emmc空間后是否存在卡頓現象。
IO讀寫速度的排查,可以借助fio, AndroBench等工具去check, 還有就是dd命令,
dd if=/dev/zero of=./largefile bs=8K count=1024 //測試寫的速度,包含了系統調用和IO
dd if=./largefile of=/dev/null bs=8k //測試讀的速度,包含了系統調用和IO
還有就是TP的check, 可以使用adb shell input swipe 350 900 350 300 300 這指令去check。如果還是卡,說明跟TP無關。
排查完上面的硬件問題后,接下來就是重點部分了。
系統性能問題分 framework 和 kernel 2 層,
framework層的性能問題,主要依賴於android 自帶的一些工具和手段,比如,systrace,traceview,android profiler.
kernel 層的性能問題,主要依賴於linux 一些調試工具和手段。比如top, strace, ftrace.
先來說說之前遇到一個整體性能變卡的問題。
對於這種問題,首先需要確認是上層的問題還是kernel層的問題。
判別的具體方法如下:
1. 使用adb shell stop, adb shell start命令判斷 重啟后是否還卡頓。
2. 使用一個簡單的命令 用strace 對比正常機器是否所有系統調用變慢。
strace -c cat /sys/kernel/debug/tracing/tracing_on
strace -tt -T -v -f -p pid -o /sdcard/logname.log
3. 制作簡單的循環腳本去執行chmod, cat, ls等命令,與正常機器做對比是否都變慢
如果確認上面幾項都變慢,那么基本上可以判定kernel的syscall都變了。
那么在kernel syscall 大部分都變慢的情況下,現在試下來沒有什么好的方法,只有通過自己寫test case 去確認哪個系統調用變慢,需要結合抓取操作變慢的場景下ramdump分析源碼里 code邏輯和變量值。如何寫test case?
可以寫一個daemon, 循環去執行ls, cat ,chmod等基本命令,跟正常的機器對比,確認出哪個命令差距較大。然后在對該命令做進一步排查
以chmod調用慢為例,首先需要編寫一個daemon,也就是一個bin檔可以循環去執行chmod, 然后編寫一個ko, 去打印chmod源碼流程里的每個函數的耗時,來確認哪個函數出了問題,再來看對應
函數可能發生的耗時操作。
ko文件的編寫以及怎么結合linux deamon進程來追蹤哪個syscall變慢,這個步驟的詳細介紹,后續在更新。
當時查到user_path_at->security_path_chmod->security_inode_setattr 這幾個函數耗時較長,最后返現跟selinux有關。
所以以selinux為例,
selinux 有這樣一個機制,當系統調用去檢查selinux policy的時候,首先會檢查avc cache里面policy, 如果存在就直接使用avc cache里面的值,如果不存在就會從db重新讀取到avc cache.
所以這個是跟性能關系很大,是可疑點。
同時也可以用cat /sys/fs/selinux/avc/cache_stats來查看,執行結果如下:
lookups hits misses allocations reclaims frees
1774786 1759569 15217 15604 14509 10688
2134509 2119175 15334 15934 15187 9169
1785290 1770881 14409 14687 14112 17670
1442715 1428358 14357 14978 14528 16211
407859 401975 5884 6009 6016 6408
318615 313979 4636 4668 4576 6008
244824 241135 3689 3783 3760 6642
221858 217765 4093 4252 4288 6614
其中 reclaims 這一項每次增加很多說明就有問題,因為reclaim操作是比較耗時的, misses每次增加很多,也是有影響,但是耗時沒reclaim那么嚴重。
來看下reclaim為什么耗時,
static inline int avc_reclaim_node(void)
{
struct avc_node *node;
int hvalue, try, ecx;
unsigned long flags;
struct hlist_head *head;
spinlock_t *lock;
for (try = 0, ecx = 0; try < AVC_CACHE_SLOTS; try++) {
hvalue = atomic_inc_return(&avc_cache.lru_hint) & (AVC_CACHE_SLOTS - 1);
head = &avc_cache.slots[hvalue];
lock = &avc_cache.slots_lock[hvalue];
if (!spin_trylock_irqsave(lock, flags))
continue;
rcu_read_lock();
hlist_for_each_entry(node, head, list) {
avc_node_delete(node);
avc_cache_stats_incr(reclaims);
ecx++;
if (ecx >= AVC_CACHE_RECLAIM) {
rcu_read_unlock();
spin_unlock_irqrestore(lock, flags);
goto out;
}
}
rcu_read_unlock();
spin_unlock_irqrestore(lock, flags);
}
out:
return ecx;
}
從relaim的源碼可以看到,里面有一個循環體,循環次數是AVC_CACHE_SLOTS,源碼定義的是512,所以需要循環512次大大增加了cpu的開銷。
再來看relaim具體做的什么,
avc_reclaim_node函數會去全部清掉所有avc nodes(512個)。
來看下調用這個avc api(avc_update_node)的大致流程,
流程1:avc_update_node: avc_alloc_node-->avc_reclaim_node-->avc_xperms_populate-->插入鏈表操作
*流程2:avc_compute_av-->avc_insert-->avc_xperms_populate-->插入鏈表操作
從上面的流程可以看出,avc_reclaim_node完了之后,接下來會去insert 1個 avc node到鏈表, 然后此時整個avc cache只有1個avc node, 如果此時其他進程來遍歷它的avc node,就會遍歷不到,以導致命中率有下降。
通過結合ramdump分析reclaim次數為什么增加這么多,
static struct avc_node *avc_alloc_node(void)
{
struct avc_node *node;
node = kmem_cache_zalloc(avc_node_cachep, GFP_NOWAIT);
if (!node)
goto out;
INIT_HLIST_NODE(&node->list);
avc_cache_stats_incr(allocations);
if (atomic_inc_return(&avc_cache.active_nodes) > avc_cache_threshold)
avc_reclaim_node();
out:
return node;
}
源碼中有寫道avc_cache.active_nodes大於某個閾值的時候,就回去reclaim, 那么就需要通過ramdump解析后,查看當時卡頓時候avc_cache.active_nodes變量值,以及avc_cache的狀態來判斷。
當時確認avc_cache.active_nodes超過了avc_cache_threshold這個閾值512, 而avc_cache里面只包含了一個node.
那么怎么解釋avc_cache里面只包含了一個node呢?
實際上,一旦出現內存緊張的時候,調用這個api又頻繁的時候,很可能會出現alloc node 成功2次(甚至更多次), 然而insert avc node失敗1次,此時
avc_cache.active_nodes自增數是按alloc node的次數來自增的,而自增之前
avc_cache.active_nodes剛好是512(再增加1就會發生relaim的臨界值),自增之后avc_cache.active_nodes變成了514,此時reclaim被觸發,但只會avc_node_delete512次,也就是avc_cache.active_nodes只會自減512次,
那么514-512=2, 正常情況應該是1,因為avc_cache鏈表里面應該只剩下新插入鏈表的那個node。然而現在avc_cache.active_nodes卻是2,實際上這種情況也可以理解為是一種泄露,是變量值的泄露。那么在內存緊張且次api調用又
頻繁的情況,發生一次這樣的情況下,avc_cache.active_nodes就會泄露一次,久而久之就會出現avc_cache.active_nodes的值是512,而這個512次都是泄露導致的,那么就會出現此時新增1個avc cache node, avc_cache.active_nodes就變成了
513,會去觸發reclaim, 然后再把這個新增的node插如avc cache 鏈表,此時鏈表里面是不是只剩下這么一個剛插入的node。這也就解釋了為什么從ramdump解釋出來的avc_cache 狀態里面只有一個avc cache node了。
所以一旦泄露次數達到了512次,那么每次去調用這個selinux avc api就會去觸發avc_reclaim_node動作,最終導致基本所有的system api調用都變慢。
所以回過頭來看,avc_cache.active_nodes沒有合理的自減操作是這個問題的根本原因,它應該插件鏈表失敗的時候去自減。
原因是上面源碼里atomic_inc_return(&avc_cache.active_nodes)有這樣一句話,增加了avc_cache.active_nodes值之后,也就是在alloc_node成功之后,並在插入鏈表失敗的情況下,並沒有去減1
操作,源碼如下:
static struct avc_node *avc_insert(u32 ssid, u32 tsid, u16 tclass,
struct av_decision *avd,
struct avc_xperms_node *xp_node)
{
struct avc_node *pos, *node = NULL;
int hvalue;
unsigned long flag;
if (avc_latest_notif_update(avd->seqno, 1))
goto out;
node = avc_alloc_node();
if (node) {
struct hlist_head *head;
spinlock_t *lock;
int rc = 0;
hvalue = avc_hash(ssid, tsid, tclass);
avc_node_populate(node, ssid, tsid, tclass, avd);
rc = avc_xperms_populate(node, xp_node);
if (rc) {
//此處應該有減1操作,但並沒有
kmem_cache_free(avc_node_cachep, node);
return NULL;
}
head = &avc_cache.slots[hvalue];
lock = &avc_cache.slots_lock[hvalue];
spin_lock_irqsave(lock, flag);
hlist_for_each_entry(pos, head, list) {
if (pos->ae.ssid == ssid &&
pos->ae.tsid == tsid &&
pos->ae.tclass == tclass) {
avc_node_replace(node, pos);
goto found;
}
}
hlist_add_head_rcu(&node->list, head);
found:
spin_unlock_irqrestore(lock, flag);
}
out:
return node;
}
//插入鏈表的函數
static int avc_xperms_populate(struct avc_node *node,
struct avc_xperms_node *src)
{
struct avc_xperms_node *dest;
struct avc_xperms_decision_node *dest_xpd;
struct avc_xperms_decision_node *src_xpd;
if (src->xp.len == 0)
return 0;
dest = avc_xperms_alloc();
if (!dest)
return -ENOMEM;
memcpy(dest->xp.drivers.p, src->xp.drivers.p, sizeof(dest->xp.drivers.p));
dest->xp.len = src->xp.len;
/* for each source xpd allocate a destination xpd and copy */
list_for_each_entry(src_xpd, &src->xpd_head, xpd_list) {
dest_xpd = avc_xperms_decision_alloc(src_xpd->xpd.used);
if (!dest_xpd)
goto error;
avc_copy_xperms_decision(&dest_xpd->xpd, &src_xpd->xpd);
list_add(&dest_xpd->xpd_list, &dest->xpd_head);
}
node->ae.xp_node = dest;
return 0;
error:
avc_xperms_free(dest);
return -ENOMEM;
}
所以在插入鏈表失敗之后,應該主動去減1
修改方案如下:
static struct avc_node *avc_insert(u32 ssid, u32 tsid, u16 tclass,
struct av_decision *avd,
struct avc_xperms_node *xp_node)
{
struct avc_node *pos, *node = NULL;
int hvalue;
unsigned long flag;
......
hvalue = avc_hash(ssid, tsid, tclass);
avc_node_populate(node, ssid, tsid, tclass, avd);
rc = avc_xperms_populate(node, xp_node);
if (rc) {
atomic_dec(&avc_cache.active_nodes);
kmem_cache_free(avc_node_cachep, node);
return NULL;
}
......
found:
spin_unlock_irqrestore(lock, flag);
}
out:
return node;
}