抖音renderD128系統級疑難OOM分析與解決
1.背景
抖音長期存在renderD128內存占用過多導致的虛擬內存OOM,且多次出現renderD128內存激增導致OOM指標嚴重劣化甚至發版熔斷。因受限于閉源的GPU驅動以及現場有效信息極少,多個團隊都進行過分析,但一直未能定位到問題根因,問題反饋到廠商也一直沒有結論。
以往發生renderD128內存激增時,解決辦法往往都是通過二分法去定位導致問題的MR進行回滾(MR代碼寫法并無問題,僅僅是正常調用系統API),但是回滾業務代碼會影響業務正常需求的合入,也無法從根本上解決該問題,每次也會消耗我們大量人力去分析排查,因此我們有必要投入更多時間和精力定位根因并徹底解決該問題。在歷經數月的深入分析和排查后,我們最終定位了問題根因并徹底解決了該問題,取得了顯著的OOM收益,renderD128內存問題導致的發版熔斷也未再發生。
接下來,將詳細介紹下我們是如何一步步分析定位到問題根因,以及最終如何將這個問題給徹底解決的。
2.問題分析
2.1問題特征
主要集中在華為Android10系統,表現為renderD128內存占用過多。
機型特征:聯發科芯片、PowerVR GPU
OS version: Android 10(主要),少量Android 8.1.0/9.0/11.0/12.0
abi:armeabi-v7a, armeabi
崩潰原因:虛擬內存耗盡,主要由于/dev/dri/renderD128類型的內存占用過多(1G左右)
堆棧:堆棧比較分散,但均為系統堆棧
2.2問題復現
我們根據抖音過往導致renderD128內存激增的MR,找到了一種能穩定復現該問題的辦法“新增View,并調用View.setAlpha會引發renderD128內存上漲”(僅為其中一種復現場景,但其他場景暫未能穩定復現)。
復現機型:華為暢享10e(Android 10)
測試方式:
- 對照組:新增10個view,每個view設置背景色,不設置透明度,查看繪制前后內存變化。
- 實驗組:新增10個view,每個view設置背景色,并設置alpha為0.5,查看繪制10個view前后renderD128類內存變化。
測試結果:
- 對照組: 新增View,renderD128內存無變化。
- 實驗組: 新增View,renderD128內存出現顯著上漲,且每增加1個View,renderD128內存增加大概25M。
結論:如果view被設置了透明度,繪制時會申請大量內存,且繪制完成不會釋放。
2.3監控工具完善
我們在線上線下都開啟了虛擬內存監控,但是均并未找到renderD128相關的內存監控信息(分配線程、堆棧等)。
2.3.1關鍵接口代理
以下是我們Hook相關接口開啟虛擬內存監控的情況
接口 | 是否可以監控 | 備注 |
mmap/mmap64/mremap/__mmap2 | 監控不到 | \ |
ioctl | 僅監控到一個命令,但該命令并沒有映射內存操作 |
|
上層接口 | 播放視頻時沒有監控到這些函數的調用(比較奇怪,講道理應該是有調用的) | |
open | 并未監控到設備文件打開的時機和路徑 | \ |
根據hook ioctl接口獲取到的相關堆棧(雖然ioctl操作并沒有影響內存,也可通過堆棧找到關鍵so庫)
- libsrv_um.so
- gralloc.mt6765.so
2.3.2調查內存映射方式
2.3.2.1從內核源碼中尋找線索
由于關鍵接口代理均無法監控到renderD128相關的內存申請,此時猜想:可能是在內核中分配的內存?
于是找到了華為暢享e的內核源代碼,閱讀其中DRM驅動的相關代碼
找到了唯一一個ioctl調用對應命令(0xc0206440)的定義和參數數據結構。
根據參數的數據結構,很容易理解驅動應該是根據傳入的bridge_id和bridge_func_id來決定做何操作的。(根據堆棧其實也能大致推測每個id對應的操作,但此處暫時不對其進行研究)
但除此之外,在內核代碼中并沒有找到“內存是在內核中分配的”證據,猜測應該還是用戶空間申請的,比較有“嫌疑”的庫是libdrm.so、libsrv_um.so和gralloc.mt6765.so。
2.3.2.2從驅動和關鍵so庫中尋找線索
- libdrm庫
DRM
DRM是Linux內核層的顯示驅動框架,它把顯示功能封裝成 open/close/ioctl 等標準接口,用戶空間的程序調用這些接口,驅動設備,顯示數據。libdrm庫封裝了DRM driver提供的這些接口。通過libdrm庫,程序可以間接調用DRM Driver
但libdrm庫中的drm_mmap是調用 mmap或__mmap2(都是監控中的接口)
#if defined(ANDROID) && !defined(__LP64__)
extern void *__mmap2(void *, size_t, int, int, int, size_t);
static inline void *drm_mmap(void *addr, size_t length, int prot, int flags,
int fd, loff_t offset)
{
/* offset must be aligned to 4096 (not necessarily the page size) */
if (offset & 4095) {
errno = EINVAL;
return MAP_FAILED;
}
return __mmap2(addr, length, prot, flags, fd, (size_t) (offset >> 12));
}
#else
/* assume large file support exists */
# define drm_mmap(addr, length, prot, flags, fd, offset) \
mmap(addr, length, prot, flags, fd, offset)
- mesa3D
mesa3D
mesa3D中是通過調用libdrm庫中的接口,間接調用DRM Driver的
https://gitlab.freedesktop.org/mesa/mesa
在mesa的源代碼中找到了類似libsrv_um.so中PRVSRVBridgeCall的函數 pvr_srv_bridge_call
static int pvr_srv_bridge_call(int fd,
uint8_t bridge_id,
uint32_t function_id,
void *input,
uint32_t input_buffer_size,
void *output,
uint32_t output_buffer_size)
{
struct drm_srvkm_cmd cmd = {
.bridge_id = bridge_id,
.bridge_func_id = function_id,
.in_data_ptr = (uint64_t)(uintptr_t)input,
.out_data_ptr = (uint64_t)(uintptr_t)output,
.in_data_size = input_buffer_size,
.out_data_size = output_buffer_size,
};
int ret = drmIoctl(fd, DRM_IOCTL_SRVKM_CMD, &cmd);
if (unlikely(ret))
return ret;
VG(VALGRIND_MAKE_MEM_DEFINED(output, output_buffer_size));
return 0U;
}
同時發現了BridgeCall的相關id定義
通過提交的commit了解到這部分代碼是為powerVR rogue GPU增加的驅動;
commit鏈接:https://gitlab.freedesktop.org/mesa/mesa/-/commit/8991e646411b73c1e03278267c80758e921f2352
存在renderD128內存問題的機型使用的GPU也是PowerVR GPU,那么內存申請關鍵邏輯應該確實就在libsrv_um.so和gralloc.mt6765.so中
Huawei Y6p - Full phone specifications
- libsrv_um.so與gralloc.mt6765.so
奇怪的是,libsrv_um.so中只有munmap的符號,卻沒有mmap的符號(gralloc.mt6765.so同樣沒有)
這比較不符合常理,一般來說,mmap和munmap都是成對出現的,猜測有幾種可能性:
- 在其他庫中mmap
- 用其他方式實現mmap操作
a.使用dlsym拿到mmap等的符號,再調用 ?
(1)這種情況,使用inline hook是可以監控到的
b.調用ioctl實現mmap操作 ?
c.直接使用系統調用 ?
(1)在libsrv_um.so中發現調用了syscall,系統調用號是0xC0(192),正是mmap的系統調用號!
(2)gralloc.mt6765.so同libsrv_um.so,也是通過系統調用進行mmap的!
結論:hook syscall 應該可以監控到renderD128相關內存的調用!
2.3.3驗證監控方案
- 監控方式:
- 使用bytehook代理了libsrv_um.so和gralloc.mt6765.so中對syscall的調用
- 記錄renderD128內存的變化
- 測試場景:播放視頻
- 測試結果:
- 系統調用mmap可以監控到renderD128內存的分配
- 在播放視頻期間renderD128內存增長大小符合通過系統調用mmap分配的大小
- 堆棧:
- 內存變化:
- 結論:底層驅動可能考慮到架構適配或者效率問題,直接使用系統調用而非通用接口調用。在之前的監控中并未考慮到這種情況,所以會導致監控不全。
2.4相關內存分配
內存監控工具完善之后,從線上我們收集到如下的堆棧信息:
從堆棧上可以看到 libIMGegl.so有一個方法KEGLGetPoolBuffers,這個方法中會調用PVRSRVAcquireCPUMapping申請內存;
從“KEGLGetPoolBuffers”這個方法名可以推斷:
a.有一個緩存池
b.可以調用KEGLGetPoolBuffers從緩存池中獲取buffer
c.如果緩存池中有空閑buffer,會直接分配,無須從系統分配內存
d.如果緩存池中無空閑buffer,會調用PVRSRVAcquireCPUMapping從系統中申請內存。
我們繼續通過hook KEGLGetPoolBuffers打印一些關鍵日志來確認猜想。
1. 日志中前兩次調用KEGLGetPoolBuffers沒有申請內存,符合“存在空閑buffer直接分配”的猜想。
2. 后面的多次調用,每次都會連續調用5次 PVRSRVAcquireCPUMapping,分配5個大小不一的內存塊(猜測應該是5類buffer),一共25M內存,和前面測試的結果剛好一致。
2.5相關內存釋放
既然有內部分配,必然有其對應的內存釋放,我們hook 泄漏線程RenderThread的munmap調用,抓到下面的堆棧,libsrv_um.so中相對偏移0xf060處(對應下面棧回溯#04棧幀,0xf061最后一位是1代表是thumb指令)的方法是DevmemReleaseCpuVirtAddr,但DevmemReleaseCpuVirtAddr這個方法并沒有導出,glUnmapBuffer其實是調用了PVRSRVReleaseCPUMapping方法,在PVRSRVReleaseCPUMapping調用了DevmemReleaseCpuVirtAddr,進而最終調用到munmap方法釋放內存的。
之所以在堆棧中沒有PVRSRVReleaseCPUMapping這層棧幀,是因為PVRSRVReleaseCPUMapping跳轉到DevmemReleaseCpuVirtAddr使用的是指令b(而非bl指令)
調用鏈路:glUnmapBuffer-->PVRSRVReleaseCPUMapping --> DevmemReleaseCpuVirtAddr --> ... --> munmap
#01 pc 00009f41 /data/app/com.example.crash.test-bqPIslSQVErr7gyFpcHl_w==/lib/arm/libnpth_vm_monitor.so (proxy_munmap)
#02 pc 0001474b /vendor/lib/libsrv_um.so
#03 pc 000115d9 /vendor/lib/libsrv_um.so
#04 pc 0000f061 /vendor/lib/libsrv_um.so(DevmemReleaseCpuVirtAddr+44)
#05 pc 00015db1 /vendor/lib/egl/libGLESv2_mtk.so (glUnmapBuffer+536)
#06 pc 003b865d /system/lib/libhwui.so!libhwui.so (offset 0x244000) (GrGLBuffer::onUnmap()+54)
#07 pc 001a0eb3 /system/lib/libhwui.so (GrResourceProvider::createPatternedIndexBuffer(unsigned short const*, int, int, int, GrUniqueKey const*)+174)
#08 pc 001666b9 /system/lib/libhwui.so (GrResourceProvider::createQuadIndexBuffer()+24)
#09 pc 00153df1 /system/lib/libhwui.so (GrResourceProvider::refQuadIndexBuffer()+44)
#10 pc 001535c9 /system/lib/libhwui.so (GrAtlasTextOp::onPrepareDraws(GrMeshDrawOp::Target*)+328)
PVRSRVAcquireCPUMapping和PVRSRVReleaseCPUMapping是libsrv_um.so中進行內存分配和釋放的一對方法
同理,KEGLGetPoolBuffers和KEGLReleasePoolBuffers是libIMGegl.so中分配和釋放緩存buffer的一對方法
但在測試過程中,并沒有看到在為buffer分配內存之后有調用PVRSRVReleaseCPUMapping釋放內存,在繪制結束前,會調用KEGLReleasePoolBuffers釋放buffer(但并未釋放內存),查看KEGLReleasePoolBuffers的匯編發現方法內部只是對buffer標記可用,并不存在內存釋放。
KEGLGetPoolBuffers申請buffer,會申請內存:
KEGLReleasePoolBuffers釋放buffer,但不釋放內存:
看來這個緩存池可能是統一釋放內存的,由于libIMGegl.so中大部分方法都沒有符號,從這層比較難推進,不妨再從上層場景分析一下,跟繪制相關的緩存池會什么時候釋放呢?首先想到的可能是Activity銷毀的時候,經過測試發現并沒有……
但是在一次測試中發現 在Activity銷毀之后,過了一段時間(1min左右)再啟動一個新的Activity時突然釋放了一堆renderD128相關的內存,抓到的是下面的堆棧。RenderThreaad中會執行銷毀CanvasContext的任務,每次銷毀CanvasContext時都會釋放在一定時間范圍內(30s)未使用的一些資源。銷毀CanvasContext的時機是Activity Destroy時。(這里其實有些疑問,應該還有釋放時機沒有被發現)
#01 pc 0000edc1 /data/app/com.example.crash.test-o-BAwGot5UWCmlHJALMy2g==/lib/arm/libnpth_vm_monitor.so
#02 pc 0001d29b /vendor/lib/libIMGegl.so
#03 pc 0001af31 /vendor/lib/libIMGegl.so
#04 pc 000187c1 /vendor/lib/libIMGegl.so
#05 pc 0001948b /vendor/lib/libIMGegl.so
#06 pc 00018753 /vendor/lib/libIMGegl.so
#07 pc 0000b179 /vendor/lib/libIMGegl.so
#08 pc 0000f473 /vendor/lib/libIMGegl.so (IMGeglDestroySurface+462)
#09 pc 000171bd /system/lib/libEGL.so (android::eglDestroySurfaceImpl(void*, void*)+48)
#10 pc 0025d40b /system/lib/libhwui.so!libhwui.so (offset 0x245000) (android::uirenderer::renderthread::EglManager::destroySurface(void*)+30)
#11 pc 0025d2f7 /system/lib/libhwui.so!libhwui.so (offset 0x245000) (android::uirenderer::skiapipeline::SkiaOpenGLPipeline::setSurface(ANativeWindow*, android::uirenderer::renderthread::SwapBehavior, android::uirenderer::renderthrea
#12 pc 00244c03 /system/lib/libhwui.so!libhwui.so (offset 0x243000) (android::uirenderer::renderthread::CanvasContext::setSurface(android::sp<android::Surface>&&)+110)
#13 pc 00244af5 /system/lib/libhwui.so!libhwui.so (offset 0x243000) (android::uirenderer::renderthread::CanvasContext::destroy()+48)
#15 pc 0023015f /system/lib/libhwui.so!libhwui.so (offset 0x208000) (std::__1::packaged_task<void ()>::operator()()+50)
#16 pc 0020da97 /system/lib/libhwui.so!libhwui.so (offset 0x208000) (android::uirenderer::WorkQueue::process()+158)
#17 pc 0020d8f5 /system/lib/libhwui.so!libhwui.so (offset 0x208000) (android::uirenderer::renderthread::RenderThread::threadLoop()+72)
#18 pc 0000d91b /system/lib/libutils.so (android::Thread::_threadLoop(void*)+182)
#19 pc 0009b543 /apex/com.android.runtime/lib/bionic/libc.so!libc.so (offset 0x8d000) (__pthread_start(void*)+20)
2.6總結
renderD128類內存導致的OOM問題,并非由于內存泄漏,而是大量內存長期不釋放導致。在大型APP中,Activity存活的時間可能會很長,如果緩存池只能等到Activity銷毀時才能釋放,大量內存長期無法釋放,就極易發生OOM。
3.優化方案
3.1手動釋放內存
3.1.1方案一:釋放空閑buffer
從相關內存的分配和釋放章節的分析來看,get & release buffer的操作有點不對稱,我們期望:
- 分配緩存:有可用buffer直接使用;無可用buffer則申請新的。
- 釋放緩存:標記buffer空閑,空閑buffer達到某一閾值后則釋放。
而現狀是空閑buffer達到某一閾值后并不會釋放,是否可以嘗試手動釋放呢?
首先需要了解緩存池的結構
由于相關so代碼閉源,我們通過反匯編推導出緩存池的結構,大致如下圖所示,pb_global是緩存池的管理結構體,其中的buffers_list中分別保存了5類buffer的list,內存組織方式如下圖所示。
KEGLReleasePoolBuffers中會標記每一個buffer->flag為0(空閑)
手動釋放內存的方式
在KEGLReleasePoolBuffers標記buffer為空閑之后,檢查當前空閑buffer個數是否超過閾值(或者檢查當前renderD128相關內存是否超過閾值),如果超過閾值則釋放一批buffer,并將buffer從鏈表中取下。
(相關代碼如下??)
static void release_freed_buffer(pb_ctx_t* ctx) {
/** 一些檢查和判空操作會省略 **/
...
/** 閾值檢查 **/
if (!limit_check(ctx)) return;
// 拿到buffer_list
pb_buffer_list_t* buffers_list = ctx->pb_global->buffers_list;
pb_buffer_info_t *buffer_info, *prev_info;
for (int i = 0; i < 5; i++) {
buffer_info = buffer_info->buffers[i];
if (buffer_info == NULL) continue;
/** 第一個buffer不釋放,簡化邏輯 **/
while(buffer_info) {
prev_info = buffer_info;
buffer_info = buffer_info->next;
if (buffer_info && buffer_info->flag == 0) {
int ret = pvrsrvReleaseCPUMapping((void**)buffer_info->sparse_buffer->cpu_mapping_info->info);
LOGE("%s, release cpu mapping ret: %d", __FUNCTION__, ret);
if (ret == 0) {
buffer_info->flag = 1;
buffer_info->sparse_buffer->mmap_ptr = NULL;
prev_info->next = buffer_info->next;
buffers_list->buffer_size[i]--;
free(buffer_info);
buffer_info = prev_info;
}
}
}
}
}
方案效果
測試環境和方式與前面“問題復現”章節一致
內存釋放時機 | 繪制結束后renderD128相關內存大小 | 結果比較 |
每次釋放緩存 | 33M 左右 | 與不設置透明度的對照組結果接近 |
renderD128內存>100M | 86M 左右 | 100M以下,符合預期 |
renderD128內存>300M | 295M左右 | 跟實驗組一致,因為并沒有超過300M的閾值。符合預期 |
buffer總數>5 | 33M左右 | 與不設置透明度的對照組結果接近,繪制結束時會釋放完所有空閑buffer |
buffer總數>10 | ||
buffer總數> 20 | 295M左右 | 跟實驗組一致,因為并沒有超過20個buffer的閾值(10個view大概會用到10~15個buffer)。符合預期 |
空閑buffer > 5 | 138M 左右 | 空閑buffer個數不太可控,無法精確控制內存水位 |
空閑buffer > 10 | 33M 左右 |
方案結論
這個方案雖然也可緩解問題,但是存在以下問題:
- 性能影響(理論,未測)
- 增加了內存申請和釋放的概率,會有一定的性能影響。
- 每次進行閾值判定,都需要統計當前buffer/內存的值,頻繁調用接口時,也會影響性能。
- 穩定性
- 硬編碼緩存池相關的數據結構,如果有些機型數據結構不一致的話,就可能會崩潰。
這個方案應該不是最優解,先做備用方案,再探索一下。
3.1.2 方案二:上層及時釋放資源
從前面“相關內存釋放”章節的分析可知,緩存池的內存并不是不會釋放,而是釋放時機很晚,那么能否早點釋放呢?
查看CanvasContext的釋放路徑,僅發現了一個可操作點(嘗試了一些方式都會崩潰,會釋放掉正在使用的資源),CacheManager::trimStaleResources用的資源),CacheManager::trimStaleResources方法中可以釋放30s內未使用的資源,改成釋放1s(或10s)內未使用的資源。
修改指令:MOVW R2, #30000 ==> MOVW R2,#1000
(相關代碼如下??)
#define ORIGIN_TIME_LIMIT_INST 0x5230f247 // 30s
#define NEW_TIME_LIMIT_INST 0x32e8f240 // 1s 提前構造好的指令編碼
#define FUNC_SYM "_ZN7android10uirenderer12renderthread12CacheManager18trimStaleResourcesEv"
static void change_destroy_wait_time() {
/** 一些檢查和判空操作會省略 **/
#ifdef __arm__
void* handle = dlopen("libhwui.so");
// 從trimStaleResources方法的起始地址開始搜索內存
void* sym_ptr = dlsym(handle, FUNC_SYM);
sym_ptr = (void*)((uint32_t)sym_ptr & 0xfffffffc);
uint32_t* inst_start = (uint32_t*)sym_ptr;
uint32_t* search_limit = inst_start + 12;
while(inst_start < search_limit) {
/* 找到并修改對應指令 */
if (*inst_start == ORIGIN_TIME_LIMIT_INST) {
if(mprotect((void*)((uint32_t)inst_start & (unsigned int) PAGE_MASK), PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC)) {
return;
}
*inst_start = NEW_TIME_LIMIT_INST;
flash_page_cache(inst_start);
if(mprotect((void*)((uint32_t)inst_start & (unsigned int) PAGE_MASK), PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC)) {
return;
}
break;
}
inst_start++;
}
#endif
}
方案結論:該方案還是依賴于Activity銷毀,只是銷毀后能更快釋放資源,所以緩解內存方面起到的作用很有限。
3.2 控制緩存池增長
在嘗試前面兩個方案之后,這個問題逐漸讓人崩潰,似乎已經沒有什么好的解決辦法了,準備就此放棄。
3.2.1 新的突破點
然而就在我們準備放棄的時候,后續的幾次壓測中我們發現了一個新的突破點“每次調用一次View.setAlpha,renderD128內存會上漲25M,但并不是無限上漲,上漲到1.3G左右就不再增長了”,且另外翻看線上相關OOM問題,renderD128內存占用也均未超過1.3G,由此我們大膽猜測renderD128 內存緩存池大小應該是有上限的,這個上限大概在1.3G上下,那么我們或許可以嘗試從調小緩存池的閾值入手。
再次嘗試
我們再次嘗試復現該問題,并hook相關內存分配;從日志可以看到,在內存增長到1.3G后
- 下一次調用KEGLGetPoolBuffers獲取buffer時,返回值是0(代表分配失敗);
- 再下一次調用KEGLGetPoolBuffers,返回值是1(代表分配成功),但沒有申請內存。
再增加多一點信息,發現當KEGLGetPoolBuffers獲取buffer失敗后,會有KEGLReleasePoolBuffers調用,釋放了大量buffer,之后再重新調用KEGLGetPoolBuffers。
KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1265852416, after: 1292066816, alloc: 26214400
KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1292066816, after: 1318281216, alloc: 26214400
KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x0 ==> before: 1318281216, after: 1318281216, alloc: 0
KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0
KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0
KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0
...
KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0
KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0
KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0
KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1318281216, after: 1318281216, alloc: 0
KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1318281216, after: 1318281216, alloc: 0
KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1318281216, after: 1318281216, alloc: 0
從堆棧看應該是提前flush了,所以就可以釋放之前的buffer
#01 pc 0000ebf5 /data/app/com.example.crash.test-1hHKnp6FBSv-HjrVtXQo1Q==/lib/arm/libnpth_vm_monitor.so (proxy_KEGLReleasePoolBuffers)
#02 pc 00047c2d /vendor/lib/egl/libGLESv2_mtk.so
#03 pc 00046a7b /vendor/lib/egl/libGLESv2_mtk.so (ResetSurface)
#04 pc 00028bf7 /vendor/lib/egl/libGLESv2_mtk.so
#05 pc 000d2165 /vendor/lib/egl/libGLESv2_mtk.so (RM_FlushHWQueue)
#06 pc 00028c73 /vendor/lib/egl/libGLESv2_mtk.so
#07 pc 000453fd /vendor/lib/egl/libGLESv2_mtk.so (PrepareToDraw)
#08 pc 0001d977 /vendor/lib/egl/libGLESv2_mtk.so (glDrawArrays+738)
#09 pc 00009edd /system/lib/libGameGraphicsOpt.so (hw_glDrawArraysHookV2+18)
#10 pc 001d1769 /system/lib/libhwui.so (GrGLGpu::sendMeshToGpu(GrPrimitiveType, GrBuffer const*, int, int)+74)
#11 pc 001d15f3 /system/lib/libhwui.so (GrMesh::sendToGpu(GrMesh::SendToGpuImpl*) const+38)
#12 pc 001d13e5 /system/lib/libhwui.so (GrGLGpu::draw(GrRenderTarget*, GrSurfaceOrigin, GrPrimitiveProcessor const&, GrPipeline const
3.2.2 方案三:KEGLGetPoolBuffers中限制buffer分配
根據上面的分析,發現可以嘗試:
- Hook KEGLGetPoolBuffers函數,判斷內存增長到一定閾值后,在KEGLGetPoolBuffers函數中就直接返回0,觸發其內部的空閑buffer釋放
- 空閑buffer釋放之后,才允許分配buffer(如下流程)
結論:該方案需要每次分配內存前讀取maps獲取renderD128占用內存大小,對性能不是很友好。
3.2.3 方案四:修改緩存池閾值
從上面的分析,我們知道KEGLGetPoolBuffers函數返回0時分配失敗,會開始釋放buffer。我們繼續反匯編KEGLGetPoolBuffers函數,根據KEGLGetPoolBuffers的返回值為0 可以回溯到匯編中進行閾值判斷的邏輯,如下圖所示。
v8:buffers_list;
v7:buffer類型(0~4);
v8+4*v7+24:v7這個buffer類型 的buffer數量(下圖中的buffer_size[i]);
v49:buffer_info;
v49+28: buffer_limit 緩存池中每種類型的buffer 的閾值(下圖中的buffer_limits);
簡單來說,這里將buffer_limits與buffer_size[i]進行比較,如果buffer_size[i]大于等于閾值,就會返回0,分配失敗
接下來的操作就很簡單了,只需對buffer_limits進行修改就行,在測試設備上buffer_limits值是50(50*25M 大約是1.25G),我們將buffer_limits值是50(50*25M 大約是1.25G),我們將buffer_limits改小一點就可以將renderD128內存緩存池控制在一個更小的閾值范圍內,以此降低renderD128內存占用。
(相關代碼如下??)
int opt_mtk_buffer(int api_level, int new_buffer_size) {
...(無關代碼省略)
if (check_buffer_size(new_buffer_size)) {
prefered_buffer_size = new_buffer_size;
}
KEGLGetPoolBuffers_stub = bytehook_hook_single(
"libGLESv2_mtk.so",
NULL,
"KEGLGetPoolBuffers",
(void*)proxy_KEGLGetPoolBuffers,
(bytehook_hooked_t)bytehook_hooked_mtk,
NULL);
...(無關代碼省略)
return 0;
}
static void* proxy_KEGLGetPoolBuffers(void** a1, void* a2, int a3, int a4) {
//修改buffer_limits
modify_buffer_size((pb_ctx_t*)a1);
void* ret = BYTEHOOK_CALL_PREV(proxy_KEGLGetPoolBuffers, KEGLGetPoolBuffers_t, a1, a2, a3, a4);
BYTEHOOK_POP_STACK();
return ret;
}
static void modify_buffer_size(pb_ctx_t* ctx) {
if (__predict_false(ctx == NULL || ctx->node == NULL || ctx->node->buffer_inner == NULL)) {
return;
}
if (ctx->node->buffer_inner->num == ORIGIN_BUFFER_SIZE) {
ctx->node->buffer_inner->num = prefered_buffer_size;
}
}
Demo驗證:
緩存值閾值 | 內存峰值 |
50 | 1.3G |
20 | 530M |
10 | 269M |
方案結論:該方案修改少,性能影響小,且穩定性可控。
3.3 最終方案
通過的上面的分析,由于方案四“修改緩存池閾值”修改少,性能影響小,且穩定性可控,最終我們決定選用該方案。
4. 修復效果
開啟修復實驗后相關機型OOM崩潰率顯著下降近-50%,觀察數周之后各項業務指標也均為正向,符合預期。全量上線后大盤renderD128內存相關OOM也大幅下降,renderD128內存引發的發版熔斷問題也被徹底根治。
5. 總結
在分析內存問題時,不論是系統申請的內存還是業務申請的內存,都需要明確申請邏輯和釋放邏輯,才能確定是否發生泄漏還是長期不釋放,再從內存申請和釋放邏輯中尋找可優化點。