偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

抖音renderD128系統(tǒng)級(jí)疑難OOM分析與解決

系統(tǒng)
抖音長(zhǎng)期存在renderD128內(nèi)存占用過(guò)多導(dǎo)致的虛擬內(nèi)存OOM,且多次出現(xiàn)renderD128內(nèi)存激增導(dǎo)致OOM指標(biāo)嚴(yán)重劣化甚至發(fā)版熔斷。在歷經(jīng)數(shù)月的深入分析和排查后,我們最終定位了問(wèn)題根因并徹底解決了該問(wèn)題,取得了顯著的OOM收益,renderD128內(nèi)存問(wèn)題導(dǎo)致的發(fā)版熔斷也未再發(fā)生。

1.背景

抖音長(zhǎng)期存在renderD128內(nèi)存占用過(guò)多導(dǎo)致的虛擬內(nèi)存OOM,且多次出現(xiàn)renderD128內(nèi)存激增導(dǎo)致OOM指標(biāo)嚴(yán)重劣化甚至發(fā)版熔斷。因受限于閉源的GPU驅(qū)動(dòng)以及現(xiàn)場(chǎng)有效信息極少,多個(gè)團(tuán)隊(duì)都進(jìn)行過(guò)分析,但一直未能定位到問(wèn)題根因,問(wèn)題反饋到廠商也一直沒(méi)有結(jié)論。

以往發(fā)生renderD128內(nèi)存激增時(shí),解決辦法往往都是通過(guò)二分法去定位導(dǎo)致問(wèn)題的MR進(jìn)行回滾(MR代碼寫(xiě)法并無(wú)問(wèn)題,僅僅是正常調(diào)用系統(tǒng)API),但是回滾業(yè)務(wù)代碼會(huì)影響業(yè)務(wù)正常需求的合入,也無(wú)法從根本上解決該問(wèn)題,每次也會(huì)消耗我們大量人力去分析排查,因此我們有必要投入更多時(shí)間和精力定位根因并徹底解決該問(wèn)題。在歷經(jīng)數(shù)月的深入分析和排查后,我們最終定位了問(wèn)題根因并徹底解決了該問(wèn)題,取得了顯著的OOM收益,renderD128內(nèi)存問(wèn)題導(dǎo)致的發(fā)版熔斷也未再發(fā)生。

接下來(lái),將詳細(xì)介紹下我們是如何一步步分析定位到問(wèn)題根因,以及最終如何將這個(gè)問(wèn)題給徹底解決的。

2.問(wèn)題分析

2.1問(wèn)題特征

主要集中在華為Android10系統(tǒng),表現(xiàn)為renderD128內(nèi)存占用過(guò)多。

機(jī)型特征:聯(lián)發(fā)科芯片、PowerVR GPU

OS version: Android 10(主要),少量Android 8.1.0/9.0/11.0/12.0

abi:armeabi-v7a, armeabi

崩潰原因:虛擬內(nèi)存耗盡,主要由于/dev/dri/renderD128類(lèi)型的內(nèi)存占用過(guò)多(1G左右)

堆棧:堆棧比較分散,但均為系統(tǒng)堆棧

圖片

圖片

2.2問(wèn)題復(fù)現(xiàn)

我們根據(jù)抖音過(guò)往導(dǎo)致renderD128內(nèi)存激增的MR,找到了一種能穩(wěn)定復(fù)現(xiàn)該問(wèn)題的辦法“新增View,并調(diào)用View.setAlpha會(huì)引發(fā)renderD128內(nèi)存上漲”(僅為其中一種復(fù)現(xiàn)場(chǎng)景,但其他場(chǎng)景暫未能穩(wěn)定復(fù)現(xiàn))。

復(fù)現(xiàn)機(jī)型:華為暢享10e(Android 10)

測(cè)試方式:

  • 對(duì)照組:新增10個(gè)view,每個(gè)view設(shè)置背景色,不設(shè)置透明度,查看繪制前后內(nèi)存變化。
  • 實(shí)驗(yàn)組:新增10個(gè)view,每個(gè)view設(shè)置背景色,并設(shè)置alpha為0.5,查看繪制10個(gè)view前后renderD128類(lèi)內(nèi)存變化。

測(cè)試結(jié)果:

  •  對(duì)照組: 新增View,renderD128內(nèi)存無(wú)變化。
  •  實(shí)驗(yàn)組: 新增View,renderD128內(nèi)存出現(xiàn)顯著上漲,且每增加1個(gè)View,renderD128內(nèi)存增加大概25M。
    圖片

結(jié)論:如果view被設(shè)置了透明度,繪制時(shí)會(huì)申請(qǐng)大量?jī)?nèi)存,且繪制完成不會(huì)釋放。

2.3監(jiān)控工具完善

我們?cè)诰€上線下都開(kāi)啟了虛擬內(nèi)存監(jiān)控,但是均并未找到renderD128相關(guān)的內(nèi)存監(jiān)控信息(分配線程、堆棧等)。

2.3.1關(guān)鍵接口代理

以下是我們Hook相關(guān)接口開(kāi)啟虛擬內(nèi)存監(jiān)控的情況

接口

是否可以監(jiān)控

備注

mmap/mmap64/mremap/__mmap2

監(jiān)控不到

\

ioctl

僅監(jiān)控到一個(gè)命令,但該命令并沒(méi)有映射內(nèi)存操作

  1. 命令調(diào)用前后renderD128相關(guān)內(nèi)存并無(wú)變化
  2. 這個(gè)命令相關(guān)的ioctl調(diào)用頻繁

上層接口


播放視頻時(shí)沒(méi)有監(jiān)控到這些函數(shù)的調(diào)用(比較奇怪,講道理應(yīng)該是有調(diào)用的)


圖片

open

并未監(jiān)控到設(shè)備文件打開(kāi)的時(shí)機(jī)和路徑

\

根據(jù)hook ioctl接口獲取到的相關(guān)堆棧(雖然ioctl操作并沒(méi)有影響內(nèi)存,也可通過(guò)堆棧找到關(guān)鍵so庫(kù))

  • libsrv_um.so圖片
  • gralloc.mt6765.so圖片

2.3.2調(diào)查內(nèi)存映射方式

2.3.2.1從內(nèi)核源碼中尋找線索

由于關(guān)鍵接口代理均無(wú)法監(jiān)控到renderD128相關(guān)的內(nèi)存申請(qǐng),此時(shí)猜想:可能是在內(nèi)核中分配的內(nèi)存?

于是找到了華為暢享e的內(nèi)核源代碼,閱讀其中DRM驅(qū)動(dòng)的相關(guān)代碼

圖片

找到了唯一一個(gè)ioctl調(diào)用對(duì)應(yīng)命令(0xc0206440)的定義和參數(shù)數(shù)據(jù)結(jié)構(gòu)。

根據(jù)參數(shù)的數(shù)據(jù)結(jié)構(gòu),很容易理解驅(qū)動(dòng)應(yīng)該是根據(jù)傳入的bridge_id和bridge_func_id來(lái)決定做何操作的。(根據(jù)堆棧其實(shí)也能大致推測(cè)每個(gè)id對(duì)應(yīng)的操作,但此處暫時(shí)不對(duì)其進(jìn)行研究)

但除此之外,在內(nèi)核代碼中并沒(méi)有找到“內(nèi)存是在內(nèi)核中分配的”證據(jù),猜測(cè)應(yīng)該還是用戶(hù)空間申請(qǐng)的,比較有“嫌疑”的庫(kù)是libdrm.so、libsrv_um.so和gralloc.mt6765.so。

2.3.2.2從驅(qū)動(dòng)和關(guān)鍵so庫(kù)中尋找線索
  • libdrm庫(kù)

DRM

DRM是Linux內(nèi)核層的顯示驅(qū)動(dòng)框架,它把顯示功能封裝成 open/close/ioctl 等標(biāo)準(zhǔn)接口,用戶(hù)空間的程序調(diào)用這些接口,驅(qū)動(dòng)設(shè)備,顯示數(shù)據(jù)。libdrm庫(kù)封裝了DRM driver提供的這些接口。通過(guò)libdrm庫(kù),程序可以間接調(diào)用DRM Driver

但libdrm庫(kù)中的drm_mmap是調(diào)用 mmap或__mmap2(都是監(jiān)控中的接口)

#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中是通過(guò)調(diào)用libdrm庫(kù)中的接口,間接調(diào)用DRM Driver的

https://gitlab.freedesktop.org/mesa/mesa

在mesa的源代碼中找到了類(lèi)似libsrv_um.so中PRVSRVBridgeCall的函數(shù) 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;
}

同時(shí)發(fā)現(xiàn)了BridgeCall的相關(guān)id定義

圖片

通過(guò)提交的commit了解到這部分代碼是為powerVR rogue GPU增加的驅(qū)動(dòng);

commit鏈接:https://gitlab.freedesktop.org/mesa/mesa/-/commit/8991e646411b73c1e03278267c80758e921f2352

圖片

存在renderD128內(nèi)存問(wèn)題的機(jī)型使用的GPU也是PowerVR GPU,那么內(nèi)存申請(qǐng)關(guān)鍵邏輯應(yīng)該確實(shí)就在libsrv_um.so和gralloc.mt6765.so中

Huawei Y6p - Full phone specifications

圖片

  • libsrv_um.so與gralloc.mt6765.so

奇怪的是,libsrv_um.so中只有munmap的符號(hào),卻沒(méi)有mmap的符號(hào)(gralloc.mt6765.so同樣沒(méi)有)

圖片

這比較不符合常理,一般來(lái)說(shuō),mmap和munmap都是成對(duì)出現(xiàn)的,猜測(cè)有幾種可能性:

  1. 在其他庫(kù)中mmap
  2. 用其他方式實(shí)現(xiàn)mmap操作

     a.使用dlsym拿到mmap等的符號(hào),再調(diào)用 ?

        (1)這種情況,使用inline hook是可以監(jiān)控到的

      b.調(diào)用ioctl實(shí)現(xiàn)mmap操作 ?

      c.直接使用系統(tǒng)調(diào)用 ?

         (1)在libsrv_um.so中發(fā)現(xiàn)調(diào)用了syscall,系統(tǒng)調(diào)用號(hào)是0xC0(192),正是mmap的系統(tǒng)調(diào)用號(hào)!                                                              圖片

          (2)gralloc.mt6765.so同libsrv_um.so,也是通過(guò)系統(tǒng)調(diào)用進(jìn)行mmap的!

結(jié)論:hook syscall 應(yīng)該可以監(jiān)控到renderD128相關(guān)內(nèi)存的調(diào)用!

2.3.3驗(yàn)證監(jiān)控方案

  • 監(jiān)控方式:
  1. 使用bytehook代理了libsrv_um.so和gralloc.mt6765.so中對(duì)syscall的調(diào)用
  2. 記錄renderD128內(nèi)存的變化
  • 測(cè)試場(chǎng)景:播放視頻
  • 測(cè)試結(jié)果:
  1. 系統(tǒng)調(diào)用mmap可以監(jiān)控到renderD128內(nèi)存的分配
  2. 在播放視頻期間renderD128內(nèi)存增長(zhǎng)大小符合通過(guò)系統(tǒng)調(diào)用mmap分配的大小
  • 堆棧:

圖片

  • 內(nèi)存變化:

圖片

  •  結(jié)論:底層驅(qū)動(dòng)可能考慮到架構(gòu)適配或者效率問(wèn)題,直接使用系統(tǒng)調(diào)用而非通用接口調(diào)用。在之前的監(jiān)控中并未考慮到這種情況,所以會(huì)導(dǎo)致監(jiān)控不全。

2.4相關(guān)內(nèi)存分配

內(nèi)存監(jiān)控工具完善之后,從線上我們收集到如下的堆棧信息:

圖片

從堆棧上可以看到 libIMGegl.so有一個(gè)方法KEGLGetPoolBuffers,這個(gè)方法中會(huì)調(diào)用PVRSRVAcquireCPUMapping申請(qǐng)內(nèi)存;

從“KEGLGetPoolBuffers”這個(gè)方法名可以推斷:

a.有一個(gè)緩存池

b.可以調(diào)用KEGLGetPoolBuffers從緩存池中獲取buffer

c.如果緩存池中有空閑buffer,會(huì)直接分配,無(wú)須從系統(tǒng)分配內(nèi)存

d.如果緩存池中無(wú)空閑buffer,會(huì)調(diào)用PVRSRVAcquireCPUMapping從系統(tǒng)中申請(qǐng)內(nèi)存。

我們繼續(xù)通過(guò)hook KEGLGetPoolBuffers打印一些關(guān)鍵日志來(lái)確認(rèn)猜想。

1. 日志中前兩次調(diào)用KEGLGetPoolBuffers沒(méi)有申請(qǐng)內(nèi)存,符合“存在空閑buffer直接分配”的猜想。

2. 后面的多次調(diào)用,每次都會(huì)連續(xù)調(diào)用5次 PVRSRVAcquireCPUMapping,分配5個(gè)大小不一的內(nèi)存塊(猜測(cè)應(yīng)該是5類(lèi)buffer),一共25M內(nèi)存,和前面測(cè)試的結(jié)果剛好一致。

圖片

2.5相關(guān)內(nèi)存釋放

既然有內(nèi)部分配,必然有其對(duì)應(yīng)的內(nèi)存釋放,我們hook 泄漏線程RenderThread的munmap調(diào)用,抓到下面的堆棧,libsrv_um.so中相對(duì)偏移0xf060處(對(duì)應(yīng)下面?;厮?04棧幀,0xf061最后一位是1代表是thumb指令)的方法是DevmemReleaseCpuVirtAddr,但DevmemReleaseCpuVirtAddr這個(gè)方法并沒(méi)有導(dǎo)出,glUnmapBuffer其實(shí)是調(diào)用了PVRSRVReleaseCPUMapping方法,在PVRSRVReleaseCPUMapping調(diào)用了DevmemReleaseCpuVirtAddr,進(jìn)而最終調(diào)用到munmap方法釋放內(nèi)存的。

之所以在堆棧中沒(méi)有PVRSRVReleaseCPUMapping這層棧幀,是因?yàn)镻VRSRVReleaseCPUMapping跳轉(zhuǎn)到DevmemReleaseCpuVirtAddr使用的是指令b(而非bl指令)

調(diào)用鏈路: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中進(jìn)行內(nèi)存分配和釋放的一對(duì)方法

同理,KEGLGetPoolBuffers和KEGLReleasePoolBuffers是libIMGegl.so中分配和釋放緩存buffer的一對(duì)方法

但在測(cè)試過(guò)程中,并沒(méi)有看到在為buffer分配內(nèi)存之后有調(diào)用PVRSRVReleaseCPUMapping釋放內(nèi)存,在繪制結(jié)束前,會(huì)調(diào)用KEGLReleasePoolBuffers釋放buffer(但并未釋放內(nèi)存),查看KEGLReleasePoolBuffers的匯編發(fā)現(xiàn)方法內(nèi)部只是對(duì)buffer標(biāo)記可用,并不存在內(nèi)存釋放。

KEGLGetPoolBuffers申請(qǐng)buffer,會(huì)申請(qǐng)內(nèi)存:

圖片

KEGLReleasePoolBuffers釋放buffer,但不釋放內(nèi)存:

圖片

看來(lái)這個(gè)緩存池可能是統(tǒng)一釋放內(nèi)存的,由于libIMGegl.so中大部分方法都沒(méi)有符號(hào),從這層比較難推進(jìn),不妨再?gòu)纳蠈訄?chǎng)景分析一下,跟繪制相關(guān)的緩存池會(huì)什么時(shí)候釋放呢?首先想到的可能是Activity銷(xiāo)毀的時(shí)候,經(jīng)過(guò)測(cè)試發(fā)現(xiàn)并沒(méi)有……

但是在一次測(cè)試中發(fā)現(xiàn) 在Activity銷(xiāo)毀之后,過(guò)了一段時(shí)間(1min左右)再啟動(dòng)一個(gè)新的Activity時(shí)突然釋放了一堆renderD128相關(guān)的內(nèi)存,抓到的是下面的堆棧。RenderThreaad中會(huì)執(zhí)行銷(xiāo)毀CanvasContext的任務(wù),每次銷(xiāo)毀CanvasContext時(shí)都會(huì)釋放在一定時(shí)間范圍內(nèi)(30s)未使用的一些資源。銷(xiāo)毀CanvasContext的時(shí)機(jī)是Activity Destroy時(shí)。(這里其實(shí)有些疑問(wèn),應(yīng)該還有釋放時(shí)機(jī)沒(méi)有被發(fā)現(xiàn))

#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總結(jié)

renderD128類(lèi)內(nèi)存導(dǎo)致的OOM問(wèn)題,并非由于內(nèi)存泄漏,而是大量?jī)?nèi)存長(zhǎng)期不釋放導(dǎo)致。在大型APP中,Activity存活的時(shí)間可能會(huì)很長(zhǎng),如果緩存池只能等到Activity銷(xiāo)毀時(shí)才能釋放,大量?jī)?nèi)存長(zhǎng)期無(wú)法釋放,就極易發(fā)生OOM。

3.優(yōu)化方案

3.1手動(dòng)釋放內(nèi)存

3.1.1方案一:釋放空閑buffer

從相關(guān)內(nèi)存的分配和釋放章節(jié)的分析來(lái)看,get & release buffer的操作有點(diǎn)不對(duì)稱(chēng),我們期望:

  1. 分配緩存:有可用buffer直接使用;無(wú)可用buffer則申請(qǐng)新的。
  2. 釋放緩存:標(biāo)記buffer空閑,空閑buffer達(dá)到某一閾值后則釋放。

而現(xiàn)狀是空閑buffer達(dá)到某一閾值后并不會(huì)釋放,是否可以嘗試手動(dòng)釋放呢?

首先需要了解緩存池的結(jié)構(gòu)

由于相關(guān)so代碼閉源,我們通過(guò)反匯編推導(dǎo)出緩存池的結(jié)構(gòu),大致如下圖所示,pb_global是緩存池的管理結(jié)構(gòu)體,其中的buffers_list中分別保存了5類(lèi)buffer的list,內(nèi)存組織方式如下圖所示。

KEGLReleasePoolBuffers中會(huì)標(biāo)記每一個(gè)buffer->flag為0(空閑)

圖片

手動(dòng)釋放內(nèi)存的方式

在KEGLReleasePoolBuffers標(biāo)記buffer為空閑之后,檢查當(dāng)前空閑buffer個(gè)數(shù)是否超過(guò)閾值(或者檢查當(dāng)前renderD128相關(guān)內(nèi)存是否超過(guò)閾值),如果超過(guò)閾值則釋放一批buffer,并將buffer從鏈表中取下。

(相關(guān)代碼如下??)

static void release_freed_buffer(pb_ctx_t* ctx) {
    /** 一些檢查和判空操作會(huì)省略 **/
    ...
    /** 閾值檢查 **/
    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;


        /** 第一個(gè)buffer不釋放,簡(jiǎn)化邏輯 **/
        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;
                }
            }
        }
    }
}

方案效果

測(cè)試環(huán)境和方式與前面“問(wèn)題復(fù)現(xiàn)”章節(jié)一致

內(nèi)存釋放時(shí)機(jī)

繪制結(jié)束后renderD128相關(guān)內(nèi)存大小

結(jié)果比較

每次釋放緩存

33M 左右

圖片

與不設(shè)置透明度的對(duì)照組結(jié)果接近

renderD128內(nèi)存>100M

86M 左右

圖片

100M以下,符合預(yù)期

renderD128內(nèi)存>300M

295M左右

圖片

跟實(shí)驗(yàn)組一致,因?yàn)椴](méi)有超過(guò)300M的閾值。符合預(yù)期

buffer總數(shù)>5

33M左右

圖片

與不設(shè)置透明度的對(duì)照組結(jié)果接近,繪制結(jié)束時(shí)會(huì)釋放完所有空閑buffer

buffer總數(shù)>10

buffer總數(shù)> 20

295M左右

圖片

跟實(shí)驗(yàn)組一致,因?yàn)椴](méi)有超過(guò)20個(gè)buffer的閾值(10個(gè)view大概會(huì)用到10~15個(gè)buffer)。符合預(yù)期

空閑buffer > 5

138M 左右

圖片

空閑buffer個(gè)數(shù)不太可控,無(wú)法精確控制內(nèi)存水位

空閑buffer > 10

33M 左右

圖片

方案結(jié)論

這個(gè)方案雖然也可緩解問(wèn)題,但是存在以下問(wèn)題:

  1. 性能影響(理論,未測(cè))
  1. 增加了內(nèi)存申請(qǐng)和釋放的概率,會(huì)有一定的性能影響。
  2. 每次進(jìn)行閾值判定,都需要統(tǒng)計(jì)當(dāng)前buffer/內(nèi)存的值,頻繁調(diào)用接口時(shí),也會(huì)影響性能。
  1. 穩(wěn)定性
  2. 硬編碼緩存池相關(guān)的數(shù)據(jù)結(jié)構(gòu),如果有些機(jī)型數(shù)據(jù)結(jié)構(gòu)不一致的話,就可能會(huì)崩潰。

這個(gè)方案應(yīng)該不是最優(yōu)解,先做備用方案,再探索一下。

3.1.2 方案二:上層及時(shí)釋放資源

從前面“相關(guān)內(nèi)存釋放”章節(jié)的分析可知,緩存池的內(nèi)存并不是不會(huì)釋放,而是釋放時(shí)機(jī)很晚,那么能否早點(diǎn)釋放呢?

查看CanvasContext的釋放路徑,僅發(fā)現(xiàn)了一個(gè)可操作點(diǎn)(嘗試了一些方式都會(huì)崩潰,會(huì)釋放掉正在使用的資源),CacheManager::trimStaleResources用的資源),CacheManager::trimStaleResources方法中可以釋放30s內(nèi)未使用的資源,改成釋放1s(或10s)內(nèi)未使用的資源。

圖片

修改指令:MOVW R2, #30000 ==> MOVW R2,#1000

圖片

(相關(guān)代碼如下??)

#define ORIGIN_TIME_LIMIT_INST      0x5230f247 // 30s
#define NEW_TIME_LIMIT_INST      0x32e8f240 // 1s 提前構(gòu)造好的指令編碼
#define FUNC_SYM "_ZN7android10uirenderer12renderthread12CacheManager18trimStaleResourcesEv"


static void change_destroy_wait_time() {
    /** 一些檢查和判空操作會(huì)省略 **/
#ifdef __arm__
    void* handle = dlopen("libhwui.so");
    // 從trimStaleResources方法的起始地址開(kāi)始搜索內(nèi)存
    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) {
        /* 找到并修改對(duì)應(yīng)指令 */
        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
}

方案結(jié)論:該方案還是依賴(lài)于Activity銷(xiāo)毀,只是銷(xiāo)毀后能更快釋放資源,所以緩解內(nèi)存方面起到的作用很有限。

3.2 控制緩存池增長(zhǎng)

在嘗試前面兩個(gè)方案之后,這個(gè)問(wèn)題逐漸讓人崩潰,似乎已經(jīng)沒(méi)有什么好的解決辦法了,準(zhǔn)備就此放棄。

3.2.1 新的突破點(diǎn)

然而就在我們準(zhǔn)備放棄的時(shí)候,后續(xù)的幾次壓測(cè)中我們發(fā)現(xiàn)了一個(gè)新的突破點(diǎn)“每次調(diào)用一次View.setAlpha,renderD128內(nèi)存會(huì)上漲25M,但并不是無(wú)限上漲,上漲到1.3G左右就不再增長(zhǎng)了”,且另外翻看線上相關(guān)OOM問(wèn)題,renderD128內(nèi)存占用也均未超過(guò)1.3G,由此我們大膽猜測(cè)renderD128 內(nèi)存緩存池大小應(yīng)該是有上限的,這個(gè)上限大概在1.3G上下,那么我們或許可以嘗試從調(diào)小緩存池的閾值入手。

圖片

再次嘗試

我們?cè)俅螄L試復(fù)現(xiàn)該問(wèn)題,并hook相關(guān)內(nèi)存分配;從日志可以看到,在內(nèi)存增長(zhǎng)到1.3G后

  1. 下一次調(diào)用KEGLGetPoolBuffers獲取buffer時(shí),返回值是0(代表分配失?。?;
  2. 再下一次調(diào)用KEGLGetPoolBuffers,返回值是1(代表分配成功),但沒(méi)有申請(qǐng)內(nèi)存。圖片

再增加多一點(diǎn)信息,發(fā)現(xiàn)當(dāng)KEGLGetPoolBuffers獲取buffer失敗后,會(huì)有KEGLReleasePoolBuffers調(diào)用,釋放了大量buffer,之后再重新調(diào)用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

從堆??磻?yīng)該是提前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分配

根據(jù)上面的分析,發(fā)現(xiàn)可以嘗試:

  • Hook KEGLGetPoolBuffers函數(shù),判斷內(nèi)存增長(zhǎng)到一定閾值后,在KEGLGetPoolBuffers函數(shù)中就直接返回0,觸發(fā)其內(nèi)部的空閑buffer釋放
  • 空閑buffer釋放之后,才允許分配buffer(如下流程)
    圖片


結(jié)論:該方案需要每次分配內(nèi)存前讀取maps獲取renderD128占用內(nèi)存大小,對(duì)性能不是很友好。

3.2.3 方案四:修改緩存池閾值

從上面的分析,我們知道KEGLGetPoolBuffers函數(shù)返回0時(shí)分配失敗,會(huì)開(kāi)始釋放buffer。我們繼續(xù)反匯編KEGLGetPoolBuffers函數(shù),根據(jù)KEGLGetPoolBuffers的返回值為0 可以回溯到匯編中進(jìn)行閾值判斷的邏輯,如下圖所示。

圖片

v8:buffers_list;

v7:buffer類(lèi)型(0~4);

v8+4*v7+24:v7這個(gè)buffer類(lèi)型 的buffer數(shù)量(下圖中的buffer_size[i]);

v49:buffer_info;

v49+28: buffer_limit 緩存池中每種類(lèi)型的buffer 的閾值(下圖中的buffer_limits);

簡(jiǎn)單來(lái)說(shuō),這里將buffer_limits與buffer_size[i]進(jìn)行比較,如果buffer_size[i]大于等于閾值,就會(huì)返回0,分配失敗

圖片

接下來(lái)的操作就很簡(jiǎn)單了,只需對(duì)buffer_limits進(jìn)行修改就行,在測(cè)試設(shè)備上buffer_limits值是50(50*25M 大約是1.25G),我們將buffer_limits值是50(50*25M 大約是1.25G),我們將buffer_limits改小一點(diǎn)就可以將renderD128內(nèi)存緩存池控制在一個(gè)更小的閾值范圍內(nèi),以此降低renderD128內(nèi)存占用。

(相關(guān)代碼如下??)

int opt_mtk_buffer(int api_level, int new_buffer_size) {
    ...(無(wú)關(guān)代碼省略)
    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);
    ...(無(wú)關(guān)代碼省略)


    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驗(yàn)證:

緩存值閾值

內(nèi)存峰值

50


1.3G

圖片

20

530M

圖片


10

269M

圖片

方案結(jié)論:該方案修改少,性能影響小,且穩(wěn)定性可控。

3.3 最終方案

通過(guò)的上面的分析,由于方案四“修改緩存池閾值”修改少,性能影響小,且穩(wěn)定性可控,最終我們決定選用該方案。

4. 修復(fù)效果

開(kāi)啟修復(fù)實(shí)驗(yàn)后相關(guān)機(jī)型OOM崩潰率顯著下降近-50%,觀察數(shù)周之后各項(xiàng)業(yè)務(wù)指標(biāo)也均為正向,符合預(yù)期。全量上線后大盤(pán)renderD128內(nèi)存相關(guān)OOM也大幅下降,renderD128內(nèi)存引發(fā)的發(fā)版熔斷問(wèn)題也被徹底根治。

5. 總結(jié) 

在分析內(nèi)存問(wèn)題時(shí),不論是系統(tǒng)申請(qǐng)的內(nèi)存還是業(yè)務(wù)申請(qǐng)的內(nèi)存,都需要明確申請(qǐng)邏輯和釋放邏輯,才能確定是否發(fā)生泄漏還是長(zhǎng)期不釋放,再?gòu)膬?nèi)存申請(qǐng)和釋放邏輯中尋找可優(yōu)化點(diǎn)。

責(zé)任編輯:龐桂玉 來(lái)源: 字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)
相關(guān)推薦

2023-03-28 08:28:34

2022-04-28 15:07:41

抖音內(nèi)存泄漏Android

2022-07-20 22:55:39

直播OOM抖動(dòng)

2022-06-23 11:19:14

抖音春節(jié)發(fā)券

2021-06-28 05:19:32

抖音電腦

2022-06-06 12:19:08

抖音功耗優(yōu)化Android 應(yīng)用

2024-10-31 08:22:56

2019-03-07 15:04:37

抖音快手同城

2022-01-22 07:44:12

抖音PC 版電腦刷抖音

2019-06-21 09:55:10

刷抖美腿App

2020-10-27 09:33:39

抖音印度移動(dòng)應(yīng)用

2020-08-06 10:09:08

抖音木馬安全隱私

2021-01-19 16:24:08

抖音支付寶微信

2024-11-13 08:47:24

2024-06-13 17:10:16

2022-08-26 16:24:19

抖音體系化建設(shè)項(xiàng)目

2022-02-09 14:13:18

Android 12冷啟動(dòng)UC內(nèi)核

2019-06-05 09:14:28

LinuxIO監(jiān)控分析

2020-10-12 19:06:06

微信直播快手

2025-01-09 08:22:05

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)