Compare commits

..
9 Commits
Author SHA1 Message Date
clzandClaude 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
clzandClaude 8060805c84 feat: 按真实落地实现接入 HXLaunchMonitorPlugin APM 插件系统
- 新增 monitor/plugin/HXLaunchMonitorPlugin.ets:仿 HXCrashMonitorPlugin 接入 APM 插件系统(基类 AbstractHXBasePlugin),initPlugin 注入固定维度配置、startPlugin 创建分桶指标(解决 create 早于 SDK 初始化的时序问题)
- 新增 monitor/plugin/ILaunchMonitorConfig.ets:7 方法配置接口(userId/deviceType/deviceModel/appVersion/buildType/gitCommit/appInstallStatus)
- LaunchMonitor 同步 v2:15 字段负载(新增 device_type/device_model/user_id/build_type),维度全部经 launchConfig 注入(不再直接依赖 AppConfigManager/StartManager/deviceInfo),ensureColdStartMetric 惰性兜底(插件未启动时 record 前自建指标),watcher 更名 launchWatcher
- APMHelper 同步:注册 HXLaunchMonitorPlugin + HXLaunchMonitorConfig(含 AppInstallStatus 三值枚举映射、deviceInfo.marketName 设备型号)
- 同步自完整 workspace 真实实现(entry_clean.tar.gz,已编译验证 BUILD_SUCCESS)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 13:41:28 +08:00
clzandClaude 96f183c2c0 feat: 启动监控维度字段 is_first_install 对齐为 app_install_status
- LaunchMonitor:维度字段改为 app_install_status(normal/new_install/over_written 三值),getAppInstallStatus() 以 StartManager 布尔值映射两值,over_written 待完整 workspace 对齐;分桶指标维度名与 record 同步
- demo LaunchDiagnostics 同步:appInstallStatus 以 preferences 自实现(无记录→new_install,有记录→normal),展示与类头注释更新,over_written 无法区分已注明
- 真机验证:首启 app_install_status=new_install,二次启动=normal

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 14:51:39 +08:00
clzandClaude 96d6f1b34e docs: 按真实落地实现更新接入方案第 4-6 章
- 维度字段 is_first_install 改为 app_install_status(normal/new_install/over_written 三值,与 StartManager 安装状态对齐)
- 新增 5.1 模块策略下发:链路 B 为策略驱动(无策略时 create/record 空转并打印 eventId has not created!),策略 JSON 对齐 EventStrategyBean.fromJson(config 嵌套 + status/sampling_rate/interval/aggre_time/aggre_count),含字段说明表与判定生效步骤
- §6 补回标题与开头段:链路 A/B 分工、链路 B API 已随 HXLaunchMonitorPlugin 编译验证(不再待确认)、插件形态(仿 HXCrashMonitorPlugin,startPlugin 建指标,APMHelper 注册)
- §7 验收标准同步策略激活条件与编译验证状态
- 新增 8.9 真实应用验证:首启 1141ms(new_install)/ 二次 1717ms(normal),链路 B 待策略下发复测;8.8 注明 demo(boolean)与生产(三值)差异
- 新增真机文档 apm_launch_monitor_config.md(真实 workspace 落地配置,含策略 JSON 与链路 A 字段定义)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 14:47:15 +08:00
clzandClaude 487ad9e074 docs: 修复接入方案第 4 节表格分桶行被说明段落打断
- 分桶行移回指标定义表格内(此前被追溯字段说明段落挤出,渲染为孤立文本)
- 说明段落调整至表格之后

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 14:34:38 +08:00
clzandClaude 9e29282be3 refactor: 启动监控 Startup 命名统一为 Launch
- demo 页面与诊断类重命名:StartupLogin/StartupNetwork/StartupPrivacy → LaunchLogin/LaunchNetwork/LaunchPrivacy,StartupDiagnostics → LaunchDiagnostics(含 main_pages.json 注册与全部引用)
- 生产监控类重命名:StartupMonitor → LaunchMonitor,StartupElkBuilder → LaunchElkBuilder(含 EntryAbility/Index/LauncherPage/NetworkAnomalyPage 引用)
- 命名与 APP_LAUNCH 事件及 launch 模块对齐
- 接入方案文档同步页面名与类名,并修正 6 处过时内容:4 维度(is_first_install 入维度出候选)、分桶 4 维度、4.1 阶段时间已上传、§6 章节顺序(6.1 在 6.2 前)、§2 双通道链路
- demo 改名后 devecocli build 编译通过;真机运行回归待设备恢复补跑

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 14:24:31 +08:00
clzandClaude 6fe0d2ec7a feat: demo 同步 is_first_install 维度并修复 preferences 落盘
- StartupDiagnostics 新增 is_first_install 维度展示:preferences 无启动记录视为安装后首启,随后写入标记并 flushSync 同步落盘(与生产 StartManager.getAppInstallStatus 语义等价,演示工程无 biz_common 依赖故自实现)
- 修复 putSync 仅写内存缓存不落盘的问题:force-stop 后数据丢失,二次启动误判为首启;对比 AuthSession.flushSync 用法定位根因
- EntryAbility 调用 initialize 时传入 UIAbilityContext
- 真机验证:安装/清数据后首启 is_first_install=true,二次启动=false;全场景 3 组(拦截/登录/冷启动/热启动/强杀/补报)24 断言全部通过,耗时 127~165ms

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 11:16:28 +08:00
clzandClaude 0312555501 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>
2026-08-06 10:53:18 +08:00
clzandClaude bd96495482 fix: 启动监控 ELK 业务来源与接入方案模块名对齐
- StartupElkBuilder 业务来源由 StartupMonitor 改为 launch
- 对齐接入方案文档第 4 节模块定义(小写字母和连字符约束),保证后端按 launch 模块建索引模板可对上

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 19:45:05 +08:00
19 changed files with 923 additions and 501 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ Mixed-purpose repository with three distinct areas:
- `HarmonyOS APM分析报告_20260731.md` — crash analysis with supporting screenshots in `report-assets/`. - `HarmonyOS APM分析报告_20260731.md` — crash analysis with supporting screenshots in `report-assets/`.
- `HarmonyOS 启动性能监控接入方案_20260805.md` — cold-start performance monitoring integration plan. - `HarmonyOS 启动性能监控接入方案_20260805.md` — cold-start performance monitoring integration plan.
2. **`CrashDiagnosticsDemo/`** — Standalone HarmonyOS demo app demonstrating HiAppEvent crash subscription, persistent crash records, and native crash simulation via NAPI. Full project with its own `CLAUDE.md`. 2. **`CrashDiagnosticsDemo/`** — Standalone HarmonyOS demo app demonstrating HiAppEvent crash subscription, persistent crash records, and native crash simulation via NAPI. Full project with its own `CLAUDE.md`.
3. **`future_entry_demo/`** — Extracted `entry` HAP module from THS Futures. NOT a full workspace — depends on sibling modules (`biz_common`, `biz_trade`, etc.) via `file:../...`. Has its own `CLAUDE.md` and `AGENTS.md`. Contains `src/main/ets/monitor/StartupMonitor.ets` (cold-start performance monitoring implementation referenced by the startup report). 3. **`future_entry_demo/`** — Extracted `entry` HAP module from THS Futures. NOT a full workspace — depends on sibling modules (`biz_common`, `biz_trade`, etc.) via `file:../...`. Has its own `CLAUDE.md` and `AGENTS.md`. Contains `src/main/ets/monitor/LaunchMonitor.ets` (cold-start performance monitoring implementation referenced by the startup report).
**When working in a subdirectory**, read that subdirectory's own `CLAUDE.md` first — it contains build commands, architecture details, and constraints specific to that project. This file covers the repo-level conventions and cross-cutting concerns. The companion `AGENTS.md` has additional detail on PR guidelines and validation checklists. **When working in a subdirectory**, read that subdirectory's own `CLAUDE.md` first — it contains build commands, architecture details, and constraints specific to that project. This file covers the repo-level conventions and cross-cutting concerns. The companion `AGENTS.md` has additional detail on PR guidelines and validation checklists.
@@ -7,6 +7,7 @@ import { common } from '@kit.AbilityKit';
import { inspector } from '@kit.ArkUI'; import { inspector } from '@kit.ArkUI';
import { hiAppEvent, hilog } from '@kit.PerformanceAnalysisKit'; import { hiAppEvent, hilog } from '@kit.PerformanceAnalysisKit';
import { deviceInfo } from '@kit.BasicServicesKit'; import { deviceInfo } from '@kit.BasicServicesKit';
import { preferences } from '@kit.ArkData';
enum DrawReportState { enum DrawReportState {
IDLE, IDLE,
@@ -21,12 +22,13 @@ enum DrawReportState {
* 监听系统 APP_LAUNCH 事件,并确保一个进程生命周期内只提交一次 * 监听系统 APP_LAUNCH 事件,并确保一个进程生命周期内只提交一次
* reportDrawnCompleted。检测结果同时输出到 hilog 和模拟页面。 * reportDrawnCompleted。检测结果同时输出到 hilog 和模拟页面。
* *
* 与 future_entry_demo 的 StartupMonitor.ets 对应:本文件为无依赖演示实现 * 与 future_entry_demo 的 LaunchMonitor.ets 对应:本文件为无依赖演示实现
* (仅 hilog + 页面展示,不做 ELK 上传),追溯字段已同步系统时间拆解字段 * (仅 hilog + 页面展示,不做 ELK 上传),追溯字段已同步系统时间拆解字段
* 与 deviceInfo 环境字段;git_commit / is_first_install / build_mode 依赖 * 与 deviceInfo 环境字段;app_install_status 以 preferences 自实现(三值语义,
* biz_common 与 BuildProfile,演示工程不适用,故未同步。 * over_written 无法区分);git_commit / build_mode 依赖 biz_common 与
* BuildProfile,演示工程不适用,故未同步。
*/ */
export class StartupDiagnostics { export class LaunchDiagnostics {
private static readonly WATCHER_NAME: string = 'startupDiagnosticsWatcher'; private static readonly WATCHER_NAME: string = 'startupDiagnosticsWatcher';
private static readonly LOG_DOMAIN: number = 0xD002; private static readonly LOG_DOMAIN: number = 0xD002;
private static readonly MAX_EVENT_KEYS: number = 20; private static readonly MAX_EVENT_KEYS: number = 20;
@@ -41,6 +43,11 @@ export class StartupDiagnostics {
* watcher 注册时间戳(epoch ms),用于识别系统补投递的上次启动遗留事件。 * watcher 注册时间戳(epoch ms),用于识别系统补投递的上次启动遗留事件。
*/ */
private static watcherRegisterTime: number = 0; private static watcherRegisterTime: number = 0;
/**
* 安装状态(维度):preferences 无启动记录视为 new_install,随后写入标记。
* 与生产实现(app_install_status 三值)对齐;演示工程无法区分 over_written(覆盖安装保留数据时显示 normal)。
*/
private static appInstallStatus: string = 'normal';
private static watcherInitialized: boolean = false; private static watcherInitialized: boolean = false;
private static drawReportState: DrawReportState = DrawReportState.IDLE; private static drawReportState: DrawReportState = DrawReportState.IDLE;
private static capturedEntryPage: string = '未捕获'; private static capturedEntryPage: string = '未捕获';
@@ -57,37 +64,46 @@ export class StartupDiagnostics {
private static lastAction: string = '等待登录页首次绘制'; private static lastAction: string = '等待登录页首次绘制';
private static lastLaunchDetail: string = '尚未收到 APP_LAUNCH'; private static lastLaunchDetail: string = '尚未收到 APP_LAUNCH';
static initialize(): void { static initialize(context: common.UIAbilityContext): void {
if (StartupDiagnostics.watcherInitialized) { if (LaunchDiagnostics.watcherInitialized) {
return; return;
} }
// 安装状态:preferences 无启动记录视为 new_install,随后写入并同步落盘;异常时按 normal 处理
try {
const pref: preferences.Preferences = preferences.getPreferencesSync(context, { name: 'startup_demo' });
LaunchDiagnostics.appInstallStatus = pref.getSync('has_launched', false) ? 'normal' : 'new_install';
pref.putSync('has_launched', true);
pref.flushSync();
} catch (e) {
LaunchDiagnostics.appInstallStatus = 'normal';
}
const filter: hiAppEvent.AppEventFilter = { const filter: hiAppEvent.AppEventFilter = {
domain: hiAppEvent.domain.OS, domain: hiAppEvent.domain.OS,
names: [hiAppEvent.event.APP_LAUNCH] names: [hiAppEvent.event.APP_LAUNCH]
}; };
const watcher: hiAppEvent.Watcher = { const watcher: hiAppEvent.Watcher = {
name: StartupDiagnostics.WATCHER_NAME, name: LaunchDiagnostics.WATCHER_NAME,
appEventFilters: [filter], appEventFilters: [filter],
onReceive: (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => { onReceive: (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => {
hilog.info(StartupDiagnostics.LOG_DOMAIN, 'StartupDiagnostics', hilog.info(LaunchDiagnostics.LOG_DOMAIN, 'LaunchDiagnostics',
'APP_LAUNCH onReceive domain=%{public}s, groups=%{public}d', domain, appEventGroups.length); 'APP_LAUNCH onReceive domain=%{public}s, groups=%{public}d', domain, appEventGroups.length);
StartupDiagnostics.processAppLaunchEvents(appEventGroups); LaunchDiagnostics.processAppLaunchEvents(appEventGroups);
} }
}; };
// 记录注册时间戳,用于区分本次启动事件与系统补投递的上次启动遗留事件 // 记录注册时间戳,用于区分本次启动事件与系统补投递的上次启动遗留事件
StartupDiagnostics.watcherRegisterTime = Date.now(); LaunchDiagnostics.watcherRegisterTime = Date.now();
try { try {
hiAppEvent.addWatcher(watcher); hiAppEvent.addWatcher(watcher);
StartupDiagnostics.watcherInitialized = true; LaunchDiagnostics.watcherInitialized = true;
StartupDiagnostics.lastAction = 'APP_LAUNCH 监听已注册,等待首帧'; LaunchDiagnostics.lastAction = 'APP_LAUNCH 监听已注册,等待首帧';
StartupDiagnostics.notifyStatusChanged(); LaunchDiagnostics.notifyStatusChanged();
} catch (err) { } catch (err) {
StartupDiagnostics.lastAction = `APP_LAUNCH 监听注册失败:${StartupDiagnostics.getErrorMessage(err)}`; LaunchDiagnostics.lastAction = `APP_LAUNCH 监听注册失败:${LaunchDiagnostics.getErrorMessage(err)}`;
StartupDiagnostics.logError(StartupDiagnostics.lastAction); LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged(); LaunchDiagnostics.notifyStatusChanged();
} }
} }
@@ -96,124 +112,124 @@ export class StartupDiagnostics {
abilityContext: common.UIAbilityContext, abilityContext: common.UIAbilityContext,
pageName: string pageName: string
): void { ): void {
if (StartupDiagnostics.drawReportState !== DrawReportState.IDLE) { if (LaunchDiagnostics.drawReportState !== DrawReportState.IDLE) {
StartupDiagnostics.skippedReportCount++; LaunchDiagnostics.skippedReportCount++;
StartupDiagnostics.lastAction = `页面 ${pageName} 的重复首帧注册已跳过`; LaunchDiagnostics.lastAction = `页面 ${pageName} 的重复首帧注册已跳过`;
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction); LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged(); LaunchDiagnostics.notifyStatusChanged();
return; return;
} }
StartupDiagnostics.disposeFirstFrame(StartupDiagnostics.activeObserverOwner); LaunchDiagnostics.disposeFirstFrame(LaunchDiagnostics.activeObserverOwner);
try { try {
const observer: inspector.ComponentObserver = createObserver(); const observer: inspector.ComponentObserver = createObserver();
const onDraw = (): void => { const onDraw = (): void => {
observer.off('draw', onDraw); observer.off('draw', onDraw);
if (StartupDiagnostics.activeObserverOwner === pageName) { if (LaunchDiagnostics.activeObserverOwner === pageName) {
StartupDiagnostics.activeObserverOwner = ''; LaunchDiagnostics.activeObserverOwner = '';
StartupDiagnostics.activeDisposer = undefined; LaunchDiagnostics.activeDisposer = undefined;
} }
if (StartupDiagnostics.drawReportState !== DrawReportState.IDLE) { if (LaunchDiagnostics.drawReportState !== DrawReportState.IDLE) {
StartupDiagnostics.skippedReportCount++; LaunchDiagnostics.skippedReportCount++;
StartupDiagnostics.lastAction = `页面 ${pageName} 的重复 draw 回调已跳过`; LaunchDiagnostics.lastAction = `页面 ${pageName} 的重复 draw 回调已跳过`;
StartupDiagnostics.notifyStatusChanged(); LaunchDiagnostics.notifyStatusChanged();
return; return;
} }
StartupDiagnostics.drawCallbackCount++; LaunchDiagnostics.drawCallbackCount++;
StartupDiagnostics.drawReportState = DrawReportState.SUBMITTED; LaunchDiagnostics.drawReportState = DrawReportState.SUBMITTED;
StartupDiagnostics.capturedEntryPage = pageName; LaunchDiagnostics.capturedEntryPage = pageName;
StartupDiagnostics.reportSubmitCount++; LaunchDiagnostics.reportSubmitCount++;
StartupDiagnostics.lastAction = `已提交 ${pageName} 的 reportDrawnCompleted`; LaunchDiagnostics.lastAction = `已提交 ${pageName} 的 reportDrawnCompleted`;
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction); LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged(); LaunchDiagnostics.notifyStatusChanged();
try { try {
abilityContext.reportDrawnCompleted((err) => { abilityContext.reportDrawnCompleted((err) => {
if (err && err.code !== 0) { if (err && err.code !== 0) {
StartupDiagnostics.drawReportState = DrawReportState.FAILED; LaunchDiagnostics.drawReportState = DrawReportState.FAILED;
StartupDiagnostics.lastAction = LaunchDiagnostics.lastAction =
`reportDrawnCompleted 失败:code=${err.code}, message=${err.message}`; `reportDrawnCompleted 失败:code=${err.code}, message=${err.message}`;
StartupDiagnostics.logError(StartupDiagnostics.lastAction); LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged(); LaunchDiagnostics.notifyStatusChanged();
return; return;
} }
StartupDiagnostics.drawReportState = DrawReportState.SUCCEEDED; LaunchDiagnostics.drawReportState = DrawReportState.SUCCEEDED;
StartupDiagnostics.lastAction = `reportDrawnCompleted 成功:${pageName}`; LaunchDiagnostics.lastAction = `reportDrawnCompleted 成功:${pageName}`;
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction); LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged(); LaunchDiagnostics.notifyStatusChanged();
}); });
} catch (err) { } catch (err) {
// 同步异常表示异步任务未成功提交,恢复 IDLE 后允许其他页面重试。 // 同步异常表示异步任务未成功提交,恢复 IDLE 后允许其他页面重试。
StartupDiagnostics.drawReportState = DrawReportState.IDLE; LaunchDiagnostics.drawReportState = DrawReportState.IDLE;
StartupDiagnostics.lastAction = LaunchDiagnostics.lastAction =
`reportDrawnCompleted 同步异常:${StartupDiagnostics.getErrorMessage(err)}`; `reportDrawnCompleted 同步异常:${LaunchDiagnostics.getErrorMessage(err)}`;
StartupDiagnostics.logError(StartupDiagnostics.lastAction); LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged(); LaunchDiagnostics.notifyStatusChanged();
} }
}; };
StartupDiagnostics.activeObserverOwner = pageName; LaunchDiagnostics.activeObserverOwner = pageName;
StartupDiagnostics.activeDisposer = (): void => { LaunchDiagnostics.activeDisposer = (): void => {
observer.off('draw', onDraw); observer.off('draw', onDraw);
}; };
observer.on('draw', onDraw); observer.on('draw', onDraw);
StartupDiagnostics.lastAction = `已监听 ${pageName} 根组件 draw`; LaunchDiagnostics.lastAction = `已监听 ${pageName} 根组件 draw`;
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction); LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged(); LaunchDiagnostics.notifyStatusChanged();
} catch (err) { } catch (err) {
StartupDiagnostics.lastAction = `创建 draw 监听失败:${StartupDiagnostics.getErrorMessage(err)}`; LaunchDiagnostics.lastAction = `创建 draw 监听失败:${LaunchDiagnostics.getErrorMessage(err)}`;
StartupDiagnostics.logError(StartupDiagnostics.lastAction); LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged(); LaunchDiagnostics.notifyStatusChanged();
} }
} }
static disposeFirstFrame(pageName: string): void { static disposeFirstFrame(pageName: string): void {
if (pageName === '' || StartupDiagnostics.activeObserverOwner !== pageName) { if (pageName === '' || LaunchDiagnostics.activeObserverOwner !== pageName) {
return; return;
} }
const disposer: (() => void) | undefined = StartupDiagnostics.activeDisposer; const disposer: (() => void) | undefined = LaunchDiagnostics.activeDisposer;
if (disposer !== undefined) { if (disposer !== undefined) {
disposer(); disposer();
} }
StartupDiagnostics.activeObserverOwner = ''; LaunchDiagnostics.activeObserverOwner = '';
StartupDiagnostics.activeDisposer = undefined; LaunchDiagnostics.activeDisposer = undefined;
} }
static setStatusUpdatedListener(owner: string, listener: () => void): void { static setStatusUpdatedListener(owner: string, listener: () => void): void {
StartupDiagnostics.statusListenerOwner = owner; LaunchDiagnostics.statusListenerOwner = owner;
StartupDiagnostics.statusUpdatedListener = listener; LaunchDiagnostics.statusUpdatedListener = listener;
listener(); listener();
} }
static clearStatusUpdatedListener(owner: string): void { static clearStatusUpdatedListener(owner: string): void {
if (StartupDiagnostics.statusListenerOwner !== owner) { if (LaunchDiagnostics.statusListenerOwner !== owner) {
return; return;
} }
StartupDiagnostics.statusListenerOwner = ''; LaunchDiagnostics.statusListenerOwner = '';
StartupDiagnostics.statusUpdatedListener = undefined; LaunchDiagnostics.statusUpdatedListener = undefined;
} }
static getStatusText(): string { static getStatusText(): string {
return `状态:${StartupDiagnostics.getStateName()}\n` + return `状态:${LaunchDiagnostics.getStateName()}\n` +
`捕获入口:${StartupDiagnostics.capturedEntryPage}\n` + `捕获入口:${LaunchDiagnostics.capturedEntryPage}\n` +
`draw 回调:${StartupDiagnostics.drawCallbackCount}\n` + `draw 回调:${LaunchDiagnostics.drawCallbackCount}\n` +
`系统提交:${StartupDiagnostics.reportSubmitCount}\n` + `系统提交:${LaunchDiagnostics.reportSubmitCount}\n` +
`重复拦截:${StartupDiagnostics.skippedReportCount}\n` + `重复拦截:${LaunchDiagnostics.skippedReportCount}\n` +
`APP_LAUNCH:${StartupDiagnostics.launchEventCount}\n` + `APP_LAUNCH:${LaunchDiagnostics.launchEventCount}\n` +
`事件去重:${StartupDiagnostics.duplicateEventCount}\n` + `事件去重:${LaunchDiagnostics.duplicateEventCount}\n` +
`最近动作:${StartupDiagnostics.lastAction}\n` + `最近动作:${LaunchDiagnostics.lastAction}\n` +
`事件详情:${StartupDiagnostics.lastLaunchDetail}`; `事件详情:${LaunchDiagnostics.lastLaunchDetail}`;
} }
private static processAppLaunchEvents(appEventGroups: Array<hiAppEvent.AppEventGroup>): void { private static processAppLaunchEvents(appEventGroups: Array<hiAppEvent.AppEventGroup>): void {
for (let groupIndex: number = 0; groupIndex < appEventGroups.length; groupIndex++) { for (let groupIndex: number = 0; groupIndex < appEventGroups.length; groupIndex++) {
const group: hiAppEvent.AppEventGroup = appEventGroups[groupIndex]; const group: hiAppEvent.AppEventGroup = appEventGroups[groupIndex];
for (let infoIndex: number = 0; infoIndex < group.appEventInfos.length; infoIndex++) { for (let infoIndex: number = 0; infoIndex < group.appEventInfos.length; infoIndex++) {
StartupDiagnostics.processAppLaunchEvent(group.appEventInfos[infoIndex]); LaunchDiagnostics.processAppLaunchEvent(group.appEventInfos[infoIndex]);
} }
} }
} }
@@ -223,13 +239,13 @@ export class StartupDiagnostics {
const params: Record<string, Object> = JSON.parse(paramsJson) as Record<string, Object>; const params: Record<string, Object> = JSON.parse(paramsJson) as Record<string, Object>;
const startType: number = (params['start_type'] as number) ?? -1; const startType: number = (params['start_type'] as number) ?? -1;
if (startType !== 0) { if (startType !== 0) {
StartupDiagnostics.logInfo(`跳过非冷启动事件:start_type=${startType}`); LaunchDiagnostics.logInfo(`跳过非冷启动事件:start_type=${startType}`);
return; return;
} }
const extendTime: number = (params['extend_time'] as number) ?? 0; const extendTime: number = (params['extend_time'] as number) ?? 0;
if (extendTime <= 0) { if (extendTime <= 0) {
StartupDiagnostics.logInfo('跳过尚未填充 extend_time 的 APP_LAUNCH'); LaunchDiagnostics.logInfo('跳过尚未填充 extend_time 的 APP_LAUNCH');
return; return;
} }
@@ -237,17 +253,17 @@ export class StartupDiagnostics {
const iconInputTime: number = (params['icon_input_time'] as number) ?? 0; const iconInputTime: number = (params['icon_input_time'] as number) ?? 0;
const processName: string = (params['process_name'] as string) ?? ''; const processName: string = (params['process_name'] as string) ?? '';
const eventKey: string = `${eventTime}|${iconInputTime}|${startType}|${processName}`; const eventKey: string = `${eventTime}|${iconInputTime}|${startType}|${processName}`;
if (StartupDiagnostics.processedEventKeys.indexOf(eventKey) !== -1) { if (LaunchDiagnostics.processedEventKeys.indexOf(eventKey) !== -1) {
StartupDiagnostics.duplicateEventCount++; LaunchDiagnostics.duplicateEventCount++;
StartupDiagnostics.lastAction = `重复 APP_LAUNCH 已拦截:${eventKey}`; LaunchDiagnostics.lastAction = `重复 APP_LAUNCH 已拦截:${eventKey}`;
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction); LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged(); LaunchDiagnostics.notifyStatusChanged();
return; return;
} }
StartupDiagnostics.processedEventKeys.push(eventKey); LaunchDiagnostics.processedEventKeys.push(eventKey);
if (StartupDiagnostics.processedEventKeys.length > StartupDiagnostics.MAX_EVENT_KEYS) { if (LaunchDiagnostics.processedEventKeys.length > LaunchDiagnostics.MAX_EVENT_KEYS) {
StartupDiagnostics.processedEventKeys.shift(); LaunchDiagnostics.processedEventKeys.shift();
} }
const bundleName: string = (params['bundle_name'] as string) ?? ''; const bundleName: string = (params['bundle_name'] as string) ?? '';
@@ -261,22 +277,23 @@ export class StartupDiagnostics {
// 补投递事件(上次启动提交绘制但进程在事件送达前退出)的 capturedEntryPage 是当前进程的快照, // 补投递事件(上次启动提交绘制但进程在事件送达前退出)的 capturedEntryPage 是当前进程的快照,
// 不能错误归因到本次启动的页面,因此补报事件的入口标记为未知。 // 不能错误归因到本次启动的页面,因此补报事件的入口标记为未知。
const isBackfill: boolean = StartupDiagnostics.isBackfillEvent(iconInputTime); const isBackfill: boolean = LaunchDiagnostics.isBackfillEvent(iconInputTime);
StartupDiagnostics.launchEventCount++; LaunchDiagnostics.launchEventCount++;
StartupDiagnostics.lastLaunchDetail = LaunchDiagnostics.lastLaunchDetail =
`extend_time=${extendTime}ms, icon_input_time=${iconInputTime}, ` + `extend_time=${extendTime}ms, icon_input_time=${iconInputTime}, ` +
`start_type=${startType}, process_name=${processName}\n` + `start_type=${startType}, process_name=${processName}\n` +
`bundle_name=${bundleName}, bundle_version=${bundleVersion}\n` + `bundle_name=${bundleName}, bundle_version=${bundleVersion}\n` +
`animation_finish_time=${animationFinishTime}ms, response_latency=${responseLatency}ms\n` + `response_latency=${responseLatency}ms, animation_finish_time=${animationFinishTime}ms\n` +
`startability_processstart_dur=${startabilityProcessStartDur}ms, ` + `startability_processstart_dur=${startabilityProcessStartDur}ms, ` +
`appattach_to_appforeground_dur=${appattachToAppForegroundDur}ms\n` + `appattach_to_appforeground_dur=${appattachToAppForegroundDur}ms\n` +
`device_type=${StartupDiagnostics.getDeviceType()}, ` + `device_type=${LaunchDiagnostics.getDeviceType()}, ` +
`system_version=${StartupDiagnostics.getSystemVersion()}\n` + `system_version=${LaunchDiagnostics.getSystemVersion()}\n` +
`app_install_status=${LaunchDiagnostics.appInstallStatus}\n` +
`补报事件=${isBackfill ? '是(entry_page 不归因,按 unknown 处理)' : '否'}`; `补报事件=${isBackfill ? '是(entry_page 不归因,按 unknown 处理)' : '否'}`;
StartupDiagnostics.lastAction = '已捕获有效冷启动 APP_LAUNCH'; LaunchDiagnostics.lastAction = '已捕获有效冷启动 APP_LAUNCH';
StartupDiagnostics.logInfo(StartupDiagnostics.lastLaunchDetail); LaunchDiagnostics.logInfo(LaunchDiagnostics.lastLaunchDetail);
StartupDiagnostics.notifyStatusChanged(); LaunchDiagnostics.notifyStatusChanged();
} }
/** /**
@@ -285,7 +302,7 @@ export class StartupDiagnostics {
* 补报事件的 icon_input_time 早于注册时间超过容差(至少 5 秒的送达延迟)。 * 补报事件的 icon_input_time 早于注册时间超过容差(至少 5 秒的送达延迟)。
*/ */
private static isBackfillEvent(iconInputTime: number): boolean { private static isBackfillEvent(iconInputTime: number): boolean {
return iconInputTime < StartupDiagnostics.watcherRegisterTime - StartupDiagnostics.BACKFILL_TOLERANCE_MS; return iconInputTime < LaunchDiagnostics.watcherRegisterTime - LaunchDiagnostics.BACKFILL_TOLERANCE_MS;
} }
private static getDeviceType(): string { private static getDeviceType(): string {
@@ -297,7 +314,7 @@ export class StartupDiagnostics {
} }
private static getStateName(): string { private static getStateName(): string {
switch (StartupDiagnostics.drawReportState) { switch (LaunchDiagnostics.drawReportState) {
case DrawReportState.IDLE: case DrawReportState.IDLE:
return 'IDLE'; return 'IDLE';
case DrawReportState.SUBMITTED: case DrawReportState.SUBMITTED:
@@ -312,7 +329,7 @@ export class StartupDiagnostics {
} }
private static notifyStatusChanged(): void { private static notifyStatusChanged(): void {
const listener: (() => void) | undefined = StartupDiagnostics.statusUpdatedListener; const listener: (() => void) | undefined = LaunchDiagnostics.statusUpdatedListener;
if (listener !== undefined) { if (listener !== undefined) {
listener(); listener();
} }
@@ -323,10 +340,10 @@ export class StartupDiagnostics {
} }
private static logInfo(message: string): void { private static logInfo(message: string): void {
hilog.info(StartupDiagnostics.LOG_DOMAIN, 'StartupDiagnostics', '%{public}s', message); hilog.info(LaunchDiagnostics.LOG_DOMAIN, 'LaunchDiagnostics', '%{public}s', message);
} }
private static logError(message: string): void { private static logError(message: string): void {
hilog.error(StartupDiagnostics.LOG_DOMAIN, 'StartupDiagnostics', '%{public}s', message); hilog.error(LaunchDiagnostics.LOG_DOMAIN, 'LaunchDiagnostics', '%{public}s', message);
} }
} }
@@ -17,7 +17,7 @@ import { AbilityConstant, common, ConfigurationConstant, UIAbility, Want } from
import { hilog } from '@kit.PerformanceAnalysisKit'; import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI'; import { window } from '@kit.ArkUI';
import { CrashDiagnostics } from '../diagnostics/CrashDiagnostics'; import { CrashDiagnostics } from '../diagnostics/CrashDiagnostics';
import { StartupDiagnostics } from '../diagnostics/StartupDiagnostics'; import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
import { AuthSession } from '../session/AuthSession'; import { AuthSession } from '../session/AuthSession';
const DOMAIN = 0x0000; const DOMAIN = 0x0000;
@@ -26,7 +26,7 @@ export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try { try {
CrashDiagnostics.initialize(this.context as common.UIAbilityContext); CrashDiagnostics.initialize(this.context as common.UIAbilityContext);
StartupDiagnostics.initialize(); LaunchDiagnostics.initialize(this.context as common.UIAbilityContext);
AuthSession.initialize(this.context as common.UIAbilityContext); AuthSession.initialize(this.context as common.UIAbilityContext);
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) { } catch (err) {
@@ -46,7 +46,7 @@ export default class EntryAbility extends UIAbility {
// Main window is created, set main page for this ability // Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
const initialPage: string = AuthSession.isLoggedIn() ? 'pages/Index' : 'pages/StartupLogin'; const initialPage: string = AuthSession.isLoggedIn() ? 'pages/Index' : 'pages/LaunchLogin';
windowStage.loadContent(initialPage, (err) => { windowStage.loadContent(initialPage, (err) => {
if (err.code) { if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
@@ -5,7 +5,7 @@
import { common } from '@kit.AbilityKit'; import { common } from '@kit.AbilityKit';
import { CrashDiagnostics } from '../diagnostics/CrashDiagnostics'; import { CrashDiagnostics } from '../diagnostics/CrashDiagnostics';
import { StartupDiagnostics } from '../diagnostics/StartupDiagnostics'; import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
import { AuthSession } from '../session/AuthSession'; import { AuthSession } from '../session/AuthSession';
import crashNative from 'libentry.so'; import crashNative from 'libentry.so';
@@ -20,7 +20,7 @@ struct Index {
@State recordCount: number = 0; @State recordCount: number = 0;
aboutToAppear(): void { aboutToAppear(): void {
StartupDiagnostics.reportFirstFrameOnDraw( LaunchDiagnostics.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_HOME_ROOT'), () => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_HOME_ROOT'),
this.getUIContext().getHostContext() as common.UIAbilityContext, this.getUIContext().getHostContext() as common.UIAbilityContext,
PAGE_NAME PAGE_NAME
@@ -34,7 +34,7 @@ struct Index {
} }
aboutToDisappear(): void { aboutToDisappear(): void {
StartupDiagnostics.disposeFirstFrame(PAGE_NAME); LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
CrashDiagnostics.clearRecordsUpdatedListener(); CrashDiagnostics.clearRecordsUpdatedListener();
} }
@@ -44,7 +44,7 @@ struct Index {
return; return;
} }
try { try {
this.getUIContext().getRouter().replaceUrl({ url: 'pages/StartupLogin' }).catch((err: Error) => { this.getUIContext().getRouter().replaceUrl({ url: 'pages/LaunchLogin' }).catch((err: Error) => {
this.statusText = `退出登录后跳转失败:${err.message}`; this.statusText = `退出登录后跳转失败:${err.message}`;
}); });
} catch (err) { } catch (err) {
@@ -4,23 +4,23 @@
*/ */
import { common } from '@kit.AbilityKit'; import { common } from '@kit.AbilityKit';
import { StartupDiagnostics } from '../diagnostics/StartupDiagnostics'; import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
import { AuthSession } from '../session/AuthSession'; import { AuthSession } from '../session/AuthSession';
const PAGE_NAME: string = 'StartupLogin'; const PAGE_NAME: string = 'LaunchLogin';
@Entry @Entry
@Component @Component
struct StartupLogin { struct LaunchLogin {
@State account: string = 'demo@example.com'; @State account: string = 'demo@example.com';
@State password: string = '123456'; @State password: string = '123456';
@State startupStatus: string = StartupDiagnostics.getStatusText(); @State startupStatus: string = LaunchDiagnostics.getStatusText();
aboutToAppear(): void { aboutToAppear(): void {
StartupDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => { LaunchDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
this.startupStatus = StartupDiagnostics.getStatusText(); this.startupStatus = LaunchDiagnostics.getStatusText();
}); });
StartupDiagnostics.reportFirstFrameOnDraw( LaunchDiagnostics.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_LOGIN_ROOT'), () => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_LOGIN_ROOT'),
this.getUIContext().getHostContext() as common.UIAbilityContext, this.getUIContext().getHostContext() as common.UIAbilityContext,
PAGE_NAME PAGE_NAME
@@ -28,41 +28,41 @@ struct StartupLogin {
} }
aboutToDisappear(): void { aboutToDisappear(): void {
StartupDiagnostics.disposeFirstFrame(PAGE_NAME); LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
StartupDiagnostics.clearStatusUpdatedListener(PAGE_NAME); LaunchDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
} }
private openNetworkScenario(): void { private openNetworkScenario(): void {
this.navigateTo('pages/StartupNetwork'); this.navigateTo('pages/LaunchNetwork');
} }
private openPrivacyScenario(): void { private openPrivacyScenario(): void {
this.navigateTo('pages/StartupPrivacy'); this.navigateTo('pages/LaunchPrivacy');
} }
private completeLogin(): void { private completeLogin(): void {
if (!AuthSession.setLoggedIn(true)) { if (!AuthSession.setLoggedIn(true)) {
this.startupStatus = `保存登录状态失败,请重试。\n${StartupDiagnostics.getStatusText()}`; this.startupStatus = `保存登录状态失败,请重试。\n${LaunchDiagnostics.getStatusText()}`;
return; return;
} }
try { try {
this.getUIContext().getRouter().replaceUrl({ url: 'pages/Index' }).catch((err: Error) => { this.getUIContext().getRouter().replaceUrl({ url: 'pages/Index' }).catch((err: Error) => {
this.startupStatus = `进入首页失败:${err.message}\n${StartupDiagnostics.getStatusText()}`; this.startupStatus = `进入首页失败:${err.message}\n${LaunchDiagnostics.getStatusText()}`;
}); });
} catch (err) { } catch (err) {
const message: string = err instanceof Error ? err.message : JSON.stringify(err); const message: string = err instanceof Error ? err.message : JSON.stringify(err);
this.startupStatus = `进入首页同步异常:${message}\n${StartupDiagnostics.getStatusText()}`; this.startupStatus = `进入首页同步异常:${message}\n${LaunchDiagnostics.getStatusText()}`;
} }
} }
private navigateTo(url: string): void { private navigateTo(url: string): void {
try { try {
this.getUIContext().getRouter().pushUrl({ url: url }).catch((err: Error) => { this.getUIContext().getRouter().pushUrl({ url: url }).catch((err: Error) => {
this.startupStatus = `页面跳转失败:${err.message}\n${StartupDiagnostics.getStatusText()}`; this.startupStatus = `页面跳转失败:${err.message}\n${LaunchDiagnostics.getStatusText()}`;
}); });
} catch (err) { } catch (err) {
const message: string = err instanceof Error ? err.message : JSON.stringify(err); const message: string = err instanceof Error ? err.message : JSON.stringify(err);
this.startupStatus = `页面跳转同步异常:${message}\n${StartupDiagnostics.getStatusText()}`; this.startupStatus = `页面跳转同步异常:${message}\n${LaunchDiagnostics.getStatusText()}`;
} }
} }
@@ -4,20 +4,20 @@
*/ */
import { common } from '@kit.AbilityKit'; import { common } from '@kit.AbilityKit';
import { StartupDiagnostics } from '../diagnostics/StartupDiagnostics'; import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
const PAGE_NAME: string = 'StartupNetwork'; const PAGE_NAME: string = 'LaunchNetwork';
@Entry @Entry
@Component @Component
struct StartupNetwork { struct LaunchNetwork {
@State startupStatus: string = StartupDiagnostics.getStatusText(); @State startupStatus: string = LaunchDiagnostics.getStatusText();
aboutToAppear(): void { aboutToAppear(): void {
StartupDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => { LaunchDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
this.startupStatus = StartupDiagnostics.getStatusText(); this.startupStatus = LaunchDiagnostics.getStatusText();
}); });
StartupDiagnostics.reportFirstFrameOnDraw( LaunchDiagnostics.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_NETWORK_ROOT'), () => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_NETWORK_ROOT'),
this.getUIContext().getHostContext() as common.UIAbilityContext, this.getUIContext().getHostContext() as common.UIAbilityContext,
PAGE_NAME PAGE_NAME
@@ -25,18 +25,18 @@ struct StartupNetwork {
} }
aboutToDisappear(): void { aboutToDisappear(): void {
StartupDiagnostics.disposeFirstFrame(PAGE_NAME); LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
StartupDiagnostics.clearStatusUpdatedListener(PAGE_NAME); LaunchDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
} }
private retryNetwork(): void { private retryNetwork(): void {
try { try {
this.getUIContext().getRouter().replaceUrl({ url: 'pages/StartupLogin' }).catch((err: Error) => { this.getUIContext().getRouter().replaceUrl({ url: 'pages/LaunchLogin' }).catch((err: Error) => {
this.startupStatus = `页面跳转失败:${err.message}\n${StartupDiagnostics.getStatusText()}`; this.startupStatus = `页面跳转失败:${err.message}\n${LaunchDiagnostics.getStatusText()}`;
}); });
} catch (err) { } catch (err) {
const message: string = err instanceof Error ? err.message : JSON.stringify(err); const message: string = err instanceof Error ? err.message : JSON.stringify(err);
this.startupStatus = `页面跳转同步异常:${message}\n${StartupDiagnostics.getStatusText()}`; this.startupStatus = `页面跳转同步异常:${message}\n${LaunchDiagnostics.getStatusText()}`;
} }
} }
@@ -4,20 +4,20 @@
*/ */
import { common } from '@kit.AbilityKit'; import { common } from '@kit.AbilityKit';
import { StartupDiagnostics } from '../diagnostics/StartupDiagnostics'; import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
const PAGE_NAME: string = 'StartupPrivacy'; const PAGE_NAME: string = 'LaunchPrivacy';
@Entry @Entry
@Component @Component
struct StartupPrivacy { struct LaunchPrivacy {
@State startupStatus: string = StartupDiagnostics.getStatusText(); @State startupStatus: string = LaunchDiagnostics.getStatusText();
aboutToAppear(): void { aboutToAppear(): void {
StartupDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => { LaunchDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
this.startupStatus = StartupDiagnostics.getStatusText(); this.startupStatus = LaunchDiagnostics.getStatusText();
}); });
StartupDiagnostics.reportFirstFrameOnDraw( LaunchDiagnostics.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_PRIVACY_ROOT'), () => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_PRIVACY_ROOT'),
this.getUIContext().getHostContext() as common.UIAbilityContext, this.getUIContext().getHostContext() as common.UIAbilityContext,
PAGE_NAME PAGE_NAME
@@ -25,26 +25,26 @@ struct StartupPrivacy {
} }
aboutToDisappear(): void { aboutToDisappear(): void {
StartupDiagnostics.disposeFirstFrame(PAGE_NAME); LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
StartupDiagnostics.clearStatusUpdatedListener(PAGE_NAME); LaunchDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
} }
private acceptPrivacy(): void { private acceptPrivacy(): void {
this.navigateTo('pages/StartupLogin'); this.navigateTo('pages/LaunchLogin');
} }
private showNetworkError(): void { private showNetworkError(): void {
this.navigateTo('pages/StartupNetwork'); this.navigateTo('pages/LaunchNetwork');
} }
private navigateTo(url: string): void { private navigateTo(url: string): void {
try { try {
this.getUIContext().getRouter().replaceUrl({ url: url }).catch((err: Error) => { this.getUIContext().getRouter().replaceUrl({ url: url }).catch((err: Error) => {
this.startupStatus = `页面跳转失败:${err.message}\n${StartupDiagnostics.getStatusText()}`; this.startupStatus = `页面跳转失败:${err.message}\n${LaunchDiagnostics.getStatusText()}`;
}); });
} catch (err) { } catch (err) {
const message: string = err instanceof Error ? err.message : JSON.stringify(err); const message: string = err instanceof Error ? err.message : JSON.stringify(err);
this.startupStatus = `页面跳转同步异常:${message}\n${StartupDiagnostics.getStatusText()}`; this.startupStatus = `页面跳转同步异常:${message}\n${LaunchDiagnostics.getStatusText()}`;
} }
} }
@@ -1,8 +1,8 @@
{ {
"src": [ "src": [
"pages/StartupLogin", "pages/LaunchLogin",
"pages/StartupNetwork", "pages/LaunchNetwork",
"pages/StartupPrivacy", "pages/LaunchPrivacy",
"pages/Index", "pages/Index",
"pages/CrashHistory" "pages/CrashHistory"
] ]
@@ -6,11 +6,11 @@
## 2. 总体链路 ## 2. 总体链路
`APP_LAUNCH` 系统事件提供启动类型和系统耗时;首个有效入口页完成首次绘制后,通过 `reportDrawnCompleted` 标记终点。当前客户端使用 `ElkService` 异步上传结构化指标,后台再按模块配置阈值、聚合和看板。若后续具备 HarmonyOS 自定义 APM SDK,可替换上传层,但保留事件采集和去重逻辑。 `APP_LAUNCH` 系统事件提供启动类型和系统耗时;首个有效入口页完成首次绘制后,通过 `reportDrawnCompleted` 标记终点。当前客户端双通道上报:`ElkService` 异步上传结构化追溯明细(11 字段),`HXEventMonitor` 分桶指标记录原始耗时由 SDK 按桶聚合(详见 6.2),后台再按模块配置阈值、聚合和看板。若后续具备 HarmonyOS 自定义 APM SDK,可替换上传层,但保留事件采集和去重逻辑。
## 3. 客户端接入 ## 3. 客户端接入
实现位于 `future_entry_demo/src/main/ets/monitor/StartupMonitor.ets`。`EntryAbility.onCreate` 必须尽早调用 `StartupMonitor.init()`,先注册 watcher,再允许入口页提交首帧完成事件。`init()` 本身应保持幂等,避免同一进程重复注册 watcher。 实现位于 `future_entry_demo/src/main/ets/monitor/LaunchMonitor.ets`。`EntryAbility.onCreate` 必须尽早调用 `LaunchMonitor.init()`,先注册 watcher,再允许入口页提交首帧完成事件。`init()` 本身应保持幂等,避免同一进程重复注册 watcher。
监听 `APP_LAUNCH` 后读取: 监听 `APP_LAUNCH` 后读取:
@@ -40,19 +40,23 @@ time + icon_input_time + start_type + process_name
## 4. 指标定义 ## 4. 指标定义
| 项目 | 建议值 | | 项目 | 建议值 |
|---|---| | ---- | -------------------------------------------------------------------------------------------------------------------------------------- |
| 模块 | `launch` | | 模块 | `launch` |
| 指标名 | `cold_start_time` | | 指标名 | `cold_start_time` |
| 指标值 | 冷启动耗时,单位 ms | | 指标值 | 冷启动耗时,单位 ms |
| 维度 | `start_type`、`entry_page`、`app_version` | | 维度 | `start_type`、`entry_page`、`app_version`、`app_install_status` |
| 追溯字段 | `event_time`、`icon_input_time`、`process_name`、`extend_time`、`animation_finish_time`、`bundle_version`、`bundle_name`、`response_latency`、`startability_processstart_dur`、`appattach_to_appforeground_dur`、`device_type`、`system_version`、`build_mode`、`target_name`、`git_commit`、`is_first_install`、`is_backfill` | | 追溯字段 | `response_latency`、`animation_finish_time`、`startability_processstart_dur`、`appattach_to_appforeground_dur`、`git_commit`、`is_backfill` |
| 分桶 | `[500, 800, 1200, 2000, 3000]` | | 分桶 | `[500, 600, 700, 800, 1000, 1500, 2000, 3000]` |
模块名使用小写字母和连字符;指标名不包含连字符。维度控制在必要范围内,避免将用户 ID、完整 URL 等高基数字段作为维度。 上传负载为最小必要集:指标 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 启动时间拆解 ### 4.1 启动时间拆解
`cold_start_time` 之外,通过系统字段可拆分冷启动各分段,用于定位劣化归属: `cold_start_time` 之外,追溯通道已随上传携带各分段耗时(见上表),可拆分冷启动定位劣化归属:
```text ```text
离手 ──response_latency──► 动效开始 ──(animation_finish_time − response_latency)──► 动效完成 ──(extend_time − animation_finish_time)──► 首帧绘制完成 离手 ──response_latency──► 动效开始 ──(animation_finish_time − response_latency)──► 动效完成 ──(extend_time − animation_finish_time)──► 首帧绘制完成
@@ -62,17 +66,90 @@ time + icon_input_time + start_type + process_name
- `response_latency`:离手到动效开始的耗时(需 API 22+),反映系统响应快慢。 - `response_latency`:离手到动效开始的耗时(需 API 22+),反映系统响应快慢。
- `startability_processstart_dur`:系统启动 Ability 到进程创建完成(仅冷启动),反映系统侧进程启动。 - `startability_processstart_dur`:系统启动 Ability 到进程创建完成(仅冷启动),反映系统侧进程启动。
- `appattach_to_appforeground_dur`:进程初始化完成到应用切前台(仅冷启动),反映系统侧应用挂载。 - `appattach_to_appforeground_dur`:进程初始化完成到应用切前台(仅冷启动),反映系统侧应用挂载。
- `extend_time − animation_finish_time`:动效完成到应用首帧的耗时,即**应用侧可优化的独占部分**;看板可据此判断劣化发生在系统侧还是应用侧。 - `extend_time − animation_finish_time`:动效完成到应用首帧的耗时,即**应用侧可优化的独占部分**;看板可据此判断劣化发生在系统侧还是应用侧。注意 `animation_finish_time` 在部分机型为 `0`(系统未填充),此时该段等于 `extend_time` 全量,聚合时需兼容。
环境追溯字段说明:`build_mode`/`target_name` 区分 debug/forTest/Official 环境,避免测试包污染线上看板;`is_first_install`(`StartManager.getAppInstallStatus()`)标记安装后首次启动,该次耗时显著偏高,不标记会拉高 P50/P95;`git_commit` 精确到代码提交,配合内部版本号做代码级追溯。 未上传的候选字段(按需启用):环境 `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. 后台与看板配置
按新增 PDF 的自定义 APM 模块流程,在 APM 后台创建 `launch` 模块并启用:全量采样(`sampling_rate: 1000`)、60 秒聚合(`aggre_time: 60`)、累计 100 条触发上报(`aggre_count: 100`)。随后为该模块补充 ELK 索引模板,在 Grafana 建立按版本、启动类型和时间聚合的 P50/P95、超 2 秒占比及分桶分布面板。 ### 5.1 模块策略下发(链路 B 激活前提)
链路 B 是**策略驱动**的:SDK 侧 `HXEventMonitorPlugin.parseMonitorStrategy()` 读取后台策略注入 `EventStrategyBean`,若 `launch` 模块无开启策略,`create()/record()` 一律空转并打印 `Monitor.Event cold_start_time eventId has not created!`,应用侧无法绕开。因此必须在 APM 后台为 **module=`launch`** 下发如下策略:
```json
{
"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` | **必须 `1`(STATUS_OPEN)**;否则 `isOpen()` 为 false,`create/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=null`,`create`/`getEventMonitor` 返回 `NullDataMonitorImp`,`record` 空转打印 `eventId has not created!`。**插件强制开启绕不过指标级门控,必须后台下发策略。**
- **`isOpen()` 语义**:`status == 1(STATUS_OPEN)` 且抽样通过(`isOpenByRandom`,对应 `sampling_rate`)。
- **聚合上传触发**:`onRecord → isNeedUpload`——累计条数 ≥ `aggregatorSizeThreshold`(`aggre_count`)或距上次上传 ≥ `aggregatorTimeThreshold`(`aggre_time`)即触发 `push`。
- **重启生效机制**:`LaunchMonitor.ensureColdStartMetric()` 的幂等标记在 `create` 被策略拒绝(返回空实现)时仍置 `true`——本进程内策略下发后不会重试创建,**必须重启应用**(与上节"判定生效"一致);策略未下发时链路 A(Elk 追溯)不受影响、正常上传。
随后为该模块补充 ELK 索引模板,在 Grafana 建立看板面板(建议形态):
```json
{
"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. 兼容与兜底 ## 6. 兼容与兜底
当前通过 `ElkService` 上传 `cold_start_time`、维度和原始系统字段,但它不具备 PDF 所述的指标聚合语义,需要后端补充索引、聚合规则和看板。不要直接复用 Android 文档中的 Java API;后续接入 HarmonyOS 自定义 APM SDK 时,仅替换上传实现。 链路 A(`ElkService` 追溯)上传 `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` 去重,兼容系统重新投递。 `APP_LAUNCH` 的事件解析必须与崩溃事件分开处理:崩溃事件依赖异常和日志字段,启动事件不包含这些字段。上传应异步执行,不阻塞首页渲染;以 `time + icon_input_time + start_type + process_name` 去重,兼容系统重新投递。
@@ -87,6 +164,15 @@ time + icon_input_time + start_type + process_name
真机实测确认(2026-08-05):部署新版本后先 force-stop 再冷启动,新进程收到旧进程的遗留事件,判定 `补报事件=是`(`icon_input_time` 早于注册时间约 4 秒);随后正常冷启动收到自身事件,判定 `补报事件=否`。双向判定均正确,正常启动无误判。 真机实测确认(2026-08-05):部署新版本后先 force-stop 再冷启动,新进程收到旧进程的遗留事件,判定 `补报事件=是`(`icon_input_time` 早于注册时间约 4 秒);随后正常冷启动收到自身事件,判定 `补报事件=否`。双向判定均正确,正常启动无误判。
### 6.2 双通道上报
启动指标同时走两条通道,职责分离:
- **分桶指标(`HXEventMonitor`,module=`launch`)**:`record(extendTime)` 后由 SDK 按桶聚合,负责看板聚合统计(P50/P95、超 2 秒占比、分桶分布)。**策略驱动**:无后台下发策略时 `create()/record()` 空转(见 5.1),`record` 失败/空转不影响启动流程,聚合逻辑在 SDK 侧。实现为 `HXLaunchMonitorPlugin`(仿 `HXCrashMonitorPlugin`,`startPlugin()` 中建指标),在 `APMHelper` 的 `.plugin(...)` 列表注册。
- **追溯明细(`ElkService`,biz=`launch`)**:11 字段最小集(指标/4 维度含 `app_install_status`/启动生命周期各阶段时间/`git_commit`/`is_backfill`),负责明细追溯与后端校验;其他字段按需加回。分桶指标同步 4 个维度(`setDimension4Name('app_install_status')`),与追溯维度一致。
两通道独立失败互不影响;`HXEventMonitor` 的 module 参数为显式传入的 `'launch'`,与文档模块定义一致,不经过 `APMElkService` 固定的 `'apm'` 来源。
## 7. 验收标准 ## 7. 验收标准
1. 单次冷启动中,隐私页、网络页和首页连续跳转时,`reportDrawnCompleted` 仍只提交一次。 1. 单次冷启动中,隐私页、网络页和首页连续跳转时,`reportDrawnCompleted` 仍只提交一次。
@@ -95,12 +181,14 @@ time + icon_input_time + start_type + process_name
4. 页面在系统异步回调前销毁时,不影响已经提交的上报;旧页面不会释放新页面 observer。 4. 页面在系统异步回调前销毁时,不影响已经提交的上报;旧页面不会释放新页面 observer。
5. 本地日志与后台数据量级一致;看板可查询 P50、P95、超 2 秒占比及分桶分布。 5. 本地日志与后台数据量级一致;看板可查询 P50、P95、超 2 秒占比及分桶分布。
6. 断网或上传失败不影响启动流程,网络恢复后按既有上传机制重试。 6. 断网或上传失败不影响启动流程,网络恢复后按既有上传机制重试。
7. 双通道上传验证:完整 workspace 编译通过(`HXEventMonitor` 的 `DataMonitorBuilder`/`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. 已验证结果
### 8.1 首次回归(2026-08-05,`CrashDiagnosticsDemo`,nova 14 Pro) ### 8.1 首次回归(2026-08-05,`CrashDiagnosticsDemo`,nova 14 Pro)
未登录冷启动入口为 `StartupLogin`,`extend_time` 约 `158ms`;持久化登录后冷启动入口为 `Index`,`extend_time` 约 `155ms`。网络异常、网络恢复、隐私确认及页面返回场景均保持一次 draw 回调和一次系统提交,后续页面注册被全局保护拦截;`start_type=1` 的热启动事件被正确忽略。 未登录冷启动入口为 `LaunchLogin`,`extend_time` 约 `158ms`;持久化登录后冷启动入口为 `Index`,`extend_time` 约 `155ms`。网络异常、网络恢复、隐私确认及页面返回场景均保持一次 draw 回调和一次系统提交,后续页面注册被全局保护拦截;`start_type=1` 的热启动事件被正确忽略。
### 8.2 追溯字段扩展后的真机验证(2026-08-05,`CrashDiagnosticsDemo`,nova 14 Pro,OpenHarmony-6.1.1.120) ### 8.2 追溯字段扩展后的真机验证(2026-08-05,`CrashDiagnosticsDemo`,nova 14 Pro,OpenHarmony-6.1.1.120)
@@ -108,8 +196,8 @@ time + icon_input_time + start_type + process_name
|---|---|---|---| |---|---|---|---|
| 1 | 已登录冷启动 | `aa force-stop` 后启动 | 入口 `Index`,draw 1 次、提交 1 次、成功 1 次,`extend_time=144ms`,`start_type=0` | | 1 | 已登录冷启动 | `aa force-stop` 后启动 | 入口 `Index`,draw 1 次、提交 1 次、成功 1 次,`extend_time=144ms`,`start_type=0` |
| 2 | 已登录冷启动(复测) | `aa force-stop` 后启动 | 入口 `Index`,`extend_time=137ms`,单次提交 | | 2 | 已登录冷启动(复测) | `aa force-stop` 后启动 | 入口 `Index`,`extend_time=137ms`,单次提交 |
| 3 | 未登录冷启动 | `bm clean -d` 清数据后启动 | 入口 `StartupLogin`,`extend_time=152ms`,单次提交 | | 3 | 未登录冷启动 | `bm clean -d` 清数据后启动 | 入口 `LaunchLogin`,`extend_time=152ms`,单次提交 |
| 4 | 多入口连续跳转 | 登录页 → 网络异常页 → 返回 → 隐私确认页 | `StartupNetwork`、`StartupPrivacy` 重复首帧注册均被拦截(`DrawReportState` 保护),无重复提交 | | 4 | 多入口连续跳转 | 登录页 → 网络异常页 → 返回 → 隐私确认页 | `LaunchNetwork`、`LaunchPrivacy` 重复首帧注册均被拦截(`DrawReportState` 保护),无重复提交 |
| 5 | 时间拆解字段捕获 | 观察三次冷启动的 APP_LAUNCH 详情 | `response_latency=19~24ms`、`startability_processstart_dur=57~83ms`、`appattach_to_appforeground_dur=28~33ms`,均为非零有效值 | | 5 | 时间拆解字段捕获 | 观察三次冷启动的 APP_LAUNCH 详情 | `response_latency=19~24ms`、`startability_processstart_dur=57~83ms`、`appattach_to_appforeground_dur=28~33ms`,均为非零有效值 |
| 6 | 热启动(进程存活) | 对运行中进程再次 `aa start` | 无新 `APP_LAUNCH` 事件产生(系统对运行中单例不视为启动),不产生上报 | | 6 | 热启动(进程存活) | 对运行中进程再次 `aa start` | 无新 `APP_LAUNCH` 事件产生(系统对运行中单例不视为启动),不产生上报 |
@@ -123,7 +211,7 @@ time + icon_input_time + start_type + process_name
| # | 场景 | 结果 | | # | 场景 | 结果 |
|---|---|---| |---|---|---|
| 1 | 清数据后未登录冷启动 | 入口 `StartupLogin`,`extend_time=161ms`,单次提交 | | 1 | 清数据后未登录冷启动 | 入口 `LaunchLogin`,`extend_time=161ms`,单次提交 |
| 2 | 登录后同进程跳转 `Index` | `Index` 重复首帧注册被拦截,无重复提交 | | 2 | 登录后同进程跳转 `Index` | `Index` 重复首帧注册被拦截,无重复提交 |
| 3 | 已登录冷启动 | 入口 `Index`,`extend_time=138ms`,单次提交 | | 3 | 已登录冷启动 | 入口 `Index`,`extend_time=138ms`,单次提交 |
| 4 | 热启动(进程存活) | 无新 `APP_LAUNCH` 事件,不产生上报 | | 4 | 热启动(进程存活) | 无新 `APP_LAUNCH` 事件,不产生上报 |
@@ -144,7 +232,7 @@ time + icon_input_time + start_type + process_name
每组完整链路:清数据 → 未登录冷启动 → 网络异常页跳转(拦截)→ 返回 → 隐私确认页跳转(拦截)→ 返回 → 模拟登录 → `Index` 跳转(拦截)→ 已登录冷启动 → 热启动(无新事件)。3 组结果: 每组完整链路:清数据 → 未登录冷启动 → 网络异常页跳转(拦截)→ 返回 → 隐私确认页跳转(拦截)→ 返回 → 模拟登录 → `Index` 跳转(拦截)→ 已登录冷启动 → 热启动(无新事件)。3 组结果:
| 组 | 未登录冷启动(入口 `StartupLogin`) | 网络页拦截 | 隐私页拦截 | `Index` 拦截 | 已登录冷启动(入口 `Index`) | | 组 | 未登录冷启动(入口 `LaunchLogin`) | 网络页拦截 | 隐私页拦截 | `Index` 拦截 | 已登录冷启动(入口 `Index`) |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 1 | `extend_time=162ms`(进程 44062) | ✓ | ✓ | ✓ | `extend_time=133ms`(进程 44644) | | 1 | `extend_time=162ms`(进程 44062) | ✓ | ✓ | ✓ | `extend_time=133ms`(进程 44644) |
| 2 | `extend_time=157ms`(进程 44927) | ✓ | ✓ | ✓ | `extend_time=131ms`(进程 45200) | | 2 | `extend_time=157ms`(进程 44927) | ✓ | ✓ | ✓ | `extend_time=131ms`(进程 45200) |
@@ -171,7 +259,7 @@ time + icon_input_time + start_type + process_name
| 步骤 | 组 1 | 组 2 | 组 3 | | 步骤 | 组 1 | 组 2 | 组 3 |
|---|---|---|---| |---|---|---|---|
| 未登录冷启动(`StartupLogin`) | 158ms / 补报=否 | 180ms / 补报=否 | 180ms / 补报=否 | | 未登录冷启动(`LaunchLogin`) | 158ms / 补报=否 | 180ms / 补报=否 | 180ms / 补报=否 |
| 网络页重复注册拦截 | ✓ | ✓ | ✓ | | 网络页重复注册拦截 | ✓ | ✓ | ✓ |
| 隐私页重复注册拦截 | ✓ | ✓ | ✓ | | 隐私页重复注册拦截 | ✓ | ✓ | ✓ |
| `Index` 重复注册拦截 | ✓ | ✓ | ✓ | | `Index` 重复注册拦截 | ✓ | ✓ | ✓ |
@@ -181,3 +269,39 @@ time + icon_input_time + start_type + process_name
| 1 秒退出(首帧已提交)后重启 | 先收遗留事件补报=是,再收自身事件补报=否 | 同左 | 同左 | | 1 秒退出(首帧已提交)后重启 | 先收遗留事件补报=是,再收自身事件补报=否 | 同左 | 同左 |
24 个断言(3 组 × 8 步)全部通过,组间一致。补报判定在每组中均双向正确:遗留事件稳定判定为补报(`entry_page` 不归因),自身事件稳定判定为非补报(`entry_page` 正常归因)。 24 个断言(3 组 × 8 步)全部通过,组间一致。补报判定在每组中均双向正确:遗留事件稳定判定为补报(`entry_page` 不归因),自身事件稳定判定为非补报(`entry_page` 正常归因)。
### 8.8 重装 + `is_first_install` 维度全场景验证(2026-08-06,`CrashDiagnosticsDemo`,nova 14 Pro)
**前置改动**:demo 同步 `is_first_install` 维度(`preferences` 无启动记录视为安装后首启,`putSync` 后 `flushSync` 同步落盘——初版遗漏 `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` 维度语义正确**:安装/清数据后首启 = `true`(161~165ms),非首启 = `false`(127~134ms);**首启耗时系统性偏高约 30ms**,该维度可将其从常规分布中区分,避免拉高 P50/P95。
- 补报判定 3 组双向正确,拦截保护 3 组 × 3 页无漏防。
- 耗时稳定(127~165ms);此前观测到的 `response_latency=5086ms` 异常值本次未复现(偶发)。
- 注意:demo 的 `is_first_install`(boolean)基于 `preferences` 记录,`bm clean` 清数据后该标记重置(再次显示首启);生产实现为 `app_install_status`(`normal`/`new_install`/`over_written` 三值,见第 4 节),以生产为准。
### 8.9 真实应用验证(2026-08-06,完整 workspace,nova 14 Pro)
在完整 workspace 落地(`HXLaunchMonitorPlugin` 插件 + `APMHelper` 注册)后的真机数据:
| 场景 | 结果 |
|------|------|
| 全新安装首启(`new_install`) | 链路 A:`cold_start_time=1141ms`,Entry=Index,Elk 上报 ✅ |
| 二次冷启动(`normal`) | 链路 A:`cold_start_time=1717ms`,Elk 上报 ✅ |
| 链路 B(分桶) | 待后台下发 `launch` 策略(5.1)后复测 |
编译:`claude_compile.sh → BUILD_SUCCESS`(链路 B API 随插件编译验证通过)。
真实应用耗时(1.1~1.7s)远高于 demo(127~165ms),符合生产首页复杂度的预期;`app_install_status` 维度可区分首启与常规启动的耗时差异。
+128
View File
@@ -0,0 +1,128 @@
# 启动性能监控(冷启动耗时)接入配置
> 作者:cheliangzhao
> 日期:2026-08-06
> 涉及模块:`entry`(HAP 入口)+ `@kernel/app_monitor_event`(APM 事件 SDK)
> 对应需求:冷启动耗时(`cold_start_time`)采集与看板聚合
---
## 1. 背景与目标
统计同花顺期货鸿蒙版**冷启动耗时**,覆盖三条启动入口(`Index` / `LauncherPage` / `NetworkAnomalyPage`),区分冷启动 / 补投递四种场景,实现启动性能的持续观测与劣化告警。
采集分两条通道:
- **链路 A(Elk 追溯,已上线验证)**:把冷启动指标结构化 JSON 经 `ElkService` 异步上报,字段最全,可明细定位。
- **链路 B(HXEventMonitor 分桶,需后台策略开启后生效)**:由 SDK 按桶自动归集,负责看板聚合曲线。
> ⚠️ 链路 B 是**策略驱动**的:SDK 侧在 `EventMonitorStrategy` 找不到 `launch` 模块的开启策略时,`create()/record()` 一律空转并打印 `Monitor.Event cold_start_time eventId has not created!`。应用侧无法绕开,必须由 **APM 后台下发对应模块策略**才能激活。
---
## 2. 链路 A:Elk 追溯(已生效,无需后台操作)
上报字段(`LaunchElkBuilder`,业务来源 `launch`):
| 字段 | 类型 | 说明 |
|------|------|------|
| `cold_start_time` | number | 冷启动耗时 ms = 系统事件 `extend_time` |
| `start_type` | number | 启动类型(当前只统计 `0`=冷启动) |
| `entry_page` | string | 入口页(`Index`/`LauncherPage`/`NetworkAnomalyPage`;补投递为 `unknown`) |
| `app_version` | string | 应用内部版本号 |
| `app_install_status` | string | `normal`/`new_install`/`over_written` |
| `response_latency` | number | 离手到动效开始(需 API 22+) |
| `animation_finish_time` | number | 动效完成耗时 |
| `startability_processstart_dur` | number | 进程创建段(仅冷启动) |
| `appattach_to_appforeground_dur` | number | 进程挂载段(仅冷启动) |
| `git_commit` | string | 代码级追溯,劣化定位用 |
| `is_backfill` | boolean | 是否系统补投递的上次启动遗留事件 |
---
## 3. 链路 B:HXEventMonitor 分桶指标(需后台开启)
### 3.1 后台需下发的模块策略
SDK 由 `HXEventMonitorPlugin.parseMonitorStrategy()` 读取,注入到 `EventStrategyBean` 后按模块打开。请为 **module=`launch`** 下发如下配置:
```json
{
"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` | **必须 `1`(STATUS_OPEN)**;否则 `isOpen()` 为 false,`create/record` 全空转 |
| `sampling_rate` | `0~1` | 抽样率,`isOpen()` 需 `status==1 && 抽样通过` 才生效 |
| `interval` | ms | 采集间隔 |
| `aggre_time` | ms | 聚合时间阈值,达到即触发一次 push |
| `aggre_count` | 条 | 聚合数量阈值,达到即触发一次 push |
### 3.2 指标定义
| 项 | 值 |
|----|----|
| 模块名 `module` | `launch` |
| 指标名 `metric` | `cold_start_time` |
| 值域 | 冷启动耗时 ms(`extend_time`) |
| 分桶 `buckets` | `[500, 600, 700, 800, 1000, 1500, 2000, 3000]`(按实测 600–800ms 主体细分,尾部留劣化锚点) |
| 维度1 `start_type` | 冷启动 `0` |
| 维度2 `entry_page` | `Index`/`LauncherPage`/`NetworkAnomalyPage`/`unknown`(补投递) |
| 维度3 `app_version` | 应用内部版本号 |
| 维度4 `app_install_status` | `normal`/`new_install`/`over_written` |
### 3.3 判定生效
后台下发后,重启应用,冷启动日志中:
- ✅ 消失:`Monitor.Event cold_start_time eventId has not created!`
- ✅ 出现:`Cold start bucket metric recorded: <ms>` 与 SDK 聚合 push 日志
- ✅ 看板出现 `launch` 模块 `cold_start_time` 桶分布
---
## 4. 相关代码与文件
| 文件 | 作用 |
|------|------|
| `entry/src/main/ets/monitor/LaunchMonitor.ets` | APP_LAUNCH 订阅、Elk 上报、首帧 `reportDrawnCompleted`、桶指标定义与 record |
| `entry/src/main/ets/monitor/LaunchMonitor.ets`(`ensureColdStartMetric`) | 创建 `launch/cold_start_time` 桶指标(幂等) |
| `entry/src/main/ets/monitor/plugin/HXLaunchMonitorPlugin.ets` | 仿 `HXCrashMonitorPlugin` 接入的启动插件,在 `startPlugin()` 建指标 |
| `entry/src/main/ets/monitor/APMHelper.ets` | 注册 `HXLaunchMonitorPlugin` 及配置类、`.plugin(...)` 列表 |
| `entry/src/main/ets/entryability/EntryAbility.ets` | `onCreate` 早注册 `LaunchMonitor.init()` |
| `entry/src/main/ets/pages/Index.ets` / `LauncherPage.ets` / `network/NetworkAnomalyPage.ets` | 首帧 `HOME_ROOT` 监听 + `reportFirstFrameOnDraw`/`dispose` |
### 打点时序
```
EntryAbility.onCreate → LaunchMonitor.init()(注册 APP_LAUNCH watcher)
入口页 aboutToAppear → reportFirstFrameOnDraw(HOME_ROOT) → 首帧 draw → reportDrawnCompleted()
系统产生 APP_LAUNCH 事件(冷启动)→ onReceive
├─ 链路A: ElkService 上传冷启动 JSON(字段见 §2)
└─ 链路B: HXEventMonitor record(cold_start_time)(依赖 launch 策略,见 §3)
补投递:icon_input_time 早于 watcher 注册 > 2000ms → 判为 backfill,entry_page=unknown
```
---
## 5. 真机验证结论(2026-08-06,nova 14 Pro)
| 场景 | 结果 |
|------|------|
| 全新安装首启(`new_install`) | 链路 A:`cold_start_time=1141ms`,Entry=Index,Elk 上报 ✅ |
| 二次冷启动(`normal`) | 链路 A:`cold_start_time=1717ms`,Elk 上报 ✅ |
| 链路 B(分桶) | ❌ 待后台下发 `launch` 策略(§3.1)后复测 |
编译:`./claude_tool/claude_compile.sh → BUILD_SUCCESS`
@@ -44,7 +44,7 @@ import { ConnectionInitiator } from '../communication/ConnectionInitiator';
import { APMHelper } from '../monitor/APMHelper'; import { APMHelper } from '../monitor/APMHelper';
import { cbasService } from '../register/cbasregister/CbasServiceRegister'; import { cbasService } from '../register/cbasregister/CbasServiceRegister';
import { eventSubscription } from '../monitor/AppEvent'; import { eventSubscription } from '../monitor/AppEvent';
import { StartupMonitor } from '../monitor/StartupMonitor'; import { LaunchMonitor } from '../monitor/LaunchMonitor';
import { StockGroupConstants } from 'biz_selfcode'; import { StockGroupConstants } from 'biz_selfcode';
import { StockGroupHandler } from '../groupsub/business/StockGroupHandler'; import { StockGroupHandler } from '../groupsub/business/StockGroupHandler';
import { VarietyGroupHandler } from '../groupsub/business/VarietyGroupHandler'; import { VarietyGroupHandler } from '../groupsub/business/VarietyGroupHandler';
@@ -142,7 +142,7 @@ export default class EntryAbility extends UIAbility {
// ===================================== // =====================================
// 初始化启动性能监控(必须在 early onCreate 注册,确保 reportDrawnCompleted 前 watcher 已就绪) // 初始化启动性能监控(必须在 early onCreate 注册,确保 reportDrawnCompleted 前 watcher 已就绪)
StartupMonitor.init(); LaunchMonitor.init();
DebugToolUtil.TARGET_NAME = BuildProfile.TARGET_NAME DebugToolUtil.TARGET_NAME = BuildProfile.TARGET_NAME
DebugToolUtil.PRODUCT_NAME = BuildProfile.PRODUCT_NAME DebugToolUtil.PRODUCT_NAME = BuildProfile.PRODUCT_NAME
@@ -9,6 +9,7 @@ import {
import { HXPerformancePlugin } from "@kernel/app_monitor_performance"; import { HXPerformancePlugin } from "@kernel/app_monitor_performance";
import { HXWebMonitorPlugin } from '@kernel/app_monitor_web'; import { HXWebMonitorPlugin } from '@kernel/app_monitor_web';
import { HXUserService } from "biz_hxservice"; import { HXUserService } from "biz_hxservice";
import { deviceInfo } from "@kit.BasicServicesKit";
import { ElkService, ElkUploadMessage, ElkUploadMessageBuilder } from "service_monitor"; import { ElkService, ElkUploadMessage, ElkUploadMessageBuilder } from "service_monitor";
import { import {
AuthLogMonitor, AuthLogMonitor,
@@ -20,9 +21,11 @@ import {
import { HXAuthLogMonitor } from "biz_auth"; import { HXAuthLogMonitor } from "biz_auth";
import { HXHttpMonitorPlugin } from "@kernel/app_monitor_http"; import { HXHttpMonitorPlugin } from "@kernel/app_monitor_http";
import { HXCrashMonitorPlugin, ICrashMonitorConfig } from "@kernel/app_monitor_crash"; import { HXCrashMonitorPlugin, ICrashMonitorConfig } from "@kernel/app_monitor_crash";
import { HXLaunchMonitorPlugin } from "./plugin/HXLaunchMonitorPlugin";
import { ILaunchMonitorConfig } from "./plugin/ILaunchMonitorConfig";
import { router } from "@kit.ArkUI"; import { router } from "@kit.ArkUI";
import { GIT_COMMIT } from "biz_common/src/main/ets/utils/debug/GitBuildConfig"; import { GIT_COMMIT } from "biz_common/src/main/ets/utils/debug/GitBuildConfig";
import { AppConfigManager, getConnectionManager } from "biz_common"; import { AppConfigManager, StartManager, AppInstallStatus, getConnectionManager } from "biz_common";
import { HxCBASAgentProvider } from "@b2c-spi/hx_cbas_api"; import { HxCBASAgentProvider } from "@b2c-spi/hx_cbas_api";
import { import {
AppContextLike, AppContextLike,
@@ -65,6 +68,7 @@ export class APMHelper {
.plugin(new HXWebMonitorPlugin()) .plugin(new HXWebMonitorPlugin())
.plugin(new HXHttpMonitorPlugin()) .plugin(new HXHttpMonitorPlugin())
.plugin(new HXCrashMonitorPlugin(new HXCrashMonitorConfig())) .plugin(new HXCrashMonitorPlugin(new HXCrashMonitorConfig()))
.plugin(new HXLaunchMonitorPlugin(new HXLaunchMonitorConfig()))
.build() .build()
.startAllPlugins(new APMMonitorOnStartCallback()) .startAllPlugins(new APMMonitorOnStartCallback())
APMHelper.initLeakWatcher(context) APMHelper.initLeakWatcher(context)
@@ -192,6 +196,51 @@ class HXMonitorAppConfig extends IMonitorAppConfig {
} }
} }
/**
* 启动监控插件配置:提供冷启动指标固定维度(user/app-version/build)
*/
class HXLaunchMonitorConfig implements ILaunchMonitorConfig {
provideUserId(): string {
return HXUserService.getInstance().getUserIdFromLocal()
}
provideDeviceType(): string {
return deviceInfo.deviceType
}
provideDeviceModel(): string {
// 营销名(设备名称),如 nova 14 Pro;无则回退品牌
return deviceInfo.marketName || deviceInfo.brand
}
provideAppVersion(): string {
if (APMHelper.isDebugOrTestBuild()) {
return DEBUG_VERSION
}
return AppConfigManager.getInstance().getConfigProvider()?.getInnerVersionFull() ?? DEBUG_VERSION
}
provideBuildType(): string {
return BuildProfile.BUILD_MODE_NAME.toString()
}
provideGitCommit(): string {
return GIT_COMMIT
}
provideAppInstallStatus(): string {
switch (StartManager.getInstance().getAppInstallStatus()) {
case AppInstallStatus.NEW:
return 'new_install'
case AppInstallStatus.OVER_WRITTEN:
return 'over_written'
case AppInstallStatus.NORMAL:
default:
return 'normal'
}
}
}
/** /**
* tcp监控配置 * tcp监控配置
*/ */
@@ -0,0 +1,317 @@
/**
* author:cheliangzhao
* created on 2026/08/06
* desc: 启动性能监控 v2,订阅 APP_LAUNCH 采集冷启动耗时,走 ElkService 追溯 + HXEventMonitor 分桶双通道
*/
import hiAppEvent from '@ohos.hiviewdfx.hiAppEvent';
import { HXLog } from 'biz_common/src/main/ets/logger/HXLog';
import { ElkService, ElkUploadMessage, ElkUploadMessageBuilder } from 'service_monitor';
import { inspector } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { HXEventMonitor, DataMonitorBuilder } from '@kernel/app_monitor_event';
import { ILaunchMonitorConfig } from './plugin/ILaunchMonitorConfig';
const TAG: string = 'LaunchMonitor';
/**
* 启动性能监控:订阅 APP_LAUNCH 事件,采集冷启动耗时(extend_time),
* 并通过 ElkService(追溯)与 HXEventMonitor(分桶)双通道上报。
*/
export class LaunchMonitor {
private static readonly MAX_EVENT_KEYS: number = 20;
/**
* 补报判定容差(ms):正常启动链间隔为毫秒~秒级,补报(上次启动遗留事件)至少早 2 秒
*/
private static readonly BACKFILL_TOLERANCE_MS: number = 2000;
/** watcher 注册时间戳(epoch ms),补报判定的锚点 */
private static watcherRegisterTime: number = 0;
/** 首帧绘制完成时快照的入口页面,在 onDraw 回调中赋值 */
private static capturedEntryPage: string = 'unknown';
/** 固定维度配置(由 HXLaunchMonitorPlugin 注入 user/app-version/build 等) */
private static launchConfig: ILaunchMonitorConfig | null = null;
static setLaunchConfig(config: ILaunchMonitorConfig): void {
LaunchMonitor.launchConfig = config
}
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> = [];
/**
* 注册入口页根节点 draw 监听:仅触发一次,快照 entryPage 并调用 reportDrawnCompleted。
* 页面在 aboutToDisappear 调用 disposeFirstFrame(pageName) 清理。
*/
static reportFirstFrameOnDraw(
createObserver: () => inspector.ComponentObserver,
abilityContext: common.UIAbilityContext,
pageName: string
): void {
if (LaunchMonitor.drawReportSubmitted) {
HXLog.i(TAG, `Skip duplicate draw registration for page: ${pageName}`)
return
}
LaunchMonitor.disposeFirstFrame(LaunchMonitor.activeObserverOwner)
try {
const observer = createObserver();
const onDraw = () => {
observer.off('draw', onDraw);
if (LaunchMonitor.activeObserverOwner === pageName) {
LaunchMonitor.activeObserverOwner = ''
LaunchMonitor.activeDisposer = undefined
}
// 必须在系统调用前设置进程级提交状态,防止后续页面或重复回调再次提交
if (LaunchMonitor.drawReportSubmitted) {
HXLog.i(TAG, `Skip duplicate draw callback for page: ${pageName}`)
return
}
LaunchMonitor.drawReportSubmitted = true
LaunchMonitor.capturedEntryPage = pageName
try {
abilityContext.reportDrawnCompleted(() => {
});
HXLog.i(TAG, `reportDrawnCompleted submitted for page: ${pageName}`)
} catch (e) {
// 同步异常表示系统调用尚未提交,允许后续入口页面重试。
LaunchMonitor.drawReportSubmitted = false
LaunchMonitor.capturedEntryPage = 'unknown'
HXLog.e(TAG, `reportDrawnCompleted failed synchronously: ${JSON.stringify(e)}`)
}
};
LaunchMonitor.activeObserverOwner = pageName
LaunchMonitor.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 === '' || LaunchMonitor.activeObserverOwner !== pageName) {
return
}
LaunchMonitor.activeDisposer?.();
LaunchMonitor.activeObserverOwner = ''
LaunchMonitor.activeDisposer = undefined;
}
/** 注册 APP_LAUNCH watcher(幂等)。应在 onCreate 尽早调用,确保事件生成前完成注册 */
static init(): void {
if (LaunchMonitor.watcherInitialized) {
return
}
LaunchMonitor.watcherRegisterTime = Date.now()
hiAppEvent.addWatcher({
name: "launchWatcher",
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}`)
LaunchMonitor.processAppLaunchEvents(appEventGroups)
}
});
LaunchMonitor.watcherInitialized = true
HXLog.i(TAG, 'LaunchMonitor watcher registered')
}
/** 处理 APP_LAUNCH:过滤冷启动且 extend_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;
// 未填充说明首帧未完成(如 5 秒内退出)
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 (LaunchMonitor.processedEventKeys.indexOf(eventKey) !== -1) {
HXLog.i(TAG, `Skip duplicate APP_LAUNCH: ${eventKey}`)
return
}
LaunchMonitor.processedEventKeys.push(eventKey)
if (LaunchMonitor.processedEventKeys.length > LaunchMonitor.MAX_EVENT_KEYS) {
LaunchMonitor.processedEventKeys.shift()
}
HXLog.i(TAG,
`cold_start_time=${extendTime}ms, ` +
`icon_input_time=${iconInputTime}, ` +
`entry_page=${LaunchMonitor.capturedEntryPage}`)
LaunchMonitor.uploadColdStartTime(extendTime, startType, eventInfo)
})
})
} catch (e) {
HXLog.e(TAG, `Error processing APP_LAUNCH: ${JSON.stringify(e)}`)
}
}
/** 上传冷启动指标:ElkService 追溯 + HXEventMonitor 分桶,异步不阻塞首帧 */
private static uploadColdStartTime(
coldStartTime: number,
startType: number,
eventInfo: hiAppEvent.AppEventInfo
): void {
try {
const iconInputTime = eventInfo.params['icon_input_time'] as number;
// 补投递事件(上次启动遗留)的 entry_page 是当前进程快照,无意义,置 unknown
const isBackfillEvent = LaunchMonitor.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;
// 负载结构与字段语义见 ColdStartMetricPayload
const metricPayload: ColdStartMetricPayload = {
cold_start_time: coldStartTime,
start_type: startType,
entry_page: isBackfillEvent ? 'unknown' : LaunchMonitor.capturedEntryPage,
device_type: LaunchMonitor.launchConfig?.provideDeviceType() ?? 'unknown',
device_model: LaunchMonitor.launchConfig?.provideDeviceModel() ?? 'unknown',
user_id: LaunchMonitor.launchConfig?.provideUserId() ?? 'unknown',
app_version: LaunchMonitor.launchConfig?.provideAppVersion() ?? 'unknown',
build_type: LaunchMonitor.launchConfig?.provideBuildType() ?? 'unknown',
app_install_status: LaunchMonitor.launchConfig?.provideAppInstallStatus() ?? 'unknown',
response_latency: responseLatency,
animation_finish_time: animationFinishTime,
startability_processstart_dur: startabilityProcessStartDur,
appattach_to_appforeground_dur: appattachToAppForegroundDur,
git_commit: LaunchMonitor.launchConfig?.provideGitCommit() ?? 'unknown',
is_backfill: isBackfillEvent,
}
const builder = new LaunchElkBuilder()
builder.userid(LaunchMonitor.launchConfig?.provideUserId() ?? 'unknown')
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${isBackfillEvent ? ' (backfill)' : ''}`)
// 分桶指标(链路 B):记录原始耗时,SDK 按桶自动归集,负责看板聚合
// 兜底:若 APM 插件尚未执行 startPlugin 创建指标,此处惰性创建
LaunchMonitor.ensureColdStartMetric()
const factory = HXEventMonitor.getEventFactory('launch')
const monitor = factory.getEventMonitor('cold_start_time')
const dim1 = LaunchMonitor.launchConfig?.provideAppVersion() ?? 'unknown'
const dim2 = LaunchMonitor.launchConfig?.provideBuildType() ?? 'unknown'
const dim3 = LaunchMonitor.launchConfig?.provideDeviceType() ?? 'unknown'
const dim4 = LaunchMonitor.launchConfig?.provideAppInstallStatus() ?? 'unknown'
monitor.setDimension1(dim1)
monitor.setDimension2(dim2)
monitor.setDimension3(dim3)
monitor.setDimension4(dim4)
monitor.record(coldStartTime)
HXLog.i(TAG, `Cold start bucket metric recorded: ${coldStartTime}ms`)
} catch (e) {
HXLog.e(TAG, `uploadColdStartTime failed: ${JSON.stringify(e)}`)
}
}
/**
* 创建 launch 分桶指标(幂等):桶 [500,600,700,800,1000,1500,2000,3000],
* 按实测主体分布(600-800ms)细分,尾部留劣化告警锚点。
* 由 HXLaunchMonitorPlugin.startPlugin() 在 APM 引擎启动后调用,避免 create 早于 SDK 初始化。
*/
static ensureColdStartMetric(): void {
if (LaunchMonitor.bucketMetricInitialized) {
return
}
const builder = new DataMonitorBuilder()
builder.setMetricName('cold_start_time')
builder.setDimension1Name('app_version')
builder.setDimension2Name('build_type')
builder.setDimension3Name('device_type')
builder.setDimension4Name('app_install_status')
builder.setBuckets([500, 600, 700, 800, 1000, 1500, 2000, 3000])
HXEventMonitor.getEventFactory('launch').create(builder)
LaunchMonitor.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 < LaunchMonitor.watcherRegisterTime - LaunchMonitor.BACKFILL_TOLERANCE_MS
}
}
/** ELK 消息构造器,业务来源标记为 launch 模块 */
class LaunchElkBuilder extends ElkUploadMessageBuilder {
constructor() {
super(Date.now(), 'launch', 'i');
}
}
/**
* 冷启动指标上报负载(ElkService 追溯通道,messageValue 的 JSON 结构)。
*/
interface ColdStartMetricPayload {
// 核心指标:冷启动耗时(ms),即系统事件的 extend_time
cold_start_time: number
// 维度:启动类型(0=冷启动,当前仅统计冷启动)
start_type: number
// 维度:入口页面(Index/LauncherPage 等;补报事件为 unknown)
entry_page: string
// 维度:设备类型(phone/tablet/2in1)
device_type: string
// 维度:设备型号(如 nova 14 Pro / MIA-AL00)
device_model: string
// 维度:用户ID
user_id: string
// 维度:应用内部版本号
app_version: string
// 维度:构建类型(debug/release/forTest 等)
build_type: string
// 维度:安装状态语义名("normal"/"new_install"/"over_written",见 AppInstallStatus;新装耗时显著偏高需区分)
app_install_status: string
// 系统响应段:离手到动效开始耗时(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
}
@@ -1,315 +0,0 @@
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, 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';
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;
/**
* 补报事件判定容差(ms)。
* 本次启动链中 icon_input_time(点击图标)与 watcher 注册时间间隔在毫秒~秒级;
* 系统补投递的上次启动遗留事件,其 icon_input_time 至少早于注册时间 5 秒(事件送达延迟),
* 因此容差取 2s 可稳定区分两者。
*/
private static readonly BACKFILL_TOLERANCE_MS: number = 2000;
/**
* watcher 注册时间戳(epoch ms),用于识别系统补投递的上次启动遗留事件。
*/
private static watcherRegisterTime: number = 0;
/**
* 首帧绘制完成时快照的入口页面名称,在 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
}
// 记录注册时间戳,用于区分本次启动事件与系统补投递的上次启动遗留事件
StartupMonitor.watcherRegisterTime = Date.now()
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 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 responseLatency = eventInfo.params['response_latency'] 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> = {
// 核心指标
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,
response_latency: responseLatency,
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(),
}
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${isBackfillEvent ? ' (backfill)' : ''}`)
} catch (e) {
HXLog.e(TAG, `uploadColdStartTime failed: ${JSON.stringify(e)}`)
}
}
/**
* 判断事件是否为系统补投递的上次启动遗留事件。
* 本次启动链中 icon_input_time 与 watcher 注册时间间隔在容差内;
* 补报事件的 icon_input_time 早于注册时间超过容差(至少 5 秒的送达延迟)。
*/
private static isBackfillEvent(iconInputTime: number): boolean {
return iconInputTime < StartupMonitor.watcherRegisterTime - StartupMonitor.BACKFILL_TOLERANCE_MS
}
private static getAppVersion(): string {
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()
}
private static getGitCommit(): string {
return GIT_COMMIT
}
/**
* 是否安装后首次进入(StartManager 在 EntryAbility.onCreate 中已 init)
*/
private static getIsFirstInstall(): boolean {
return StartManager.getInstance().getAppInstallStatus()
}
}
/**
* ELK 消息构造器,业务来源标记为 StartupMonitor
*/
class StartupElkBuilder extends ElkUploadMessageBuilder {
constructor(biz_level: string = 'i') {
super(Date.now(), 'StartupMonitor', biz_level);
}
}
@@ -0,0 +1,66 @@
/**
* author:cheliangzhao
* created on 2026/08/06
* desc: 冷启动监控插件,仿 HXCrashMonitorPlugin 接入 APM 插件系统;在插件启动后创建冷启动分桶指标,
* 并把固定维度配置(user/app-version/build)注入 LaunchMonitor。
* 解决分桶 create 早于 HXEventMonitor SDK 而失效的时序问题
*/
import { AbstractHXBasePlugin, IMonitorStrategy, StrategyModuleConfig, HXMonitorLogger } from '@kernel/app_monitor_lib';
import { LaunchMonitor } from '../LaunchMonitor';
import { ILaunchMonitorConfig } from './ILaunchMonitorConfig';
/**
* 冷启动监控插件
*
* 职责:在 APM 插件引擎启动后,创建 launch 分桶指标(链路B),并把固定维度配置
* (user/app-version/build 等)经 LaunchMonitor.setLaunchConfig 注入。APP_LAUNCH
* 事件订阅、Elk 追溯上传与首帧埋点仍由 LaunchMonitor 承接(在 onCreate 早注册)。
*/
export class HXLaunchMonitorPlugin extends AbstractHXBasePlugin {
private TAG: string = 'HXLaunchMonitorPlugin';
private static MODULE_NAME = 'launch';
private readonly config: ILaunchMonitorConfig;
constructor(config: ILaunchMonitorConfig) {
super();
this.config = config;
}
getModuleName(): string {
return HXLaunchMonitorPlugin.MODULE_NAME;
}
/**
* 新指标无后台策略下发,强制随主配置开启;后续如需开关控制可改读 isSwitchOpen
*/
isSupport(): boolean {
return true
}
parseMonitorStrategy(config: StrategyModuleConfig): IMonitorStrategy | null {
return null
}
isSwitchOpen(gatherSwitch: number): boolean {
return true
}
initPlugin(): void {
// 插件初始化时即把固定维度配置注入 LaunchMonitor(user/app-version/build 等)
LaunchMonitor.setLaunchConfig(this.config)
}
startPlugin(): void {
// 指标创建延后到 startPlugin(此时 HX 引擎已初始化)
LaunchMonitor.ensureColdStartMetric()
HXMonitorLogger.d(this.TAG, 'startPlugin: launch cold_start_time metric ready')
}
stopPlugin(): void {
// 无长连接需要释放
}
destroyPlugin(): void {
// 无需清理
}
}
@@ -0,0 +1,36 @@
/**
* author:cheliangzhao
* created on 2026/08/06
* desc: 冷启动指标固定维度(user/app-version/build 等)的配置提供接口。
* 由 HXLaunchMonitorConfig 实现传入,LaunchMonitor 据此注入指标维度。
*/
export interface ILaunchMonitorConfig {
/**
* 提供用户ID(Elk 消息用户标识)
*/
provideUserId(): string;
/**
* 提供设备类型(如 phone/tablet/2in1)
*/
provideDeviceType(): string;
/**
* 提供设备品牌(如 HUAWEI)
*/
provideDeviceModel(): string;
/**
* 提供应用内部版本号
*/
provideAppVersion(): string;
/**
* 提供构建类型
*/
provideBuildType(): string;
/**
* 提供代码级追溯 Git 提交号
*/
provideGitCommit(): string;
/**
* 提供安装状态语义名(normal/new_install/over_written)
*/
provideAppInstallStatus(): string;
}
@@ -8,7 +8,7 @@ import { HXNetwork } from './HXNetwork'
import { ConnectionSeq } from '@kernel/lib_communication' import { ConnectionSeq } from '@kernel/lib_communication'
import { AuthProviderService } from 'biz_auth' import { AuthProviderService } from 'biz_auth'
import { AdvertisingMonitorService } from 'biz_hxservice' import { AdvertisingMonitorService } from 'biz_hxservice'
import { StartupMonitor } from '../monitor/StartupMonitor' import { LaunchMonitor } from '../monitor/LaunchMonitor'
const SettingWant: Want = { const SettingWant: Want = {
bundleName: 'com.huawei.hmos.settings', bundleName: 'com.huawei.hmos.settings',
@@ -49,13 +49,13 @@ struct NetworkAnomalyPage {
aboutToAppear(): void { aboutToAppear(): void {
this.setSystemBar() this.setSystemBar()
StartupMonitor.reportFirstFrameOnDraw( LaunchMonitor.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'), () => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
this.context, 'NetworkAnomalyPage'); this.context, 'NetworkAnomalyPage');
} }
aboutToDisappear(): void { aboutToDisappear(): void {
StartupMonitor.disposeFirstFrame('NetworkAnomalyPage') LaunchMonitor.disposeFirstFrame('NetworkAnomalyPage')
} }
onExit() { onExit() {
@@ -48,7 +48,7 @@ import { enableDarkMode, ThemeManagerEmitterConstants } from '@kernel/theme_mana
import { CmeTraceForeignManager } from 'biz_quote/src/main/ets/manager/CmeTraceForeignManager'; import { CmeTraceForeignManager } from 'biz_quote/src/main/ets/manager/CmeTraceForeignManager';
import { HQUGCManager } from 'biz_market'; import { HQUGCManager } from 'biz_market';
import { AdsManager } from '@b2c-f/fuhm-ads'; import { AdsManager } from '@b2c-f/fuhm-ads';
import { StartupMonitor } from '../monitor/StartupMonitor'; import { LaunchMonitor } from '../monitor/LaunchMonitor';
const TAG: string = 'Struct Index'; const TAG: string = 'Struct Index';
@@ -311,7 +311,7 @@ struct IndexM {
aboutToDisappear() { aboutToDisappear() {
HXLog.d(TAG, 'Index aboutToDisappear triggered at ' + Date.now()) HXLog.d(TAG, 'Index aboutToDisappear triggered at ' + Date.now())
// 页面退出时取消首帧绘制监听 // 页面退出时取消首帧绘制监听
StartupMonitor.disposeFirstFrame('Index'); LaunchMonitor.disposeFirstFrame('Index');
emitter.off(EmitterConstants.SWIPER_CHANGETOINDEX_1_TEMP) emitter.off(EmitterConstants.SWIPER_CHANGETOINDEX_1_TEMP)
emitter.off(EmitterConstants.TAB_UI_MANAGER_CHANGE_INDEX) emitter.off(EmitterConstants.TAB_UI_MANAGER_CHANGE_INDEX)
emitter.off(QuoteSettingEvents.SETTING_UPDATE, this.onSettingUpdateCallback) emitter.off(QuoteSettingEvents.SETTING_UPDATE, this.onSettingUpdateCallback)
@@ -366,7 +366,7 @@ struct IndexM {
DrawLineInit.init() DrawLineInit.init()
// 注册首帧绘制监听,绘制完成时自动调用 reportDrawnCompleted // 注册首帧绘制监听,绘制完成时自动调用 reportDrawnCompleted
StartupMonitor.reportFirstFrameOnDraw( LaunchMonitor.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'), () => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
this.context, 'Index'); this.context, 'Index');
} }
@@ -9,7 +9,7 @@ import { window } from '@kit.ArkUI'
import { ConnectionSeq } from '@kernel/lib_communication/src/main/ets/data/CommunicationConstants' import { ConnectionSeq } from '@kernel/lib_communication/src/main/ets/data/CommunicationConstants'
import { deviceInfo } from '@kit.BasicServicesKit' import { deviceInfo } from '@kit.BasicServicesKit'
import { AdvertisingMonitorService } from 'biz_hxservice' import { AdvertisingMonitorService } from 'biz_hxservice'
import { StartupMonitor } from '../monitor/StartupMonitor' import { LaunchMonitor } from '../monitor/LaunchMonitor'
/** /**
* 启动页面 * 启动页面
@@ -47,13 +47,13 @@ struct LauncherPage {
aboutToAppear(): void { aboutToAppear(): void {
this.setSystemBar() this.setSystemBar()
this.userPrivacyDialogVisible = !PreferenceService.getBooleanValueSync('Privacy', 'isUserAgree', false); this.userPrivacyDialogVisible = !PreferenceService.getBooleanValueSync('Privacy', 'isUserAgree', false);
StartupMonitor.reportFirstFrameOnDraw( LaunchMonitor.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'), () => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
this.context, 'LauncherPage'); this.context, 'LauncherPage');
} }
aboutToDisappear(): void { aboutToDisappear(): void {
StartupMonitor.disposeFirstFrame('LauncherPage') LaunchMonitor.disposeFirstFrame('LauncherPage')
} }
onCancel() { onCancel() {