feat: 启动监控接入分桶指标并精简追溯字段

- StartupMonitor 新增 HXEventMonitor 分桶指标(链路 B):launch 模块 cold_start_time,桶 [500,600,700,800,1000,1500,2000,3000],record 原始耗时由 SDK 按桶聚合,负责看板分布
- 分桶按实测主体分布(600-800ms)在主体区间细分,尾部保留劣化告警锚点
- ElkService 追溯通道定稿上传字段(11 个):cold_start_time、start_type、entry_page、app_version、is_first_install、response_latency、animation_finish_time、startability_processstart_dur、appattach_to_appforeground_dur、git_commit、is_backfill;指标 1 + 维度 4(含 is_first_install,区分安装后首启的偏高耗时)+ 启动生命周期各阶段时间 4(劣化分段定位,response_latency 需 API 22+、两个 *dur 仅冷启动存在)+ 代码级追溯 git_commit + 补报标识;去重为客户端行为(§3.2 进程内存 20 键缓存),负载不携带去重键与事件时间字段
- 分桶指标同步 4 维度(含 setDimension4Name('is_first_install')),与追溯维度对齐
- 上传负载抽象为 ColdStartMetricPayload 强类型接口,各字段含义注释
- 删除未使用环境字段 getter 与 import,文件从 387 行精简至 278 行
- CrashDiagnosticsDemo 阶段时间展示顺序按启动生命周期对齐,构建通过
- 接入方案文档同步:第 4 节追溯字段与分桶实现、第 4.1 节按需启用、第 5 节发送机制、第 6.2 节双通道
- 待确认:链路 B 的 HXEventMonitor API(DataMonitorBuilder/getEventFactory/setBuckets/setDimension_4/record)形态基于 Android APM 文档推断,本机无 @kernel/app_monitor_event 类型声明,需在完整 workspace 以 .d.ts 核对后编译验证

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
clz
2026-08-06 10:53:18 +08:00
co-authored by Claude
parent bd96495482
commit 0312555501
3 changed files with 110 additions and 109 deletions
@@ -6,55 +6,35 @@ import { AppConfigManager, StartManager } from 'biz_common';
import { GIT_COMMIT } from 'biz_common/src/main/ets/utils/debug/GitBuildConfig';
import { inspector } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { deviceInfo } from '@kit.BasicServicesKit';
import BuildProfile from 'BuildProfile';
import { HXEventMonitor, DataMonitorBuilder } from '@kernel/app_monitor_event';
const TAG: string = 'StartupMonitor';
/**
* 启动性能监控
*
* 订阅系统 APP_LAUNCH 事件,获取系统计算的冷启动耗时(extend_time),
* 并通过现有的 ElkService 上传结构化指标。与 AppEvent.ets 的故障事件监听相互独立。
*
* 指标定义(见 HarmonyOS 启动性能监控接入方案_20260805.md):
* - module: launch
* - metric: cold_start_time(ms)
* - dimensions: start_type / entry_page / app_version
* 启动性能监控:订阅 APP_LAUNCH 事件,采集冷启动耗时(extend_time),
*/
export class StartupMonitor {
private static readonly MAX_EVENT_KEYS: number = 20;
/**
* 补报事件判定容差(ms)。
* 本次启动链中 icon_input_time(点击图标)与 watcher 注册时间间隔在毫秒~秒级;
* 系统补投递的上次启动遗留事件,其 icon_input_time 至少早于注册时间 5 秒(事件送达延迟),
* 因此容差取 2s 可稳定区分两者。
* 补报判定容差(ms):正常启动链间隔毫秒~秒级,补报(上次启动遗留事件)至少早 5 秒
*/
private static readonly BACKFILL_TOLERANCE_MS: number = 2000;
/**
* watcher 注册时间戳(epoch ms),用于识别系统补投递的上次启动遗留事件。
*/
/** watcher 注册时间戳(epoch ms),补报判定的锚点 */
private static watcherRegisterTime: number = 0;
/**
* 首帧绘制完成时快照的入口页面名称,在 onDraw 回调中赋值。
* 与 APP_LAUNCH 事件处理处在同一同步时序内,避免页面跳转覆盖。
*/
/** 首帧绘制完成时快照的入口页面,在 onDraw 回调中赋值 */
private static capturedEntryPage: string = 'unknown';
private static watcherInitialized: boolean = false;
private static bucketMetricInitialized: boolean = false;
private static drawReportSubmitted: boolean = false;
private static activeObserverOwner: string = '';
private static activeDisposer?: () => void;
private static processedEventKeys: Array<string> = [];
/**
* 在入口页面注册 HOME_ROOT 首次绘制监听,自动完成:
* 1. 创建 observer
* 2. 监听 draw → 仅触发一次 → 快照 entryPage → 调用 reportDrawnCompleted
*
* 页面在 aboutToDisappear 中调用 disposeFirstFrame(pageName) 清理即可,
* 无需在页面侧持有清理函数。
* 注册入口页根节点 draw 监听:仅触发一次,快照 entryPage 并调用 reportDrawnCompleted。
* 页面在 aboutToDisappear 调用 disposeFirstFrame(pageName) 清理。
*/
static reportFirstFrameOnDraw(
createObserver: () => inspector.ComponentObserver,
@@ -77,8 +57,7 @@ export class StartupMonitor {
StartupMonitor.activeDisposer = undefined
}
// 多个入口页面可能在同一冷启动中依次绘制。必须在调用系统接口前设置全局状态,
// 防止后续页面或重复 draw 回调再次提交 reportDrawnCompleted。
// 必须在系统调用前设置进程级提交状态,防止后续页面或重复回调再次提交
if (StartupMonitor.drawReportSubmitted) {
HXLog.i(TAG, `Skip duplicate draw callback for page: ${pageName}`)
return
@@ -121,16 +100,13 @@ export class StartupMonitor {
StartupMonitor.activeDisposer = undefined;
}
/**
* 初始化 APP_LAUNCH 事件监听。
* 应在 EntryAbility.onCreate 中尽早调用,确保在 reportDrawnCompleted 触发事件前完成注册。
*/
/** 注册 APP_LAUNCH watcher(幂等)。应在 onCreate 尽早调用,确保事件生成前完成注册 */
static init(): void {
if (StartupMonitor.watcherInitialized) {
return
}
// 记录注册时间戳,用于区分本次启动事件与系统补投递的上次启动遗留事件
StartupMonitor.watcherRegisterTime = Date.now()
StartupMonitor.ensureBucketMetric()
hiAppEvent.addWatcher({
name: "startupWatcher",
appEventFilters: [{
@@ -146,10 +122,7 @@ export class StartupMonitor {
HXLog.i(TAG, 'StartupMonitor watcher registered')
}
/**
* 遍历 APP_LAUNCH 事件,仅处理冷启动(start_type === 0)且 extend_time 已填充的记录,
* 计算 cold_start_time 并上传。
*/
/** 处理 APP_LAUNCH:过滤冷启动且 extend_time 有效,去重后上传 */
private static processAppLaunchEvents(appEventGroups: Array<hiAppEvent.AppEventGroup>): void {
try {
if (!appEventGroups || appEventGroups.length === 0) {
@@ -158,14 +131,12 @@ export class StartupMonitor {
appEventGroups.forEach((eventGroup: hiAppEvent.AppEventGroup) => {
eventGroup.appEventInfos.forEach((eventInfo: hiAppEvent.AppEventInfo) => {
const startType = eventInfo.params['start_type'] as number;
// 仅统计冷启动
if (startType !== 0) {
HXLog.i(TAG, `Skip non-cold-start: start_type=${startType}`)
return
}
const extendTime = eventInfo.params['extend_time'] as number;
// extend_time 即为冷启动耗时(ms):从 icon_input_time 到 reportDrawnCompleted 的时间差,
// 由系统在 reportDrawnCompleted 调用后填充。未填充说明首帧尚未完成。
// 未填充说明首帧未完成(如 5 秒内退出)
if (!extendTime || extendTime <= 0) {
HXLog.w(TAG, `APP_LAUNCH has no valid extend_time, skip`)
return
@@ -197,57 +168,35 @@ export class StartupMonitor {
}
}
/**
* 上传冷启动耗时指标到 ELK。
* 使用 isInstant=true 异步上传,不阻塞页面渲染。
*/
/** 上传冷启动指标:ElkService 追溯 + HXEventMonitor 分桶,异步不阻塞首帧 */
private static uploadColdStartTime(
coldStartTime: number,
startType: number,
eventInfo: hiAppEvent.AppEventInfo
): void {
try {
const bundleVersion = (eventInfo.params['bundle_version'] as string) ?? '';
const bundleName = (eventInfo.params['bundle_name'] as string) ?? '';
const animationFinishTime = eventInfo.params['animation_finish_time'] as number;
// 时间拆解字段(详见接入方案文档第 4 节):
// response_latency 需要 API 22+;startability_processstart_dur / appattach_to_appforeground_dur 仅冷启动存在
const iconInputTime = eventInfo.params['icon_input_time'] as number;
// 补投递事件(上次启动遗留)的 entry_page 是当前进程快照,无意义,置 unknown
const isBackfillEvent = StartupMonitor.isBackfillEvent(iconInputTime);
// 启动生命周期各阶段时间(ms):response_latency 需 API 22+,两个 *dur 仅冷启动存在
const responseLatency = eventInfo.params['response_latency'] as number;
const animationFinishTime = eventInfo.params['animation_finish_time'] as number;
const startabilityProcessStartDur = eventInfo.params['startability_processstart_dur'] as number;
const appattachToAppForegroundDur = eventInfo.params['appattach_to_appforeground_dur'] as number;
const iconInputTime = eventInfo.params['icon_input_time'] as number;
// 补投递事件(上次启动提交绘制但进程在事件送达前退出)的 entry_page 无意义,
// capturedEntryPage 是当前进程的快照,不能错误归因到本次启动的页面。
const isBackfillEvent = StartupMonitor.isBackfillEvent(iconInputTime);
const metricPayload: Record<string, Object> = {
// 核心指标
// 负载结构与字段语义见 ColdStartMetricPayload
const metricPayload: ColdStartMetricPayload = {
cold_start_time: coldStartTime,
// 维度
start_type: startType,
entry_page: isBackfillEvent ? 'unknown' : StartupMonitor.capturedEntryPage,
app_version: StartupMonitor.getAppVersion(),
bundle_version: bundleVersion,
// 补报标识:true 表示系统补投递的上次启动遗留事件(entry_page 无意义),看板可据此过滤或单独统计
is_backfill: isBackfillEvent,
// 原始系统字段,用于后端校验和去重
event_time: eventInfo.params['time'],
icon_input_time: eventInfo.params['icon_input_time'],
process_name: eventInfo.params['process_name'],
extend_time: eventInfo.params['extend_time'],
animation_finish_time: animationFinishTime,
// 追溯字段:系统时间拆解
bundle_name: bundleName,
is_first_install: StartupMonitor.getIsFirstInstall(),
response_latency: responseLatency,
animation_finish_time: animationFinishTime,
startability_processstart_dur: startabilityProcessStartDur,
appattach_to_appforeground_dur: appattachToAppForegroundDur,
// 追溯字段:客户端环境
device_type: StartupMonitor.getDeviceType(),
system_version: StartupMonitor.getSystemVersion(),
build_mode: StartupMonitor.getBuildMode(),
target_name: StartupMonitor.getTargetName(),
git_commit: StartupMonitor.getGitCommit(),
is_first_install: StartupMonitor.getIsFirstInstall(),
is_backfill: isBackfillEvent,
}
const builder = new StartupElkBuilder('i')
@@ -259,16 +208,44 @@ export class StartupMonitor {
// 异步上传,避免阻塞启动流程
ElkService.getInstance().pushBusinessLog(message, true)
HXLog.i(TAG, `Cold start metric uploaded: ${coldStartTime}ms${isBackfillEvent ? ' (backfill)' : ''}`)
// 分桶指标(链路 B):记录原始耗时,SDK 按桶自动归集,负责看板聚合
// TODO(api-confirm): setDimension_1 / record 签名需以 .d.ts 核对
const factory = HXEventMonitor.getEventFactory('launch')
const monitor = factory.getEventMonitor('cold_start_time')
monitor.setDimension_1(startType.toString())
monitor.setDimension_2(isBackfillEvent ? 'unknown' : StartupMonitor.capturedEntryPage)
monitor.setDimension_3(StartupMonitor.getAppVersion())
monitor.setDimension_4(StartupMonitor.getIsFirstInstall().toString())
monitor.record(coldStartTime)
HXLog.i(TAG, `Cold start bucket metric recorded: ${coldStartTime}ms`)
} catch (e) {
HXLog.e(TAG, `uploadColdStartTime failed: ${JSON.stringify(e)}`)
}
}
/**
* 判断事件是否为系统补投递的上次启动遗留事件。
* 本次启动链中 icon_input_time 与 watcher 注册时间间隔在容差内;
* 补报事件的 icon_input_time 早于注册时间超过容差(至少 5 秒的送达延迟)。
* 创建 launch 分桶指标(幂等):桶 [500,600,700,800,1000,1500,2000,3000],
* 按实测主体分布(600-800ms)细分,尾部留劣化告警锚点。
* TODO(api-confirm): DataMonitorBuilder / setBuckets / create 签名需以 .d.ts 核对
*/
private static ensureBucketMetric(): void {
if (StartupMonitor.bucketMetricInitialized) {
return
}
const builder = new DataMonitorBuilder()
builder.setMetricName('cold_start_time')
builder.setDimension1Name('start_type')
builder.setDimension2Name('entry_page')
builder.setDimension3Name('app_version')
builder.setDimension4Name('is_first_install')
builder.setBuckets([500, 600, 700, 800, 1000, 1500, 2000, 3000])
HXEventMonitor.getEventFactory('launch').create(builder)
StartupMonitor.bucketMetricInitialized = true
HXLog.i(TAG, 'launch bucket metric created: cold_start_time')
}
/** 补投递判定:icon_input_time 早于 watcher 注册时间超过容差(2s)视为上次启动遗留事件 */
private static isBackfillEvent(iconInputTime: number): boolean {
return iconInputTime < StartupMonitor.watcherRegisterTime - StartupMonitor.BACKFILL_TOLERANCE_MS
}
@@ -277,40 +254,49 @@ export class StartupMonitor {
return AppConfigManager.getInstance().getConfigProvider()?.getInnerVersionFull() ?? 'unknown'
}
private static getDeviceType(): string {
return deviceInfo.deviceType
}
private static getSystemVersion(): string {
return deviceInfo.osFullName
}
private static getBuildMode(): string {
return BuildProfile.BUILD_MODE_NAME.toString()
}
private static getTargetName(): string {
return BuildProfile.TARGET_NAME.toString()
}
/** 代码级追溯:Git 提交号(biz_common 构建时注入) */
private static getGitCommit(): string {
return GIT_COMMIT
}
/**
* 是否安装后首次进入(StartManager 在 EntryAbility.onCreate 中已 init)
*/
/** 维度:是否安装后首次进入(StartManager 在 EntryAbility.onCreate 中已 init) */
private static getIsFirstInstall(): boolean {
return StartManager.getInstance().getAppInstallStatus()
}
}
/**
* ELK 消息构造器,业务来源标记为 launch 模块(见接入方案文档第 4 节:
* 模块名使用小写字母和连字符;指标名不包含连字符)
*/
/** ELK 消息构造器,业务来源为 launch 模块(命名约束见文档第 4 节) */
class StartupElkBuilder extends ElkUploadMessageBuilder {
constructor(biz_level: string = 'i') {
super(Date.now(), 'launch', biz_level);
}
}
/**
* 冷启动指标上报负载(ElkService 追溯通道,messageValue 的 JSON 结构)。
* 最小必要字段:指标/维度/补报标识;其余字段按需加回(见文档第 4 节)。
*/
interface ColdStartMetricPayload {
// 核心指标:冷启动耗时(ms),即系统事件的 extend_time
cold_start_time: number
// 维度:启动类型(0=冷启动,当前仅统计冷启动)
start_type: number
// 维度:入口页面(Index/StartupLogin 等;补报事件为 unknown)
entry_page: string
// 维度:应用内部版本号
app_version: string
// 维度:是否安装后首次启动(首次启动耗时显著偏高,需从常规分布中区分)
is_first_install: boolean
// 系统响应段:离手到动效开始耗时(ms,需 API 22+)
response_latency: number
// 系统动效段:动效完成耗时(ms)
animation_finish_time: number
// 进程创建段:系统启动 Ability 到进程创建完成(ms,仅冷启动存在)
startability_processstart_dur: number
// 进程挂载段:进程初始化完成到应用切前台(ms,仅冷启动存在)
appattach_to_appforeground_dur: number
// 代码级追溯:Git 提交号,劣化时可定位到具体提交
git_commit: string
// 补报标识:true 表示系统补投递的上次启动遗留事件(entry_page 无意义)
is_backfill: boolean
}