Files
harmony-crash-analysis-report/HarmonyOS 启动性能监控接入方案_20260805.md
T
clz ec8cf06c66 docs: 补充策略门控机制源码级验证结论(§5.2)
- 新增 5.2 策略门控机制:注入链(后台 JSON → parseMonitorStrategy → EventStrategyBean → configMap → DataMonitorModule)、两级门控(插件级可绕过,指标级不可绕过——插件强制开启仍必须后台下发策略)、isOpen 语义(status==1 且抽样通过)、聚合上传触发(aggre_count/aggre_time)、重启生效机制(幂等标记无条件置 true,策略下发后需重启)
- 基于 @kernel/app_monitor_event 与 app_monitor_lib 源码核实(~/Work/apm)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 14:16:31 +08:00

25 KiB
Raw Blame History

HarmonyOS 启动性能监控接入方案

1. 目标

建立冷启动耗时的线上采集、聚合和看板能力,用于发现版本或分支引入的启动性能劣化。统计口径以“用户点击图标”至“首页首帧完成”为准,单位为毫秒。

2. 总体链路

APP_LAUNCH 系统事件提供启动类型和系统耗时;首个有效入口页完成首次绘制后,通过 reportDrawnCompleted 标记终点。当前客户端双通道上报:ElkService 异步上传结构化追溯明细(11 字段),HXEventMonitor 分桶指标记录原始耗时由 SDK 按桶聚合(详见 6.2),后台再按模块配置阈值、聚合和看板。若后续具备 HarmonyOS 自定义 APM SDK,可替换上传层,但保留事件采集和去重逻辑。

3. 客户端接入

实现位于 future_entry_demo/src/main/ets/monitor/LaunchMonitor.etsEntryAbility.onCreate 必须尽早调用 LaunchMonitor.init(),先注册 watcher,再允许入口页提交首帧完成事件。init() 本身应保持幂等,避免同一进程重复注册 watcher。

监听 APP_LAUNCH 后读取:

  • start_type:启动类型;仅 0 纳入冷启动统计。
  • extend_time:从系统记录的 icon_input_time 到应用报告绘制完成的耗时。
  • timeicon_input_timeprocess_name:标识启动事件并用于去重。
  • animation_finish_timebundle_version:用于分析和问题追溯。

3.1 首帧单次提交

LauncherPageNetworkAnomalyPageIndex 都应尝试监听根节点首次 draw,以最先有效绘制的页面作为 entry_page。必须遵循以下约束:

  1. 在调用 reportDrawnCompleted 之前设置进程级已提交状态;异步回调尚未执行时,其他页面也不得再次提交。
  2. observer 触发后立即注销,页面 aboutToDisappear 仅释放与自身 pageName 匹配的 observer,避免旧页面销毁新页面的监听。
  3. 仅同步调用异常表示任务未提交,此时恢复未提交状态,允许后续入口页重试;页面销毁不得重置已经提交的状态。

3.2 事件过滤与去重

仅处理 start_type === 0extend_time > 0 的记录。上传前按以下键去重:

time + icon_input_time + start_type + process_name

客户端保留最近 20 个事件键,兼容同一回调组内重复记录和系统重新投递,同时避免缓存无限增长。

4. 指标定义

项目 建议值
模块 launch
指标名 cold_start_time
指标值 冷启动耗时,单位 ms
维度 start_typeentry_pageapp_versionapp_install_status
追溯字段 response_latencyanimation_finish_timestartability_processstart_durappattach_to_appforeground_durgit_commitis_backfill
分桶 [500, 600, 700, 800, 1000, 1500, 2000, 3000]

上传负载为最小必要集:指标 1、维度 4(含 app_install_status)、启动生命周期各阶段时间 4(劣化分段定位)、git_commit(代码级追溯)、is_backfill(补报标识);去重为客户端行为(第 3.2 节),负载不含去重键。其余候选字段按需加回,无需改动采集逻辑。

模块名小写+连字符,指标名不含连字符;维度避免高基数(用户 ID、完整 URL 等)。

分桶指标由 HXEventMonitor 实现(8 桶、4 维度,与追溯一致),record(extendTime) 由 SDK 归桶聚合;桶边界按实测主体(600-800ms)细分、尾部留告警锚点,分桶变更需后台重建并档(历史分布不可比)。维度 app_install_status 取值 normal/new_install/over_written(安装状态:常规/全新安装/覆盖安装),与 StartManager 安装状态对齐。

4.1 启动时间拆解

cold_start_time 之外,追溯通道已随上传携带各分段耗时(见上表),可拆分冷启动定位劣化归属:

离手 ──response_latency──► 动效开始 ──(animation_finish_time  response_latency)──► 动效完成 ──(extend_time  animation_finish_time)──► 首帧绘制完成
        系统响应段                    系统动效段                                                         应用可优化段
  • response_latency:离手到动效开始的耗时(需 API 22+),反映系统响应快慢。
  • startability_processstart_dur:系统启动 Ability 到进程创建完成(仅冷启动),反映系统侧进程启动。
  • appattach_to_appforeground_dur:进程初始化完成到应用切前台(仅冷启动),反映系统侧应用挂载。
  • extend_time animation_finish_time:动效完成到应用首帧的耗时,即应用侧可优化的独占部分;看板可据此判断劣化发生在系统侧还是应用侧。注意 animation_finish_time 在部分机型为 0(系统未填充),此时该段等于 extend_time 全量,聚合时需兼容。

未上传的候选字段(按需启用):环境 build_mode/target_name 区分 debug/forTest/Official 环境,避免测试包污染线上看板;device_type/system_version 排除设备差异;版本 bundle_version/bundle_name;去重键 time/icon_input_time/process_name(去重为客户端行为,见第 3.2 节)。git_commit 已上传(代码级追溯)。

5. 后台与看板配置

5.1 模块策略下发(链路 B 激活前提)

链路 B 是策略驱动的:SDK 侧 HXEventMonitorPlugin.parseMonitorStrategy() 读取后台策略注入 EventStrategyBean,若 launch 模块无开启策略,create()/record() 一律空转并打印 Monitor.Event cold_start_time eventId has not created!,应用侧无法绕开。因此必须在 APM 后台为 module=launch 下发如下策略:

{
  "module": "launch",
  "config": {
    "status": 1,
    "sampling_rate": 1.0,
    "interval": 5000,
    "aggre_time": 60000,
    "aggre_count": 10
  }
}

字段说明(对齐 EventStrategyBean.fromJson,仅这些字段生效):

字段 取值 说明
module launch 必须与代码 HXEventMonitor.getEventFactory('launch') 一致
status 1 必须 1STATUS_OPEN;否则 isOpen() 为 falsecreate/record 全空转
sampling_rate 0~1 抽样率,isOpen()status==1 且抽样通过才生效
interval ms 采集间隔
aggre_time ms 聚合时间阈值,达到即触发一次 push
aggre_count 聚合数量阈值,达到即触发一次 push

客户端 record 后由 SDK 本地聚合,达到聚合周期或条数任一触发条件后自动上报至 APM 平台(发送通道为 HXMonitor 注入的 IElkService,即 APMElkService 所走的 ElkService 通道)。

判定生效:后台下发后重启应用,冷启动日志中应消失 Monitor.Event cold_start_time eventId has not created!、出现 Cold start bucket metric recorded: <ms> 与 SDK 聚合 push 日志,看板出现 launch 模块 cold_start_time 桶分布。

5.2 策略门控机制(源码级验证,2026-08-07,@kernel/app_monitor_event/app_monitor_lib 源码)

策略门控发生在 SDK 指标层,插件无法绕过:

  • 注入链:后台策略 JSON → HXEventMonitorPlugin.parseMonitorStrategy()EventStrategyBean.fromJson()EventMonitorStrategy.addStrategy(module, bean)(全局 configMap)→ DataMonitorModule 构造时 getStrategy('launch') 取策略。
  • 两级门控:插件级(AbstractHXBasePlugin.isSupport(),默认"有策略且开启",HXLaunchMonitorPlugin override 为 true 强制通过)仅控制插件生命周期;指标级DataMonitorModule.create/getEventMonitor 检查 _strategy?.isOpen())独立生效——launch 无策略 bean 时 _strategy=nullcreate/getEventMonitor 返回 NullDataMonitorImprecord 空转打印 eventId has not created!插件强制开启绕不过指标级门控,必须后台下发策略。
  • isOpen() 语义status == 1STATUS_OPEN 且抽样通过(isOpenByRandom,对应 sampling_rate)。
  • 聚合上传触发onRecord → isNeedUpload——累计条数 ≥ aggregatorSizeThresholdaggre_count)或距上次上传 ≥ aggregatorTimeThresholdaggre_time)即触发 push
  • 重启生效机制LaunchMonitor.ensureColdStartMetric() 的幂等标记在 create 被策略拒绝(返回空实现)时仍置 true——本进程内策略下发后不会重试创建,必须重启应用(与上节"判定生效"一致);策略未下发时链路 A(Elk 追溯)不受影响、正常上传。

随后为该模块补充 ELK 索引模板,在 Grafana 建立看板面板(建议形态):

{
  "panels": [
    {
      "title": "冷启动 P50/P95",
      "type": "percentile",
      "metric": "cold_start_time",
      "group_by": ["app_version", "start_type"],
      "window": "1d"
    },
    {
      "title": "超 2 秒占比",
      "type": "ratio",
      "condition": "cold_start_time >= 2000",
      "group_by": ["app_version"],
      "window": "1d"
    },
    {
      "title": "分桶分布",
      "type": "bucket_distribution",
      "metric": "cold_start_time",
      "buckets": [500, 600, 700, 800, 1000, 1500, 2000, 3000],
      "group_by": ["entry_page", "app_install_status"],
      "window": "1d"
    }
  ]
}

看板按版本、启动类型和时间聚合;app_install_status 维度(new_install/over_written)用于排除安装后首启的偏高数据。

6. 兼容与兜底

链路 AElkService 追溯)上传 cold_start_time、4 维度和阶段时间字段,不具备分桶聚合语义,由链路 B(HXEventMonitor 分桶指标,策略驱动见 5.1)承担看板聚合;后台需补充 ELK 索引模板、聚合规则和看板。链路 B 的 DataMonitorBuilder/record API 形态以 @kernel/app_monitor_event.d.ts 为准(已随 HXLaunchMonitorPlugin 编译验证),不直接复用 Android 文档中的 Java API。

APP_LAUNCH 的事件解析必须与崩溃事件分开处理:崩溃事件依赖异常和日志字段,启动事件不包含这些字段。上传应异步执行,不阻塞首页渲染;以 time + icon_input_time + start_type + process_name 去重,兼容系统重新投递。

6.1 启动 5 秒内退出的边界行为

系统 extend_time 的定义:手指离手到 reportDrawnCompleted 的耗时,若 5 秒内未调用则该值为 0。真机实测(nova 14 Pro)启动后立即退出的行为链:

  • 退出时首帧已提交reportDrawnCompleted 已调用,真实 app 首帧通常在 1 秒内):事件正常生成、extend_time 有效;若进程在事件送达(约 5 秒延迟)前退出,事件被系统持久化,下次启动 watcher 收到并补报,数据有效。
  • 退出时首帧未提交(启动极慢或立即强杀):reportDrawnCompleted 从未调用,5 秒窗口过期,系统未生成有效事件(实测无补投递),不产生上报。

补报事件的 entry_page 语义:capturedEntryPage 是进程级变量,补报事件到达新进程时该值为新进程的快照(或默认值),不能归因到新启动页面。因此客户端以 watcher 注册时间戳为锚点判定补报事件:icon_input_time 早于注册时间超过 2 秒容差(正常启动链间隔为毫秒~秒级,补报至少早 5 秒)即视为补报,entry_page'unknown' 上报,避免错误归因。同时上报字段新增 is_backfill 标识(true 为补报事件),看板可据此过滤补报数据或单独统计:补报的耗时数据有效可纳入 P50/P95 统计,但 entry_page 分布分析应排除。

真机实测确认(2026-08-05):部署新版本后先 force-stop 再冷启动,新进程收到旧进程的遗留事件,判定 补报事件=是icon_input_time 早于注册时间约 4 秒);随后正常冷启动收到自身事件,判定 补报事件=否。双向判定均正确,正常启动无误判。

6.2 双通道上报

启动指标同时走两条通道,职责分离:

  • 分桶指标(HXEventMonitormodule=launchrecord(extendTime) 后由 SDK 按桶聚合,负责看板聚合统计(P50/P95、超 2 秒占比、分桶分布)。策略驱动:无后台下发策略时 create()/record() 空转(见 5.1),record 失败/空转不影响启动流程,聚合逻辑在 SDK 侧。实现为 HXLaunchMonitorPlugin(仿 HXCrashMonitorPluginstartPlugin() 中建指标),在 APMHelper.plugin(...) 列表注册。
  • 追溯明细(ElkServicebiz=launch11 字段最小集(指标/4 维度含 app_install_status/启动生命周期各阶段时间/git_commit/is_backfill),负责明细追溯与后端校验;其他字段按需加回。分桶指标同步 4 个维度(setDimension4Name('app_install_status')),与追溯维度一致。

两通道独立失败互不影响;HXEventMonitor 的 module 参数为显式传入的 'launch',与文档模块定义一致,不经过 APMElkService 固定的 'apm' 来源。

7. 验收标准

  1. 单次冷启动中,隐私页、网络页和首页连续跳转时,reportDrawnCompleted 仍只提交一次。
  2. 未登录启动以登录/启动页为入口;已登录启动以 Index 为入口,entry_page 不被后续页面覆盖。
  3. 每次冷启动只产生一条可查询记录,且 extend_time > 0;热启动、无效耗时和重复事件不上传。
  4. 页面在系统异步回调前销毁时,不影响已经提交的上报;旧页面不会释放新页面 observer。
  5. 本地日志与后台数据量级一致;看板可查询 P50、P95、超 2 秒占比及分桶分布。
  6. 断网或上传失败不影响启动流程,网络恢复后按既有上传机制重试。
  7. 双通道上传验证:完整 workspace 编译通过(HXEventMonitorDataMonitorBuilder/getEventFactory/setBuckets/record/setDimension_4 已随 HXLaunchMonitorPlugin 编译验证);真机冷启动 hilog 出现 Cold start metric uploaded(链路 A)与 Cold start bucket metric recorded(链路 B,需后台下发策略,见 5.1);ELK 索引可查 biz='launch' 的 11 字段记录,APM 平台 launch 模块出现分桶分布。
  8. 维度与分桶:分桶分布可按 4 个维度(start_type/entry_page/app_version/app_install_status)分组查询;分桶边界变更需后台重建或并档(历史分布不可比)。

8. 已验证结果

8.1 首次回归(2026-08-05CrashDiagnosticsDemonova 14 Pro

未登录冷启动入口为 LaunchLoginextend_time158ms;持久化登录后冷启动入口为 Indexextend_time155ms。网络异常、网络恢复、隐私确认及页面返回场景均保持一次 draw 回调和一次系统提交,后续页面注册被全局保护拦截;start_type=1 的热启动事件被正确忽略。

8.2 追溯字段扩展后的真机验证(2026-08-05,CrashDiagnosticsDemonova 14 ProOpenHarmony-6.1.1.120

# 场景 操作 结果
1 已登录冷启动 aa force-stop 后启动 入口 Index,draw 1 次、提交 1 次、成功 1 次,extend_time=144msstart_type=0
2 已登录冷启动(复测) aa force-stop 后启动 入口 Indexextend_time=137ms,单次提交
3 未登录冷启动 bm clean -d 清数据后启动 入口 LaunchLoginextend_time=152ms,单次提交
4 多入口连续跳转 登录页 → 网络异常页 → 返回 → 隐私确认页 LaunchNetworkLaunchPrivacy 重复首帧注册均被拦截(DrawReportState 保护),无重复提交
5 时间拆解字段捕获 观察三次冷启动的 APP_LAUNCH 详情 response_latency=19~24msstartability_processstart_dur=57~83msappattach_to_appforeground_dur=28~33ms,均为非零有效值
6 热启动(进程存活) 对运行中进程再次 aa start 无新 APP_LAUNCH 事件产生(系统对运行中单例不视为启动),不产生上报

验证结论:新增字段在真机全部可捕获且数值合理(response_latency 反映系统响应,两个 *_dur 拆解系统侧进程启动与挂载耗时)。已登录/未登录冷启动耗时(137~152ms)与首次回归(155/158ms)量级一致,字段扩展未引入回归。

发现的边界情况animation_finish_time 在真机上为 0(系统未填充该字段),因此计算"应用可优化段(extend_time animation_finish_time)"时需处理 0 值场景——此时该段等于 extend_time 全量,应在看板聚合时兼容。

8.3 完整重跑确认(2026-08-05 当天,同设备)

全部场景重新模拟一遍,结果与 8.2 一致:

# 场景 结果
1 清数据后未登录冷启动 入口 LaunchLoginextend_time=161ms,单次提交
2 登录后同进程跳转 Index Index 重复首帧注册被拦截,无重复提交
3 已登录冷启动 入口 Indexextend_time=138ms,单次提交
4 热启动(进程存活) 无新 APP_LAUNCH 事件,不产生上报

8.4 连续 3 次冷启动稳定性验证(2026-08-05 当天,同设备)

已登录态下连续 3 次 force-stop → 启动,每次均为独立进程、单次 draw 回调、单次 reportDrawnCompleted 提交与成功回调、独立有效 APP_LAUNCH 事件:

次数 进程 入口 提交次数 extend_time start_type
1 43415 Index 1 131ms 0
2 43785 Index 1 132ms 0
3 43914 Index 1 126ms 0

连续启动耗时稳定在 126~138ms,无事件串扰、无重复上报。

8.5 全场景矩阵 3 组验证(2026-08-05 当天,同设备)

每组完整链路:清数据 → 未登录冷启动 → 网络异常页跳转(拦截)→ 返回 → 隐私确认页跳转(拦截)→ 返回 → 模拟登录 → Index 跳转(拦截)→ 已登录冷启动 → 热启动(无新事件)。3 组结果:

未登录冷启动(入口 LaunchLogin 网络页拦截 隐私页拦截 Index 拦截 已登录冷启动(入口 Index
1 extend_time=162ms(进程 44062 extend_time=133ms(进程 44644
2 extend_time=157ms(进程 44927 extend_time=131ms(进程 45200
3 extend_time=159ms(进程 45571 extend_time=135ms(进程 45862

每组 6 个场景全部按预期执行:单次冷启动只提交一次 reportDrawnCompleted,后续页面(网络异常页、隐私页、Index)的重复首帧注册全部被拦截,热启动不产生新事件。未登录冷启动耗时稳定在 157162ms,已登录稳定在 131135ms,组间一致,验证可复现。

8.6 启动 5 秒内退出场景验证(2026-08-05 当天,同设备)

系统 extend_time 语义:手指离手 5 秒内未调用 reportDrawnCompleted 则该值为 0。真机实测退出场景:

# 场景 操作 结果
1 启动后 1 秒内退出(首帧已提交) aa start → 1 秒后 force-stop reportDrawnCompleted 已在退出前提交,事件正常生成、extend_time 有效;事件送达时进程已退出,由后续进程补投递处理
2 启动后立即强杀(首帧未提交) aa start → 立即 force-stop(无间隔) 仅有"已监听 draw"日志,reportDrawnCompleted 从未调用;5 秒窗口过期,系统未生成有效事件,重启后无补投递,不产生上报
3 补报判定(修复验证) 部署新版本后 force-stop → 冷启动 新进程收到旧进程遗留事件,icon_input_time 早于注册时间约 4 秒,判定 补报事件=是entry_pageunknown 处理
4 正常冷启动(修复验证) 干净 force-stop → 启动 收到自身事件,判定 补报事件=否,无误判

结论:首帧未提交时退出由 extend_time<=0 过滤兜底不上报;首帧已提交后退出的补报数据有效,修复后补报事件与正常事件可稳定区分(判定逻辑见 6.1)。

8.7 全场景(含 5 秒退出)3 组验证(2026-08-05 当天,同设备)

每组 8 步链路:清数据 → 未登录冷启动 → 网络页拦截 → 隐私页拦截 → 登录 Index 拦截 → 已登录冷启动 → 热启动 → 立即强杀(首帧未提交)→ 1 秒退出(首帧已提交)→ 补报判定。3 组结果:

步骤 组 1 组 2 组 3
未登录冷启动(LaunchLogin 158ms / 补报=否 180ms / 补报=否 180ms / 补报=否
网络页重复注册拦截
隐私页重复注册拦截
Index 重复注册拦截
已登录冷启动(Index 129ms / 补报=否 132ms / 补报=否 131ms / 补报=否
热启动(进程存活) 无新事件 无新事件 无新事件
立即强杀(首帧未提交)后重启 被强杀进程无有效事件,重启进程正常捕获 同左 同左
1 秒退出(首帧已提交)后重启 先收遗留事件补报=是,再收自身事件补报=否 同左 同左

24 个断言(3 组 × 8 步)全部通过,组间一致。补报判定在每组中均双向正确:遗留事件稳定判定为补报(entry_page 不归因),自身事件稳定判定为非补报(entry_page 正常归因)。

8.8 重装 + is_first_install 维度全场景验证(2026-08-06CrashDiagnosticsDemonova 14 Pro

前置改动demo 同步 is_first_install 维度(preferences 无启动记录视为安装后首启,putSyncflushSync 同步落盘——初版遗漏 flushSync 导致 force-stop 后数据丢失、二次启动误判首启,对比 AuthSession 用法定位修复);页面重命名为 LaunchLogin/LaunchNetwork/LaunchPrivacy,诊断类更名为 LaunchDiagnostics

验证方式bm uninstall -n 完全卸载重装(真实安装后首启语义),随后全场景 3 组 × 8 步(冷启动/网络页拦截/隐私页拦截/登录跳转拦截/已登录冷启动/热启动/立即强杀/1 秒退出补报)。表中数据为页面重命名前的同一逻辑验证(页面名仅为日志展示用途,不影响捕获语义);重命名后 devecocli build 编译通过,真机运行回归待设备恢复后补跑。

步骤 组 1 组 2 组 3
未登录冷启动(入口 LaunchLogin 165ms / 首启=true / 补报=否 165ms / 首启=true 161ms / 首启=true
网络页/隐私页/Index 重复注册拦截 ×3 ×3 ×3
已登录冷启动(入口 Index 134ms / 首启=false 129ms / 首启=false 131ms / 首启=false
热启动(进程存活) 无新事件 无新事件 无新事件
立即强杀后重启 142ms / 补报=否 132ms / 补报=否 127ms / 补报=否
1 秒退出后重启 补报=是 + 自身=否 同左 同左

24 断言全部通过。关键结论:

  • is_first_install 维度语义正确:安装/清数据后首启 = true161165ms),非首启 = false127134ms);首启耗时系统性偏高约 30ms,该维度可将其从常规分布中区分,避免拉高 P50/P95。
  • 补报判定 3 组双向正确,拦截保护 3 组 × 3 页无漏防。
  • 耗时稳定(127~165ms);此前观测到的 response_latency=5086ms 异常值本次未复现(偶发)。
  • 注意:demo 的 is_first_installboolean)基于 preferences 记录,bm clean 清数据后该标记重置(再次显示首启);生产实现为 app_install_statusnormal/new_install/over_written 三值,见第 4 节),以生产为准。

8.9 真实应用验证(2026-08-06,完整 workspacenova 14 Pro

在完整 workspace 落地(HXLaunchMonitorPlugin 插件 + APMHelper 注册)后的真机数据:

场景 结果
全新安装首启(new_install 链路 Acold_start_time=1141msEntry=IndexElk 上报
二次冷启动(normal 链路 Acold_start_time=1717msElk 上报
链路 B(分桶) 待后台下发 launch 策略(5.1)后复测

编译:claude_compile.sh → BUILD_SUCCESS(链路 B API 随插件编译验证通过)。

真实应用耗时(1.11.7s)远高于 demo127165ms),符合生产首页复杂度的预期;app_install_status 维度可区分首启与常规启动的耗时差异。