资源泄漏别躲了,HiDebug 来抓现行

0 评论 121 浏览 0 收藏 20 分钟

应用一边跑,一边偷偷涨资源?
FD、线程、Native 内存、GPU 内存、Global Handle,到底是谁在“悄悄变胖”?
这一次,不靠玄学猜测,也不只盯着日志发呆。HarmonyOS 6.1 带来的 HiDebug 资源采集 API,可以让调用栈自己站出来说话。

本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:

资源泄漏别躲了,HiDebug 来抓现行-华为开发者话题 | 华为开发者联盟

 

应用一边跑,一边偷偷涨资源?

FD、线程、Native 内存、GPU 内存、Global Handle,到底是谁在“悄悄变胖”?

这一次,不靠玄学猜测,也不只盯着日志发呆。HarmonyOS 6.1 带来的 HiDebug 资源采集 API,可以让调用栈自己站出来说话。

适合场景:HarmonyOS 应用稳定性建设、线上自诊断、DFX 调优、资源泄漏排查。

先说痛点:线上资源问题,真的很会藏 🙈

       资源调优和资源泄漏,是鸿蒙应用开发中常见又棘手的两类问题。

       很多时候,开发者能看到“现象”:

   • 应用越跑越卡。

   • 内存曲线一路抬头。

   • 线程数不知不觉变多。

   • FD 数量悄悄逼近上限。

   • GPU 内存或 VM 堆使用率看起来不太对劲。

       但要回答“是谁分配的”“从哪条调用链来的”“为什么没释放”,光靠流水日志、/proc/[pid]/smaps/proc/meminfo/proc/memviewhidumper –mem 往往还差一点火候。

       HiDebug 资源采集 API 就是来补这一刀的。

       通过 Performance Analysis Kit 开放的 OH_HiDebug_StartProfiler / OH_HiDebug_StopProfiler,应用可以在合适条件下采集资源分配栈,把“感觉有泄漏”变成“这里有证据”。

它能盯住哪些资源?👀

资源类型 说明
文件描述符 通过 open/fopen、epoll、eventfd、socket、pipe、dup 等创建的 FD。
线程 通过线程函数创建的线程,例如 pthread_create。
Native 内存 通过 malloc/calloc/realloc/new 分配的堆内存,以及通过 mmap 映射的文件映射、匿名映射等内存。
GPU 内存 通过 OpenGLES、Vulkan、OpenCL 等图形标准 API 创建的纹理、缓冲区等内存。
全局句柄 Native 侧通过 napi_create_reference 创建的 ArkTS 对象全局引用,即 napi_ref。

       简单说:FD 泄漏、线程泄漏、Native 内存涨、GPU 内存涨、Global Handle 忘记释放,都可以纳入观察范围。

核心接口:启动、停止,一套闭环 ⚙️

       HarmonyOS 6.1 中,资源采集接口 API 24.0 的核心原型如下。

       一个负责选择资源类型并启动采集,一个负责结束采集,结果通过回调返回。

// 资源类型
typedef enum OH_HiDebug_ResourceType {
  OH_RES_TYPE_FD,
  OH_RES_TYPE_THREAD,
  OH_RES_TYPE_NATIVE,
  OH_RES_TYPE_GPU,
  OH_RES_TYPE_GLOBAL_HANDLE
} OH_HiDebug_ResourceType;

// 采集参数
typedef struct OH_HiDebug_ResProfilerConfig {
  uint32_t maxDuration;
  uint32_t filterSize;
  uint32_t maxStackDepth;
  uint32_t statisticsInterval;
  uint32_t sampleInterval;
} OH_HiDebug_ResProfilerConfig;

// 采集结果
typedef struct OH_HiDebug_ProfilingResult {
  OH_HiDebug_ResourceType resourceType;
  const char* filePath;
} OH_HiDebug_ProfilingResult;

// 采集结果回调函数
typedef void (*OH_HiDebug_ProfilingCallback)(OH_HiDebug_ProfilingResult* result);

// 启动采集
HiDebug_ErrorCode OH_HiDebug_StartProfiler(
  OH_HiDebug_ResourceType type,
  OH_HiDebug_ResProfilerConfig* config,
  OH_HiDebug_ProfilingCallback callback);

// 停止采集
HiDebug_ErrorCode OH_HiDebug_StopProfiler(void);

type:先决定要抓谁 🎯

       type 参数用于选择采集资源类型,支持以下五类:

   • OH_RES_TYPE_FD:文件描述符。

   • OH_RES_TYPE_THREAD:线程。

   • OH_RES_TYPE_NATIVE:Native 内存。

   • OH_RES_TYPE_GPU:GPU 内存。

   • OH_RES_TYPE_GLOBAL_HANDLE:全局句柄。

       你怀疑谁不老实,就先把观察镜头对准谁。

config:让采集既有用,也别太打扰用户 🧭

       config 采集参数支持五种配置项。它们的核心价值,是在“采得够准”和“别把应用拖慢”之间做平衡。

参数 描述 建议
maxDuration 最大采集时间,单位为秒,最大支持 1 小时。 根据实际开销调整。
filterSize 过滤小于等于 filter_size 的内存分配栈,减少采集数据量。 数据太多时可适当增大。
maxStackDepth 设置最大资源分配调用栈回栈深度。 建议默认值 30。
statisticsInterval 统计间隔,将一个统计周期内的栈进行汇总,单位为秒。 建议默认值 10s。
sampleInterval 采样大小,单位为字节,采样率为 1/采样大小。 建议默认值 384 bytes;Native 内存分配小于 384 字节按 1/384 采样,大于等于 384 字节全采。

小提醒 💡
采集参数可根据实际采集开销情况调整,目标是达成性能损耗与应用体验之间的自适应平衡。默认值只是参考,不是放之四海皆准的魔法数字。

callback:采完以后去哪儿看 📦

       采集完成后,结果会通过 callback 返回。

回调参数 描述
resourceType 采集资源类型。
filePath 资源分配栈 .htrace 文件的沙箱路径。

       开发者可以拿到 .htrace 文件路径后,将文件上传到云侧做泄漏根因分析、聚类,或者导入 IDE 查看 Call Trees。

       这就很关键了:资源问题终于可以从“猜一猜”进入“查一查”阶段。

实现逻辑:本质上就是把分配路径照亮 💡

       资源分配栈采集的核心逻辑,是对不同资源的分配和释放函数进行跟踪,然后记录调用栈、去重、符号化,并最终以 protobuf 格式的 .htrace 文件落盘至应用沙箱。

       不同资源的采集逻辑大致如下:

   • FD:hook 文件描述符打开和关闭函数,并记录调用栈。

   • 线程:hook 线程创建和销毁函数,并记录调用栈。

   • NativeHeap:hook musl 库 malloc/calloc/realloc/freemmap/munmap 函数,并记录调用栈。

   • GPU 内存:hook OpenGLES、Vulkan、OpenCL 的 texture、buffer 创建和释放函数,并记录调用栈。

   • Global Handle:hook global handle 对象的创建和释放函数,并记录调用栈。

       换句话说,它不是只告诉你“涨了”,而是尽力告诉你“从哪儿涨起来的”。

什么时候触发?别手滑,要有策略 🚦

       线上采集最讲究“该出手时再出手”。

       下面是推荐触发条件,可作为接入策略参考:

资源类型 推荐触发条件 检测间隔 参考建议
文件描述符 FD 超过 5000 个 60s 可读取 /proc/self/fd_num 获取 FD 总数。1 个检测周期内超过阈值,可认为可能存在 FD 泄漏。
线程 超过 700 个 60s 可读取 /proc/self/status 的 Threads 字段。线程总数超过 700 个时,可触发采栈。
Native 内存 超过 3 GB 200s 可读取 /proc/self/status,将 VmRss + VmSwap 作为 Native 内存总量。连续 2 个检测周期均超过 3GB,可认为可能存在泄漏。
GPU 内存 超过 2300 MB 60s 可调用 OH_HiDebug_GetGraphicsMemory 获取 GPU 内存总量。以 12G 手机为例,连续 2 个周期均超过 2.3GB 可触发。
Global Handle VM 堆内存使用率超过 70% 60s 可用 hidebug.getAppVMObjectUsedSize() / hidebug.getAppVMMemoryInfo().totalHeap > 0.7 作为触发条件。

注意 ⚠️
应用 Native 内存超过 4GB,且整机内存压力过大时可能触发系统管控。实践中建议超过 3GB 或更小时就启动采集,别等问题冲到脸上才开始找伞。

规格约束:强能力,也有边界感 🧱

       HiDebug 资源采集能力很实用,但它不是无限量供应的“随便采套餐”。

采集配额限制

• 整机所有应用共享:每日最多可采集 4 次;同一时刻最高支持 4 个不同应用并行采集。

• 单应用独享:每日最多可采集 2 次;同一时刻最高支持应用内 2 个进程并行采集。

系统负载熔断机制

       当系统触发以下任一负载保护条件时,API 采集请求可能被拒绝,并返回相应错误码:

   • 整机系统 CPU 占用率超过 70%。

   • 整机剩余可用内存 RAM 或剩余可用存储空间 ROM 低于影响应用体验和整机读写性能的阈值。

       负载保护条件可能随系统版本迭代优化,具体以实际返回错误码为准。

并发冲突约束

       本 API 与命令行工具或系统采集任务存在排他性。

       当与命令行工具或系统采集任务发生冲突时,API 采集请求将被拒绝并返回相应错误码。

采集结果交付

• 存储路径:采集到的调用栈日志文件自动保存在应用沙箱目录 /data/storage/el2/base/files/

• 文件名规则:资源采集类型-进程名-进程号-时间戳.htrace

老化管控

       每种资源类型均受以下条件约束:

   • 按文件个数管控:只保存最新 5 份文件,滚动老化。

   • 按文件大小管控:单文件上限 1G,达到上限即停止,避免文件过大导致磁盘压力或 IDE 无法加载。

   • 按目录空间管控:沙箱下采集文件存储空间超过 4G 时,删除最早的采集文件。

   • 按存储时效管控:单个日志最大存储 24 小时,到期后清理。

系统采集模式优先级

• 命令行模式 hdc shell hiprofiler_cmd 优先,可抢占 API 采集、系统采集、应用灰度采集。

• API 采集与系统采集、应用灰度采集并发时,按 FIFO 原则处理,先发起的模式优先响应,其它模式拒绝访问。

再敲一下重点 🛎️
本 API 不保证每次采集请求都成功。调用它会对当前应用进程及整机性能功耗产生不可忽略的影响,包括但不限于 CPU、内存占用升高、丢帧卡顿、发热。线上环境严禁盲目或高频次触发。

推荐接入姿势:稳一点,更香 ✅

       想把 HiDebug 资源采集能力接进线上诊断链路,建议遵循这几个原则:

   1. 只在灰度范围内开启,避免影响全部用户。

   2. 只在资源超过业务基线或推荐阈值时触发。

   3. 采集前检查系统负载,避免在高压状态下继续加压。

   4. 合理配置 maxDurationmaxStackDepthstatisticsIntervalsampleInterval

   5. 采集完成后及时上传、分析、清理,避免日志文件堆积。

   6. 将 .htrace 文件与业务场景、版本、设备信息关联,方便后续聚类分析。

       这才是线上诊断的正确打开方式:精准出手,拿到证据,快速闭环。

Global Handle 泄漏:几个高频现场 🧯

       Global Handle 泄漏是 Native 侧很容易踩的坑。下面三个例子很典型,适合作为排查时的“对照组”。

场景一:全局引用忘记 delete

#include <napi.h>

// 全局引用,泄漏高发点
static napi_ref g_my_ref = nullptr;

napi_value LeakRef(napi_env env, napi_callback_info info)
{
  napi_value obj;
  napi_get_cb_info(env, info, nullptr, nullptr, &obj, nullptr);

  // 创建强引用,初始计数为 1,但只创建不释放
  napi_create_reference(env, obj, 1, &g_my_ref);
  return nullptr;
}

// 应补充清理函数:
// void Cleanup(napi_env env)
// {
//   if (g_my_ref) {
//     napi_delete_reference(env, g_my_ref);
//     g_my_ref = nullptr;
//   }
// }

       这个场景很常见:创建引用时很顺手,释放时忘得很自然。
但全局引用不会自己消失,时间一长就会变成稳定性隐患。

场景二:循环或重复创建不释放

napi_value CreateAndLeak(napi_env env, napi_callback_info info)
{
  napi_value obj;
  napi_get_cb_info(env, info, nullptr, nullptr, &obj, nullptr);

  napi_ref ref;
  // 每次调用都新建引用,但没有 delete
  napi_create_reference(env, obj, 1, &ref);

  // 如果覆盖旧 ref,旧句柄也会永久丢失
  // g_ref = ref;
  return nullptr;
}

       一次泄漏看起来不显眼,循环调用就很热闹。
这类问题最适合用资源分配栈采集来确认调用来源。

场景三:类或实例持有引用,析构不清理

class NativeHolder {
public:
  napi_ref m_ref;

  NativeHolder(napi_env env, napi_value obj)
  {
    napi_create_reference(env, obj, 1, &m_ref);
  }

  // 析构时缺少 napi_delete_reference(env, m_ref)
  ~NativeHolder()
  {
  }
};

napi_value CreateHolder(napi_env env, napi_callback_info info)
{
  napi_value obj;
  napi_get_cb_info(env, info, nullptr, nullptr, &obj, nullptr);
  NativeHolder* holder = new NativeHolder(env, obj);
  // 若不主动清理:holder 泄漏 + m_ref 泄漏
  return nullptr;
}

       类实例和引用绑定在一起时,生命周期管理尤其要小心。
对象销毁、引用释放、异常路径回收,都要配套考虑。

一句话总结 ✨

       如果你的 HarmonyOS 应用正在做稳定性建设、性能优化或线上自诊断,HiDebug 资源采集 API 值得加入工具箱。

       它不能替你自动修 Bug,但能把“谁分配、谁没放、从哪条调用链来”照得更清楚。

       资源泄漏再会藏,也怕调用栈开灯。把 HiDebug 接入诊断链路,让线上问题少一点玄学,多一点证据。

参考链接 🔗

• 官方文档:OH_HiDebug_StartProfiler

• 原文:资源采集API特性指导

———————————————————————————————————-

🔗 官网开发者学堂视频:https://developer.huawei.com/consumer/cn/training/result?type2List=201783644516849879&orderBy=1&courseType=5

🔗 社区DFX专题文章:  https://developer.huawei.com/consumer/cn/forum/subject/2101218731402391001

【扫码加入 HarmonyOS DFX 技术交流群】

本文由 @华为开发者联盟 授权发布于人人都是产品经理。未经作者许可,禁止转载

题图来自Unsplash,基于CC0协议

该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务

更多精彩内容,请关注人人都是产品经理微信公众号或下载App
评论
评论请登录
  1. 目前还没评论,等你发挥!