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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user