Hats测试用例HatsHdfDisplayBufferUtTest会导致设备panic问题解决
摘要: 在OpenHarmony 5.0.3系统(UIS7885平台)测试中,运行HatsHdfDisplayBufferUtTest时出现严重内存压力问题。测试进程高频申请图形缓冲区(累计2234MB),触发35次低内存回收(LMK),导致系统服务卡顿(最长阻塞38秒)。分析表明问题根源在于高频大块内存申请与回收机制失衡,而非内存泄漏。临时解决方案包括降低测试压力、增加延迟;根治方案通过引入分级
·
文档概述
说明:
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/媒体等业务受影响)。
- 复现步骤:
- 通过 HDC 下发并执行
/data/local/tmp/HatsHdfDisplayBufferUtTest。 - 进入 display buffer 相关用例(高频 alloc/mmap)。
- 触发系统内存压力,LMK 连续介入并出现系统级卡顿。
- 通过 HDC 下发并执行
- 实际结果:用例执行期间系统明显失稳,出现大面积慢调用和进程回收。
- 期望结果:用例可执行完成,不触发系统级内存危机。
三、问题分析
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层的单调未释放泄漏。 - 根因证据链:
pid=11428统计到Mmap353 次,累计映射流量约 2234.06MiB(短时冲击高)。- metadata 不支持错误 5 类各 353 次,表示每轮都走无效 metadata 调用链,放大开销。
- instrumentation 显示
max_sprd_live=1、max_live_map=8,不支持“只增不减”的泄漏结论。 - LMK 触发 35 次,
KillOneBundleByPrio21 次,且出现AllocMem over time超长卡顿,证明系统处于 reclaim/分配失衡状态。
- 是否存在竞态/时序抖动:存在,高频申请与回收并发导致 IPC 和调度抖动。
- 是否与电源路径/触发路径相关:当前证据不支持电源路径主因,集中在 display buffer/内存回收路径。
4. 风险评估
- 对功能的风险:display 相关用例稳定性差,执行结果不可信。
- 对稳定性的风险:高,可诱发系统级雪崩(LMK 连续杀进程、关键服务异常)。
- 对性能的风险:高,出现秒级至几十秒级慢调用。
- 对量产/发布的风险:高,若线上触发同类压力,存在用户感知卡死/重启风险。
四、问题解决方案
1. 临时规避方案
- 方案描述:在测试侧降压与节流,避免单测瞬时申请风暴把系统拖入不可恢复压力区。
- 修改代码路径:
sig/test/xts/hats/hdf/display/buffer/unittest/display_buffer_ut.cppsig/test/xts/hats/hdf/display/buffer/unittest/display_buffer_ut.h
- 修改点(临时止血):
- metadata 能力只探测一次,平台不支持则跳过后续 metadata 重复调用。
- 降低循环压力(示例:
TEST_COUNT下调)。 - 每轮
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*/
- 已修改源码(最新):
sprd_allocator.cpp:新增 alloc/free/live/live_bytes 原子计数与低频日志采样。sprd_allocator.cpp:分阶段阈值(70%/85%)+ 动态 quota(按 usage tier 与 stage 计算)。sprd_allocator.cpp:短退避重试(毫秒级以内),并在退避前unlock、退避后lock,避免阻塞FreeMem。sprd_allocator.cpp:usage 按HBM_USE_*位显式分级(critical/high/normal/low),优先保障HW_COMPOSER/FB/PROTECTED/VIDEO/CAMERA/HW_RENDER/HW_TEXTURE路径。sprd_allocator.cpp:新增 按 usage 分级执行预算:SPRD_HARD_BUDGET_ENFORCE=false,超预算默认仅告警+继续分配(软限流),避免误判导致重启后开机动画失败,同时保留分级与退避机制用于削峰。
- 回归验证项:
HatsHdfDisplayBufferUtTest全量通过,且无 LMK 连发。AllocMem over time不出现秒级慢调用。live/live_map长时间运行无持续增长。- 关键体验不回退:开机动画、桌面点击/滑动、视频播放、拍照/预览无新增异常。
3. 验证结果
- 修复前数据:
pid=11428:mmap=353,累计约2234.06MiB;metadata 不支持各类型353次。LowMemoryKiller::PsiHandlerInner:35次;KillOneBundleByPrio:21次。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,优先保障,风险低。 - 边界说明:极端内存压力下低优先级分配会更早被限流,属于预期的“削峰保护”,目的是避免系统级雪崩与卡死。
六、影响的范围
-
修改前影响范围:
HatsHdfDisplayBufferUtTest在高频 alloc/mmap 场景下可稳定触发系统级高压。- 影响从 display buffer 用例扩散到系统公共能力(LMK 连发、服务慢调用、业务进程被回收)。
- 用户可感知风险包括:开机/桌面卡顿、应用点击与滑动响应变差、视频播放抖动、拍照预览时延上升。
-
修改后影响范围:
- 通过分阶段阈值、动态阈值、短退避重试和 usage 分级,优先保护
HW_COMPOSER/FB/VIDEO/CAMERA/HW_RENDER/HW_TEXTURE关键路径。 - 主要影响被收敛在“低优先级 buffer 申请在极端压力下更早限流”,避免扩散为系统级雪崩。
- 对关键体验目标为“无新增功能回退”:开机动画、桌面点击/滑动、视频播放、拍照/预览保持可用;最终以设备侧同口径回归结果为准。
- 通过分阶段阈值、动态阈值、短退避重试和 usage 分级,优先保护
七、知识分享
- 本次问题的通用经验:判断泄漏不能只看 OOM 现象,必须同时看
alloc/free、mmap/unmap的 live 指标与系统级 LMK 时序。 - 排查方法沉淀:先定位触发进程,再做单进程计数化统计(调用次数、累计字节、慢调用直方图)。
- 可复用脚本/命令:
- 统计 metadata 不支持次数:
grep + wc -l - 统计 mmap 次数和累计 size:
grep + sed + awk - 提取 LMK 级别与慢调用:
grep PsiHandlerInner/AllocMem over time
- 统计 metadata 不支持次数:
- 后续优化建议:
- 将 instrumentation 保留为可开关诊断能力。
- HATS 压测用例引入平台能力探测与速率限制。
- 建立“LMK 次数/慢调用时长”自动门禁阈值,提前阻断回归风险。
更多推荐

所有评论(0)