Compare commits

...

2 Commits

Author SHA1 Message Date
clz 1fce98f7c3 docs: 记录仓库提交规范
- 约定 Conventional Commit 主题格式
- 要求非简单改动使用列表说明实现与验证
- 同步当前 Obsidian 工作区状态
2026-08-05 16:22:26 +08:00
clz d8513d1f1d feat: 完善启动性能监控流程
- 增加登录、网络异常和隐私确认启动场景
- 持久化登录状态并支持首页退出登录
- 保证每个进程只提交一次 reportDrawnCompleted
- 按启动事件唯一键去重 APP_LAUNCH 上报
- 同步优化逻辑到 future_entry_demo 并在 CrashDiagnosticsDemo 真机验证
2026-08-05 16:19:08 +08:00
15 changed files with 975 additions and 15 deletions
+10 -9
View File
@@ -191,21 +191,22 @@
},
"active": "9775ada2a6c4ad3e",
"lastOpenFiles": [
"HarmonyOS 启动性能监控接入方案_20260805.md",
"CrashDiagnosticsDemo/entry/src/main/ets/pages/CrashHistory.ets.tmp.64840.c1c356d07c03",
"CrashDiagnosticsDemo/entry/src/main/ets/pages/CrashHistory.ets.tmp.64840.e9cfc48c9755",
"CrashDiagnosticsDemo/entry/build/default/outputs/default/entry-default-signed.hap",
"CrashDiagnosticsDemo/entry/build/default/intermediates/default/temp/signConfigCheckJson.json",
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/AuthSession.ets-fdb57a143b7ecc425dbeed2e5ed65f533bc4b7678045d59c12cc1712bd95861e.msgpack",
"CrashDiagnosticsDemo/entry/build/default/intermediates/loader_out/default/ets/modules.abc",
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/new-CrashHistory.ets-3362d7b55dd443f6915a35dcdf55398d52857eca3d1670c4e3aca017d6197ce1.msgpack",
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/new-StartupLogin.ets-2334e21c16054ff88652c51913810e6a1bb6e8c3b9caae78194e6fa61d22454f.msgpack",
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/new-Index.ets-f90047a353dd9b221b44a8e9fb622244ad78231d625fc4cd827257cdb1f3fdd9.msgpack",
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/new-AuthSession.ets-fdb57a143b7ecc425dbeed2e5ed65f533bc4b7678045d59c12cc1712bd95861e.msgpack",
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/new-EntryAbility.ets-6cb6772688a3032e3da35b425e18d98ba9971eb7777604aea10e726599977f77.msgpack",
"CrashDiagnosticsDemo/entry/build/default/intermediates/loader_out/default/ets/sourceMaps.map",
"CrashDiagnosticsDemo/entry/src/main/ets/pages/CrashHistory.ets.tmp.64840.e796abd1053c",
"CrashDiagnosticsDemo/entry/src/main/ets/pages/CrashHistory.ets.tmp.64840.d67860e3655a",
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/CrashHistory.ets-3362d7b55dd443f6915a35dcdf55398d52857eca3d1670c4e3aca017d6197ce1.msgpack",
"CrashDiagnosticsDemo/CLAUDE.md",
"CrashDiagnosticsDemo/entry/build/default/intermediates/res/default/resources/base/media/background.png",
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/session/AuthSession.ts",
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/session/AuthSession.protoBin",
"CrashDiagnosticsDemo/entry/build/default/intermediates/res/default/resources/base/media/startIcon.png",
"CrashDiagnosticsDemo/entry/build/default/intermediates/res/default/resources/base/media/foreground.png",
"CrashDiagnosticsDemo/entry/build/default/intermediates/res/default/resources/base/media/background.png",
"HarmonyOS 启动性能监控接入方案_20260805.md",
"CrashDiagnosticsDemo/CLAUDE.md",
"CrashDiagnosticsDemo/entry/src/main/resources/base/media/startIcon.png",
"CrashDiagnosticsDemo/entry/src/main/resources/base/media/foreground.png",
"CrashDiagnosticsDemo/entry/src/main/resources/base/media/background.png",
+9 -1
View File
@@ -40,6 +40,14 @@ Validation is manual. Before submitting changes:
## Commit & Pull Request Guidelines
History is currently minimal (`first commit`), so use concise, imperative commit subjects such as `docs: clarify OOM crash accounting` or `assets: add native crash evidence`. Keep report and supporting screenshots in the same commit.
Use Conventional Commit-style subjects in the form `<type>: <summary>`, such as `docs: clarify OOM crash accounting` or `feat: 完善启动性能监控流程`. For non-trivial changes, add a blank line followed by `-` list items describing the concrete behavior, implementation, and verification. Keep the subject concise and do not repeat it in the body. Keep related code, documentation, and supporting assets in the same commit.
```text
feat: 完善启动性能监控流程
- 保证每个进程只提交一次首帧完成事件
- 按启动事件唯一键去重上报
- 在真机验证登录与网络异常场景
```
Pull requests should summarize the conclusion changed, identify the data period and source, list added or replaced assets, and call out any unresolved evidence gaps. Include rendered screenshots when formatting changes are substantial and link the relevant issue when one exists.
@@ -0,0 +1,275 @@
/*
* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
*/
import { common } from '@kit.AbilityKit';
import { inspector } from '@kit.ArkUI';
import { hiAppEvent, hilog } from '@kit.PerformanceAnalysisKit';
enum DrawReportState {
IDLE,
SUBMITTED,
SUCCEEDED,
FAILED
}
/**
* 启动性能检测演示。
*
* 监听系统 APP_LAUNCH 事件,并确保一个进程生命周期内只提交一次
* reportDrawnCompleted。检测结果同时输出到 hilog 和模拟页面。
*/
export class StartupDiagnostics {
private static readonly WATCHER_NAME: string = 'startupDiagnosticsWatcher';
private static readonly LOG_DOMAIN: number = 0xD002;
private static readonly MAX_EVENT_KEYS: number = 20;
private static watcherInitialized: boolean = false;
private static drawReportState: DrawReportState = DrawReportState.IDLE;
private static capturedEntryPage: string = '未捕获';
private static activeObserverOwner: string = '';
private static activeDisposer: (() => void) | undefined = undefined;
private static statusListenerOwner: string = '';
private static statusUpdatedListener: (() => void) | undefined = undefined;
private static processedEventKeys: Array<string> = [];
private static drawCallbackCount: number = 0;
private static reportSubmitCount: number = 0;
private static skippedReportCount: number = 0;
private static launchEventCount: number = 0;
private static duplicateEventCount: number = 0;
private static lastAction: string = '等待登录页首次绘制';
private static lastLaunchDetail: string = '尚未收到 APP_LAUNCH';
static initialize(): void {
if (StartupDiagnostics.watcherInitialized) {
return;
}
const filter: hiAppEvent.AppEventFilter = {
domain: hiAppEvent.domain.OS,
names: [hiAppEvent.event.APP_LAUNCH]
};
const watcher: hiAppEvent.Watcher = {
name: StartupDiagnostics.WATCHER_NAME,
appEventFilters: [filter],
onReceive: (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => {
hilog.info(StartupDiagnostics.LOG_DOMAIN, 'StartupDiagnostics',
'APP_LAUNCH onReceive domain=%{public}s, groups=%{public}d', domain, appEventGroups.length);
StartupDiagnostics.processAppLaunchEvents(appEventGroups);
}
};
try {
hiAppEvent.addWatcher(watcher);
StartupDiagnostics.watcherInitialized = true;
StartupDiagnostics.lastAction = 'APP_LAUNCH 监听已注册,等待首帧';
StartupDiagnostics.notifyStatusChanged();
} catch (err) {
StartupDiagnostics.lastAction = `APP_LAUNCH 监听注册失败:${StartupDiagnostics.getErrorMessage(err)}`;
StartupDiagnostics.logError(StartupDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged();
}
}
static reportFirstFrameOnDraw(
createObserver: () => inspector.ComponentObserver,
abilityContext: common.UIAbilityContext,
pageName: string
): void {
if (StartupDiagnostics.drawReportState !== DrawReportState.IDLE) {
StartupDiagnostics.skippedReportCount++;
StartupDiagnostics.lastAction = `页面 ${pageName} 的重复首帧注册已跳过`;
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged();
return;
}
StartupDiagnostics.disposeFirstFrame(StartupDiagnostics.activeObserverOwner);
try {
const observer: inspector.ComponentObserver = createObserver();
const onDraw = (): void => {
observer.off('draw', onDraw);
if (StartupDiagnostics.activeObserverOwner === pageName) {
StartupDiagnostics.activeObserverOwner = '';
StartupDiagnostics.activeDisposer = undefined;
}
if (StartupDiagnostics.drawReportState !== DrawReportState.IDLE) {
StartupDiagnostics.skippedReportCount++;
StartupDiagnostics.lastAction = `页面 ${pageName} 的重复 draw 回调已跳过`;
StartupDiagnostics.notifyStatusChanged();
return;
}
StartupDiagnostics.drawCallbackCount++;
StartupDiagnostics.drawReportState = DrawReportState.SUBMITTED;
StartupDiagnostics.capturedEntryPage = pageName;
StartupDiagnostics.reportSubmitCount++;
StartupDiagnostics.lastAction = `已提交 ${pageName} 的 reportDrawnCompleted`;
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged();
try {
abilityContext.reportDrawnCompleted((err) => {
if (err && err.code !== 0) {
StartupDiagnostics.drawReportState = DrawReportState.FAILED;
StartupDiagnostics.lastAction =
`reportDrawnCompleted 失败:code=${err.code}, message=${err.message}`;
StartupDiagnostics.logError(StartupDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged();
return;
}
StartupDiagnostics.drawReportState = DrawReportState.SUCCEEDED;
StartupDiagnostics.lastAction = `reportDrawnCompleted 成功:${pageName}`;
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged();
});
} catch (err) {
// 同步异常表示异步任务未成功提交,恢复 IDLE 后允许其他页面重试。
StartupDiagnostics.drawReportState = DrawReportState.IDLE;
StartupDiagnostics.lastAction =
`reportDrawnCompleted 同步异常:${StartupDiagnostics.getErrorMessage(err)}`;
StartupDiagnostics.logError(StartupDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged();
}
};
StartupDiagnostics.activeObserverOwner = pageName;
StartupDiagnostics.activeDisposer = (): void => {
observer.off('draw', onDraw);
};
observer.on('draw', onDraw);
StartupDiagnostics.lastAction = `已监听 ${pageName} 根组件 draw`;
StartupDiagnostics.logInfo(StartupDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged();
} catch (err) {
StartupDiagnostics.lastAction = `创建 draw 监听失败:${StartupDiagnostics.getErrorMessage(err)}`;
StartupDiagnostics.logError(StartupDiagnostics.lastAction);
StartupDiagnostics.notifyStatusChanged();
}
}
static disposeFirstFrame(pageName: string): void {
if (pageName === '' || StartupDiagnostics.activeObserverOwner !== pageName) {
return;
}
const disposer: (() => void) | undefined = StartupDiagnostics.activeDisposer;
if (disposer !== undefined) {
disposer();
}
StartupDiagnostics.activeObserverOwner = '';
StartupDiagnostics.activeDisposer = undefined;
}
static setStatusUpdatedListener(owner: string, listener: () => void): void {
StartupDiagnostics.statusListenerOwner = owner;
StartupDiagnostics.statusUpdatedListener = listener;
listener();
}
static clearStatusUpdatedListener(owner: string): void {
if (StartupDiagnostics.statusListenerOwner !== owner) {
return;
}
StartupDiagnostics.statusListenerOwner = '';
StartupDiagnostics.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}`;
}
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]);
}
}
}
private static processAppLaunchEvent(eventInfo: hiAppEvent.AppEventInfo): void {
const paramsJson: string = JSON.stringify(eventInfo.params);
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}`);
return;
}
const extendTime: number = (params['extend_time'] as number) ?? 0;
if (extendTime <= 0) {
StartupDiagnostics.logInfo('跳过尚未填充 extend_time 的 APP_LAUNCH');
return;
}
const eventTime: number = (params['time'] as number) ?? 0;
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();
return;
}
StartupDiagnostics.processedEventKeys.push(eventKey);
if (StartupDiagnostics.processedEventKeys.length > StartupDiagnostics.MAX_EVENT_KEYS) {
StartupDiagnostics.processedEventKeys.shift();
}
StartupDiagnostics.launchEventCount++;
StartupDiagnostics.lastLaunchDetail =
`extend_time=${extendTime}ms, icon_input_time=${iconInputTime}, ` +
`start_type=${startType}, process_name=${processName}`;
StartupDiagnostics.lastAction = '已捕获有效冷启动 APP_LAUNCH';
StartupDiagnostics.logInfo(StartupDiagnostics.lastLaunchDetail);
StartupDiagnostics.notifyStatusChanged();
}
private static getStateName(): string {
switch (StartupDiagnostics.drawReportState) {
case DrawReportState.IDLE:
return 'IDLE';
case DrawReportState.SUBMITTED:
return 'SUBMITTED';
case DrawReportState.SUCCEEDED:
return 'SUCCEEDED';
case DrawReportState.FAILED:
return 'FAILED';
default:
return 'UNKNOWN';
}
}
private static notifyStatusChanged(): void {
const listener: (() => void) | undefined = StartupDiagnostics.statusUpdatedListener;
if (listener !== undefined) {
listener();
}
}
private static getErrorMessage(err: Object): string {
return err instanceof Error ? err.message : JSON.stringify(err);
}
private static logInfo(message: string): void {
hilog.info(StartupDiagnostics.LOG_DOMAIN, 'StartupDiagnostics', '%{public}s', message);
}
private static logError(message: string): void {
hilog.error(StartupDiagnostics.LOG_DOMAIN, 'StartupDiagnostics', '%{public}s', message);
}
}
@@ -17,6 +17,8 @@ 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 { AuthSession } from '../session/AuthSession';
const DOMAIN = 0x0000;
@@ -24,6 +26,8 @@ export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
CrashDiagnostics.initialize(this.context as common.UIAbilityContext);
StartupDiagnostics.initialize();
AuthSession.initialize(this.context as common.UIAbilityContext);
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
if (err instanceof Error) {
@@ -42,7 +46,8 @@ export default class EntryAbility extends UIAbility {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
const initialPage: string = AuthSession.isLoggedIn() ? 'pages/Index' : 'pages/StartupLogin';
windowStage.loadContent(initialPage, (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
@@ -3,10 +3,14 @@
* Licensed under the Apache License, Version 2.0 (the "License");
*/
import { common } from '@kit.AbilityKit';
import { CrashDiagnostics } from '../diagnostics/CrashDiagnostics';
import { StartupDiagnostics } from '../diagnostics/StartupDiagnostics';
import { AuthSession } from '../session/AuthSession';
import crashNative from 'libentry.so';
const ENABLE_DEBUG_CRASH_DIAGNOSTICS: boolean = true;
const PAGE_NAME: string = 'Index';
let startupCrashDialogShown: boolean = false;
@Entry
@@ -16,6 +20,11 @@ struct Index {
@State recordCount: number = 0;
aboutToAppear(): void {
StartupDiagnostics.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_HOME_ROOT'),
this.getUIContext().getHostContext() as common.UIAbilityContext,
PAGE_NAME
);
CrashDiagnostics.setRecordsUpdatedListener(() => {
this.updateRecordStatus();
this.showLastCrashIfNeeded();
@@ -25,9 +34,25 @@ struct Index {
}
aboutToDisappear(): void {
StartupDiagnostics.disposeFirstFrame(PAGE_NAME);
CrashDiagnostics.clearRecordsUpdatedListener();
}
private logout(): void {
if (!AuthSession.setLoggedIn(false)) {
this.statusText = '退出登录失败:无法保存登录状态';
return;
}
try {
this.getUIContext().getRouter().replaceUrl({ url: 'pages/StartupLogin' }).catch((err: Error) => {
this.statusText = `退出登录后跳转失败:${err.message}`;
});
} catch (err) {
const message: string = err instanceof Error ? err.message : JSON.stringify(err);
this.statusText = `退出登录同步异常:${message}`;
}
}
private updateRecordStatus(): void {
this.recordCount = CrashDiagnostics.getRecentRecords().length;
this.statusText = this.recordCount === 0 ?
@@ -123,8 +148,19 @@ struct Index {
.onClick(() => {
this.getUIContext().getRouter().pushUrl({ url: 'pages/CrashHistory' });
})
Button('退出登录')
.width('100%')
.height(44)
.fontSize(16)
.borderRadius(8)
.backgroundColor('#666666')
.onClick(() => {
this.logout();
})
}
}
.id('STARTUP_HOME_ROOT')
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
@@ -0,0 +1,138 @@
/*
* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
*/
import { common } from '@kit.AbilityKit';
import { StartupDiagnostics } from '../diagnostics/StartupDiagnostics';
import { AuthSession } from '../session/AuthSession';
const PAGE_NAME: string = 'StartupLogin';
@Entry
@Component
struct StartupLogin {
@State account: string = 'demo@example.com';
@State password: string = '123456';
@State startupStatus: string = StartupDiagnostics.getStatusText();
aboutToAppear(): void {
StartupDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
this.startupStatus = StartupDiagnostics.getStatusText();
});
StartupDiagnostics.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_LOGIN_ROOT'),
this.getUIContext().getHostContext() as common.UIAbilityContext,
PAGE_NAME
);
}
aboutToDisappear(): void {
StartupDiagnostics.disposeFirstFrame(PAGE_NAME);
StartupDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
}
private openNetworkScenario(): void {
this.navigateTo('pages/StartupNetwork');
}
private openPrivacyScenario(): void {
this.navigateTo('pages/StartupPrivacy');
}
private completeLogin(): void {
if (!AuthSession.setLoggedIn(true)) {
this.startupStatus = `保存登录状态失败,请重试。\n${StartupDiagnostics.getStatusText()}`;
return;
}
try {
this.getUIContext().getRouter().replaceUrl({ url: 'pages/Index' }).catch((err: Error) => {
this.startupStatus = `进入首页失败:${err.message}\n${StartupDiagnostics.getStatusText()}`;
});
} catch (err) {
const message: string = err instanceof Error ? err.message : JSON.stringify(err);
this.startupStatus = `进入首页同步异常:${message}\n${StartupDiagnostics.getStatusText()}`;
}
}
private navigateTo(url: string): void {
try {
this.getUIContext().getRouter().pushUrl({ url: url }).catch((err: Error) => {
this.startupStatus = `页面跳转失败:${err.message}\n${StartupDiagnostics.getStatusText()}`;
});
} catch (err) {
const message: string = err instanceof Error ? err.message : JSON.stringify(err);
this.startupStatus = `页面跳转同步异常:${message}\n${StartupDiagnostics.getStatusText()}`;
}
}
build() {
Scroll() {
Column({ space: 16 }) {
Text('账号登录')
.fontSize(28)
.fontWeight(FontWeight.Bold)
Text('首次绘制会触发启动性能检测;进入其他场景后会再次尝试注册,用于验证单次保护。')
.fontSize(14)
.fontColor('#666666')
TextInput({ placeholder: '账号', text: this.account })
.width('100%')
.height(48)
.onChange((value: string) => {
this.account = value;
})
TextInput({ placeholder: '密码', text: this.password })
.type(InputType.Password)
.width('100%')
.height(48)
.onChange((value: string) => {
this.password = value;
})
Button('模拟登录成功')
.width('100%')
.height(46)
.onClick(() => {
this.completeLogin();
})
Row({ space: 12 }) {
Button('模拟网络异常')
.layoutWeight(1)
.onClick(() => {
this.openNetworkScenario();
})
Button('模拟隐私确认')
.layoutWeight(1)
.onClick(() => {
this.openPrivacyScenario();
})
}
Divider()
Text('启动检测结果')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.width('100%')
Text(this.startupStatus)
.fontSize(13)
.fontColor('#335F8A')
.width('100%')
.padding(12)
.backgroundColor('#EEF6FF')
.borderRadius(8)
}
.width('100%')
.padding(24)
}
.id('STARTUP_LOGIN_ROOT')
.width('100%')
.height('100%')
}
}
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
*/
import { common } from '@kit.AbilityKit';
import { StartupDiagnostics } from '../diagnostics/StartupDiagnostics';
const PAGE_NAME: string = 'StartupNetwork';
@Entry
@Component
struct StartupNetwork {
@State startupStatus: string = StartupDiagnostics.getStatusText();
aboutToAppear(): void {
StartupDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
this.startupStatus = StartupDiagnostics.getStatusText();
});
StartupDiagnostics.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_NETWORK_ROOT'),
this.getUIContext().getHostContext() as common.UIAbilityContext,
PAGE_NAME
);
}
aboutToDisappear(): void {
StartupDiagnostics.disposeFirstFrame(PAGE_NAME);
StartupDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
}
private retryNetwork(): void {
try {
this.getUIContext().getRouter().replaceUrl({ url: 'pages/StartupLogin' }).catch((err: Error) => {
this.startupStatus = `页面跳转失败:${err.message}\n${StartupDiagnostics.getStatusText()}`;
});
} catch (err) {
const message: string = err instanceof Error ? err.message : JSON.stringify(err);
this.startupStatus = `页面跳转同步异常:${message}\n${StartupDiagnostics.getStatusText()}`;
}
}
build() {
Column({ space: 18 }) {
Text('网络连接异常')
.fontSize(26)
.fontWeight(FontWeight.Bold)
Text('这是启动过程中的网络异常模拟页。点击重试后进入登录页,后续页面不应再次提交绘制完成事件。')
.fontSize(15)
.fontColor('#666666')
.textAlign(TextAlign.Center)
Button('网络已恢复,重试')
.width('100%')
.height(46)
.onClick(() => {
this.retryNetwork();
})
Text(this.startupStatus)
.fontSize(13)
.fontColor('#8A4B08')
.width('100%')
.padding(12)
.backgroundColor('#FFF7E8')
.borderRadius(8)
}
.id('STARTUP_NETWORK_ROOT')
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(24)
}
}
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
*/
import { common } from '@kit.AbilityKit';
import { StartupDiagnostics } from '../diagnostics/StartupDiagnostics';
const PAGE_NAME: string = 'StartupPrivacy';
@Entry
@Component
struct StartupPrivacy {
@State startupStatus: string = StartupDiagnostics.getStatusText();
aboutToAppear(): void {
StartupDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
this.startupStatus = StartupDiagnostics.getStatusText();
});
StartupDiagnostics.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_PRIVACY_ROOT'),
this.getUIContext().getHostContext() as common.UIAbilityContext,
PAGE_NAME
);
}
aboutToDisappear(): void {
StartupDiagnostics.disposeFirstFrame(PAGE_NAME);
StartupDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
}
private acceptPrivacy(): void {
this.navigateTo('pages/StartupLogin');
}
private showNetworkError(): void {
this.navigateTo('pages/StartupNetwork');
}
private navigateTo(url: string): void {
try {
this.getUIContext().getRouter().replaceUrl({ url: url }).catch((err: Error) => {
this.startupStatus = `页面跳转失败:${err.message}\n${StartupDiagnostics.getStatusText()}`;
});
} catch (err) {
const message: string = err instanceof Error ? err.message : JSON.stringify(err);
this.startupStatus = `页面跳转同步异常:${message}\n${StartupDiagnostics.getStatusText()}`;
}
}
build() {
Column({ space: 18 }) {
Text('隐私政策确认')
.fontSize(26)
.fontWeight(FontWeight.Bold)
Text('这是启动过程中的隐私确认模拟页。页面跳转后,捕获入口和绘制完成时间不应被后续登录页覆盖。')
.fontSize(15)
.fontColor('#666666')
Button('同意并进入登录页')
.width('100%')
.height(46)
.onClick(() => {
this.acceptPrivacy();
})
Button('模拟网络不可用')
.width('100%')
.height(46)
.onClick(() => {
this.showNetworkError();
})
Text(this.startupStatus)
.fontSize(13)
.fontColor('#3F5D3F')
.width('100%')
.padding(12)
.backgroundColor('#EFF8EF')
.borderRadius(8)
}
.id('STARTUP_PRIVACY_ROOT')
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(24)
}
}
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
*/
import { common } from '@kit.AbilityKit';
import { preferences } from '@kit.ArkData';
import { hilog } from '@kit.PerformanceAnalysisKit';
export class AuthSession {
private static readonly STORE_NAME: string = 'auth_session';
private static readonly LOGGED_IN_KEY: string = 'is_logged_in';
private static readonly LOG_DOMAIN: number = 0xD003;
private static store: preferences.Preferences | undefined = undefined;
static initialize(context: common.UIAbilityContext): void {
try {
const options: preferences.Options = { name: AuthSession.STORE_NAME };
AuthSession.store = preferences.getPreferencesSync(context, options);
} catch (err) {
AuthSession.logFailure('初始化登录状态失败。', err);
}
}
static isLoggedIn(): boolean {
const store: preferences.Preferences | undefined = AuthSession.store;
if (store === undefined) {
return false;
}
try {
return store.getSync(AuthSession.LOGGED_IN_KEY, false) as boolean;
} catch (err) {
AuthSession.logFailure('读取登录状态失败。', err);
return false;
}
}
static setLoggedIn(loggedIn: boolean): boolean {
const store: preferences.Preferences | undefined = AuthSession.store;
if (store === undefined) {
return false;
}
try {
store.putSync(AuthSession.LOGGED_IN_KEY, loggedIn);
store.flushSync();
return true;
} catch (err) {
AuthSession.logFailure('保存登录状态失败。', err);
return false;
}
}
private static logFailure(message: string, err: Object): void {
const detail: string = err instanceof Error ? err.message : 'Unknown error';
hilog.error(AuthSession.LOG_DOMAIN, 'AuthSession', '%{public}s %{public}s', message, detail);
}
}
@@ -1,5 +1,8 @@
{
"src": [
"pages/StartupLogin",
"pages/StartupNetwork",
"pages/StartupPrivacy",
"pages/Index",
"pages/CrashHistory"
]
@@ -44,6 +44,7 @@ import { ConnectionInitiator } from '../communication/ConnectionInitiator';
import { APMHelper } from '../monitor/APMHelper';
import { cbasService } from '../register/cbasregister/CbasServiceRegister';
import { eventSubscription } from '../monitor/AppEvent';
import { StartupMonitor } from '../monitor/StartupMonitor';
import { StockGroupConstants } from 'biz_selfcode';
import { StockGroupHandler } from '../groupsub/business/StockGroupHandler';
import { VarietyGroupHandler } from '../groupsub/business/VarietyGroupHandler';
@@ -140,6 +141,9 @@ export default class EntryAbility extends UIAbility {
hilog.info(0x0000, this.tag, 'Custom Figures registered successfully');
// =====================================
// 初始化启动性能监控(必须在 early onCreate 注册,确保 reportDrawnCompleted 前 watcher 已就绪)
StartupMonitor.init();
DebugToolUtil.TARGET_NAME = BuildProfile.TARGET_NAME
DebugToolUtil.PRODUCT_NAME = BuildProfile.PRODUCT_NAME
DebugToolUtil.BUILD_MODE_NAME = BuildProfile.BUILD_MODE_NAME
@@ -0,0 +1,239 @@
import hiAppEvent from '@ohos.hiviewdfx.hiAppEvent';
import { HXLog } from 'biz_common/src/main/ets/logger/HXLog';
import { HXUserService } from 'biz_hxservice';
import { ElkService, ElkUploadMessage, ElkUploadMessageBuilder } from 'service_monitor';
import { AppConfigManager } from 'biz_common';
import { inspector } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
const TAG: string = 'StartupMonitor';
/**
* 启动性能监控
*
* 订阅系统 APP_LAUNCH 事件,获取系统计算的冷启动耗时(extend_time),
* 并通过现有的 ElkService 上传结构化指标。与 AppEvent.ets 的故障事件监听相互独立。
*
* 指标定义(见 HarmonyOS 启动性能监控接入方案_20260805.md):
* - module: launch
* - metric: cold_start_timems
* - dimensions: start_type / entry_page / app_version
*/
export class StartupMonitor {
private static readonly MAX_EVENT_KEYS: number = 20;
/**
* 首帧绘制完成时快照的入口页面名称,在 onDraw 回调中赋值。
* 与 APP_LAUNCH 事件处理处在同一同步时序内,避免页面跳转覆盖。
*/
private static capturedEntryPage: string = 'unknown';
private static watcherInitialized: boolean = false;
private static drawReportSubmitted: boolean = false;
private static activeObserverOwner: string = '';
private static activeDisposer?: () => void;
private static processedEventKeys: Array<string> = [];
/**
* 在入口页面注册 HOME_ROOT 首次绘制监听,自动完成:
* 1. 创建 observer
* 2. 监听 draw → 仅触发一次 → 快照 entryPage → 调用 reportDrawnCompleted
*
* 页面在 aboutToDisappear 中调用 disposeFirstFrame(pageName) 清理即可,
* 无需在页面侧持有清理函数。
*/
static reportFirstFrameOnDraw(
createObserver: () => inspector.ComponentObserver,
abilityContext: common.UIAbilityContext,
pageName: string
): void {
if (StartupMonitor.drawReportSubmitted) {
HXLog.i(TAG, `Skip duplicate draw registration for page: ${pageName}`)
return
}
StartupMonitor.disposeFirstFrame(StartupMonitor.activeObserverOwner)
try {
const observer = createObserver();
const onDraw = () => {
observer.off('draw', onDraw);
if (StartupMonitor.activeObserverOwner === pageName) {
StartupMonitor.activeObserverOwner = ''
StartupMonitor.activeDisposer = undefined
}
// 多个入口页面可能在同一冷启动中依次绘制。必须在调用系统接口前设置全局状态,
// 防止后续页面或重复 draw 回调再次提交 reportDrawnCompleted。
if (StartupMonitor.drawReportSubmitted) {
HXLog.i(TAG, `Skip duplicate draw callback for page: ${pageName}`)
return
}
StartupMonitor.drawReportSubmitted = true
StartupMonitor.capturedEntryPage = pageName
try {
abilityContext.reportDrawnCompleted(() => {
});
HXLog.i(TAG, `reportDrawnCompleted submitted for page: ${pageName}`)
} catch (e) {
// 同步异常表示系统调用尚未提交,允许后续入口页面重试。
StartupMonitor.drawReportSubmitted = false
StartupMonitor.capturedEntryPage = 'unknown'
HXLog.e(TAG, `reportDrawnCompleted failed synchronously: ${JSON.stringify(e)}`)
}
};
StartupMonitor.activeObserverOwner = pageName
StartupMonitor.activeDisposer = () => {
observer.off('draw', onDraw);
};
observer.on('draw', onDraw);
HXLog.i(TAG, `Draw listener registered for page: ${pageName}`);
} catch (e) {
HXLog.e(TAG, `Create draw listener failed for page ${pageName}: ${JSON.stringify(e)}`)
}
}
/**
* 取消入口页面的首帧绘制监听。
*/
static disposeFirstFrame(pageName: string): void {
if (pageName === '' || StartupMonitor.activeObserverOwner !== pageName) {
return
}
StartupMonitor.activeDisposer?.();
StartupMonitor.activeObserverOwner = ''
StartupMonitor.activeDisposer = undefined;
}
/**
* 初始化 APP_LAUNCH 事件监听。
* 应在 EntryAbility.onCreate 中尽早调用,确保在 reportDrawnCompleted 触发事件前完成注册。
*/
static init(): void {
if (StartupMonitor.watcherInitialized) {
return
}
hiAppEvent.addWatcher({
name: "startupWatcher",
appEventFilters: [{
domain: hiAppEvent.domain.OS,
names: [hiAppEvent.event.APP_LAUNCH]
}],
onReceive: async (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => {
HXLog.i(TAG, `APP_LAUNCH onReceive: domain=${domain}`)
StartupMonitor.processAppLaunchEvents(appEventGroups)
}
});
StartupMonitor.watcherInitialized = true
HXLog.i(TAG, 'StartupMonitor watcher registered')
}
/**
* 遍历 APP_LAUNCH 事件,仅处理冷启动(start_type === 0)且 extend_time 已填充的记录,
* 计算 cold_start_time 并上传。
*/
private static processAppLaunchEvents(appEventGroups: Array<hiAppEvent.AppEventGroup>): void {
try {
if (!appEventGroups || appEventGroups.length === 0) {
return
}
appEventGroups.forEach((eventGroup: hiAppEvent.AppEventGroup) => {
eventGroup.appEventInfos.forEach((eventInfo: hiAppEvent.AppEventInfo) => {
const startType = eventInfo.params['start_type'] as number;
// 仅统计冷启动
if (startType !== 0) {
HXLog.i(TAG, `Skip non-cold-start: start_type=${startType}`)
return
}
const extendTime = eventInfo.params['extend_time'] as number;
// extend_time 即为冷启动耗时(ms):从 icon_input_time 到 reportDrawnCompleted 的时间差,
// 由系统在 reportDrawnCompleted 调用后填充。未填充说明首帧尚未完成。
if (!extendTime || extendTime <= 0) {
HXLog.w(TAG, `APP_LAUNCH has no valid extend_time, skip`)
return
}
const iconInputTime = eventInfo.params['icon_input_time'] as number;
const eventTime = eventInfo.params['time'] as number;
const processName = (eventInfo.params['process_name'] as string) ?? '';
const eventKey = `${eventTime}|${iconInputTime}|${startType}|${processName}`;
if (StartupMonitor.processedEventKeys.indexOf(eventKey) !== -1) {
HXLog.i(TAG, `Skip duplicate APP_LAUNCH: ${eventKey}`)
return
}
StartupMonitor.processedEventKeys.push(eventKey)
if (StartupMonitor.processedEventKeys.length > StartupMonitor.MAX_EVENT_KEYS) {
StartupMonitor.processedEventKeys.shift()
}
HXLog.i(TAG,
`cold_start_time=${extendTime}ms, ` +
`icon_input_time=${iconInputTime}, ` +
`entry_page=${StartupMonitor.capturedEntryPage}`)
StartupMonitor.uploadColdStartTime(extendTime, startType, eventInfo)
})
})
} catch (e) {
HXLog.e(TAG, `Error processing APP_LAUNCH: ${JSON.stringify(e)}`)
}
}
/**
* 上传冷启动耗时指标到 ELK。
* 使用 isInstant=true 异步上传,不阻塞页面渲染。
*/
private static uploadColdStartTime(
coldStartTime: number,
startType: number,
eventInfo: hiAppEvent.AppEventInfo
): void {
try {
const bundleVersion = (eventInfo.params['bundle_version'] as string) ?? '';
const animationFinishTime = eventInfo.params['animation_finish_time'] as number;
const metricPayload: Record<string, Object> = {
// 核心指标
cold_start_time: coldStartTime,
// 维度
start_type: startType,
entry_page: StartupMonitor.capturedEntryPage,
app_version: StartupMonitor.getAppVersion(),
bundle_version: bundleVersion,
// 原始系统字段,用于后端校验和去重
event_time: eventInfo.params['time'],
icon_input_time: eventInfo.params['icon_input_time'],
process_name: eventInfo.params['process_name'],
extend_time: eventInfo.params['extend_time'],
animation_finish_time: animationFinishTime,
}
const builder = new StartupElkBuilder('i')
builder.userid(HXUserService.getInstance().getUserId())
builder.messageKey('cold_start_time')
builder.messageValue(JSON.stringify(metricPayload))
const message = new ElkUploadMessage(builder)
// 异步上传,避免阻塞启动流程
ElkService.getInstance().pushBusinessLog(message, true)
HXLog.i(TAG, `Cold start metric uploaded: ${coldStartTime}ms`)
} catch (e) {
HXLog.e(TAG, `uploadColdStartTime failed: ${JSON.stringify(e)}`)
}
}
private static getAppVersion(): string {
return AppConfigManager.getInstance().getConfigProvider()?.getInnerVersionFull() ?? 'unknown'
}
}
/**
* ELK 消息构造器,业务来源标记为 StartupMonitor
*/
class StartupElkBuilder extends ElkUploadMessageBuilder {
constructor(biz_level: string = 'i') {
super(Date.now(), 'StartupMonitor', biz_level);
}
}
@@ -8,6 +8,7 @@ import { HXNetwork } from './HXNetwork'
import { ConnectionSeq } from '@kernel/lib_communication'
import { AuthProviderService } from 'biz_auth'
import { AdvertisingMonitorService } from 'biz_hxservice'
import { StartupMonitor } from '../monitor/StartupMonitor'
const SettingWant: Want = {
bundleName: 'com.huawei.hmos.settings',
@@ -24,6 +25,7 @@ struct NetworkAnomalyPage {
private TAG: string = "HXNetwork";
private systemBarColor = "#00000000";
private systemBarContentColor = "#FFFFFFFF";
context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
async setSystemBar() {
let context = (this.getUIContext().getHostContext() as common.UIAbilityContext);
@@ -47,6 +49,13 @@ struct NetworkAnomalyPage {
aboutToAppear(): void {
this.setSystemBar()
StartupMonitor.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
this.context, 'NetworkAnomalyPage');
}
aboutToDisappear(): void {
StartupMonitor.disposeFirstFrame('NetworkAnomalyPage')
}
onExit() {
@@ -118,6 +127,8 @@ struct NetworkAnomalyPage {
},
'middle': { 'anchor': '__container__', 'align': HorizontalAlign.Center },
})
}.width('100%').height('100%')
}
.id('HOME_ROOT')
.width('100%').height('100%')
}
}
}
@@ -48,6 +48,7 @@ import { enableDarkMode, ThemeManagerEmitterConstants } from '@kernel/theme_mana
import { CmeTraceForeignManager } from 'biz_quote/src/main/ets/manager/CmeTraceForeignManager';
import { HQUGCManager } from 'biz_market';
import { AdsManager } from '@b2c-f/fuhm-ads';
import { StartupMonitor } from '../monitor/StartupMonitor';
const TAG: string = 'Struct Index';
@@ -309,6 +310,8 @@ struct IndexM {
aboutToDisappear() {
HXLog.d(TAG, 'Index aboutToDisappear triggered at ' + Date.now())
// 页面退出时取消首帧绘制监听
StartupMonitor.disposeFirstFrame('Index');
emitter.off(EmitterConstants.SWIPER_CHANGETOINDEX_1_TEMP)
emitter.off(EmitterConstants.TAB_UI_MANAGER_CHANGE_INDEX)
emitter.off(QuoteSettingEvents.SETTING_UPDATE, this.onSettingUpdateCallback)
@@ -361,6 +364,11 @@ struct IndexM {
// 冷启动先初始化一次 drawline 基础能力,保证首次进入行情页即可绘制同步线。
DrawLineInit.init()
// 注册首帧绘制监听,绘制完成时自动调用 reportDrawnCompleted
StartupMonitor.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
this.context, 'Index');
}
onPageShow() {
@@ -522,6 +530,7 @@ struct IndexM {
Blank().height(px2vp(GlobalContext.navigationBarHeight)).flexShrink(0).backgroundColor($r('app.color.surface_layer1_foreground'))
}
}
.id('HOME_ROOT')
.height('100%')
.width('100%')
}
@@ -9,6 +9,7 @@ import { window } from '@kit.ArkUI'
import { ConnectionSeq } from '@kernel/lib_communication/src/main/ets/data/CommunicationConstants'
import { deviceInfo } from '@kit.BasicServicesKit'
import { AdvertisingMonitorService } from 'biz_hxservice'
import { StartupMonitor } from '../monitor/StartupMonitor'
/**
* 启动页面
@@ -46,6 +47,13 @@ struct LauncherPage {
aboutToAppear(): void {
this.setSystemBar()
this.userPrivacyDialogVisible = !PreferenceService.getBooleanValueSync('Privacy', 'isUserAgree', false);
StartupMonitor.reportFirstFrameOnDraw(
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
this.context, 'LauncherPage');
}
aboutToDisappear(): void {
StartupMonitor.disposeFirstFrame('LauncherPage')
}
onCancel() {
@@ -102,6 +110,8 @@ struct LauncherPage {
},
'middle': { 'anchor': '__container__', 'align': HorizontalAlign.Center },
}).id("image")
}.width('100%').height('100%')
}
.id('HOME_ROOT')
.width('100%').height('100%')
}
}
}