feat: 完善启动性能监控流程

- 增加登录、网络异常和隐私确认启动场景
- 持久化登录状态并支持首页退出登录
- 保证每个进程只提交一次 reportDrawnCompleted
- 按启动事件唯一键去重 APP_LAUNCH 上报
- 同步优化逻辑到 future_entry_demo 并在 CrashDiagnosticsDemo 真机验证
This commit is contained in:
clz
2026-08-05 16:19:08 +08:00
parent 56ad003e04
commit d8513d1f1d
13 changed files with 956 additions and 5 deletions
@@ -44,6 +44,7 @@ import { ConnectionInitiator } from '../communication/ConnectionInitiator';
import { APMHelper } from '../monitor/APMHelper';
import { cbasService } from '../register/cbasregister/CbasServiceRegister';
import { eventSubscription } from '../monitor/AppEvent';
import { StartupMonitor } from '../monitor/StartupMonitor';
import { StockGroupConstants } from 'biz_selfcode';
import { StockGroupHandler } from '../groupsub/business/StockGroupHandler';
import { VarietyGroupHandler } from '../groupsub/business/VarietyGroupHandler';
@@ -140,6 +141,9 @@ export default class EntryAbility extends UIAbility {
hilog.info(0x0000, this.tag, 'Custom Figures registered successfully');
// =====================================
// 初始化启动性能监控(必须在 early onCreate 注册,确保 reportDrawnCompleted 前 watcher 已就绪)
StartupMonitor.init();
DebugToolUtil.TARGET_NAME = BuildProfile.TARGET_NAME
DebugToolUtil.PRODUCT_NAME = BuildProfile.PRODUCT_NAME
DebugToolUtil.BUILD_MODE_NAME = BuildProfile.BUILD_MODE_NAME
@@ -0,0 +1,239 @@
import hiAppEvent from '@ohos.hiviewdfx.hiAppEvent';
import { HXLog } from 'biz_common/src/main/ets/logger/HXLog';
import { HXUserService } from 'biz_hxservice';
import { ElkService, ElkUploadMessage, ElkUploadMessageBuilder } from 'service_monitor';
import { AppConfigManager } from 'biz_common';
import { inspector } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
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
*/
export class StartupMonitor {
private static readonly MAX_EVENT_KEYS: number = 20;
/**
* 首帧绘制完成时快照的入口页面名称,在 onDraw 回调中赋值。
* 与 APP_LAUNCH 事件处理处在同一同步时序内,避免页面跳转覆盖。
*/
private static capturedEntryPage: string = 'unknown';
private static watcherInitialized: 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) 清理即可,
* 无需在页面侧持有清理函数。
*/
static reportFirstFrameOnDraw(
createObserver: () => inspector.ComponentObserver,
abilityContext: common.UIAbilityContext,
pageName: string
): void {
if (StartupMonitor.drawReportSubmitted) {
HXLog.i(TAG, `Skip duplicate draw registration for page: ${pageName}`)
return
}
StartupMonitor.disposeFirstFrame(StartupMonitor.activeObserverOwner)
try {
const observer = createObserver();
const onDraw = () => {
observer.off('draw', onDraw);
if (StartupMonitor.activeObserverOwner === pageName) {
StartupMonitor.activeObserverOwner = ''
StartupMonitor.activeDisposer = undefined
}
// 多个入口页面可能在同一冷启动中依次绘制。必须在调用系统接口前设置全局状态,
// 防止后续页面或重复 draw 回调再次提交 reportDrawnCompleted。
if (StartupMonitor.drawReportSubmitted) {
HXLog.i(TAG, `Skip duplicate draw callback for page: ${pageName}`)
return
}
StartupMonitor.drawReportSubmitted = true
StartupMonitor.capturedEntryPage = pageName
try {
abilityContext.reportDrawnCompleted(() => {
});
HXLog.i(TAG, `reportDrawnCompleted submitted for page: ${pageName}`)
} catch (e) {
// 同步异常表示系统调用尚未提交,允许后续入口页面重试。
StartupMonitor.drawReportSubmitted = false
StartupMonitor.capturedEntryPage = 'unknown'
HXLog.e(TAG, `reportDrawnCompleted failed synchronously: ${JSON.stringify(e)}`)
}
};
StartupMonitor.activeObserverOwner = pageName
StartupMonitor.activeDisposer = () => {
observer.off('draw', onDraw);
};
observer.on('draw', onDraw);
HXLog.i(TAG, `Draw listener registered for page: ${pageName}`);
} catch (e) {
HXLog.e(TAG, `Create draw listener failed for page ${pageName}: ${JSON.stringify(e)}`)
}
}
/**
* 取消入口页面的首帧绘制监听。
*/
static disposeFirstFrame(pageName: string): void {
if (pageName === '' || StartupMonitor.activeObserverOwner !== pageName) {
return
}
StartupMonitor.activeDisposer?.();
StartupMonitor.activeObserverOwner = ''
StartupMonitor.activeDisposer = undefined;
}
/**
* 初始化 APP_LAUNCH 事件监听。
* 应在 EntryAbility.onCreate 中尽早调用,确保在 reportDrawnCompleted 触发事件前完成注册。
*/
static init(): void {
if (StartupMonitor.watcherInitialized) {
return
}
hiAppEvent.addWatcher({
name: "startupWatcher",
appEventFilters: [{
domain: hiAppEvent.domain.OS,
names: [hiAppEvent.event.APP_LAUNCH]
}],
onReceive: async (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => {
HXLog.i(TAG, `APP_LAUNCH onReceive: domain=${domain}`)
StartupMonitor.processAppLaunchEvents(appEventGroups)
}
});
StartupMonitor.watcherInitialized = true
HXLog.i(TAG, 'StartupMonitor watcher registered')
}
/**
* 遍历 APP_LAUNCH 事件,仅处理冷启动(start_type === 0)且 extend_time 已填充的记录,
* 计算 cold_start_time 并上传。
*/
private static processAppLaunchEvents(appEventGroups: Array<hiAppEvent.AppEventGroup>): void {
try {
if (!appEventGroups || appEventGroups.length === 0) {
return
}
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 调用后填充。未填充说明首帧尚未完成。
if (!extendTime || extendTime <= 0) {
HXLog.w(TAG, `APP_LAUNCH has no valid extend_time, skip`)
return
}
const iconInputTime = eventInfo.params['icon_input_time'] as number;
const eventTime = eventInfo.params['time'] as number;
const processName = (eventInfo.params['process_name'] as string) ?? '';
const eventKey = `${eventTime}|${iconInputTime}|${startType}|${processName}`;
if (StartupMonitor.processedEventKeys.indexOf(eventKey) !== -1) {
HXLog.i(TAG, `Skip duplicate APP_LAUNCH: ${eventKey}`)
return
}
StartupMonitor.processedEventKeys.push(eventKey)
if (StartupMonitor.processedEventKeys.length > StartupMonitor.MAX_EVENT_KEYS) {
StartupMonitor.processedEventKeys.shift()
}
HXLog.i(TAG,
`cold_start_time=${extendTime}ms, ` +
`icon_input_time=${iconInputTime}, ` +
`entry_page=${StartupMonitor.capturedEntryPage}`)
StartupMonitor.uploadColdStartTime(extendTime, startType, eventInfo)
})
})
} catch (e) {
HXLog.e(TAG, `Error processing APP_LAUNCH: ${JSON.stringify(e)}`)
}
}
/**
* 上传冷启动耗时指标到 ELK。
* 使用 isInstant=true 异步上传,不阻塞页面渲染。
*/
private static uploadColdStartTime(
coldStartTime: number,
startType: number,
eventInfo: hiAppEvent.AppEventInfo
): void {
try {
const bundleVersion = (eventInfo.params['bundle_version'] as string) ?? '';
const animationFinishTime = eventInfo.params['animation_finish_time'] as number;
const metricPayload: Record<string, Object> = {
// 核心指标
cold_start_time: coldStartTime,
// 维度
start_type: startType,
entry_page: StartupMonitor.capturedEntryPage,
app_version: StartupMonitor.getAppVersion(),
bundle_version: bundleVersion,
// 原始系统字段,用于后端校验和去重
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,
}
const builder = new StartupElkBuilder('i')
builder.userid(HXUserService.getInstance().getUserId())
builder.messageKey('cold_start_time')
builder.messageValue(JSON.stringify(metricPayload))
const message = new ElkUploadMessage(builder)
// 异步上传,避免阻塞启动流程
ElkService.getInstance().pushBusinessLog(message, true)
HXLog.i(TAG, `Cold start metric uploaded: ${coldStartTime}ms`)
} catch (e) {
HXLog.e(TAG, `uploadColdStartTime failed: ${JSON.stringify(e)}`)
}
}
private static getAppVersion(): string {
return AppConfigManager.getInstance().getConfigProvider()?.getInnerVersionFull() ?? 'unknown'
}
}
/**
* ELK 消息构造器,业务来源标记为 StartupMonitor
*/
class StartupElkBuilder extends ElkUploadMessageBuilder {
constructor(biz_level: string = 'i') {
super(Date.now(), 'StartupMonitor', biz_level);
}
}
@@ -8,6 +8,7 @@ import { HXNetwork } from './HXNetwork'
import { ConnectionSeq } from '@kernel/lib_communication'
import { AuthProviderService } from 'biz_auth'
import { AdvertisingMonitorService } from 'biz_hxservice'
import { StartupMonitor } from '../monitor/StartupMonitor'
const SettingWant: Want = {
bundleName: 'com.huawei.hmos.settings',
@@ -24,6 +25,7 @@ struct NetworkAnomalyPage {
private TAG: string = "HXNetwork";
private systemBarColor = "#00000000";
private systemBarContentColor = "#FFFFFFFF";
context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
async setSystemBar() {
let context = (this.getUIContext().getHostContext() as common.UIAbilityContext);
@@ -47,6 +49,13 @@ struct NetworkAnomalyPage {
aboutToAppear(): void {
this.setSystemBar()
StartupMonitor.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
this.context, 'NetworkAnomalyPage');
}
aboutToDisappear(): void {
StartupMonitor.disposeFirstFrame('NetworkAnomalyPage')
}
onExit() {
@@ -118,6 +127,8 @@ struct NetworkAnomalyPage {
},
'middle': { 'anchor': '__container__', 'align': HorizontalAlign.Center },
})
}.width('100%').height('100%')
}
.id('HOME_ROOT')
.width('100%').height('100%')
}
}
}
@@ -48,6 +48,7 @@ import { enableDarkMode, ThemeManagerEmitterConstants } from '@kernel/theme_mana
import { CmeTraceForeignManager } from 'biz_quote/src/main/ets/manager/CmeTraceForeignManager';
import { HQUGCManager } from 'biz_market';
import { AdsManager } from '@b2c-f/fuhm-ads';
import { StartupMonitor } from '../monitor/StartupMonitor';
const TAG: string = 'Struct Index';
@@ -309,6 +310,8 @@ struct IndexM {
aboutToDisappear() {
HXLog.d(TAG, 'Index aboutToDisappear triggered at ' + Date.now())
// 页面退出时取消首帧绘制监听
StartupMonitor.disposeFirstFrame('Index');
emitter.off(EmitterConstants.SWIPER_CHANGETOINDEX_1_TEMP)
emitter.off(EmitterConstants.TAB_UI_MANAGER_CHANGE_INDEX)
emitter.off(QuoteSettingEvents.SETTING_UPDATE, this.onSettingUpdateCallback)
@@ -361,6 +364,11 @@ struct IndexM {
// 冷启动先初始化一次 drawline 基础能力,保证首次进入行情页即可绘制同步线。
DrawLineInit.init()
// 注册首帧绘制监听,绘制完成时自动调用 reportDrawnCompleted
StartupMonitor.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
this.context, 'Index');
}
onPageShow() {
@@ -522,6 +530,7 @@ struct IndexM {
Blank().height(px2vp(GlobalContext.navigationBarHeight)).flexShrink(0).backgroundColor($r('app.color.surface_layer1_foreground'))
}
}
.id('HOME_ROOT')
.height('100%')
.width('100%')
}
@@ -9,6 +9,7 @@ import { window } from '@kit.ArkUI'
import { ConnectionSeq } from '@kernel/lib_communication/src/main/ets/data/CommunicationConstants'
import { deviceInfo } from '@kit.BasicServicesKit'
import { AdvertisingMonitorService } from 'biz_hxservice'
import { StartupMonitor } from '../monitor/StartupMonitor'
/**
* 启动页面
@@ -46,6 +47,13 @@ struct LauncherPage {
aboutToAppear(): void {
this.setSystemBar()
this.userPrivacyDialogVisible = !PreferenceService.getBooleanValueSync('Privacy', 'isUserAgree', false);
StartupMonitor.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
this.context, 'LauncherPage');
}
aboutToDisappear(): void {
StartupMonitor.disposeFirstFrame('LauncherPage')
}
onCancel() {
@@ -102,6 +110,8 @@ struct LauncherPage {
},
'middle': { 'anchor': '__container__', 'align': HorizontalAlign.Center },
}).id("image")
}.width('100%').height('100%')
}
.id('HOME_ROOT')
.width('100%').height('100%')
}
}
}