Compare commits

...

2 Commits

Author SHA1 Message Date
clz 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
clz 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
4 changed files with 128 additions and 111 deletions
@@ -7,6 +7,7 @@ import { common } from '@kit.AbilityKit';
import { inspector } from '@kit.ArkUI';
import { hiAppEvent, hilog } from '@kit.PerformanceAnalysisKit';
import { deviceInfo } from '@kit.BasicServicesKit';
import { preferences } from '@kit.ArkData';
enum DrawReportState {
IDLE,
@@ -41,6 +42,11 @@ export class StartupDiagnostics {
* watcher 注册时间戳(epoch ms),用于识别系统补投递的上次启动遗留事件。
*/
private static watcherRegisterTime: number = 0;
/**
* 是否安装后首次启动(维度):preferences 无启动记录即为首次,随后写入标记。
* 与生产实现(StartManager.getAppInstallStatus)语义等价,演示工程无 biz_common 依赖故自实现。
*/
private static isFirstInstall: boolean = false;
private static watcherInitialized: boolean = false;
private static drawReportState: DrawReportState = DrawReportState.IDLE;
private static capturedEntryPage: string = '未捕获';
@@ -57,10 +63,19 @@ export class StartupDiagnostics {
private static lastAction: string = '等待登录页首次绘制';
private static lastLaunchDetail: string = '尚未收到 APP_LAUNCH';
static initialize(): void {
static initialize(context: common.UIAbilityContext): void {
if (StartupDiagnostics.watcherInitialized) {
return;
}
// 首次安装标志:preferences 无启动记录视为首次,随后写入并同步落盘;异常时按非首次处理
try {
const pref: preferences.Preferences = preferences.getPreferencesSync(context, { name: 'startup_demo' });
StartupDiagnostics.isFirstInstall = !pref.getSync('has_launched', false);
pref.putSync('has_launched', true);
pref.flushSync();
} catch (e) {
StartupDiagnostics.isFirstInstall = false;
}
const filter: hiAppEvent.AppEventFilter = {
domain: hiAppEvent.domain.OS,
@@ -268,11 +283,12 @@ export class StartupDiagnostics {
`extend_time=${extendTime}ms, icon_input_time=${iconInputTime}, ` +
`start_type=${startType}, process_name=${processName}\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, ` +
`appattach_to_appforeground_dur=${appattachToAppForegroundDur}ms\n` +
`device_type=${StartupDiagnostics.getDeviceType()}, ` +
`system_version=${StartupDiagnostics.getSystemVersion()}\n` +
`is_first_install=${StartupDiagnostics.isFirstInstall}\n` +
`补报事件=${isBackfill ? '是(entry_page 不归因,按 unknown 处理)' : '否'}`;
StartupDiagnostics.lastAction = '已捕获有效冷启动 APP_LAUNCH';
StartupDiagnostics.logInfo(StartupDiagnostics.lastLaunchDetail);
@@ -26,7 +26,7 @@ export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
CrashDiagnostics.initialize(this.context as common.UIAbilityContext);
StartupDiagnostics.initialize();
StartupDiagnostics.initialize(this.context as common.UIAbilityContext);
AuthSession.initialize(this.context as common.UIAbilityContext);
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
@@ -44,15 +44,21 @@ time + icon_input_time + start_type + process_name
| 模块 | `launch` |
| 指标名 | `cold_start_time` |
| 指标值 | 冷启动耗时,单位 ms |
| 维度 | `start_type``entry_page``app_version` |
| 追溯字段 | `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` |
| 分桶 | `[500, 800, 1200, 2000, 3000]` |
| 维度 | `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` |
追溯通道上传最小必要字段:核心指标、3 个维度、启动生命周期各阶段时间(`response_latency`/`animation_finish_time`/`startability_processstart_dur`/`appattach_to_appforeground_dur`,用于劣化分段定位;`response_latency` 需 API 22+,两个 `*dur` 仅冷启动存在)、`git_commit`(代码级追溯)与补报标识。去重为客户端行为(第 3.2 节,进程内存保留最近 20 个事件键),上传负载无需携带去重键与事件时间字段。其余候选字段(`process_name``time`/`icon_input_time`、环境 `device_type`/`system_version`/`build_mode`/`target_name`/`is_first_install`、版本 `bundle_version`/`bundle_name` 等)在系统事件与客户端均可获取,按需加回上传即可,无需改动采集逻辑。
| 分桶 | `[500, 600, 700, 800, 1000, 1500, 2000, 3000]` |
模块名使用小写字母和连字符;指标名不包含连字符。维度控制在必要范围内,避免将用户 ID、完整 URL 等高基数字段作为维度。
### 4.1 启动时间拆解
分桶指标通过 `HXEventMonitor``@kernel/app_monitor_event`)实现:`StartupMonitor.init()` 中以 `DataMonitorBuilder` 创建 `launch` 模块的 `cold_start_time` 指标(`setBuckets([500, 600, 700, 800, 1000, 1500, 2000, 3000])`,维度 `start_type`/`entry_page`/`app_version`),每次有效冷启动 `record(extendTime)` 原始值,由 SDK 按桶自动归集聚合,无需客户端手动分桶。
`cold_start_time` 之外,通过系统字段可拆分冷启动各分段,用于定位劣化归属:
桶边界按实测主体分布(600-800ms)设计:`<500` 无感、`500-600`/`600-700`/`700-800` 在主体区间细分(各 100ms 分辨率)、`800-1000`/`1000-1500`/`1500-2000` 逐级放宽、`2000-3000`/`>3000` 作为劣化与告警锚点。后续以 ElkService 追溯通道的原始值(P25/P50/P75/P95/P99)复核边界;分桶变更会使历史分布不可比,需在后台重建或并档。
### 4.1 启动时间拆解(按需启用)
`cold_start_time` 之外,系统事件还提供各分段耗时,可拆分冷启动定位劣化归属。**当前追溯通道未上传这些字段**(最小字段集,见上表),需要做分段分析时按需加回:
```text
离手 ──response_latency──► 动效开始 ──(animation_finish_time response_latency)──► 动效完成 ──(extend_time animation_finish_time)──► 首帧绘制完成
@@ -64,11 +70,11 @@ time + icon_input_time + start_type + process_name
- `appattach_to_appforeground_dur`:进程初始化完成到应用切前台(仅冷启动),反映系统侧应用挂载。
- `extend_time animation_finish_time`:动效完成到应用首帧的耗时,即**应用侧可优化的独占部分**;看板可据此判断劣化发生在系统侧还是应用侧。
环境追溯字段说明`build_mode`/`target_name` 区分 debug/forTest/Official 环境,避免测试包污染线上看板;`is_first_install``StartManager.getAppInstallStatus()`)标记安装后首次启动,该次耗时显著偏高,不标记会拉高 P50/P95;`git_commit` 精确到代码提交,配合内部版本号做代码级追溯
环境候选字段(按需启用)`build_mode`/`target_name` 区分 debug/forTest/Official 环境,避免测试包污染线上看板;`is_first_install``StartManager.getAppInstallStatus()`)标记安装后首次启动,该次耗时显著偏高,不标记会拉高 P50/P95;`git_commit` 精确到代码提交`device_type`/`system_version` 排除设备差异;`bundle_version`/`bundle_name` 为系统版本字段
## 5. 后台与看板配置
按新增 PDF 的自定义 APM 模块流程,在 APM 后台创建 `launch` 模块并启用:全量采样(`sampling_rate: 1000`)、60 秒聚合(`aggre_time: 60`)、累计 100 条触发上报(`aggre_count: 100`)。随后为该模块补充 ELK 索引模板,在 Grafana 建立按版本、启动类型和时间聚合的 P50/P95、超 2 秒占比及分桶分布面板。
按新增 PDF 的自定义 APM 模块流程,在 APM 后台创建 `launch` 模块并启用:全量采样(`sampling_rate: 1000`)、60 秒聚合(`aggre_time: 60`)、累计 100 条触发上报(`aggre_count: 100`)。客户端 `record` 后由 SDK 本地聚合,达到聚合周期或条数触发条件后自动上报至 APM 平台(发送通道为 `HXMonitor` 注入的 `IElkService`,即 `APMElkService` 所走的 `ElkService` 通道)。随后为该模块补充 ELK 索引模板,在 Grafana 建立按版本、启动类型和时间聚合的 P50/P95、超 2 秒占比及分桶分布面板。
## 6. 兼容与兜底
@@ -76,6 +82,15 @@ time + icon_input_time + start_type + process_name
`APP_LAUNCH` 的事件解析必须与崩溃事件分开处理:崩溃事件依赖异常和日志字段,启动事件不包含这些字段。上传应异步执行,不阻塞首页渲染;以 `time + icon_input_time + start_type + process_name` 去重,兼容系统重新投递。
### 6.2 双通道上报
启动指标同时走两条通道,职责分离:
- **分桶指标(`HXEventMonitor`module=`launch`**`record(extendTime)` 后由 SDK 按桶聚合,负责看板聚合统计(P50/P95、超 2 秒占比、分桶分布)。`record` 失败不影响启动流程,聚合逻辑在 SDK 侧。
- **追溯明细(`ElkService`biz=`launch`)**:11 字段最小集(指标/4 维度/启动生命周期各阶段时间/`git_commit`/`is_backfill`),负责明细追溯与后端校验;其他字段按需加回。分桶指标同步 4 个维度(`setDimension4Name('is_first_install')`),与追溯维度一致。
两通道独立失败互不影响;`HXEventMonitor` 的 module 参数为显式传入的 `'launch'`,与文档模块定义一致,不经过 `APMElkService` 固定的 `'apm'` 来源。
### 6.1 启动 5 秒内退出的边界行为
系统 `extend_time` 的定义:手指离手到 `reportDrawnCompleted` 的耗时,若 5 秒内未调用则该值为 0。真机实测(nova 14 Pro)启动后立即退出的行为链:
@@ -6,55 +6,35 @@ import { AppConfigManager, StartManager } from 'biz_common';
import { GIT_COMMIT } from 'biz_common/src/main/ets/utils/debug/GitBuildConfig';
import { inspector } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { deviceInfo } from '@kit.BasicServicesKit';
import BuildProfile from 'BuildProfile';
import { HXEventMonitor, DataMonitorBuilder } from '@kernel/app_monitor_event';
const TAG: string = 'StartupMonitor';
/**
* 启动性能监控
*
* 订阅系统 APP_LAUNCH 事件,获取系统计算的冷启动耗时(extend_time),
* 并通过现有的 ElkService 上传结构化指标。与 AppEvent.ets 的故障事件监听相互独立。
*
* 指标定义(见 HarmonyOS 启动性能监控接入方案_20260805.md):
* - module: launch
* - metric: cold_start_timems
* - dimensions: start_type / entry_page / app_version
* 启动性能监控:订阅 APP_LAUNCH 事件,采集冷启动耗时(extend_time),
*/
export class StartupMonitor {
private static readonly MAX_EVENT_KEYS: number = 20;
/**
* 补报事件判定容差(ms
* 本次启动链中 icon_input_time(点击图标)与 watcher 注册时间间隔在毫秒~秒级;
* 系统补投递的上次启动遗留事件,其 icon_input_time 至少早于注册时间 5 秒(事件送达延迟),
* 因此容差取 2s 可稳定区分两者。
* 补报判定容差(ms:正常启动链间隔毫秒~秒级,补报(上次启动遗留事件)至少早 5 秒
*/
private static readonly BACKFILL_TOLERANCE_MS: number = 2000;
/**
* watcher 注册时间戳(epoch ms),用于识别系统补投递的上次启动遗留事件。
*/
/** watcher 注册时间戳(epoch ms),补报判定的锚点 */
private static watcherRegisterTime: number = 0;
/**
* 首帧绘制完成时快照的入口页面名称,在 onDraw 回调中赋值。
* 与 APP_LAUNCH 事件处理处在同一同步时序内,避免页面跳转覆盖。
*/
/** 首帧绘制完成时快照的入口页面,在 onDraw 回调中赋值 */
private static capturedEntryPage: string = 'unknown';
private static watcherInitialized: boolean = false;
private static bucketMetricInitialized: boolean = false;
private static drawReportSubmitted: boolean = false;
private static activeObserverOwner: string = '';
private static activeDisposer?: () => void;
private static processedEventKeys: Array<string> = [];
/**
* 入口页面注册 HOME_ROOT 首次绘制监听,自动完成:
* 1. 创建 observer
* 2. 监听 draw → 仅触发一次 → 快照 entryPage → 调用 reportDrawnCompleted
*
* 页面在 aboutToDisappear 中调用 disposeFirstFrame(pageName) 清理即可,
* 无需在页面侧持有清理函数。
* 注册入口页根节点 draw 监听:仅触发一次,快照 entryPage 并调用 reportDrawnCompleted。
* 页面在 aboutToDisappear 调用 disposeFirstFrame(pageName) 清理。
*/
static reportFirstFrameOnDraw(
createObserver: () => inspector.ComponentObserver,
@@ -77,8 +57,7 @@ export class StartupMonitor {
StartupMonitor.activeDisposer = undefined
}
// 多个入口页面可能在同一冷启动中依次绘制。必须在调用系统接口前设置全局状态,
// 防止后续页面或重复 draw 回调再次提交 reportDrawnCompleted。
// 必须在系统调用前设置进程级提交状态,防止后续页面或重复回调再次提交
if (StartupMonitor.drawReportSubmitted) {
HXLog.i(TAG, `Skip duplicate draw callback for page: ${pageName}`)
return
@@ -121,16 +100,13 @@ export class StartupMonitor {
StartupMonitor.activeDisposer = undefined;
}
/**
* 初始化 APP_LAUNCH 事件监听。
* 应在 EntryAbility.onCreate 中尽早调用,确保在 reportDrawnCompleted 触发事件前完成注册。
*/
/** 注册 APP_LAUNCH watcher(幂等)。应在 onCreate 尽早调用,确保事件生成前完成注册 */
static init(): void {
if (StartupMonitor.watcherInitialized) {
return
}
// 记录注册时间戳,用于区分本次启动事件与系统补投递的上次启动遗留事件
StartupMonitor.watcherRegisterTime = Date.now()
StartupMonitor.ensureBucketMetric()
hiAppEvent.addWatcher({
name: "startupWatcher",
appEventFilters: [{
@@ -146,10 +122,7 @@ export class StartupMonitor {
HXLog.i(TAG, 'StartupMonitor watcher registered')
}
/**
* 遍历 APP_LAUNCH 事件,仅处理冷启动(start_type === 0)且 extend_time 已填充的记录,
* 计算 cold_start_time 并上传。
*/
/** 处理 APP_LAUNCH:过滤冷启动且 extend_time 有效,去重后上传 */
private static processAppLaunchEvents(appEventGroups: Array<hiAppEvent.AppEventGroup>): void {
try {
if (!appEventGroups || appEventGroups.length === 0) {
@@ -158,14 +131,12 @@ export class StartupMonitor {
appEventGroups.forEach((eventGroup: hiAppEvent.AppEventGroup) => {
eventGroup.appEventInfos.forEach((eventInfo: hiAppEvent.AppEventInfo) => {
const startType = eventInfo.params['start_type'] as number;
// 仅统计冷启动
if (startType !== 0) {
HXLog.i(TAG, `Skip non-cold-start: start_type=${startType}`)
return
}
const extendTime = eventInfo.params['extend_time'] as number;
// extend_time 即为冷启动耗时(ms):从 icon_input_time 到 reportDrawnCompleted 的时间差,
// 由系统在 reportDrawnCompleted 调用后填充。未填充说明首帧尚未完成。
// 未填充说明首帧未完成(如 5 秒内退出)
if (!extendTime || extendTime <= 0) {
HXLog.w(TAG, `APP_LAUNCH has no valid extend_time, skip`)
return
@@ -197,57 +168,35 @@ export class StartupMonitor {
}
}
/**
* 上传冷启动耗时指标到 ELK。
* 使用 isInstant=true 异步上传,不阻塞页面渲染。
*/
/** 上传冷启动指标:ElkService 追溯 + HXEventMonitor 分桶,异步不阻塞首帧 */
private static uploadColdStartTime(
coldStartTime: number,
startType: number,
eventInfo: hiAppEvent.AppEventInfo
): void {
try {
const bundleVersion = (eventInfo.params['bundle_version'] as string) ?? '';
const bundleName = (eventInfo.params['bundle_name'] as string) ?? '';
const animationFinishTime = eventInfo.params['animation_finish_time'] as number;
// 时间拆解字段(详见接入方案文档第 4 节):
// response_latency 需要 API 22+startability_processstart_dur / appattach_to_appforeground_dur 仅冷启动存在
const iconInputTime = eventInfo.params['icon_input_time'] as number;
// 补投递事件(上次启动遗留)的 entry_page 是当前进程快照,无意义,置 unknown
const isBackfillEvent = StartupMonitor.isBackfillEvent(iconInputTime);
// 启动生命周期各阶段时间(ms):response_latency 需 API 22+,两个 *dur 仅冷启动存在
const responseLatency = eventInfo.params['response_latency'] as number;
const animationFinishTime = eventInfo.params['animation_finish_time'] as number;
const startabilityProcessStartDur = eventInfo.params['startability_processstart_dur'] as number;
const appattachToAppForegroundDur = eventInfo.params['appattach_to_appforeground_dur'] as number;
const iconInputTime = eventInfo.params['icon_input_time'] as number;
// 补投递事件(上次启动提交绘制但进程在事件送达前退出)的 entry_page 无意义,
// capturedEntryPage 是当前进程的快照,不能错误归因到本次启动的页面。
const isBackfillEvent = StartupMonitor.isBackfillEvent(iconInputTime);
const metricPayload: Record<string, Object> = {
// 核心指标
// 负载结构与字段语义见 ColdStartMetricPayload
const metricPayload: ColdStartMetricPayload = {
cold_start_time: coldStartTime,
// 维度
start_type: startType,
entry_page: isBackfillEvent ? 'unknown' : StartupMonitor.capturedEntryPage,
app_version: StartupMonitor.getAppVersion(),
bundle_version: bundleVersion,
// 补报标识:true 表示系统补投递的上次启动遗留事件(entry_page 无意义),看板可据此过滤或单独统计
is_backfill: isBackfillEvent,
// 原始系统字段,用于后端校验和去重
event_time: eventInfo.params['time'],
icon_input_time: eventInfo.params['icon_input_time'],
process_name: eventInfo.params['process_name'],
extend_time: eventInfo.params['extend_time'],
animation_finish_time: animationFinishTime,
// 追溯字段:系统时间拆解
bundle_name: bundleName,
is_first_install: StartupMonitor.getIsFirstInstall(),
response_latency: responseLatency,
animation_finish_time: animationFinishTime,
startability_processstart_dur: startabilityProcessStartDur,
appattach_to_appforeground_dur: appattachToAppForegroundDur,
// 追溯字段:客户端环境
device_type: StartupMonitor.getDeviceType(),
system_version: StartupMonitor.getSystemVersion(),
build_mode: StartupMonitor.getBuildMode(),
target_name: StartupMonitor.getTargetName(),
git_commit: StartupMonitor.getGitCommit(),
is_first_install: StartupMonitor.getIsFirstInstall(),
is_backfill: isBackfillEvent,
}
const builder = new StartupElkBuilder('i')
@@ -259,16 +208,44 @@ export class StartupMonitor {
// 异步上传,避免阻塞启动流程
ElkService.getInstance().pushBusinessLog(message, true)
HXLog.i(TAG, `Cold start metric uploaded: ${coldStartTime}ms${isBackfillEvent ? ' (backfill)' : ''}`)
// 分桶指标(链路 B):记录原始耗时,SDK 按桶自动归集,负责看板聚合
// TODO(api-confirm): setDimension_1 / record 签名需以 .d.ts 核对
const factory = HXEventMonitor.getEventFactory('launch')
const monitor = factory.getEventMonitor('cold_start_time')
monitor.setDimension_1(startType.toString())
monitor.setDimension_2(isBackfillEvent ? 'unknown' : StartupMonitor.capturedEntryPage)
monitor.setDimension_3(StartupMonitor.getAppVersion())
monitor.setDimension_4(StartupMonitor.getIsFirstInstall().toString())
monitor.record(coldStartTime)
HXLog.i(TAG, `Cold start bucket metric recorded: ${coldStartTime}ms`)
} catch (e) {
HXLog.e(TAG, `uploadColdStartTime failed: ${JSON.stringify(e)}`)
}
}
/**
* 判断事件是否为系统补投递的上次启动遗留事件。
* 本次启动链中 icon_input_time 与 watcher 注册时间间隔在容差内;
* 补报事件的 icon_input_time 早于注册时间超过容差(至少 5 秒的送达延迟)。
* 创建 launch 分桶指标(幂等):桶 [500,600,700,800,1000,1500,2000,3000]
* 按实测主体分布(600-800ms)细分,尾部留劣化告警锚点。
* TODO(api-confirm): DataMonitorBuilder / setBuckets / create 签名需以 .d.ts 核对
*/
private static ensureBucketMetric(): void {
if (StartupMonitor.bucketMetricInitialized) {
return
}
const builder = new DataMonitorBuilder()
builder.setMetricName('cold_start_time')
builder.setDimension1Name('start_type')
builder.setDimension2Name('entry_page')
builder.setDimension3Name('app_version')
builder.setDimension4Name('is_first_install')
builder.setBuckets([500, 600, 700, 800, 1000, 1500, 2000, 3000])
HXEventMonitor.getEventFactory('launch').create(builder)
StartupMonitor.bucketMetricInitialized = true
HXLog.i(TAG, 'launch bucket metric created: cold_start_time')
}
/** 补投递判定:icon_input_time 早于 watcher 注册时间超过容差(2s)视为上次启动遗留事件 */
private static isBackfillEvent(iconInputTime: number): boolean {
return iconInputTime < StartupMonitor.watcherRegisterTime - StartupMonitor.BACKFILL_TOLERANCE_MS
}
@@ -277,40 +254,49 @@ export class StartupMonitor {
return AppConfigManager.getInstance().getConfigProvider()?.getInnerVersionFull() ?? 'unknown'
}
private static getDeviceType(): string {
return deviceInfo.deviceType
}
private static getSystemVersion(): string {
return deviceInfo.osFullName
}
private static getBuildMode(): string {
return BuildProfile.BUILD_MODE_NAME.toString()
}
private static getTargetName(): string {
return BuildProfile.TARGET_NAME.toString()
}
/** 代码级追溯:Git 提交号(biz_common 构建时注入) */
private static getGitCommit(): string {
return GIT_COMMIT
}
/**
* 是否安装后首次进入(StartManager 在 EntryAbility.onCreate 中已 init
*/
/** 维度:是否安装后首次进入(StartManager 在 EntryAbility.onCreate 中已 init */
private static getIsFirstInstall(): boolean {
return StartManager.getInstance().getAppInstallStatus()
}
}
/**
* ELK 消息构造器,业务来源标记为 launch 模块(见接入方案文档第 4 节:
* 模块名使用小写字母和连字符;指标名不包含连字符)
*/
/** ELK 消息构造器,业务来源为 launch 模块(命名约束见文档第 4 节) */
class StartupElkBuilder extends ElkUploadMessageBuilder {
constructor(biz_level: string = 'i') {
super(Date.now(), 'launch', biz_level);
}
}
/**
* 冷启动指标上报负载(ElkService 追溯通道,messageValue 的 JSON 结构)。
* 最小必要字段:指标/维度/补报标识;其余字段按需加回(见文档第 4 节)。
*/
interface ColdStartMetricPayload {
// 核心指标:冷启动耗时(ms),即系统事件的 extend_time
cold_start_time: number
// 维度:启动类型(0=冷启动,当前仅统计冷启动)
start_type: number
// 维度:入口页面(Index/StartupLogin 等;补报事件为 unknown
entry_page: string
// 维度:应用内部版本号
app_version: string
// 维度:是否安装后首次启动(首次启动耗时显著偏高,需从常规分布中区分)
is_first_install: boolean
// 系统响应段:离手到动效开始耗时(ms,需 API 22+)
response_latency: number
// 系统动效段:动效完成耗时(ms)
animation_finish_time: number
// 进程创建段:系统启动 Ability 到进程创建完成(ms,仅冷启动存在)
startability_processstart_dur: number
// 进程挂载段:进程初始化完成到应用切前台(ms,仅冷启动存在)
appattach_to_appforeground_dur: number
// 代码级追溯:Git 提交号,劣化时可定位到具体提交
git_commit: string
// 补报标识:true 表示系统补投递的上次启动遗留事件(entry_page 无意义)
is_backfill: boolean
}