抖音renderD128系统级疑难OOM分析与解决
抖音长期存在renderD128内存占用过多导致的虚拟内存OOM,且多次出现renderD128内存激增导致OOM指标严重劣化甚至发版熔断。因受限于闭源的GPU驱动以及现场有效信息极少,多个团队都进行过分析,但一直未能定位到问题根因,问题反馈到厂商也一直没有结论。
以往发生renderD128内存激增时,解决办法往往都是通过二分法去定位导致问题的MR进行回滚(MR代码写法并无问题,仅仅是正常调用系统API),但是回滚业务代码会影响业务正常需求的合入,也无法从根本上解决该问题,每次也会消耗我们大量人力去分析排查,因此我们有必要投入更多时间和精力定位根因并彻底解决该问题。在历经数月的深入分析和排查后,我们最终定位了问题根因并彻底解决了该问题,取得了显著的OOM收益,renderD128内存问题导致的发版熔断也未再发生。
接下来,将详细介绍下我们是如何一步步分析定位到问题根因,以及最终如何将这个问题给彻底解决的。
2.1问题特征
2.2问题复现
复现机型:华为畅享10e(Android 10)
对照组:新增10个view,每个view设置背景色,不设置透明度,查看绘制前后内存变化。 实验组:新增10个view,每个view设置背景色,并设置alpha为0.5,查看绘制10个view前后renderD128类内存变化。
对照组: 新增View,renderD128内存无变化。 实验组: 新增View,renderD128内存出现显著上涨,且每增加1个View,renderD128内存增加大概25M。
2.3监控工具完善
2.3.1关键接口代理
| ||
libsrv_um.so
gralloc.mt6765.so
2.3.2调查内存映射方式
2.3.2.1从内核源码中寻找线索
2.3.2.2从驱动和关键so库中寻找线索
libdrm库
DRM DRM是Linux内核层的显示驱动框架,它把显示功能封装成 open/close/ioctl 等标准接口,用户空间的程序调用这些接口,驱动设备,显示数据。libdrm库封装了DRM driver提供的这些接口。通过libdrm库,程序可以间接调用DRM Driver
#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
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;}
- libsrv_um.so与gralloc.mt6765.so
在其他库中mmap
用其他方式实现mmap操作
使用dlsym拿到mmap等的符号,再调用 ❌
这种情况,使用inline hook是可以监控到的
调用ioctl实现mmap操作 ❌
直接使用系统调用 ✅
在libsrv_um.so中发现调用了syscall,系统调用号是0xC0(192),正是mmap的系统调用号!
gralloc.mt6765.so同libsrv_um.so,也是通过系统调用进行mmap的!
2.3.3验证监控方案
监控方式:
使用bytehook代理了libsrv_um.so和gralloc.mt6765.so中对syscall的调用
记录renderD128内存的变化
测试场景:播放视频
测试结果:
系统调用mmap可以监控到renderD128内存的分配
在播放视频期间renderD128内存增长大小符合通过系统调用mmap分配的大小
堆栈:
内存变化:
结论:底层驱动可能考虑到架构适配或者效率问题,直接使用系统调用而非通用接口调用。在之前的监控中并未考虑到这种情况,所以会导致监控不全。
2.4相关内存分配
从“KEGLGetPoolBuffers”这个方法名可以推断:
a.有一个缓存池
b.可以调用KEGLGetPoolBuffers从缓存池中获取buffer
c.如果缓存池中有空闲buffer,会直接分配,无须从系统分配内存
d.如果缓存池中无空闲buffer,会调用PVRSRVAcquireCPUMapping从系统中申请内存。
我们继续通过hook KEGLGetPoolBuffers打印一些关键日志来确认猜想。
1. 日志中前两次调用KEGLGetPoolBuffers没有申请内存,符合“存在空闲buffer直接分配”的猜想。
2. 后面的多次调用,每次都会连续调用5次 PVRSRVAcquireCPUMapping,分配5个大小不一的内存块(猜测应该是5类buffer),一共25M内存,和前面测试的结果刚好一致。
2.5相关内存释放
#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)
#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总结
3.1手动释放内存
3.1.1方案一:释放空闲buffer
分配缓存:有可用buffer直接使用;无可用buffer则申请新的。 释放缓存:标记buffer空闲,空闲buffer达到某一阈值后则释放。
static void release_freed_buffer(pb_ctx_t* ctx) {/** 一些检查和判空操作会省略 **/.../** 阈值检查 **/if (!limit_check(ctx)) return;// 拿到buffer_listpb_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/内存的值,频繁调用接口时,也会影响性能。 稳定性 硬编码缓存池相关的数据结构,如果有些机型数据结构不一致的话,就可能会崩溃。
#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}
下一次调用KEGLGetPoolBuffers获取buffer时,返回值是0(代表分配失败); 再下一次调用KEGLGetPoolBuffers,返回值是1(代表分配成功),但没有申请内存。
KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1265852416, after: 1292066816, alloc: 26214400KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1292066816, after: 1318281216, alloc: 26214400KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x0 ==> before: 1318281216, after: 1318281216, alloc: 0KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0...KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0KEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1318281216, after: 1318281216, alloc: 0KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1318281216, after: 1318281216, alloc: 0KEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1318281216, after: 1318281216, alloc: 0
#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
Hook KEGLGetPoolBuffers函数,判断内存增长到一定阈值后,在KEGLGetPoolBuffers函数中就直接返回0,触发其内部的空闲buffer释放 空闲buffer释放之后,才允许分配buffer(如下流程)
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_limitsmodify_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;}}
4. 修复效果
5. 总结
6. 加入我们
抖音基础技术-客户端质量团队是一个深度追求极致的团队,我们专注于性能、稳定性方向的深耕,保障数亿用户的使用体验,欢迎有志之士与我们共同建设亿级用户全球化APP!目前质量优化方向在Andriod、鸿蒙、iOS都有HC,可以base 北京、上海、深圳。
长按图片扫码查看相关职位,直接发送简历内推。也可以邮件联系:[email protected]咨询相关信息!
华为畅享e内核源代码链接:https://consumer.huawei.com/en/opensource/detail/?siteCode=worldwide&keywords=med&fileType=openSourceSoftware&pageSize=10&curPage=1 mesa源代码链接:https://gitlab.freedesktop.org/mesa/mesa