AppFreeze依靠3s和6s堆栈“热点函数”定位主线程繁忙型卡死

0 评论 772 浏览 0 收藏 11 分钟

本系列实战第七篇,我们将分析一个更具有挑战性的场景:3s/6s栈不一致且没有增强采样日志,如何定位Appfreeze问题。

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

AppFreeze依靠3s和6s堆栈“热点函数”定位主线程繁忙型卡死-华为开发者话题 | 华为开发者联盟

 

本系列实战第七篇,我们将分析一个更具有挑战性的场景:3s/6s栈不一致且没有增强采样日志,如何定位Appfreeze问题。

在之前几期我们分析了UI渲染过载、计算耗时等繁忙场景,而今天这个案例中,主线程没有阻塞在锁上,也没有卡在复杂的Diff计算中,而是由于循环处理多个不同类型的耗时子任务导致的繁忙问题。且该案例系统中没有增强采样栈日志,传统的找热点栈方法失效,我们需要换一种思路:通过对比3s/6s的堆栈差异,提取共同点,从而锁定问题根因。

问题现象

       用户触发某项业务操作后,应用界面出现明显卡死,随后进入无响应状态,最终被系统强制退出。开发者拿到 AppFreeze 日志,发现主线程堆栈在 3s 6s 两个时间点不一致。

问题分析:从不一致中找线索

第一步:常规快照排查(初步诊断)

       查看 AppFreeze 日志中的 THREAD_BLOCK_3S  THREAD_BLOCK_6S 主线程堆栈快照。

       THREAD_BLOCK_3S 部分(第3秒快照):

Tid:22780Name:pfreezeanalysis
state=R, utime=1239, stime=92, priority=-54, nice=-10, clk=100
#00 pc 0000000000240030 /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::JSDate::Now()+52)
#01 pc 0000000000e8b488 /system/lib64/module/arkcompiler/stub.an(RTStub_PushCallArgsAndDispatchNative+40)
#02 pc 000000000047dd94 /system/lib64/module/arkcompiler/stub.an(BCStub_HandleCallthis0Imm8V8StwCopy+396)
#03 at delay entry (entry/src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ets:997:14)
#04 at businessOperationA3 entry (entry/src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ets:989:3)
#05 at businessOperationA entry (entry/src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ets:951:9)
#06 at triggerComplexOperations entry (entry/src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ets:912:9)

       THREAD_BLOCK_6S 部分(第6秒快照):

Tid:22780Name:pfreezeanalysis
state=R, utime=1529, stime=92, priority=-54, nice=-10, clk=100
#00 pc 0000000000497270 /system/lib64/module/arkcompiler/stub.an(BCStub_HandleLdobjbynameImm8Id16StwCopy+48)
#01 at delay entry (entry/src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ets:996:15)
#02 at businessOperationB2 entry (entry/src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ets:984:3)
#03 at businessOperationB entry (entry/src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ets:948:9)
#04 at triggerComplexOperations entry (entry/src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ets:912:9)
特征 行为分析 结论
状态均为 R (Running) 主线程没有等待锁,也没有阻塞I/O,一直在执行指令。 非阻塞型卡顿
3s栈顶是 JSDate::Now 3秒时刻,主线程正在执行 businessOperationA 内部的某个耗时操作(如时间计算或复杂逻辑)。 路径 A
6s栈顶是 HandleLdobjbyname 6秒时刻,主线程正在执行 businessOperationB 内部的某个耗时操作(如对象查找或数据加载)。 路径 B
底层调用者均为 triggerComplexOperations 尽管中间执行的业务逻辑不同(A vs B),但它们的调用源头完全一致。 共同热点

       关键洞察:

1. 如果主线程是阻塞的(比如死锁),堆栈通常会完全一致,卡在同一个函数上。

2. 堆栈不一致 + 状态为R,说明主线程在快速切换执行不同的耗时任务。虽然每次执行的子任务不同,但它们都汇聚到了同一个父函数中。

3. 因此,triggerComplexOperations 就是我们要找的热点函数,即使没有增强日志,通过提取两次堆栈的交集,也能精准定位。

锁定根因(代码审查)

       定位到热点函数 triggerComplexOperations 后,查看源码:

function triggerComplexOperations() {
  // 循环处理多个不同类型的耗时子任务
  for (let loopIndex = 0; loopIndex < OPERATION_LOOP_COUNT; loopIndex++) {
    let randomValue = Math.random();
    switch (Math.floor(randomValue * RANDOM_MULTIPLIER % RANDOM_MODULO)) {
      case 0:
        businessOperationA(); // 耗时操作 1:可能在3s快照中
        break;
      case 1:
        businessOperationB(); // 耗时操作 2:可能在6s快照中
        break;
      case 2:
        businessOperationC(); // 耗时操作 3
        break;
    }
  }
}

       问题根源:

1. 主线程在执行一个 for 循环,每次随机选择并执行一个耗时子任务。

2. 循环阻塞:由于 OPERATION_LOOP_COUNT 较大,且每个子任务耗时较长,主线程长时间无法回归空闲状态。

3. 随机性干扰:由于是随机执行不同任务,导致不同时间点采样到的堆栈路径不同,容易误导开发者认为没有固定热点

4. 缺乏异步:所有操作均在主线程同步执行,未利用子线程或异步机制。

预防建议:执行策略异步化,保障主线程响应性

       正确做法:
将耗时操作移至子线程,或使用异步任务处理。

// 示例:使用 async/await 或 Worker 将耗时任务移出主线程
async function triggerComplexOperationsAsync() {
  for (let loopIndex = 0; loopIndex < OPERATION_LOOP_COUNT; loopIndex++) {
    // 模拟异步执行,避免阻塞主线程
    // 注意:如果是纯计算密集型,建议使用 Worker;如果是I/O密集型,使用 async/await
    await executeHeavyTask(); 
  }
}

总结与启示

       当遇到冻屏问题,请记住这个“三步走策略:

1. 对比快照:查看 3s 6s(甚至更长时间点)的堆栈。

2. 判断状态

   • 若堆栈一致且状态为 W (Waiting) D (Deadlock):通常是阻塞型卡顿,直接找栈顶函数。

   • 若堆栈不一致且状态为 R (Running):通常是繁忙型卡顿,主线程在持续计算。

3. 提取交集:在繁忙型卡顿中,寻找多次堆栈中共同出现的上层业务函数。这就是导致主线程繁忙的总开关

       开发者贴士: 不要只盯着栈顶看!当栈顶跳动不停时,往上看几层,找到那个始终存在的函数,它就是你要优化的目标。

—————————————————————————————————–

🔗 官网开发者学堂视频: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. 目前还没评论,等你发挥!