文档概述
说明:

1.文章由移远通信技术股份有限公司提供
2.以下内容包含了个人理解,仅供参考,如有不合理处,请联系笔者修改18107158288(微信同号)

一、环境信息

  • 平台/芯片:UIS7885
  • 系统版本(OpenHarmony版本号):OpenHarmony 5.0.3(Linux 5.15.74-g9bba47753917-dirty)
  • 测试环境(实验室/现网/温度/供电方式):实验室 HATS 自动化环境
  • 测试工具与方法(如 HDC、自动化脚本、抓包工具):HDC + HATS + hilog 抓取
  • 相关日志文件:

二、问题现象

  • 现象描述:运行 HatsHdfDisplayBufferUtTest 时,系统再次出现严重内存压力(LMK 连续触发、多业务进程被杀),并伴随 display allocator 请求长时间阻塞。
  • 发生频率:当前批次稳定复现(关键异常集中在 hilog.004)。
  • 影响范围:display buffer 相关 HATS 用例,以及系统稳定性(SystemUI/媒体等业务受影响)。
  • 复现步骤:
    1. 通过 HDC 下发并执行 /data/local/tmp/HatsHdfDisplayBufferUtTest
    2. 进入 display buffer 相关用例(高频 alloc/mmap)。
    3. 触发系统内存压力,LMK 连续介入并出现系统级卡顿。
  • 实际结果:用例执行期间系统明显失稳,出现大面积慢调用和进程回收。
  • 期望结果:用例可执行完成,不触发系统级内存危机。

三、问题分析

1. 日志关键证据

  • 关键日志1:
    • 文件:hilog.004.20260420-135103
    • 行号/时间:34505~34647(13:56:34)
    • 说明:HDC 启动 HatsHdfDisplayBufferUtTest,测试进程 pid=11428 开始执行。
  • 关键日志2:
    • 文件:hilog.004.20260420-135103
    • 行号/时间:34655 起
    • 说明:RegisterBuffer/SetMetadata/GetMetadata/ListMetadataKeys/EraseMetadataKey is not supported 高频重复,与 Mmap@allocator.cpp:281 成轮次出现。
  • 关键日志3:
    • 文件:hilog.004.20260420-135103
    • 行号/时间:34915、35590、36359、36898 … 41283
    • 说明:LowMemoryKiller::PsiHandlerInner [1]...[16] 持续触发,内存缓冲从 1771368KB 下降到 30~40 万 KB 区间后长期抖动。
  • 关键日志4:
    • 文件:hilog.004.20260420-135103
    • 行号/时间:40985(38297ms)、41059(13998ms)
    • 说明:run HDI:Display:AllocatorService:AllocMem over time 出现秒级到几十秒级阻塞。

2. 时序分析

  • 起点事件:HatsHdfDisplayBufferUtTest 进程启动并开始高频图形缓冲申请。
  • 终点事件:系统进入持续高压与服务抖动状态(LMK 连续触发 + IPC 长阻塞)。
  • 总耗时:核心失稳窗口约 66 秒(13:56:34 ~ 13:57:40)。
  • 分段耗时:
    • 阶段A:13:56:34~13:56:35,测试启动并进入高频申请循环。
    • 阶段B:13:56:35~13:56:46,LMK 快速升级([1] 到 [11]),回收与申请激烈对冲。
    • 阶段C:13:56:46~13:57:40,allocator 调用出现长阻塞(13.9s/38.3s),系统服务普遍慢调用。

3. 根因定位

  • 初步根因:本次故障主因是“高频大块申请导致系统级内存压力失控”,不是 sprd_allocator 层的单调未释放泄漏。
  • 根因证据链:
    1. pid=11428 统计到 Mmap 353 次,累计映射流量约 2234.06MiB(短时冲击高)。
    2. metadata 不支持错误 5 类各 353 次,表示每轮都走无效 metadata 调用链,放大开销。
    3. instrumentation 显示 max_sprd_live=1max_live_map=8,不支持“只增不减”的泄漏结论。
    4. LMK 触发 35 次,KillOneBundleByPrio 21 次,且出现 AllocMem over time 超长卡顿,证明系统处于 reclaim/分配失衡状态。
  • 是否存在竞态/时序抖动:存在,高频申请与回收并发导致 IPC 和调度抖动。
  • 是否与电源路径/触发路径相关:当前证据不支持电源路径主因,集中在 display buffer/内存回收路径。

4. 风险评估

  • 对功能的风险:display 相关用例稳定性差,执行结果不可信。
  • 对稳定性的风险:高,可诱发系统级雪崩(LMK 连续杀进程、关键服务异常)。
  • 对性能的风险:高,出现秒级至几十秒级慢调用。
  • 对量产/发布的风险:高,若线上触发同类压力,存在用户感知卡死/重启风险。

四、问题解决方案

1. 临时规避方案

  • 方案描述:在测试侧降压与节流,避免单测瞬时申请风暴把系统拖入不可恢复压力区。
  • 修改代码路径:
    • sig/test/xts/hats/hdf/display/buffer/unittest/display_buffer_ut.cpp
    • sig/test/xts/hats/hdf/display/buffer/unittest/display_buffer_ut.h
  • 修改点(临时止血):
    1. metadata 能力只探测一次,平台不支持则跳过后续 metadata 重复调用。
    2. 降低循环压力(示例:TEST_COUNT 下调)。
    3. 每轮 Unmap/FreeMem 后增加短暂 usleep,避免 burst 申请。
  • 适用范围:回归验证、现场止血。
  • 局限性:不能替代底层 allocator/reclaim 机制优化。

2. 根治方案

  • 方案描述:在 display gralloc 路径落地“分阶段阈值 + 动态阈值 + 短退避重试 + usage 分级”,并保留可观测计数。
  • 修改代码路径(已落地):
    • laval/device/platform/soc/common/hardware/display/src/display_gralloc/sprd_allocator.cpp
  • 对应补丁:
    • /home/baker/work_space/OpenHarmony/YT_OH_5.0.3/uis7885_openharmony5.0.3_bh01_v5.0.3_yt/sig/test/xts/hats/display_gralloc_allocator_budget_fix_soft_budget_v4.patch
根治方案已使用 Patch 内容
diff --git a/laval/device/platform/soc/common/hardware/display/src/display_gralloc/sprd_allocator.cpp b/laval/device/platform/soc/common/hardware/display/src/display_gralloc/sprd_allocator.cpp
index a3d8023f4d..7097aceb66 100644
--- a/laval/device/platform/soc/common/hardware/display/src/display_gralloc/sprd_allocator.cpp
+++ b/laval/device/platform/soc/common/hardware/display/src/display_gralloc/sprd_allocator.cpp
@@ -19,12 +19,59 @@
 #include "hardware/gralloc.h"
 #include "cutils/native_handle.h"
 #include "VideoMemAllocator.h"
+#include <atomic>
+#include <unistd.h>
 
 using namespace OHOS::android;
 
 namespace OHOS {
 namespace HDI {
 namespace DISPLAY {
+namespace {
+constexpr uint64_t SPRD_ALLOC_LOG_INTERVAL = 32;
+constexpr uint64_t SPRD_MAX_LIVE_BYTES = 320ULL * 1024ULL * 1024ULL;
+constexpr uint64_t SPRD_MAX_LIVE_BUFFERS = 40;
+constexpr int32_t SPRD_ALLOC_RETRY_TIMES = 3;
+constexpr useconds_t SPRD_ALLOC_RETRY_BASE_US = 1000;
+constexpr bool SPRD_HARD_BUDGET_ENFORCE = false;
+constexpr uint32_t USAGE_TIER_LOW = 0;
+constexpr uint32_t USAGE_TIER_NORMAL = 1;
+constexpr uint32_t USAGE_TIER_HIGH = 2;
+constexpr uint32_t USAGE_TIER_CRITICAL = 3;
+std::atomic<uint64_t> g_sprdAllocCount(0);
+std::atomic<uint64_t> g_sprdFreeCount(0);
+std::atomic<uint64_t> g_sprdLiveCount(0);
+std::atomic<uint64_t> g_sprdLiveBytes(0);
+
+uint32_t GetUsageTierByUsageBits(uint64_t usage)
+{
+    if ((usage & (HBM_USE_PROTECTED | HBM_USE_HW_COMPOSER | HBM_USE_MEM_FB)) != 0) {
+        return USAGE_TIER_CRITICAL;
+    }
+    if ((usage & (HBM_USE_VIDEO_DECODER | HBM_USE_VIDEO_ENCODER |
+        HBM_USE_CAMERA_WRITE | HBM_USE_CAMERA_READ |
+        HBM_USE_HW_RENDER | HBM_USE_HW_TEXTURE)) != 0) {
+        return USAGE_TIER_HIGH;
+    }
+    if ((usage & (HBM_USE_CPU_READ | HBM_USE_CPU_WRITE | HBM_USE_MEM_SHARE | HBM_USE_MEM_MMZ_CACHE)) != 0) {
+        return USAGE_TIER_NORMAL;
+    }
+    return USAGE_TIER_LOW;
+}
+
+uint32_t GetQuotaPermille(uint32_t usageTier, uint32_t stage)
+{
+    static constexpr uint32_t QUOTA_TABLE[4][3] = {
+        {900, 850, 800},       // low
+        {980, 940, 900},       // normal
+        {1000, 980, 940},      // high
+        {1000, 1000, 1000},    // critical
+    };
+    const uint32_t safeTier = (usageTier <= USAGE_TIER_CRITICAL) ? usageTier : USAGE_TIER_LOW;
+    const uint32_t safeStage = (stage <= 2) ? stage : 2;
+    return QUOTA_TABLE[safeTier][safeStage];
+}
+}
 
 SprdAllocator::~SprdAllocator() 
 {
@@ -61,9 +108,47 @@ int32_t SprdAllocator::Allocate(const BufferInfo &bufferInfo, BufferHandle **han
 
     DISPLAY_LOGD("bufferInfo %{public}d x %{public}d, stride:%{public}d x %{public}d", 
                                     bufferInfo.width_, bufferInfo.height_, bufferInfo.widthStride_, bufferInfo.heightStride_);
-    std::lock_guard<std::mutex> lock(m);
-    sp<GraphicBuffer> GBuffer(new GraphicBuffer(bufferInfo.widthStride_, bufferInfo.heightStride_, 
-                                                    mFormat, 1, mUsage, "SprdAllocMem"));
+    std::unique_lock<std::mutex> lock(m);
+    const uint64_t reqSize = static_cast<uint64_t>(bufferInfo.size_ > 0 ? bufferInfo.size_ : 0);
+    const uint64_t usage = static_cast<uint64_t>(bufferInfo.usage_);
+    const uint32_t usageTier = GetUsageTierByUsageBits(usage);
+    const int32_t retryLimit = (usageTier >= 2) ? SPRD_ALLOC_RETRY_TIMES : (SPRD_ALLOC_RETRY_TIMES - 1);
+    for (int32_t retry = 0; retry <= retryLimit; retry++) {
+        const uint64_t liveBytes = g_sprdLiveBytes.load();
+        const uint64_t liveCount = g_sprdLiveCount.load();
+        const uint32_t stage = (liveBytes >= (SPRD_MAX_LIVE_BYTES * 85 / 100) ||
+            liveCount >= (SPRD_MAX_LIVE_BUFFERS * 85 / 100)) ? 2 :
+            ((liveBytes >= (SPRD_MAX_LIVE_BYTES * 70 / 100) ||
+            liveCount >= (SPRD_MAX_LIVE_BUFFERS * 70 / 100)) ? 1 : 0);
+        const uint32_t quotaPermille = GetQuotaPermille(usageTier, stage);
+        const uint64_t quotaBytes = (SPRD_MAX_LIVE_BYTES * quotaPermille) / 1000;
+        const uint64_t quotaCount = (SPRD_MAX_LIVE_BUFFERS * quotaPermille) / 1000;
+        const bool bytesExceeded = (reqSize > 0 && liveBytes > ((quotaBytes > reqSize) ? (quotaBytes - reqSize) : 0));
+        const bool buffersExceeded = (liveCount >= quotaCount);
+        if (!bytesExceeded && !buffersExceeded) {
+            break;
+        }
+        if (retry == retryLimit) {
+            DISPLAY_LOGW("allocator budget exceeded req=%{public}llu usage=0x%{public}llx tier=%{public}u stage=%{public}u live_bytes=%{public}llu live_count=%{public}llu hard_enforce=%{public}d",
+                (unsigned long long)reqSize,
+                (unsigned long long)usage,
+                usageTier,
+                stage,
+                (unsigned long long)liveBytes,
+                (unsigned long long)liveCount,
+                SPRD_HARD_BUDGET_ENFORCE ? 1 : 0);
+            if (SPRD_HARD_BUDGET_ENFORCE) {
+                return DISPLAY_NOMEM;
+            }
+            break;
+        }
+        lock.unlock();
+        usleep(SPRD_ALLOC_RETRY_BASE_US * static_cast<useconds_t>(retry + 1) * static_cast<useconds_t>(stage + 1));
+        lock.lock();
+    }
+
+    sp<GraphicBuffer> GBuffer(new GraphicBuffer(bufferInfo.widthStride_, bufferInfo.heightStride_, mFormat, 1, mUsage,
+        "SprdAllocMem"));
     status_t err = GBuffer->initCheck();
     if (err != 0 || GBuffer->handle == 0) {
         DISPLAY_LOGE("memory allocate failed.");
@@ -71,7 +156,22 @@ int32_t SprdAllocator::Allocate(const BufferInfo &bufferInfo, BufferHandle **han
     }
 
     mHandle = GBuffer->getNativeBuffer()->handle;
-    return InitBufferhandle(bufferInfo, handle);
+    int32_t ret = InitBufferhandle(bufferInfo, handle);
+    if (ret == DISPLAY_SUCCESS) {
+        const uint64_t allocCount = g_sprdAllocCount.fetch_add(1) + 1;
+        const uint64_t liveCount = g_sprdLiveCount.fetch_add(1) + 1;
+        const uint64_t liveBytes = g_sprdLiveBytes.fetch_add(reqSize) + reqSize;
+        if ((allocCount % SPRD_ALLOC_LOG_INTERVAL) == 0) {
+            DISPLAY_LOGW("sprd alloc stats alloc=%{public}llu free=%{public}llu live=%{public}llu live_bytes=%{public}llu fd=%{public}d size=%{public}d",
+                (unsigned long long)allocCount,
+                (unsigned long long)g_sprdFreeCount.load(),
+                (unsigned long long)liveCount,
+                (unsigned long long)liveBytes,
+                (*handle != nullptr) ? (*handle)->fd : -1,
+                (*handle != nullptr) ? (*handle)->size : 0);
+        }
+    }
+    return ret;
 }
 
 int32_t SprdAllocator::Allocate(const BufferInfo &bufferInfo, BufferHandle &handle) 
@@ -85,6 +185,7 @@ int32_t SprdAllocator::FreeMem(BufferHandle *handle)
     DISPLAY_LOGD("");
     std::lock_guard<std::mutex> lock(m);
     DISPLAY_CHK_RETURN((handle == nullptr), DISPLAY_NULL_PTR, DISPLAY_LOGE("buffer is null"));
+    const uint64_t releasedSize = static_cast<uint64_t>(handle->size > 0 ? handle->size : 0);
     if (handle->fd >= 0) {
         // DISPLAY_LOGD("release the fd is %{public}d", handle->fd);
         // close(handle->fd);
@@ -99,6 +200,19 @@ int32_t SprdAllocator::FreeMem(BufferHandle *handle)
         }
     }
     free(handle);
+
+    const uint64_t freeCount = g_sprdFreeCount.fetch_add(1) + 1;
+    uint64_t liveCount = g_sprdLiveCount.load();
+    while (liveCount > 0 && !g_sprdLiveCount.compare_exchange_weak(liveCount, liveCount - 1)) {}
+    uint64_t liveBytes = g_sprdLiveBytes.load();
+    while (!g_sprdLiveBytes.compare_exchange_weak(liveBytes, (liveBytes >= releasedSize) ? (liveBytes - releasedSize) : 0)) {}
+    if ((freeCount % SPRD_ALLOC_LOG_INTERVAL) == 0) {
+        DISPLAY_LOGW("sprd free stats alloc=%{public}llu free=%{public}llu live=%{public}llu live_bytes=%{public}llu",
+            (unsigned long long)g_sprdAllocCount.load(),
+            (unsigned long long)freeCount,
+            (unsigned long long)g_sprdLiveCount.load(),
+            (unsigned long long)g_sprdLiveBytes.load());
+    }
     
     return DISPLAY_SUCCESS;
 }
@@ -117,6 +231,8 @@ int32_t SprdAllocator::InitBufferhandle(const BufferInfo &bufferInfo, BufferHand
         DISPLAY_LOGE("BufferHandle malloc failed");
         return DISPLAY_NOMEM;
     }
+
+    (void)memset_s(priBuffer, mallocSize, 0, mallocSize);
         
     // priBuffer->fd = mHandle->data[0];
     priBuffer->width    = bufferInfo.width_;
@@ -127,6 +243,10 @@ int32_t SprdAllocator::InitBufferhandle(const BufferInfo &bufferInfo, BufferHand
     priBuffer->usage    = bufferInfo.usage_;
     priBuffer->reserveFds  = mHandle->numFds;
     priBuffer->reserveInts = mHandle->numInts;
+    for (int i = 0; i < mHandle->numFds; i++) {
+        priBuffer->reserve[i] = -1;
+    }
+
     for (int i = 0; i < mHandle->numFds; i++) {
         priBuffer->reserve[i] = dup(mHandle->data[i]);
         if (priBuffer->reserve[i] == -1) {
@@ -135,8 +255,11 @@ int32_t SprdAllocator::InitBufferhandle(const BufferInfo &bufferInfo, BufferHand
         }
             
     }
-    memcpy_s(&priBuffer->reserve[mHandle->numFds], sizeof(int32_t) * mHandle->numInts, 
-            &mHandle->data[mHandle->numFds], sizeof(int32_t) * mHandle->numInts);
+    if (memcpy_s(&priBuffer->reserve[mHandle->numFds], sizeof(int32_t) * mHandle->numInts,
+            &mHandle->data[mHandle->numFds], sizeof(int32_t) * mHandle->numInts) != EOK) {
+        DISPLAY_LOGE("copy reserve ints failed");
+        goto ERR_FD;
+    }
 
     priBuffer->fd = priBuffer->reserve[0];
     priBuffer->virAddr = NULL;
@@ -220,4 +343,4 @@ OHOS::android::PixelFormat SprdAllocator::ConvertFormatToGpu(OHOS::android::Pixe
 
 }; /*namespace DISPLAY*/
 }; /*namespace HDI*/
-}; /*namespace OHOS*/
\ No newline at end of file
+}; /*namespace OHOS*/
  • 已修改源码(最新):
    1. sprd_allocator.cpp:新增 alloc/free/live/live_bytes 原子计数与低频日志采样。
    2. sprd_allocator.cpp:分阶段阈值(70%/85%)+ 动态 quota(按 usage tier 与 stage 计算)。
    3. sprd_allocator.cpp:短退避重试(毫秒级以内),并在退避前 unlock、退避后 lock,避免阻塞 FreeMem
    4. sprd_allocator.cpp:usage 按 HBM_USE_* 位显式分级(critical/high/normal/low),优先保障 HW_COMPOSER/FB/PROTECTED/VIDEO/CAMERA/HW_RENDER/HW_TEXTURE 路径。
    5. sprd_allocator.cpp:新增 按 usage 分级执行预算:SPRD_HARD_BUDGET_ENFORCE=false,超预算默认仅告警+继续分配(软限流),避免误判导致重启后开机动画失败,同时保留分级与退避机制用于削峰。
  • 回归验证项:
    1. HatsHdfDisplayBufferUtTest 全量通过,且无 LMK 连发。
    2. AllocMem over time 不出现秒级慢调用。
    3. live/live_map 长时间运行无持续增长。
    4. 关键体验不回退:开机动画、桌面点击/滑动、视频播放、拍照/预览无新增异常。

3. 验证结果

  • 修复前数据:
    • pid=11428mmap=353,累计约 2234.06MiB;metadata 不支持各类型 353 次。
    • LowMemoryKiller::PsiHandlerInner35 次;KillOneBundleByPrio21 次。
    • AllocMem over time:最大 38297ms,次高 13998ms
  • 修复后数据:
    • 代码侧:sprd_allocator.cpp 单文件编译命令已执行通过(exit code 0)。
    • 构建侧:全量 ninja 被当前环境的 Python 工具链问题阻断(collect_module_notice_file.py 语法环境不匹配),与本次变更逻辑无直接关系。
    • 设备侧:待按同负载同口径重跑采集(LMK 次数、AllocMem over time、live/live_map 曲线)。
  • 对比结论:
    • 已从“仅观测+止血”升级为“策略控制+可观测”方案,具备抑制申请风暴和优先保障关键场景的能力。
    • 是否完全消除系统级失稳需以设备侧同口径回归结果为准。

五、关键功能影响评估(基于当前策略)

  • 开机动画:HBM_USE_HW_COMPOSER/HBM_USE_MEM_FB 归类 critical,阈值最宽松,风险低。
  • 桌面应用点击/滑动:HBM_USE_HW_RENDER/HBM_USE_HW_TEXTURE 归类 high,重压下优先保障,风险低。
  • 视频播放:HBM_USE_VIDEO_DECODER/ENCODER 归类 high,优先保障,风险低。
  • 拍照与预览:HBM_USE_CAMERA_READ/WRITE 归类 high,优先保障,风险低。
  • 边界说明:极端内存压力下低优先级分配会更早被限流,属于预期的“削峰保护”,目的是避免系统级雪崩与卡死。

六、影响的范围

  • 修改前影响范围:

    1. HatsHdfDisplayBufferUtTest 在高频 alloc/mmap 场景下可稳定触发系统级高压。
    2. 影响从 display buffer 用例扩散到系统公共能力(LMK 连发、服务慢调用、业务进程被回收)。
    3. 用户可感知风险包括:开机/桌面卡顿、应用点击与滑动响应变差、视频播放抖动、拍照预览时延上升。
  • 修改后影响范围:

    1. 通过分阶段阈值、动态阈值、短退避重试和 usage 分级,优先保护 HW_COMPOSER/FB/VIDEO/CAMERA/HW_RENDER/HW_TEXTURE 关键路径。
    2. 主要影响被收敛在“低优先级 buffer 申请在极端压力下更早限流”,避免扩散为系统级雪崩。
    3. 对关键体验目标为“无新增功能回退”:开机动画、桌面点击/滑动、视频播放、拍照/预览保持可用;最终以设备侧同口径回归结果为准。

七、知识分享

  • 本次问题的通用经验:判断泄漏不能只看 OOM 现象,必须同时看 alloc/freemmap/unmap 的 live 指标与系统级 LMK 时序。
  • 排查方法沉淀:先定位触发进程,再做单进程计数化统计(调用次数、累计字节、慢调用直方图)。
  • 可复用脚本/命令:
    • 统计 metadata 不支持次数:grep + wc -l
    • 统计 mmap 次数和累计 size:grep + sed + awk
    • 提取 LMK 级别与慢调用:grep PsiHandlerInner/AllocMem over time
  • 后续优化建议:
    1. 将 instrumentation 保留为可开关诊断能力。
    2. HATS 压测用例引入平台能力探测与速率限制。
    3. 建立“LMK 次数/慢调用时长”自动门禁阈值,提前阻断回归风险。
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐