资源泄漏采集开发实践

0 评论 128 浏览 0 收藏 27 分钟

 资源调优和资源泄漏是应用开发中常见且难以快速解决的两类棘手问题。在鸿蒙应用开发过程中,这些问题通常难以利用常规调试手段(如hilog流水日志、/proc/[pid]/smaps、/proc/meminfo、/proc/memview、hidumper --mem [pid])直接定位和处理。

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

资源泄漏采集开发实践-华为开发者话题 | 华为开发者联盟

 

概述

       资源调优和资源泄漏是应用开发中常见且难以快速解决的两类棘手问题。在鸿蒙应用开发过程中,这些问题通常难以利用常规调试手段(如hilog流水日志、/proc/[pid]/smaps、/proc/meminfo、/proc/memview、hidumper –mem [pid])直接定位和处理。

资源类型 描述
文件描述符 通过open/fopen、epoll、eventfd、socket、pipe、dup等创建的fd。
线程 通过线程函数创建的线程(如pthread_create)。
Native 内存(包含堆内存和映射区内存) 通过malloc/calloc/realloc/new分配的堆内存(NativeHeap); 通过mmap映射的映射区内存(包含so等文件映射以及匿名映射)
GPU内存 通过图形标准API(如opengles、vlukan、opencl)创建的纹理(texture)、缓冲区(buffer)等内存。
全局句柄 (Global Handle) 在Native侧通过napi_create_reference创建的ArkTS对象全局引用(napi_ref)。

       为快速解决上述两类痛点问题,鸿蒙系统通过Performance Analysis Kit开放的HiDebug资源采集接口(OH_HiDebug_StartProfiler/OH_HiDebug_StopProfiler),提供线上资源分配栈采集功能,赋能开发者高效实现问题的自诊断与自闭环。

       该资源采集接口性能功耗开销较大,盲目或高频调用易触发应用卡顿、发热。集成时,请务必谨慎评估开销,建立严格的条件触发策略和配置合理的采集参数,兼顾线上功能正常运行和性能开销的平衡。

       本文将介绍以下内容:

          • HiDebug资源采集简介

          • 资源泄漏检测流程

          • 场景案例

实现原理

HiDebug资源采集简介

       为帮助开发人员快速定位进程内的资源泄漏问题,HarmonyOS提供了HiDebug资源采集能力(HiDebug API参考 ),开发者可调用OH_HiDebug_StartProfiler / OH_HiDebug_StopProfiler接口,针对指定资源类型启动采集,在采集窗口内记录资源分配的调用栈与统计信息,并将结果输出为.htrace文件。

       开发者可将生成的.htrace文件导入DevEco Studio Profiler进行关联分析,按调用栈分组统计,频率最高的调用栈即为泄漏嫌疑点,从而加速定位泄漏代码位置,降低定界定位成本。

       OH_HiDebug_StartProfiler支持以下资源类型:

类型枚举 说明
OH_RES_TYPE_FD 0 文件描述符
OH_RES_TYPE_THREAD 1 线程
OH_RES_TYPE_NATIVE 2 Native内存(包含堆内存和映射区内存)
OH_RES_TYPE_GPU 3 GPU内存
OH_RES_TYPE_GLOBAL_HANDLE 4 全局句柄(Native持有的ArkTS对象引用)

       资源泄漏通常是因为资源被分配后未在生命周期结束时正确释放,常见原因包括:

   • FD泄漏:open/fopen、epoll、eventfd、socket、pipe、dup等创建的fd打开后,未调用close。

   • 线程泄漏:pthread / std::thread创建后未正确join / detach,或线程函数未正常退出。

   • Native内存泄漏:通过malloc/calloc/realloc/new分配的堆内存(NativeHeap)未free / delete; 通过mmap映射的映射区内存(包含so等文件映射以及匿名映射)未munmap。

   • GPU内存泄漏:纹理、缓冲区等GPU资源创建后未释放。

   • 全局句柄泄漏:Native层对ArkTS对象创建了持久化强引用(如napi_create_reference)后未napi_delete_reference。

生成文件类型介绍

文件类型 介绍
.htrace 记录采集时间窗口内资源分配的调用栈与统计信息,包含分配频次、栈深度、归属线程等。
可导入DevEco Studio Profiler,按调用栈分组统计,定位泄漏嫌疑点。

错误码

       OH_HiDebug_StartProfiler与OH_HiDebug_StopProfiler返回HiDebug_ErrorCode,取值如下(HIDEBUG_RES_PROF_SUCCESS与HIDEBUG_RES_PROF_FAILURE为两接口共有,HIDEBUG_RES_PROF_NOT_STARTED仅StopProfiler返回,其余仅StartProfiler返回):

错误码 含义 处理建议
HIDEBUG_RES_PROF_SUCCESS 操作成功(启动/停止)  
HIDEBUG_RES_PROF_INVALID_ARG 资源采集参数无效 检查callback与config是否合法
HIDEBUG_RES_PROF_INVALID_MAX_DURATION 资源采集最大持续时间参数无效 检查maxDuration取值
HIDEBUG_RES_PROF_INVALID_FILTER_SIZE 资源采集过滤大小参数无效 检查filterSize取值
HIDEBUG_RES_PROF_INVALID_MAX_STACK_DEPTH 资源采集最大回栈深度参数无效。 检查maxStackDepth取值。
HIDEBUG_RES_PROF_INVALID_STATISTICS_INTERVAL 资源采集统计间隔参数无效。 检查statisticsInterval取值。
HIDEBUG_RES_PROF_INVALID_SAMPLE_INTERVAL 资源采集采样大小参数无效。 检查sampleInterval取值。
HIDEBUG_RES_PROF_INVALID_RESOURCE_TYPE 资源采集类型参数无效。 检查type取值(0/1/2/3/4)。
HIDEBUG_RES_PROF_PERMISSION_DENIED 权限不足,仅支持采集调用接口进程本身。 仅采集本进程。
HIDEBUG_RES_PROF_ALREADY_STARTED 资源采集重复启动。 先Stop再Start,确保配对调用。
HIDEBUG_RES_PROF_PROCESS_OVERLIMIT 资源采集进程数超出限制(单应用内2个进程)。 减少同时采集的进程数。
HIDEBUG_RES_PROF_CONFLICT 与开发者工具或系统采集任务冲突。 先退出DevEco Studio Profiler / hiprofiler_cmd。
HIDEBUG_RES_PROF_DAILY_QUOTA_EXCEEDED 每日配额超出限制(整机4次 / 单应用2次)。 减少采集次数,或延长maxDuration。
HIDEBUG_RES_PROF_CPU_OVERLOADED 系统CPU占用率超过70%。 等待CPU负载降低后重试。
HIDEBUG_RES_PROF_MEM_PRESSURE_CRITICAL 内存可用空间低于2GB。 释放内存后重试。
HIDEBUG_RES_PROF_STORAGE_PRESSURE_CRITICAL 存储可用空间低于15GB与整机总量3%中的较大值。 清理存储空间后重试。
HIDEBUG_RES_PROF_NOT_STARTED 资源采集未启动,停止失败 先调用Start
HIDEBUG_RES_PROF_FAILURE 启动/停止资源采集失败 排查系统状态后重试

资源泄漏检测流程

   应用启动后,通过NAPI拉起ResourceProfilerManager单例。

   • 触发采集的三条路径(择一执行,且任一时刻仅允许一路采集):

    (1) C++ 统一轮询:后台线程按各自周期检测FD / Thread / Native / GPU四类资源(Native每200s、其余每60s);Native与GPU需连续2次超阈值、FD与Thread单次超阈值即自动触发对应类型的采集,避免偶发波动误触发。

资源类型

触发采集条件 (推荐值)

检测间隔 (推荐值)

参考建议

文件描述符(FD)

超过5000个

60s

1、开发者可通过读取/proc/self/fd_num节点获取被采集应用的FD总数。
2、1个检测周期内(每60s),若发现应用的FD总数超过阈值(5000个),则认为可能存在FD泄漏,此时可调用资源分配栈采集接口采栈。

线程

超过700个

60s

1、开发者可通过读取/proc/self/status节点的Threads字段获取被采集应用的线程总数。
2、1个检测周期内(每60s),发现线程总数超过700个,则认为可能存在线程泄漏,此时可调用资源分配栈采集接口采栈。

Native内存(包含堆内存和映射区内存)

超过3GB

200s

1、开发者可通过读取/proc/self/status节点,将VmRss+VmSwap字段之和,作为被采集应用占用的Native内存总量。
注:应用Native内存超4GB,且整机内存压力过大时可能触发系统管控,建议超3GB或更小时就启动采集。
2、连续2个检测周期(每200s一个周期),Native内存均超过阈值(3GB),则认为可能存在Native内存泄漏,此时可调用资源分配栈采集接口采栈。

GPU内存

超过2.3GB

60s

1、开发者可通过调用OH_HiDebug_GetGraphicsMemory获取被采集应用的GPU内存总量。
2、连续2个检测周期(每60s一个周期),GPU内存均超过阈值,则认为可能存在GPU内存泄漏,此时可调用资源分配栈采集接口采栈。

全局句柄(Global Handle)

VM堆内存使用率超过70%

60s

1、开发者可通过hidebug.getAppVMObjectUsedSize() / hidebug.getAppVMMemoryInfo().totalHeap > 0.7作为触发采集条件。
2、1个检测周期内(每60s),VM堆内存使用率超过70%,则认为可能存在Global Handle泄漏,此时可调用资源分配栈采集接口采栈。

       说明: 以上Native内存和GPU内存触发采集条件推荐值均基于12GB RAM设备,其他RAM大小设备采集阈值请开发者自行根据实际情况调整。
(2) ArkTS定时检测:ArkTS侧通过setInterval周期检测VM堆内存使用率,超过阈值即触发Global Handle采集。
(3) 手动按需采集:在可疑业务函数执行前后调用OH_HiDebug_StartProfiler/ OH_HiDebug_StopProfiler,采集该时间窗口内的资源分配数据。

      • OH_HiDebug_StartProfiler接口调用后,采集器会记录资源分配数据。

      • 采集结束(调用OH_HiDebug_StopProfiler或maxDuration到期自动停止)后,系统生成.htrace文件并通过回调返回其沙箱路径。

      • 将.htrace文件导入DevEco Studio Profiler,按调用栈分组分析,定位泄漏点。

场景案例

场景描述

       开发人员观测到应用进程的Native内存持续增长、或/proc/self/fd条目数异常增多、或/proc/self/status中Threads值持续上升,需要定位是哪段代码在泄漏资源、并分析其调用栈。

开发步骤

       1. 添加依赖

       Native侧在CMakeLists.txt中链接HiDebug NDK:

target_link_libraries(entry PUBLIC libace_napi.z.so ohhidebug.so)

       ArkTS侧引入PerformanceAnalysisKit与Native模块:

import { hilog, hidebug } from '@kit.PerformanceAnalysisKit';
import resourceProfiling from 'libentry.so';

       2. 接入资源采集管理器

       ResourceProfilerManager是对OH_HiDebug_StartProfiler / OH_HiDebug_StopProfiler的单例封装,统一管理采集状态与回调,对外暴露采集接口与统一轮询接口。其公开接口签名如下:

class ResourceProfilerManager {
public:
    static ResourceProfilerManager &GetInstance()
    {
        static ResourceProfilerManager instance;
        return instance;
    }

    // Basic profiling interface with custom config
    HiDebug_ErrorCode StartProfiling(
        OH_HiDebug_ResourceType type,
        OH_HiDebug_ResProfilerConfig& config,
        std::function<void(OH_HiDebug_ProfilingResult*)> userCallback);

    // Basic profiling interface with default config
    HiDebug_ErrorCode StartProfiling(
        OH_HiDebug_ResourceType type,
        std::function<void(OH_HiDebug_ProfilingResult*)> userCallback);

    HiDebug_ErrorCode StopProfiling();

    bool IsProfiling() const;

    // Unified polling detection interface
    // Polls multiple resource types; triggers profiling for the matching type
    // when any one exceeds its threshold.
    // Skips triggering if a profiling task is already running.
    void StartUnifiedPolling(
        std::function<void(const char* resultPath, OH_HiDebug_ResourceType type)> onResult);

    void StopUnifiedPolling();

    bool IsPolling() const;

private:
    static void InternalProfilingCallback(OH_HiDebug_ProfilingResult* result);
    void PollingLoop(std::function<void(const char* resultPath, OH_HiDebug_ResourceType type)> onResult);
    std::atomic<bool> m_isProfiling{false};
    std::function<void(OH_HiDebug_ProfilingResult*)> m_userCallback;
    std::atomic<bool> m_isPolling{false};
    std::thread m_pollingThread;
};

       OH_HiDebug_ResProfilerConfig各参数含义如下,默认配置已在开发实践示例代码中内置,开发者亦可按场景自定义:

参数 类型 默认值 含义
maxDuration uint32_t 30 采集时长上限(秒),到期自动停止
filterSize uint32_t 256 过滤器大小
maxStackDepth uint32_t 30 最大栈深度
sampleInterval uint32_t 384 采样间隔(字节)
statisticsInterval uint32_t 10 统计间隔(秒)

       完整实现(含轮询主循环、阈值检测、回调处理)参考示例代码

       3. 启动统一轮询检测

       调用startUnifiedPolling即可启动C++ 后台线程,对FD / Thread / Native / GPU四类资源按各自周期轮询(Native每200s、其余每60s);Native与GPU需连续2次超阈值、FD与Thread单次超阈值即自动触发采集,并通过回调返回结果文件路径:

// Start polling with a callback
resourceProfiling.startUnifiedPolling((path: stringtypenumber) => {
  let typeName = 'Unknown';
  switch(type) {
     case 0: typeName = 'FD'break;
     case 1: typeName = 'Thread'break;
     case 2: typeName = 'Native Mem'break;
     case 3: typeName = 'GPU Mem'break;
     case 4: typeName = 'Global Handle'break;
    default: typeName = `Type ${type}`break;
  }

  const msg = `[${typeName}] Profiling Finished!\\nPath: ${path}`;
  hilog.info(0x3300'ResourceProfiling', msg);
});

       统一轮询内置阈值与触发策略:FD数量5000个、线程数700个、Native内存3GB、GPU内存2.3G;Native每200s轮询且需连续2次超阈值、GPU每60s轮询且需连续2次超阈值、FD与Thread每60s轮询且单次超阈值即触发采集。

       4. ArkTS侧VM Heap检测

       Global Handle泄漏由ArkTS侧独立检测。通过hidebug.getAppVMMemoryInfo()获取VM堆信息,当已使用占比超过70%时触发Global Handle采集:

// VM heap memory detection threshold (70%)
const VM_HEAP_THRESHOLD = 0.7;
let vmTimernumber = -1;

function detectVMHeapLeak(onResult: (path: string) => void): void {
  try {
    const vmInfo = hidebug.getAppVMMemoryInfo();
    const heapUsed = hidebug.getAppVMObjectUsedSize();

    if (vmInfo.totalHeap > 0 && (heapUsed / vmInfo.totalHeap) > VM_HEAP_THRESHOLD) {
       // Call NAPI startProfiling with type 4 (OH_RES_TYPE_GLOBAL_HANDLE)
       const retnumber = resourceProfiling.startProfiling(41802563038410);
      if (ret === 0) { // HIDEBUG_RES_PROF_SUCCESS
        // Set timeout to stop profiling after 180s
        setTimeout(() => {
          resourceProfiling.stopProfiling();
        }, 180000);
      }
    }
  } catch (e) {
    hilog.error(0x3300'ResourceProfiling''VM heap detection failed: %{public}s'JSON.stringify(e));
  }
}

// Start periodic VM heap memory detection (60s)
vmTimer = setInterval(() => {
  detectVMHeapLeak((path: string) => {
    const msg = `[Global Handle] VM Heap Exceeded!\\nPath: ${path}`;
    hilog.info(0x3300'ResourceProfiling', msg);
  });
}, 60000);

       5. 手动按需采集

       当能定位到可疑的业务函数时,可在其执行前后手动启停采集,精确覆盖该业务窗口内的资源分配:

// Manually start profiling for FD (type=0)
resourceProfiling.startProfiling(0302563038410);
// ...execute the suspicious business logic (profiler records allocations after Start)
resourceProfiling.stopProfiling();

       6. 文件导出

       采集完成后,回调中的result->filePath即为结果文件沙箱路径,格式如下:

/data/storage/el2/base/files/<类型>-<被采集应用进程名>-<进程号>-<时间戳>.htrace

       示例:/data/storage/el2/base/files/native-com.example.resourceleakprofilingdemo-33367-20260708_113011.htrace

       文件导出:参考应用沙箱路径和真实物理路径的对应关系

       7. 关闭检测

       抓取到所需维测数据后,停止轮询与定时检测:

// Stop polling and VM detection when the page is destroyed
if (vmTimer !== -1) {
  clearInterval(vmTimer);
}
resourceProfiling.stopUnifiedPolling();

       8. 分析生成的文件

       将.htrace文件导入DevEco Studio Profiler后,按调用栈分组统计,出现频次最高的调用栈即为泄漏嫌疑点,据此跳转到对应源码位置完成修复,可参考内存分析介绍。

       分析要点:

   • 以调用栈为维度分组,关注高频出现的栈。

   • 同一栈反复出现,说明对应代码路径存在资源分配未释放。

   • 结合业务逻辑确认该路径是否缺少对应的释放调用。

采集规格约束

       采集配额约束

       采集配额分整机与应用两级管控:

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

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

       超出配额时返回HIDEBUG_RES_PROF_DAILY_QUOTA_EXCEEDED或HIDEBUG_RES_PROF_PROCESS_OVERLIMIT。
说明:多个应用进程并行采集会对整机性能产生较大影响,请开发者根据实际情况谨慎评估后使用。

       采集负载熔断机制

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

   • CPU限制:整机系统CPU占用率超过70%,返回HIDEBUG_RES_PROF_CPU_OVERLOADED。

   • RAM限制:整机剩余可用内存(RAM)低于2GB,返回HIDEBUG_RES_PROF_MEM_PRESSURE_CRITICAL。

    • ROM限制:整机剩余可用存储空间(ROM)低于15GB与整机总量3%中的较大值,返回HIDEBUG_RES_PROF_STORAGE_PRESSURE_CRITICAL。

       采集并发冲突约束

       本API与开发者工具( DevEco Studio Profiler/hiprofiler_cmd)或系统采集任务存在排他性,发生冲突时采集请求将被拒绝并返回HIDEBUG_RES_PROF_CONFLICT,使用前先退出相关工具或任务。

       采集文件老化管控

       每种资源类型的采集文件均受以下滚动老化约束:

   • 按文件个数:只保留最新5份文件,超出后滚动删除最早文件。

   • 按单文件大小:单文件上限1GB。

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

   • 按存储时效:每个采集文件最大存储时长24小时,到期后清理。

       采集性能功耗与触发策略

       受上述采集规格与约束限制,本API不保证采集请求一定成功。调用本API会对当前应用进程及整机性能功耗产生不可忽略的影响,包括但不限于CPU、内存占用升高,丢帧卡顿、发热。集成时必须谨慎评估性能功耗开销,建立严格的条件触发策略(如仅在特定灰度范围内、应用资源超基线阈值时触发,并配置合理的采集参数以兼顾性能功耗平衡)后再决策是否开启。严禁在生产环境下盲目或高频次触发。

       采集接口调用约束

• OH_HiDebug_StartProfiler/OH_HiDebug_StopProfiler配对:未配对调用会导致HIDEBUG_RES_PROF_ALREADY_STARTED,确保一次采集完整覆盖一个业务场景后再启动下一次。

• 采集时间窗口语义:采集器仅记录OH_HiDebug_StartProfiler之后的资源分配数据,OH_HiDebug_StartProfiler前已分配的资源不会被采集,因此接口调用采集建议包裹完整业务时间窗口。

示例代码

• 资源泄漏采集示例

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

题图来自Unsplash,基于CC0协议

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

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