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>
This commit is contained in:
@@ -10,7 +10,7 @@ Mixed-purpose repository with three distinct areas:
|
||||
- `HarmonyOS APM分析报告_20260731.md` — crash analysis with supporting screenshots in `report-assets/`.
|
||||
- `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`.
|
||||
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.
|
||||
|
||||
|
||||
+99
-99
@@ -22,12 +22,12 @@ enum DrawReportState {
|
||||
* 监听系统 APP_LAUNCH 事件,并确保一个进程生命周期内只提交一次
|
||||
* reportDrawnCompleted。检测结果同时输出到 hilog 和模拟页面。
|
||||
*
|
||||
* 与 future_entry_demo 的 StartupMonitor.ets 对应:本文件为无依赖演示实现
|
||||
* 与 future_entry_demo 的 LaunchMonitor.ets 对应:本文件为无依赖演示实现
|
||||
* (仅 hilog + 页面展示,不做 ELK 上传),追溯字段已同步系统时间拆解字段
|
||||
* 与 deviceInfo 环境字段;git_commit / is_first_install / build_mode 依赖
|
||||
* biz_common 与 BuildProfile,演示工程不适用,故未同步。
|
||||
*/
|
||||
export class StartupDiagnostics {
|
||||
export class LaunchDiagnostics {
|
||||
private static readonly WATCHER_NAME: string = 'startupDiagnosticsWatcher';
|
||||
private static readonly LOG_DOMAIN: number = 0xD002;
|
||||
private static readonly MAX_EVENT_KEYS: number = 20;
|
||||
@@ -64,17 +64,17 @@ export class StartupDiagnostics {
|
||||
private static lastLaunchDetail: string = '尚未收到 APP_LAUNCH';
|
||||
|
||||
static initialize(context: common.UIAbilityContext): void {
|
||||
if (StartupDiagnostics.watcherInitialized) {
|
||||
if (LaunchDiagnostics.watcherInitialized) {
|
||||
return;
|
||||
}
|
||||
// 首次安装标志:preferences 无启动记录视为首次,随后写入并同步落盘;异常时按非首次处理
|
||||
try {
|
||||
const pref: preferences.Preferences = preferences.getPreferencesSync(context, { name: 'startup_demo' });
|
||||
StartupDiagnostics.isFirstInstall = !pref.getSync('has_launched', false);
|
||||
LaunchDiagnostics.isFirstInstall = !pref.getSync('has_launched', false);
|
||||
pref.putSync('has_launched', true);
|
||||
pref.flushSync();
|
||||
} catch (e) {
|
||||
StartupDiagnostics.isFirstInstall = false;
|
||||
LaunchDiagnostics.isFirstInstall = false;
|
||||
}
|
||||
|
||||
const filter: hiAppEvent.AppEventFilter = {
|
||||
@@ -82,27 +82,27 @@ export class StartupDiagnostics {
|
||||
names: [hiAppEvent.event.APP_LAUNCH]
|
||||
};
|
||||
const watcher: hiAppEvent.Watcher = {
|
||||
name: StartupDiagnostics.WATCHER_NAME,
|
||||
name: LaunchDiagnostics.WATCHER_NAME,
|
||||
appEventFilters: [filter],
|
||||
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);
|
||||
StartupDiagnostics.processAppLaunchEvents(appEventGroups);
|
||||
LaunchDiagnostics.processAppLaunchEvents(appEventGroups);
|
||||
}
|
||||
};
|
||||
|
||||
// 记录注册时间戳,用于区分本次启动事件与系统补投递的上次启动遗留事件
|
||||
StartupDiagnostics.watcherRegisterTime = Date.now();
|
||||
LaunchDiagnostics.watcherRegisterTime = Date.now();
|
||||
|
||||
try {
|
||||
hiAppEvent.addWatcher(watcher);
|
||||
StartupDiagnostics.watcherInitialized = true;
|
||||
StartupDiagnostics.lastAction = 'APP_LAUNCH 监听已注册,等待首帧';
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
LaunchDiagnostics.watcherInitialized = true;
|
||||
LaunchDiagnostics.lastAction = 'APP_LAUNCH 监听已注册,等待首帧';
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
} catch (err) {
|
||||
StartupDiagnostics.lastAction = `APP_LAUNCH 监听注册失败:${StartupDiagnostics.getErrorMessage(err)}`;
|
||||
StartupDiagnostics.logError(StartupDiagnostics.lastAction);
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
LaunchDiagnostics.lastAction = `APP_LAUNCH 监听注册失败:${LaunchDiagnostics.getErrorMessage(err)}`;
|
||||
LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,124 +111,124 @@ export class StartupDiagnostics {
|
||||
abilityContext: common.UIAbilityContext,
|
||||
pageName: string
|
||||
): void {
|
||||
if (StartupDiagnostics.drawReportState !== DrawReportState.IDLE) {
|
||||
StartupDiagnostics.skippedReportCount++;
|
||||
StartupDiagnostics.lastAction = `页面 ${pageName} 的重复首帧注册已跳过`;
|
||||
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction);
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
if (LaunchDiagnostics.drawReportState !== DrawReportState.IDLE) {
|
||||
LaunchDiagnostics.skippedReportCount++;
|
||||
LaunchDiagnostics.lastAction = `页面 ${pageName} 的重复首帧注册已跳过`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
StartupDiagnostics.disposeFirstFrame(StartupDiagnostics.activeObserverOwner);
|
||||
LaunchDiagnostics.disposeFirstFrame(LaunchDiagnostics.activeObserverOwner);
|
||||
|
||||
try {
|
||||
const observer: inspector.ComponentObserver = createObserver();
|
||||
const onDraw = (): void => {
|
||||
observer.off('draw', onDraw);
|
||||
if (StartupDiagnostics.activeObserverOwner === pageName) {
|
||||
StartupDiagnostics.activeObserverOwner = '';
|
||||
StartupDiagnostics.activeDisposer = undefined;
|
||||
if (LaunchDiagnostics.activeObserverOwner === pageName) {
|
||||
LaunchDiagnostics.activeObserverOwner = '';
|
||||
LaunchDiagnostics.activeDisposer = undefined;
|
||||
}
|
||||
|
||||
if (StartupDiagnostics.drawReportState !== DrawReportState.IDLE) {
|
||||
StartupDiagnostics.skippedReportCount++;
|
||||
StartupDiagnostics.lastAction = `页面 ${pageName} 的重复 draw 回调已跳过`;
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
if (LaunchDiagnostics.drawReportState !== DrawReportState.IDLE) {
|
||||
LaunchDiagnostics.skippedReportCount++;
|
||||
LaunchDiagnostics.lastAction = `页面 ${pageName} 的重复 draw 回调已跳过`;
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
StartupDiagnostics.drawCallbackCount++;
|
||||
StartupDiagnostics.drawReportState = DrawReportState.SUBMITTED;
|
||||
StartupDiagnostics.capturedEntryPage = pageName;
|
||||
StartupDiagnostics.reportSubmitCount++;
|
||||
StartupDiagnostics.lastAction = `已提交 ${pageName} 的 reportDrawnCompleted`;
|
||||
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction);
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
LaunchDiagnostics.drawCallbackCount++;
|
||||
LaunchDiagnostics.drawReportState = DrawReportState.SUBMITTED;
|
||||
LaunchDiagnostics.capturedEntryPage = pageName;
|
||||
LaunchDiagnostics.reportSubmitCount++;
|
||||
LaunchDiagnostics.lastAction = `已提交 ${pageName} 的 reportDrawnCompleted`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
|
||||
try {
|
||||
abilityContext.reportDrawnCompleted((err) => {
|
||||
if (err && err.code !== 0) {
|
||||
StartupDiagnostics.drawReportState = DrawReportState.FAILED;
|
||||
StartupDiagnostics.lastAction =
|
||||
LaunchDiagnostics.drawReportState = DrawReportState.FAILED;
|
||||
LaunchDiagnostics.lastAction =
|
||||
`reportDrawnCompleted 失败:code=${err.code}, message=${err.message}`;
|
||||
StartupDiagnostics.logError(StartupDiagnostics.lastAction);
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
StartupDiagnostics.drawReportState = DrawReportState.SUCCEEDED;
|
||||
StartupDiagnostics.lastAction = `reportDrawnCompleted 成功:${pageName}`;
|
||||
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction);
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
LaunchDiagnostics.drawReportState = DrawReportState.SUCCEEDED;
|
||||
LaunchDiagnostics.lastAction = `reportDrawnCompleted 成功:${pageName}`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
});
|
||||
} catch (err) {
|
||||
// 同步异常表示异步任务未成功提交,恢复 IDLE 后允许其他页面重试。
|
||||
StartupDiagnostics.drawReportState = DrawReportState.IDLE;
|
||||
StartupDiagnostics.lastAction =
|
||||
`reportDrawnCompleted 同步异常:${StartupDiagnostics.getErrorMessage(err)}`;
|
||||
StartupDiagnostics.logError(StartupDiagnostics.lastAction);
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
LaunchDiagnostics.drawReportState = DrawReportState.IDLE;
|
||||
LaunchDiagnostics.lastAction =
|
||||
`reportDrawnCompleted 同步异常:${LaunchDiagnostics.getErrorMessage(err)}`;
|
||||
LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
};
|
||||
|
||||
StartupDiagnostics.activeObserverOwner = pageName;
|
||||
StartupDiagnostics.activeDisposer = (): void => {
|
||||
LaunchDiagnostics.activeObserverOwner = pageName;
|
||||
LaunchDiagnostics.activeDisposer = (): void => {
|
||||
observer.off('draw', onDraw);
|
||||
};
|
||||
observer.on('draw', onDraw);
|
||||
StartupDiagnostics.lastAction = `已监听 ${pageName} 根组件 draw`;
|
||||
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction);
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
LaunchDiagnostics.lastAction = `已监听 ${pageName} 根组件 draw`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
} catch (err) {
|
||||
StartupDiagnostics.lastAction = `创建 draw 监听失败:${StartupDiagnostics.getErrorMessage(err)}`;
|
||||
StartupDiagnostics.logError(StartupDiagnostics.lastAction);
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
LaunchDiagnostics.lastAction = `创建 draw 监听失败:${LaunchDiagnostics.getErrorMessage(err)}`;
|
||||
LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
}
|
||||
|
||||
static disposeFirstFrame(pageName: string): void {
|
||||
if (pageName === '' || StartupDiagnostics.activeObserverOwner !== pageName) {
|
||||
if (pageName === '' || LaunchDiagnostics.activeObserverOwner !== pageName) {
|
||||
return;
|
||||
}
|
||||
const disposer: (() => void) | undefined = StartupDiagnostics.activeDisposer;
|
||||
const disposer: (() => void) | undefined = LaunchDiagnostics.activeDisposer;
|
||||
if (disposer !== undefined) {
|
||||
disposer();
|
||||
}
|
||||
StartupDiagnostics.activeObserverOwner = '';
|
||||
StartupDiagnostics.activeDisposer = undefined;
|
||||
LaunchDiagnostics.activeObserverOwner = '';
|
||||
LaunchDiagnostics.activeDisposer = undefined;
|
||||
}
|
||||
|
||||
static setStatusUpdatedListener(owner: string, listener: () => void): void {
|
||||
StartupDiagnostics.statusListenerOwner = owner;
|
||||
StartupDiagnostics.statusUpdatedListener = listener;
|
||||
LaunchDiagnostics.statusListenerOwner = owner;
|
||||
LaunchDiagnostics.statusUpdatedListener = listener;
|
||||
listener();
|
||||
}
|
||||
|
||||
static clearStatusUpdatedListener(owner: string): void {
|
||||
if (StartupDiagnostics.statusListenerOwner !== owner) {
|
||||
if (LaunchDiagnostics.statusListenerOwner !== owner) {
|
||||
return;
|
||||
}
|
||||
StartupDiagnostics.statusListenerOwner = '';
|
||||
StartupDiagnostics.statusUpdatedListener = undefined;
|
||||
LaunchDiagnostics.statusListenerOwner = '';
|
||||
LaunchDiagnostics.statusUpdatedListener = undefined;
|
||||
}
|
||||
|
||||
static getStatusText(): string {
|
||||
return `状态:${StartupDiagnostics.getStateName()}\n` +
|
||||
`捕获入口:${StartupDiagnostics.capturedEntryPage}\n` +
|
||||
`draw 回调:${StartupDiagnostics.drawCallbackCount}\n` +
|
||||
`系统提交:${StartupDiagnostics.reportSubmitCount}\n` +
|
||||
`重复拦截:${StartupDiagnostics.skippedReportCount}\n` +
|
||||
`APP_LAUNCH:${StartupDiagnostics.launchEventCount}\n` +
|
||||
`事件去重:${StartupDiagnostics.duplicateEventCount}\n` +
|
||||
`最近动作:${StartupDiagnostics.lastAction}\n` +
|
||||
`事件详情:${StartupDiagnostics.lastLaunchDetail}`;
|
||||
return `状态:${LaunchDiagnostics.getStateName()}\n` +
|
||||
`捕获入口:${LaunchDiagnostics.capturedEntryPage}\n` +
|
||||
`draw 回调:${LaunchDiagnostics.drawCallbackCount}\n` +
|
||||
`系统提交:${LaunchDiagnostics.reportSubmitCount}\n` +
|
||||
`重复拦截:${LaunchDiagnostics.skippedReportCount}\n` +
|
||||
`APP_LAUNCH:${LaunchDiagnostics.launchEventCount}\n` +
|
||||
`事件去重:${LaunchDiagnostics.duplicateEventCount}\n` +
|
||||
`最近动作:${LaunchDiagnostics.lastAction}\n` +
|
||||
`事件详情:${LaunchDiagnostics.lastLaunchDetail}`;
|
||||
}
|
||||
|
||||
private static processAppLaunchEvents(appEventGroups: Array<hiAppEvent.AppEventGroup>): void {
|
||||
for (let groupIndex: number = 0; groupIndex < appEventGroups.length; groupIndex++) {
|
||||
const group: hiAppEvent.AppEventGroup = appEventGroups[groupIndex];
|
||||
for (let infoIndex: number = 0; infoIndex < group.appEventInfos.length; infoIndex++) {
|
||||
StartupDiagnostics.processAppLaunchEvent(group.appEventInfos[infoIndex]);
|
||||
LaunchDiagnostics.processAppLaunchEvent(group.appEventInfos[infoIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,13 +238,13 @@ export class StartupDiagnostics {
|
||||
const params: Record<string, Object> = JSON.parse(paramsJson) as Record<string, Object>;
|
||||
const startType: number = (params['start_type'] as number) ?? -1;
|
||||
if (startType !== 0) {
|
||||
StartupDiagnostics.logInfo(`跳过非冷启动事件:start_type=${startType}`);
|
||||
LaunchDiagnostics.logInfo(`跳过非冷启动事件:start_type=${startType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const extendTime: number = (params['extend_time'] as number) ?? 0;
|
||||
if (extendTime <= 0) {
|
||||
StartupDiagnostics.logInfo('跳过尚未填充 extend_time 的 APP_LAUNCH');
|
||||
LaunchDiagnostics.logInfo('跳过尚未填充 extend_time 的 APP_LAUNCH');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -252,17 +252,17 @@ export class StartupDiagnostics {
|
||||
const iconInputTime: number = (params['icon_input_time'] as number) ?? 0;
|
||||
const processName: string = (params['process_name'] as string) ?? '';
|
||||
const eventKey: string = `${eventTime}|${iconInputTime}|${startType}|${processName}`;
|
||||
if (StartupDiagnostics.processedEventKeys.indexOf(eventKey) !== -1) {
|
||||
StartupDiagnostics.duplicateEventCount++;
|
||||
StartupDiagnostics.lastAction = `重复 APP_LAUNCH 已拦截:${eventKey}`;
|
||||
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction);
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
if (LaunchDiagnostics.processedEventKeys.indexOf(eventKey) !== -1) {
|
||||
LaunchDiagnostics.duplicateEventCount++;
|
||||
LaunchDiagnostics.lastAction = `重复 APP_LAUNCH 已拦截:${eventKey}`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
StartupDiagnostics.processedEventKeys.push(eventKey);
|
||||
if (StartupDiagnostics.processedEventKeys.length > StartupDiagnostics.MAX_EVENT_KEYS) {
|
||||
StartupDiagnostics.processedEventKeys.shift();
|
||||
LaunchDiagnostics.processedEventKeys.push(eventKey);
|
||||
if (LaunchDiagnostics.processedEventKeys.length > LaunchDiagnostics.MAX_EVENT_KEYS) {
|
||||
LaunchDiagnostics.processedEventKeys.shift();
|
||||
}
|
||||
|
||||
const bundleName: string = (params['bundle_name'] as string) ?? '';
|
||||
@@ -276,23 +276,23 @@ export class StartupDiagnostics {
|
||||
|
||||
// 补投递事件(上次启动提交绘制但进程在事件送达前退出)的 capturedEntryPage 是当前进程的快照,
|
||||
// 不能错误归因到本次启动的页面,因此补报事件的入口标记为未知。
|
||||
const isBackfill: boolean = StartupDiagnostics.isBackfillEvent(iconInputTime);
|
||||
const isBackfill: boolean = LaunchDiagnostics.isBackfillEvent(iconInputTime);
|
||||
|
||||
StartupDiagnostics.launchEventCount++;
|
||||
StartupDiagnostics.lastLaunchDetail =
|
||||
LaunchDiagnostics.launchEventCount++;
|
||||
LaunchDiagnostics.lastLaunchDetail =
|
||||
`extend_time=${extendTime}ms, icon_input_time=${iconInputTime}, ` +
|
||||
`start_type=${startType}, process_name=${processName}\n` +
|
||||
`bundle_name=${bundleName}, bundle_version=${bundleVersion}\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` +
|
||||
`device_type=${LaunchDiagnostics.getDeviceType()}, ` +
|
||||
`system_version=${LaunchDiagnostics.getSystemVersion()}\n` +
|
||||
`is_first_install=${LaunchDiagnostics.isFirstInstall}\n` +
|
||||
`补报事件=${isBackfill ? '是(entry_page 不归因,按 unknown 处理)' : '否'}`;
|
||||
StartupDiagnostics.lastAction = '已捕获有效冷启动 APP_LAUNCH';
|
||||
StartupDiagnostics.logInfo(StartupDiagnostics.lastLaunchDetail);
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
LaunchDiagnostics.lastAction = '已捕获有效冷启动 APP_LAUNCH';
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastLaunchDetail);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,7 +301,7 @@ export class StartupDiagnostics {
|
||||
* 补报事件的 icon_input_time 早于注册时间超过容差(至少 5 秒的送达延迟)。
|
||||
*/
|
||||
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 {
|
||||
@@ -313,7 +313,7 @@ export class StartupDiagnostics {
|
||||
}
|
||||
|
||||
private static getStateName(): string {
|
||||
switch (StartupDiagnostics.drawReportState) {
|
||||
switch (LaunchDiagnostics.drawReportState) {
|
||||
case DrawReportState.IDLE:
|
||||
return 'IDLE';
|
||||
case DrawReportState.SUBMITTED:
|
||||
@@ -328,7 +328,7 @@ export class StartupDiagnostics {
|
||||
}
|
||||
|
||||
private static notifyStatusChanged(): void {
|
||||
const listener: (() => void) | undefined = StartupDiagnostics.statusUpdatedListener;
|
||||
const listener: (() => void) | undefined = LaunchDiagnostics.statusUpdatedListener;
|
||||
if (listener !== undefined) {
|
||||
listener();
|
||||
}
|
||||
@@ -339,10 +339,10 @@ export class StartupDiagnostics {
|
||||
}
|
||||
|
||||
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 {
|
||||
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 { window } from '@kit.ArkUI';
|
||||
import { CrashDiagnostics } from '../diagnostics/CrashDiagnostics';
|
||||
import { StartupDiagnostics } from '../diagnostics/StartupDiagnostics';
|
||||
import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
import { AuthSession } from '../session/AuthSession';
|
||||
|
||||
const DOMAIN = 0x0000;
|
||||
@@ -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(this.context as common.UIAbilityContext);
|
||||
LaunchDiagnostics.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) {
|
||||
@@ -46,7 +46,7 @@ export default class EntryAbility extends UIAbility {
|
||||
// Main window is created, set main page for this ability
|
||||
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) => {
|
||||
if (err.code) {
|
||||
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 { CrashDiagnostics } from '../diagnostics/CrashDiagnostics';
|
||||
import { StartupDiagnostics } from '../diagnostics/StartupDiagnostics';
|
||||
import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
import { AuthSession } from '../session/AuthSession';
|
||||
import crashNative from 'libentry.so';
|
||||
|
||||
@@ -20,7 +20,7 @@ struct Index {
|
||||
@State recordCount: number = 0;
|
||||
|
||||
aboutToAppear(): void {
|
||||
StartupDiagnostics.reportFirstFrameOnDraw(
|
||||
LaunchDiagnostics.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_HOME_ROOT'),
|
||||
this.getUIContext().getHostContext() as common.UIAbilityContext,
|
||||
PAGE_NAME
|
||||
@@ -34,7 +34,7 @@ struct Index {
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
StartupDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
CrashDiagnostics.clearRecordsUpdatedListener();
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ struct Index {
|
||||
return;
|
||||
}
|
||||
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}`;
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
+16
-16
@@ -4,23 +4,23 @@
|
||||
*/
|
||||
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { StartupDiagnostics } from '../diagnostics/StartupDiagnostics';
|
||||
import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
import { AuthSession } from '../session/AuthSession';
|
||||
|
||||
const PAGE_NAME: string = 'StartupLogin';
|
||||
const PAGE_NAME: string = 'LaunchLogin';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct StartupLogin {
|
||||
struct LaunchLogin {
|
||||
@State account: string = 'demo@example.com';
|
||||
@State password: string = '123456';
|
||||
@State startupStatus: string = StartupDiagnostics.getStatusText();
|
||||
@State startupStatus: string = LaunchDiagnostics.getStatusText();
|
||||
|
||||
aboutToAppear(): void {
|
||||
StartupDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
|
||||
this.startupStatus = StartupDiagnostics.getStatusText();
|
||||
LaunchDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
|
||||
this.startupStatus = LaunchDiagnostics.getStatusText();
|
||||
});
|
||||
StartupDiagnostics.reportFirstFrameOnDraw(
|
||||
LaunchDiagnostics.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_LOGIN_ROOT'),
|
||||
this.getUIContext().getHostContext() as common.UIAbilityContext,
|
||||
PAGE_NAME
|
||||
@@ -28,41 +28,41 @@ struct StartupLogin {
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
StartupDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
StartupDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
|
||||
LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
LaunchDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
|
||||
}
|
||||
|
||||
private openNetworkScenario(): void {
|
||||
this.navigateTo('pages/StartupNetwork');
|
||||
this.navigateTo('pages/LaunchNetwork');
|
||||
}
|
||||
|
||||
private openPrivacyScenario(): void {
|
||||
this.navigateTo('pages/StartupPrivacy');
|
||||
this.navigateTo('pages/LaunchPrivacy');
|
||||
}
|
||||
|
||||
private completeLogin(): void {
|
||||
if (!AuthSession.setLoggedIn(true)) {
|
||||
this.startupStatus = `保存登录状态失败,请重试。\n${StartupDiagnostics.getStatusText()}`;
|
||||
this.startupStatus = `保存登录状态失败,请重试。\n${LaunchDiagnostics.getStatusText()}`;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
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) {
|
||||
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 {
|
||||
try {
|
||||
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) {
|
||||
const message: string = err instanceof Error ? err.message : JSON.stringify(err);
|
||||
this.startupStatus = `页面跳转同步异常:${message}\n${StartupDiagnostics.getStatusText()}`;
|
||||
this.startupStatus = `页面跳转同步异常:${message}\n${LaunchDiagnostics.getStatusText()}`;
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -4,20 +4,20 @@
|
||||
*/
|
||||
|
||||
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
|
||||
@Component
|
||||
struct StartupNetwork {
|
||||
@State startupStatus: string = StartupDiagnostics.getStatusText();
|
||||
struct LaunchNetwork {
|
||||
@State startupStatus: string = LaunchDiagnostics.getStatusText();
|
||||
|
||||
aboutToAppear(): void {
|
||||
StartupDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
|
||||
this.startupStatus = StartupDiagnostics.getStatusText();
|
||||
LaunchDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
|
||||
this.startupStatus = LaunchDiagnostics.getStatusText();
|
||||
});
|
||||
StartupDiagnostics.reportFirstFrameOnDraw(
|
||||
LaunchDiagnostics.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_NETWORK_ROOT'),
|
||||
this.getUIContext().getHostContext() as common.UIAbilityContext,
|
||||
PAGE_NAME
|
||||
@@ -25,18 +25,18 @@ struct StartupNetwork {
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
StartupDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
StartupDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
|
||||
LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
LaunchDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
|
||||
}
|
||||
|
||||
private retryNetwork(): void {
|
||||
try {
|
||||
this.getUIContext().getRouter().replaceUrl({ url: 'pages/StartupLogin' }).catch((err: Error) => {
|
||||
this.startupStatus = `页面跳转失败:${err.message}\n${StartupDiagnostics.getStatusText()}`;
|
||||
this.getUIContext().getRouter().replaceUrl({ url: 'pages/LaunchLogin' }).catch((err: Error) => {
|
||||
this.startupStatus = `页面跳转失败:${err.message}\n${LaunchDiagnostics.getStatusText()}`;
|
||||
});
|
||||
} catch (err) {
|
||||
const message: string = err instanceof Error ? err.message : JSON.stringify(err);
|
||||
this.startupStatus = `页面跳转同步异常:${message}\n${StartupDiagnostics.getStatusText()}`;
|
||||
this.startupStatus = `页面跳转同步异常:${message}\n${LaunchDiagnostics.getStatusText()}`;
|
||||
}
|
||||
}
|
||||
|
||||
+13
-13
@@ -4,20 +4,20 @@
|
||||
*/
|
||||
|
||||
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
|
||||
@Component
|
||||
struct StartupPrivacy {
|
||||
@State startupStatus: string = StartupDiagnostics.getStatusText();
|
||||
struct LaunchPrivacy {
|
||||
@State startupStatus: string = LaunchDiagnostics.getStatusText();
|
||||
|
||||
aboutToAppear(): void {
|
||||
StartupDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
|
||||
this.startupStatus = StartupDiagnostics.getStatusText();
|
||||
LaunchDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
|
||||
this.startupStatus = LaunchDiagnostics.getStatusText();
|
||||
});
|
||||
StartupDiagnostics.reportFirstFrameOnDraw(
|
||||
LaunchDiagnostics.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_PRIVACY_ROOT'),
|
||||
this.getUIContext().getHostContext() as common.UIAbilityContext,
|
||||
PAGE_NAME
|
||||
@@ -25,26 +25,26 @@ struct StartupPrivacy {
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
StartupDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
StartupDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
|
||||
LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
LaunchDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
|
||||
}
|
||||
|
||||
private acceptPrivacy(): void {
|
||||
this.navigateTo('pages/StartupLogin');
|
||||
this.navigateTo('pages/LaunchLogin');
|
||||
}
|
||||
|
||||
private showNetworkError(): void {
|
||||
this.navigateTo('pages/StartupNetwork');
|
||||
this.navigateTo('pages/LaunchNetwork');
|
||||
}
|
||||
|
||||
private navigateTo(url: string): void {
|
||||
try {
|
||||
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) {
|
||||
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": [
|
||||
"pages/StartupLogin",
|
||||
"pages/StartupNetwork",
|
||||
"pages/StartupPrivacy",
|
||||
"pages/LaunchLogin",
|
||||
"pages/LaunchNetwork",
|
||||
"pages/LaunchPrivacy",
|
||||
"pages/Index",
|
||||
"pages/CrashHistory"
|
||||
]
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
## 2. 总体链路
|
||||
|
||||
`APP_LAUNCH` 系统事件提供启动类型和系统耗时;首个有效入口页完成首次绘制后,通过 `reportDrawnCompleted` 标记终点。当前客户端使用 `ElkService` 异步上传结构化指标,后台再按模块配置阈值、聚合和看板。若后续具备 HarmonyOS 自定义 APM SDK,可替换上传层,但保留事件采集和去重逻辑。
|
||||
`APP_LAUNCH` 系统事件提供启动类型和系统耗时;首个有效入口页完成首次绘制后,通过 `reportDrawnCompleted` 标记终点。当前客户端双通道上报:`ElkService` 异步上传结构化追溯明细(11 字段),`HXEventMonitor` 分桶指标记录原始耗时由 SDK 按桶聚合(详见 6.2),后台再按模块配置阈值、聚合和看板。若后续具备 HarmonyOS 自定义 APM SDK,可替换上传层,但保留事件采集和去重逻辑。
|
||||
|
||||
## 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` 后读取:
|
||||
|
||||
@@ -47,18 +47,18 @@ time + icon_input_time + start_type + process_name
|
||||
| 维度 | `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` 等)在系统事件与客户端均可获取,按需加回上传即可,无需改动采集逻辑。
|
||||
追溯通道上传最小必要字段:核心指标、4 个维度(含 `is_first_install`,区分安装后首启的偏高耗时)、启动生命周期各阶段时间(`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`、版本 `bundle_version`/`bundle_name` 等)在系统事件与客户端均可获取,按需加回上传即可,无需改动采集逻辑。
|
||||
| 分桶 | `[500, 600, 700, 800, 1000, 1500, 2000, 3000]` |
|
||||
|
||||
模块名使用小写字母和连字符;指标名不包含连字符。维度控制在必要范围内,避免将用户 ID、完整 URL 等高基数字段作为维度。
|
||||
|
||||
分桶指标通过 `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 按桶自动归集聚合,无需客户端手动分桶。
|
||||
分桶指标通过 `HXEventMonitor`(`@kernel/app_monitor_event`)实现:`LaunchMonitor.init()` 中以 `DataMonitorBuilder` 创建 `launch` 模块的 `cold_start_time` 指标(`setBuckets([500, 600, 700, 800, 1000, 1500, 2000, 3000])`,4 个维度 `start_type`/`entry_page`/`app_version`/`is_first_install`),每次有效冷启动 `record(extendTime)` 原始值,由 SDK 按桶自动归集聚合,无需客户端手动分桶。
|
||||
|
||||
桶边界按实测主体分布(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 启动时间拆解(按需启用)
|
||||
### 4.1 启动时间拆解
|
||||
|
||||
`cold_start_time` 之外,系统事件还提供各分段耗时,可拆分冷启动定位劣化归属。**当前追溯通道未上传这些字段**(最小字段集,见上表),需要做分段分析时按需加回:
|
||||
`cold_start_time` 之外,追溯通道已随上传携带各分段耗时(见上表),可拆分冷启动定位劣化归属:
|
||||
|
||||
```text
|
||||
离手 ──response_latency──► 动效开始 ──(animation_finish_time − response_latency)──► 动效完成 ──(extend_time − animation_finish_time)──► 首帧绘制完成
|
||||
@@ -68,9 +68,9 @@ time + icon_input_time + start_type + process_name
|
||||
- `response_latency`:离手到动效开始的耗时(需 API 22+),反映系统响应快慢。
|
||||
- `startability_processstart_dur`:系统启动 Ability 到进程创建完成(仅冷启动),反映系统侧进程启动。
|
||||
- `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` 精确到代码提交;`device_type`/`system_version` 排除设备差异;`bundle_version`/`bundle_name` 为系统版本字段。
|
||||
未上传的候选字段(按需启用):环境 `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. 后台与看板配置
|
||||
|
||||
@@ -82,15 +82,6 @@ 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)启动后立即退出的行为链:
|
||||
@@ -102,6 +93,15 @@ time + icon_input_time + start_type + process_name
|
||||
|
||||
真机实测确认(2026-08-05):部署新版本后先 force-stop 再冷启动,新进程收到旧进程的遗留事件,判定 `补报事件=是`(`icon_input_time` 早于注册时间约 4 秒);随后正常冷启动收到自身事件,判定 `补报事件=否`。双向判定均正确,正常启动无误判。
|
||||
|
||||
### 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'` 来源。
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
1. 单次冷启动中,隐私页、网络页和首页连续跳转时,`reportDrawnCompleted` 仍只提交一次。
|
||||
@@ -110,12 +110,14 @@ time + icon_input_time + start_type + process_name
|
||||
4. 页面在系统异步回调前销毁时,不影响已经提交的上报;旧页面不会释放新页面 observer。
|
||||
5. 本地日志与后台数据量级一致;看板可查询 P50、P95、超 2 秒占比及分桶分布。
|
||||
6. 断网或上传失败不影响启动流程,网络恢复后按既有上传机制重试。
|
||||
7. 双通道上传验证:完整 workspace 编译通过(`HXEventMonitor` 的 `DataMonitorBuilder`/`getEventFactory`/`setBuckets`/`record`/`setDimension_4` 与 `.d.ts` 一致);真机冷启动 hilog 出现 `Cold start metric uploaded` 与 `Cold start bucket metric recorded`;ELK 索引可查 `biz='launch'` 的 11 字段记录,APM 平台 `launch` 模块出现分桶分布。
|
||||
8. 维度与分桶:分桶分布可按 4 个维度(`start_type`/`entry_page`/`app_version`/`is_first_install`)分组查询;分桶边界变更需后台重建或并档(历史分布不可比)。
|
||||
|
||||
## 8. 已验证结果
|
||||
|
||||
### 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)
|
||||
|
||||
@@ -123,8 +125,8 @@ time + icon_input_time + start_type + process_name
|
||||
|---|---|---|---|
|
||||
| 1 | 已登录冷启动 | `aa force-stop` 后启动 | 入口 `Index`,draw 1 次、提交 1 次、成功 1 次,`extend_time=144ms`,`start_type=0` |
|
||||
| 2 | 已登录冷启动(复测) | `aa force-stop` 后启动 | 入口 `Index`,`extend_time=137ms`,单次提交 |
|
||||
| 3 | 未登录冷启动 | `bm clean -d` 清数据后启动 | 入口 `StartupLogin`,`extend_time=152ms`,单次提交 |
|
||||
| 4 | 多入口连续跳转 | 登录页 → 网络异常页 → 返回 → 隐私确认页 | `StartupNetwork`、`StartupPrivacy` 重复首帧注册均被拦截(`DrawReportState` 保护),无重复提交 |
|
||||
| 3 | 未登录冷启动 | `bm clean -d` 清数据后启动 | 入口 `LaunchLogin`,`extend_time=152ms`,单次提交 |
|
||||
| 4 | 多入口连续跳转 | 登录页 → 网络异常页 → 返回 → 隐私确认页 | `LaunchNetwork`、`LaunchPrivacy` 重复首帧注册均被拦截(`DrawReportState` 保护),无重复提交 |
|
||||
| 5 | 时间拆解字段捕获 | 观察三次冷启动的 APP_LAUNCH 详情 | `response_latency=19~24ms`、`startability_processstart_dur=57~83ms`、`appattach_to_appforeground_dur=28~33ms`,均为非零有效值 |
|
||||
| 6 | 热启动(进程存活) | 对运行中进程再次 `aa start` | 无新 `APP_LAUNCH` 事件产生(系统对运行中单例不视为启动),不产生上报 |
|
||||
|
||||
@@ -138,7 +140,7 @@ time + icon_input_time + start_type + process_name
|
||||
|
||||
| # | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | 清数据后未登录冷启动 | 入口 `StartupLogin`,`extend_time=161ms`,单次提交 |
|
||||
| 1 | 清数据后未登录冷启动 | 入口 `LaunchLogin`,`extend_time=161ms`,单次提交 |
|
||||
| 2 | 登录后同进程跳转 `Index` | `Index` 重复首帧注册被拦截,无重复提交 |
|
||||
| 3 | 已登录冷启动 | 入口 `Index`,`extend_time=138ms`,单次提交 |
|
||||
| 4 | 热启动(进程存活) | 无新 `APP_LAUNCH` 事件,不产生上报 |
|
||||
@@ -159,7 +161,7 @@ time + icon_input_time + start_type + process_name
|
||||
|
||||
每组完整链路:清数据 → 未登录冷启动 → 网络异常页跳转(拦截)→ 返回 → 隐私确认页跳转(拦截)→ 返回 → 模拟登录 → `Index` 跳转(拦截)→ 已登录冷启动 → 热启动(无新事件)。3 组结果:
|
||||
|
||||
| 组 | 未登录冷启动(入口 `StartupLogin`) | 网络页拦截 | 隐私页拦截 | `Index` 拦截 | 已登录冷启动(入口 `Index`) |
|
||||
| 组 | 未登录冷启动(入口 `LaunchLogin`) | 网络页拦截 | 隐私页拦截 | `Index` 拦截 | 已登录冷启动(入口 `Index`) |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | `extend_time=162ms`(进程 44062) | ✓ | ✓ | ✓ | `extend_time=133ms`(进程 44644) |
|
||||
| 2 | `extend_time=157ms`(进程 44927) | ✓ | ✓ | ✓ | `extend_time=131ms`(进程 45200) |
|
||||
@@ -186,7 +188,7 @@ time + icon_input_time + start_type + process_name
|
||||
|
||||
| 步骤 | 组 1 | 组 2 | 组 3 |
|
||||
|---|---|---|---|
|
||||
| 未登录冷启动(`StartupLogin`) | 158ms / 补报=否 | 180ms / 补报=否 | 180ms / 补报=否 |
|
||||
| 未登录冷启动(`LaunchLogin`) | 158ms / 补报=否 | 180ms / 补报=否 | 180ms / 补报=否 |
|
||||
| 网络页重复注册拦截 | ✓ | ✓ | ✓ |
|
||||
| 隐私页重复注册拦截 | ✓ | ✓ | ✓ |
|
||||
| `Index` 重复注册拦截 | ✓ | ✓ | ✓ |
|
||||
@@ -196,3 +198,25 @@ time + icon_input_time + start_type + process_name
|
||||
| 1 秒退出(首帧已提交)后重启 | 先收遗留事件补报=是,再收自身事件补报=否 | 同左 | 同左 |
|
||||
|
||||
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` 基于 `preferences` 记录,`bm clean` 清数据后该标记重置(再次显示首启);生产实现 `StartManager.getAppInstallStatus()` 无此行为,语义以生产为准。
|
||||
|
||||
@@ -44,7 +44,7 @@ import { ConnectionInitiator } from '../communication/ConnectionInitiator';
|
||||
import { APMHelper } from '../monitor/APMHelper';
|
||||
import { cbasService } from '../register/cbasregister/CbasServiceRegister';
|
||||
import { eventSubscription } from '../monitor/AppEvent';
|
||||
import { StartupMonitor } from '../monitor/StartupMonitor';
|
||||
import { LaunchMonitor } from '../monitor/LaunchMonitor';
|
||||
import { StockGroupConstants } from 'biz_selfcode';
|
||||
import { StockGroupHandler } from '../groupsub/business/StockGroupHandler';
|
||||
import { VarietyGroupHandler } from '../groupsub/business/VarietyGroupHandler';
|
||||
@@ -142,7 +142,7 @@ export default class EntryAbility extends UIAbility {
|
||||
// =====================================
|
||||
|
||||
// 初始化启动性能监控(必须在 early onCreate 注册,确保 reportDrawnCompleted 前 watcher 已就绪)
|
||||
StartupMonitor.init();
|
||||
LaunchMonitor.init();
|
||||
|
||||
DebugToolUtil.TARGET_NAME = BuildProfile.TARGET_NAME
|
||||
DebugToolUtil.PRODUCT_NAME = BuildProfile.PRODUCT_NAME
|
||||
|
||||
+44
-44
@@ -8,12 +8,12 @@ import { inspector } from '@kit.ArkUI';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { HXEventMonitor, DataMonitorBuilder } from '@kernel/app_monitor_event';
|
||||
|
||||
const TAG: string = 'StartupMonitor';
|
||||
const TAG: string = 'LaunchMonitor';
|
||||
|
||||
/**
|
||||
* 启动性能监控:订阅 APP_LAUNCH 事件,采集冷启动耗时(extend_time),
|
||||
*/
|
||||
export class StartupMonitor {
|
||||
export class LaunchMonitor {
|
||||
private static readonly MAX_EVENT_KEYS: number = 20;
|
||||
/**
|
||||
* 补报判定容差(ms):正常启动链间隔毫秒~秒级,补报(上次启动遗留事件)至少早 5 秒
|
||||
@@ -41,29 +41,29 @@ export class StartupMonitor {
|
||||
abilityContext: common.UIAbilityContext,
|
||||
pageName: string
|
||||
): void {
|
||||
if (StartupMonitor.drawReportSubmitted) {
|
||||
if (LaunchMonitor.drawReportSubmitted) {
|
||||
HXLog.i(TAG, `Skip duplicate draw registration for page: ${pageName}`)
|
||||
return
|
||||
}
|
||||
|
||||
StartupMonitor.disposeFirstFrame(StartupMonitor.activeObserverOwner)
|
||||
LaunchMonitor.disposeFirstFrame(LaunchMonitor.activeObserverOwner)
|
||||
|
||||
try {
|
||||
const observer = createObserver();
|
||||
const onDraw = () => {
|
||||
observer.off('draw', onDraw);
|
||||
if (StartupMonitor.activeObserverOwner === pageName) {
|
||||
StartupMonitor.activeObserverOwner = ''
|
||||
StartupMonitor.activeDisposer = undefined
|
||||
if (LaunchMonitor.activeObserverOwner === pageName) {
|
||||
LaunchMonitor.activeObserverOwner = ''
|
||||
LaunchMonitor.activeDisposer = undefined
|
||||
}
|
||||
|
||||
// 必须在系统调用前设置进程级提交状态,防止后续页面或重复回调再次提交
|
||||
if (StartupMonitor.drawReportSubmitted) {
|
||||
if (LaunchMonitor.drawReportSubmitted) {
|
||||
HXLog.i(TAG, `Skip duplicate draw callback for page: ${pageName}`)
|
||||
return
|
||||
}
|
||||
StartupMonitor.drawReportSubmitted = true
|
||||
StartupMonitor.capturedEntryPage = pageName
|
||||
LaunchMonitor.drawReportSubmitted = true
|
||||
LaunchMonitor.capturedEntryPage = pageName
|
||||
|
||||
try {
|
||||
abilityContext.reportDrawnCompleted(() => {
|
||||
@@ -71,14 +71,14 @@ export class StartupMonitor {
|
||||
HXLog.i(TAG, `reportDrawnCompleted submitted for page: ${pageName}`)
|
||||
} catch (e) {
|
||||
// 同步异常表示系统调用尚未提交,允许后续入口页面重试。
|
||||
StartupMonitor.drawReportSubmitted = false
|
||||
StartupMonitor.capturedEntryPage = 'unknown'
|
||||
LaunchMonitor.drawReportSubmitted = false
|
||||
LaunchMonitor.capturedEntryPage = 'unknown'
|
||||
HXLog.e(TAG, `reportDrawnCompleted failed synchronously: ${JSON.stringify(e)}`)
|
||||
}
|
||||
};
|
||||
|
||||
StartupMonitor.activeObserverOwner = pageName
|
||||
StartupMonitor.activeDisposer = () => {
|
||||
LaunchMonitor.activeObserverOwner = pageName
|
||||
LaunchMonitor.activeDisposer = () => {
|
||||
observer.off('draw', onDraw);
|
||||
};
|
||||
observer.on('draw', onDraw);
|
||||
@@ -92,21 +92,21 @@ export class StartupMonitor {
|
||||
* 取消入口页面的首帧绘制监听。
|
||||
*/
|
||||
static disposeFirstFrame(pageName: string): void {
|
||||
if (pageName === '' || StartupMonitor.activeObserverOwner !== pageName) {
|
||||
if (pageName === '' || LaunchMonitor.activeObserverOwner !== pageName) {
|
||||
return
|
||||
}
|
||||
StartupMonitor.activeDisposer?.();
|
||||
StartupMonitor.activeObserverOwner = ''
|
||||
StartupMonitor.activeDisposer = undefined;
|
||||
LaunchMonitor.activeDisposer?.();
|
||||
LaunchMonitor.activeObserverOwner = ''
|
||||
LaunchMonitor.activeDisposer = undefined;
|
||||
}
|
||||
|
||||
/** 注册 APP_LAUNCH watcher(幂等)。应在 onCreate 尽早调用,确保事件生成前完成注册 */
|
||||
static init(): void {
|
||||
if (StartupMonitor.watcherInitialized) {
|
||||
if (LaunchMonitor.watcherInitialized) {
|
||||
return
|
||||
}
|
||||
StartupMonitor.watcherRegisterTime = Date.now()
|
||||
StartupMonitor.ensureBucketMetric()
|
||||
LaunchMonitor.watcherRegisterTime = Date.now()
|
||||
LaunchMonitor.ensureBucketMetric()
|
||||
hiAppEvent.addWatcher({
|
||||
name: "startupWatcher",
|
||||
appEventFilters: [{
|
||||
@@ -115,11 +115,11 @@ export class StartupMonitor {
|
||||
}],
|
||||
onReceive: async (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => {
|
||||
HXLog.i(TAG, `APP_LAUNCH onReceive: domain=${domain}`)
|
||||
StartupMonitor.processAppLaunchEvents(appEventGroups)
|
||||
LaunchMonitor.processAppLaunchEvents(appEventGroups)
|
||||
}
|
||||
});
|
||||
StartupMonitor.watcherInitialized = true
|
||||
HXLog.i(TAG, 'StartupMonitor watcher registered')
|
||||
LaunchMonitor.watcherInitialized = true
|
||||
HXLog.i(TAG, 'LaunchMonitor watcher registered')
|
||||
}
|
||||
|
||||
/** 处理 APP_LAUNCH:过滤冷启动且 extend_time 有效,去重后上传 */
|
||||
@@ -146,21 +146,21 @@ export class StartupMonitor {
|
||||
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) {
|
||||
if (LaunchMonitor.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()
|
||||
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=${StartupMonitor.capturedEntryPage}`)
|
||||
`entry_page=${LaunchMonitor.capturedEntryPage}`)
|
||||
|
||||
StartupMonitor.uploadColdStartTime(extendTime, startType, eventInfo)
|
||||
LaunchMonitor.uploadColdStartTime(extendTime, startType, eventInfo)
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
@@ -177,7 +177,7 @@ export class StartupMonitor {
|
||||
try {
|
||||
const iconInputTime = eventInfo.params['icon_input_time'] as number;
|
||||
// 补投递事件(上次启动遗留)的 entry_page 是当前进程快照,无意义,置 unknown
|
||||
const isBackfillEvent = StartupMonitor.isBackfillEvent(iconInputTime);
|
||||
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;
|
||||
@@ -188,18 +188,18 @@ export class StartupMonitor {
|
||||
const metricPayload: ColdStartMetricPayload = {
|
||||
cold_start_time: coldStartTime,
|
||||
start_type: startType,
|
||||
entry_page: isBackfillEvent ? 'unknown' : StartupMonitor.capturedEntryPage,
|
||||
app_version: StartupMonitor.getAppVersion(),
|
||||
is_first_install: StartupMonitor.getIsFirstInstall(),
|
||||
entry_page: isBackfillEvent ? 'unknown' : LaunchMonitor.capturedEntryPage,
|
||||
app_version: LaunchMonitor.getAppVersion(),
|
||||
is_first_install: LaunchMonitor.getIsFirstInstall(),
|
||||
response_latency: responseLatency,
|
||||
animation_finish_time: animationFinishTime,
|
||||
startability_processstart_dur: startabilityProcessStartDur,
|
||||
appattach_to_appforeground_dur: appattachToAppForegroundDur,
|
||||
git_commit: StartupMonitor.getGitCommit(),
|
||||
git_commit: LaunchMonitor.getGitCommit(),
|
||||
is_backfill: isBackfillEvent,
|
||||
}
|
||||
|
||||
const builder = new StartupElkBuilder('i')
|
||||
const builder = new LaunchElkBuilder('i')
|
||||
builder.userid(HXUserService.getInstance().getUserId())
|
||||
builder.messageKey('cold_start_time')
|
||||
builder.messageValue(JSON.stringify(metricPayload))
|
||||
@@ -214,9 +214,9 @@ export class StartupMonitor {
|
||||
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.setDimension_2(isBackfillEvent ? 'unknown' : LaunchMonitor.capturedEntryPage)
|
||||
monitor.setDimension_3(LaunchMonitor.getAppVersion())
|
||||
monitor.setDimension_4(LaunchMonitor.getIsFirstInstall().toString())
|
||||
monitor.record(coldStartTime)
|
||||
HXLog.i(TAG, `Cold start bucket metric recorded: ${coldStartTime}ms`)
|
||||
} catch (e) {
|
||||
@@ -230,7 +230,7 @@ export class StartupMonitor {
|
||||
* TODO(api-confirm): DataMonitorBuilder / setBuckets / create 签名需以 .d.ts 核对
|
||||
*/
|
||||
private static ensureBucketMetric(): void {
|
||||
if (StartupMonitor.bucketMetricInitialized) {
|
||||
if (LaunchMonitor.bucketMetricInitialized) {
|
||||
return
|
||||
}
|
||||
const builder = new DataMonitorBuilder()
|
||||
@@ -241,13 +241,13 @@ export class StartupMonitor {
|
||||
builder.setDimension4Name('is_first_install')
|
||||
builder.setBuckets([500, 600, 700, 800, 1000, 1500, 2000, 3000])
|
||||
HXEventMonitor.getEventFactory('launch').create(builder)
|
||||
StartupMonitor.bucketMetricInitialized = true
|
||||
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 < StartupMonitor.watcherRegisterTime - StartupMonitor.BACKFILL_TOLERANCE_MS
|
||||
return iconInputTime < LaunchMonitor.watcherRegisterTime - LaunchMonitor.BACKFILL_TOLERANCE_MS
|
||||
}
|
||||
|
||||
private static getAppVersion(): string {
|
||||
@@ -266,7 +266,7 @@ export class StartupMonitor {
|
||||
}
|
||||
|
||||
/** ELK 消息构造器,业务来源为 launch 模块(命名约束见文档第 4 节) */
|
||||
class StartupElkBuilder extends ElkUploadMessageBuilder {
|
||||
class LaunchElkBuilder extends ElkUploadMessageBuilder {
|
||||
constructor(biz_level: string = 'i') {
|
||||
super(Date.now(), 'launch', biz_level);
|
||||
}
|
||||
@@ -281,7 +281,7 @@ interface ColdStartMetricPayload {
|
||||
cold_start_time: number
|
||||
// 维度:启动类型(0=冷启动,当前仅统计冷启动)
|
||||
start_type: number
|
||||
// 维度:入口页面(Index/StartupLogin 等;补报事件为 unknown)
|
||||
// 维度:入口页面(Index/LauncherPage 等;补报事件为 unknown)
|
||||
entry_page: string
|
||||
// 维度:应用内部版本号
|
||||
app_version: string
|
||||
@@ -8,7 +8,7 @@ import { HXNetwork } from './HXNetwork'
|
||||
import { ConnectionSeq } from '@kernel/lib_communication'
|
||||
import { AuthProviderService } from 'biz_auth'
|
||||
import { AdvertisingMonitorService } from 'biz_hxservice'
|
||||
import { StartupMonitor } from '../monitor/StartupMonitor'
|
||||
import { LaunchMonitor } from '../monitor/LaunchMonitor'
|
||||
|
||||
const SettingWant: Want = {
|
||||
bundleName: 'com.huawei.hmos.settings',
|
||||
@@ -49,13 +49,13 @@ struct NetworkAnomalyPage {
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.setSystemBar()
|
||||
StartupMonitor.reportFirstFrameOnDraw(
|
||||
LaunchMonitor.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
|
||||
this.context, 'NetworkAnomalyPage');
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
StartupMonitor.disposeFirstFrame('NetworkAnomalyPage')
|
||||
LaunchMonitor.disposeFirstFrame('NetworkAnomalyPage')
|
||||
}
|
||||
|
||||
onExit() {
|
||||
|
||||
@@ -48,7 +48,7 @@ import { enableDarkMode, ThemeManagerEmitterConstants } from '@kernel/theme_mana
|
||||
import { CmeTraceForeignManager } from 'biz_quote/src/main/ets/manager/CmeTraceForeignManager';
|
||||
import { HQUGCManager } from 'biz_market';
|
||||
import { AdsManager } from '@b2c-f/fuhm-ads';
|
||||
import { StartupMonitor } from '../monitor/StartupMonitor';
|
||||
import { LaunchMonitor } from '../monitor/LaunchMonitor';
|
||||
|
||||
|
||||
const TAG: string = 'Struct Index';
|
||||
@@ -311,7 +311,7 @@ struct IndexM {
|
||||
aboutToDisappear() {
|
||||
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.TAB_UI_MANAGER_CHANGE_INDEX)
|
||||
emitter.off(QuoteSettingEvents.SETTING_UPDATE, this.onSettingUpdateCallback)
|
||||
@@ -366,7 +366,7 @@ struct IndexM {
|
||||
DrawLineInit.init()
|
||||
|
||||
// 注册首帧绘制监听,绘制完成时自动调用 reportDrawnCompleted
|
||||
StartupMonitor.reportFirstFrameOnDraw(
|
||||
LaunchMonitor.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
|
||||
this.context, 'Index');
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { window } from '@kit.ArkUI'
|
||||
import { ConnectionSeq } from '@kernel/lib_communication/src/main/ets/data/CommunicationConstants'
|
||||
import { deviceInfo } from '@kit.BasicServicesKit'
|
||||
import { AdvertisingMonitorService } from 'biz_hxservice'
|
||||
import { StartupMonitor } from '../monitor/StartupMonitor'
|
||||
import { LaunchMonitor } from '../monitor/LaunchMonitor'
|
||||
|
||||
/**
|
||||
* 启动页面
|
||||
@@ -47,13 +47,13 @@ struct LauncherPage {
|
||||
aboutToAppear(): void {
|
||||
this.setSystemBar()
|
||||
this.userPrivacyDialogVisible = !PreferenceService.getBooleanValueSync('Privacy', 'isUserAgree', false);
|
||||
StartupMonitor.reportFirstFrameOnDraw(
|
||||
LaunchMonitor.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
|
||||
this.context, 'LauncherPage');
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
StartupMonitor.disposeFirstFrame('LauncherPage')
|
||||
LaunchMonitor.disposeFirstFrame('LauncherPage')
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
|
||||
Reference in New Issue
Block a user