Compare commits
14
Commits
56ad003e04
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec8cf06c66 | ||
|
|
8060805c84 | ||
|
|
96f183c2c0 | ||
|
|
96d6f1b34e | ||
|
|
487ad9e074 | ||
|
|
9e29282be3 | ||
|
|
6fe0d2ec7a | ||
|
|
0312555501 | ||
|
|
bd96495482 | ||
|
|
84063edcb2 | ||
|
|
10bbfc4c82 | ||
|
|
d0949704ed | ||
|
|
1fce98f7c3 | ||
|
|
d8513d1f1d |
Vendored
+10
-9
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -6,11 +6,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
Mixed-purpose repository with three distinct areas:
|
||||
|
||||
1. **Root level** — Obsidian-friendly documentation workspace for HarmonyOS APM (Application Performance Management) crash analysis of 同花顺期货 (THS Futures). The main report is `HarmonyOS APM分析报告_20260731.md` with supporting screenshots in `report-assets/`.
|
||||
1. **Root level** — Obsidian-friendly documentation workspace for HarmonyOS APM (Application Performance Management) analysis of 同花顺期货 (THS Futures). Two main reports:
|
||||
- `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`.
|
||||
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.
|
||||
**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.
|
||||
|
||||
## Build & Development Commands
|
||||
|
||||
@@ -22,6 +24,14 @@ There is no top-level build system. Work in the appropriate subdirectory:
|
||||
|
||||
The `.claude/settings.local.json` pre-approves `devecocli build *`, `devecocli device *`, and `devecocli run *` commands.
|
||||
|
||||
Useful repo-wide checks before committing documentation changes:
|
||||
|
||||
```sh
|
||||
git status --short
|
||||
rg 'report-assets/' '*.md'
|
||||
find report-assets -type f | sort
|
||||
```
|
||||
|
||||
## Cross-Cutting Conventions
|
||||
|
||||
### HarmonyOS / ArkTS
|
||||
@@ -31,12 +41,20 @@ All HarmonyOS projects in this repo use the **Hvigor** build system with **DevEc
|
||||
### Documentation (Markdown)
|
||||
|
||||
- UTF-8 Markdown with ATX headings (`#`, `##`), fenced code blocks with language tags, pipe tables with right-aligned numeric columns.
|
||||
- Name screenshots with a two-digit sequence and short lowercase description (e.g., `08-grafana-native-detail.png`).
|
||||
- Name screenshots with a two-digit sequence and short lowercase description (e.g., `08-grafana-native-detail.png`). Do not reuse an existing image filename for unrelated evidence.
|
||||
- New reports use descriptive filenames with a date (`YYYYMMDD`). Place images in `report-assets/` or a report-specific subdirectory.
|
||||
- Validate manually: preview all headings/tables/images, recalculate totals against source screenshots, verify no absolute paths or sensitive data leak in.
|
||||
- Use backticks for event types, API names, versions, and identifiers.
|
||||
|
||||
### Before Committing Documentation
|
||||
|
||||
1. Preview all headings, tables, code blocks, and image captions in a Markdown viewer.
|
||||
2. Recalculate totals and confirm date ranges, versions, and event classifications against source screenshots.
|
||||
3. Verify all referenced asset files exist and no local absolute paths or sensitive data are exposed.
|
||||
4. Ensure stray images at the repo root are moved into `report-assets/` or deleted.
|
||||
|
||||
### Git
|
||||
|
||||
- Concise, imperative commit subjects (e.g., `docs: clarify OOM crash accounting`, `assets: add native crash evidence`).
|
||||
- Use Conventional Commits format: `<type>: <summary>` (e.g., `docs: clarify OOM crash accounting`, `feat: 完善启动性能监控流程`, `chore: save current workspace changes`).
|
||||
- For non-trivial changes, add a blank line followed by `-` list items describing the concrete change, implementation, and verification.
|
||||
- Keep report text and supporting screenshots in the same commit.
|
||||
- Avoid committing personal Obsidian workspace-state changes unless intentionally shared.
|
||||
- Avoid committing personal Obsidian workspace-state changes (`.obsidian/`) unless intentionally shared.
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* 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';
|
||||
import { deviceInfo } from '@kit.BasicServicesKit';
|
||||
import { preferences } from '@kit.ArkData';
|
||||
|
||||
enum DrawReportState {
|
||||
IDLE,
|
||||
SUBMITTED,
|
||||
SUCCEEDED,
|
||||
FAILED
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动性能检测演示。
|
||||
*
|
||||
* 监听系统 APP_LAUNCH 事件,并确保一个进程生命周期内只提交一次
|
||||
* reportDrawnCompleted。检测结果同时输出到 hilog 和模拟页面。
|
||||
*
|
||||
* 与 future_entry_demo 的 LaunchMonitor.ets 对应:本文件为无依赖演示实现
|
||||
* (仅 hilog + 页面展示,不做 ELK 上传),追溯字段已同步系统时间拆解字段
|
||||
* 与 deviceInfo 环境字段;app_install_status 以 preferences 自实现(三值语义,
|
||||
* over_written 无法区分);git_commit / build_mode 依赖 biz_common 与
|
||||
* BuildProfile,演示工程不适用,故未同步。
|
||||
*/
|
||||
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;
|
||||
/**
|
||||
* 补报事件判定容差(ms)。
|
||||
* 本次启动链中 icon_input_time(点击图标)与 watcher 注册时间间隔在毫秒~秒级;
|
||||
* 系统补投递的上次启动遗留事件,其 icon_input_time 至少早于注册时间 5 秒(事件送达延迟),
|
||||
* 因此容差取 2s 可稳定区分两者。
|
||||
*/
|
||||
private static readonly BACKFILL_TOLERANCE_MS: number = 2000;
|
||||
/**
|
||||
* watcher 注册时间戳(epoch ms),用于识别系统补投递的上次启动遗留事件。
|
||||
*/
|
||||
private static watcherRegisterTime: number = 0;
|
||||
/**
|
||||
* 安装状态(维度):preferences 无启动记录视为 new_install,随后写入标记。
|
||||
* 与生产实现(app_install_status 三值)对齐;演示工程无法区分 over_written(覆盖安装保留数据时显示 normal)。
|
||||
*/
|
||||
private static appInstallStatus: string = 'normal';
|
||||
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(context: common.UIAbilityContext): void {
|
||||
if (LaunchDiagnostics.watcherInitialized) {
|
||||
return;
|
||||
}
|
||||
// 安装状态:preferences 无启动记录视为 new_install,随后写入并同步落盘;异常时按 normal 处理
|
||||
try {
|
||||
const pref: preferences.Preferences = preferences.getPreferencesSync(context, { name: 'startup_demo' });
|
||||
LaunchDiagnostics.appInstallStatus = pref.getSync('has_launched', false) ? 'normal' : 'new_install';
|
||||
pref.putSync('has_launched', true);
|
||||
pref.flushSync();
|
||||
} catch (e) {
|
||||
LaunchDiagnostics.appInstallStatus = 'normal';
|
||||
}
|
||||
|
||||
const filter: hiAppEvent.AppEventFilter = {
|
||||
domain: hiAppEvent.domain.OS,
|
||||
names: [hiAppEvent.event.APP_LAUNCH]
|
||||
};
|
||||
const watcher: hiAppEvent.Watcher = {
|
||||
name: LaunchDiagnostics.WATCHER_NAME,
|
||||
appEventFilters: [filter],
|
||||
onReceive: (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => {
|
||||
hilog.info(LaunchDiagnostics.LOG_DOMAIN, 'LaunchDiagnostics',
|
||||
'APP_LAUNCH onReceive domain=%{public}s, groups=%{public}d', domain, appEventGroups.length);
|
||||
LaunchDiagnostics.processAppLaunchEvents(appEventGroups);
|
||||
}
|
||||
};
|
||||
|
||||
// 记录注册时间戳,用于区分本次启动事件与系统补投递的上次启动遗留事件
|
||||
LaunchDiagnostics.watcherRegisterTime = Date.now();
|
||||
|
||||
try {
|
||||
hiAppEvent.addWatcher(watcher);
|
||||
LaunchDiagnostics.watcherInitialized = true;
|
||||
LaunchDiagnostics.lastAction = 'APP_LAUNCH 监听已注册,等待首帧';
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
} catch (err) {
|
||||
LaunchDiagnostics.lastAction = `APP_LAUNCH 监听注册失败:${LaunchDiagnostics.getErrorMessage(err)}`;
|
||||
LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
}
|
||||
|
||||
static reportFirstFrameOnDraw(
|
||||
createObserver: () => inspector.ComponentObserver,
|
||||
abilityContext: common.UIAbilityContext,
|
||||
pageName: string
|
||||
): void {
|
||||
if (LaunchDiagnostics.drawReportState !== DrawReportState.IDLE) {
|
||||
LaunchDiagnostics.skippedReportCount++;
|
||||
LaunchDiagnostics.lastAction = `页面 ${pageName} 的重复首帧注册已跳过`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
LaunchDiagnostics.disposeFirstFrame(LaunchDiagnostics.activeObserverOwner);
|
||||
|
||||
try {
|
||||
const observer: inspector.ComponentObserver = createObserver();
|
||||
const onDraw = (): void => {
|
||||
observer.off('draw', onDraw);
|
||||
if (LaunchDiagnostics.activeObserverOwner === pageName) {
|
||||
LaunchDiagnostics.activeObserverOwner = '';
|
||||
LaunchDiagnostics.activeDisposer = undefined;
|
||||
}
|
||||
|
||||
if (LaunchDiagnostics.drawReportState !== DrawReportState.IDLE) {
|
||||
LaunchDiagnostics.skippedReportCount++;
|
||||
LaunchDiagnostics.lastAction = `页面 ${pageName} 的重复 draw 回调已跳过`;
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
LaunchDiagnostics.drawReportState = DrawReportState.FAILED;
|
||||
LaunchDiagnostics.lastAction =
|
||||
`reportDrawnCompleted 失败:code=${err.code}, message=${err.message}`;
|
||||
LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
LaunchDiagnostics.drawReportState = DrawReportState.SUCCEEDED;
|
||||
LaunchDiagnostics.lastAction = `reportDrawnCompleted 成功:${pageName}`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
});
|
||||
} catch (err) {
|
||||
// 同步异常表示异步任务未成功提交,恢复 IDLE 后允许其他页面重试。
|
||||
LaunchDiagnostics.drawReportState = DrawReportState.IDLE;
|
||||
LaunchDiagnostics.lastAction =
|
||||
`reportDrawnCompleted 同步异常:${LaunchDiagnostics.getErrorMessage(err)}`;
|
||||
LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
};
|
||||
|
||||
LaunchDiagnostics.activeObserverOwner = pageName;
|
||||
LaunchDiagnostics.activeDisposer = (): void => {
|
||||
observer.off('draw', onDraw);
|
||||
};
|
||||
observer.on('draw', onDraw);
|
||||
LaunchDiagnostics.lastAction = `已监听 ${pageName} 根组件 draw`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
} catch (err) {
|
||||
LaunchDiagnostics.lastAction = `创建 draw 监听失败:${LaunchDiagnostics.getErrorMessage(err)}`;
|
||||
LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
}
|
||||
|
||||
static disposeFirstFrame(pageName: string): void {
|
||||
if (pageName === '' || LaunchDiagnostics.activeObserverOwner !== pageName) {
|
||||
return;
|
||||
}
|
||||
const disposer: (() => void) | undefined = LaunchDiagnostics.activeDisposer;
|
||||
if (disposer !== undefined) {
|
||||
disposer();
|
||||
}
|
||||
LaunchDiagnostics.activeObserverOwner = '';
|
||||
LaunchDiagnostics.activeDisposer = undefined;
|
||||
}
|
||||
|
||||
static setStatusUpdatedListener(owner: string, listener: () => void): void {
|
||||
LaunchDiagnostics.statusListenerOwner = owner;
|
||||
LaunchDiagnostics.statusUpdatedListener = listener;
|
||||
listener();
|
||||
}
|
||||
|
||||
static clearStatusUpdatedListener(owner: string): void {
|
||||
if (LaunchDiagnostics.statusListenerOwner !== owner) {
|
||||
return;
|
||||
}
|
||||
LaunchDiagnostics.statusListenerOwner = '';
|
||||
LaunchDiagnostics.statusUpdatedListener = undefined;
|
||||
}
|
||||
|
||||
static getStatusText(): string {
|
||||
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++) {
|
||||
LaunchDiagnostics.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) {
|
||||
LaunchDiagnostics.logInfo(`跳过非冷启动事件:start_type=${startType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const extendTime: number = (params['extend_time'] as number) ?? 0;
|
||||
if (extendTime <= 0) {
|
||||
LaunchDiagnostics.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 (LaunchDiagnostics.processedEventKeys.indexOf(eventKey) !== -1) {
|
||||
LaunchDiagnostics.duplicateEventCount++;
|
||||
LaunchDiagnostics.lastAction = `重复 APP_LAUNCH 已拦截:${eventKey}`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
LaunchDiagnostics.processedEventKeys.push(eventKey);
|
||||
if (LaunchDiagnostics.processedEventKeys.length > LaunchDiagnostics.MAX_EVENT_KEYS) {
|
||||
LaunchDiagnostics.processedEventKeys.shift();
|
||||
}
|
||||
|
||||
const bundleName: string = (params['bundle_name'] as string) ?? '';
|
||||
const bundleVersion: string = (params['bundle_version'] as string) ?? '';
|
||||
const animationFinishTime: number = (params['animation_finish_time'] as number) ?? 0;
|
||||
// 时间拆解字段(与接入方案文档第 4 节一致):
|
||||
// response_latency 需 API 22+;startability_processstart_dur / appattach_to_appforeground_dur 仅冷启动存在
|
||||
const responseLatency: number = (params['response_latency'] as number) ?? 0;
|
||||
const startabilityProcessStartDur: number = (params['startability_processstart_dur'] as number) ?? 0;
|
||||
const appattachToAppForegroundDur: number = (params['appattach_to_appforeground_dur'] as number) ?? 0;
|
||||
|
||||
// 补投递事件(上次启动提交绘制但进程在事件送达前退出)的 capturedEntryPage 是当前进程的快照,
|
||||
// 不能错误归因到本次启动的页面,因此补报事件的入口标记为未知。
|
||||
const isBackfill: boolean = LaunchDiagnostics.isBackfillEvent(iconInputTime);
|
||||
|
||||
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=${LaunchDiagnostics.getDeviceType()}, ` +
|
||||
`system_version=${LaunchDiagnostics.getSystemVersion()}\n` +
|
||||
`app_install_status=${LaunchDiagnostics.appInstallStatus}\n` +
|
||||
`补报事件=${isBackfill ? '是(entry_page 不归因,按 unknown 处理)' : '否'}`;
|
||||
LaunchDiagnostics.lastAction = '已捕获有效冷启动 APP_LAUNCH';
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastLaunchDetail);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断事件是否为系统补投递的上次启动遗留事件。
|
||||
* 本次启动链中 icon_input_time 与 watcher 注册时间间隔在容差内;
|
||||
* 补报事件的 icon_input_time 早于注册时间超过容差(至少 5 秒的送达延迟)。
|
||||
*/
|
||||
private static isBackfillEvent(iconInputTime: number): boolean {
|
||||
return iconInputTime < LaunchDiagnostics.watcherRegisterTime - LaunchDiagnostics.BACKFILL_TOLERANCE_MS;
|
||||
}
|
||||
|
||||
private static getDeviceType(): string {
|
||||
return deviceInfo.deviceType;
|
||||
}
|
||||
|
||||
private static getSystemVersion(): string {
|
||||
return deviceInfo.osFullName;
|
||||
}
|
||||
|
||||
private static getStateName(): string {
|
||||
switch (LaunchDiagnostics.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 = LaunchDiagnostics.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(LaunchDiagnostics.LOG_DOMAIN, 'LaunchDiagnostics', '%{public}s', message);
|
||||
}
|
||||
|
||||
private static logError(message: string): void {
|
||||
hilog.error(LaunchDiagnostics.LOG_DOMAIN, 'LaunchDiagnostics', '%{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 { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
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);
|
||||
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) {
|
||||
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/LaunchLogin';
|
||||
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 { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
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 {
|
||||
LaunchDiagnostics.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 {
|
||||
LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
CrashDiagnostics.clearRecordsUpdatedListener();
|
||||
}
|
||||
|
||||
private logout(): void {
|
||||
if (!AuthSession.setLoggedIn(false)) {
|
||||
this.statusText = '退出登录失败:无法保存登录状态';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.getUIContext().getRouter().replaceUrl({ url: 'pages/LaunchLogin' }).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 { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
import { AuthSession } from '../session/AuthSession';
|
||||
|
||||
const PAGE_NAME: string = 'LaunchLogin';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct LaunchLogin {
|
||||
@State account: string = 'demo@example.com';
|
||||
@State password: string = '123456';
|
||||
@State startupStatus: string = LaunchDiagnostics.getStatusText();
|
||||
|
||||
aboutToAppear(): void {
|
||||
LaunchDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
|
||||
this.startupStatus = LaunchDiagnostics.getStatusText();
|
||||
});
|
||||
LaunchDiagnostics.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_LOGIN_ROOT'),
|
||||
this.getUIContext().getHostContext() as common.UIAbilityContext,
|
||||
PAGE_NAME
|
||||
);
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
LaunchDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
|
||||
}
|
||||
|
||||
private openNetworkScenario(): void {
|
||||
this.navigateTo('pages/LaunchNetwork');
|
||||
}
|
||||
|
||||
private openPrivacyScenario(): void {
|
||||
this.navigateTo('pages/LaunchPrivacy');
|
||||
}
|
||||
|
||||
private completeLogin(): void {
|
||||
if (!AuthSession.setLoggedIn(true)) {
|
||||
this.startupStatus = `保存登录状态失败,请重试。\n${LaunchDiagnostics.getStatusText()}`;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.getUIContext().getRouter().replaceUrl({ url: 'pages/Index' }).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${LaunchDiagnostics.getStatusText()}`;
|
||||
}
|
||||
}
|
||||
|
||||
private navigateTo(url: string): void {
|
||||
try {
|
||||
this.getUIContext().getRouter().pushUrl({ url: url }).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${LaunchDiagnostics.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 { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
|
||||
const PAGE_NAME: string = 'LaunchNetwork';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct LaunchNetwork {
|
||||
@State startupStatus: string = LaunchDiagnostics.getStatusText();
|
||||
|
||||
aboutToAppear(): void {
|
||||
LaunchDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
|
||||
this.startupStatus = LaunchDiagnostics.getStatusText();
|
||||
});
|
||||
LaunchDiagnostics.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_NETWORK_ROOT'),
|
||||
this.getUIContext().getHostContext() as common.UIAbilityContext,
|
||||
PAGE_NAME
|
||||
);
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
LaunchDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
|
||||
}
|
||||
|
||||
private retryNetwork(): void {
|
||||
try {
|
||||
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${LaunchDiagnostics.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 { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
|
||||
const PAGE_NAME: string = 'LaunchPrivacy';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct LaunchPrivacy {
|
||||
@State startupStatus: string = LaunchDiagnostics.getStatusText();
|
||||
|
||||
aboutToAppear(): void {
|
||||
LaunchDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
|
||||
this.startupStatus = LaunchDiagnostics.getStatusText();
|
||||
});
|
||||
LaunchDiagnostics.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_PRIVACY_ROOT'),
|
||||
this.getUIContext().getHostContext() as common.UIAbilityContext,
|
||||
PAGE_NAME
|
||||
);
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
LaunchDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
|
||||
}
|
||||
|
||||
private acceptPrivacy(): void {
|
||||
this.navigateTo('pages/LaunchLogin');
|
||||
}
|
||||
|
||||
private showNetworkError(): void {
|
||||
this.navigateTo('pages/LaunchNetwork');
|
||||
}
|
||||
|
||||
private navigateTo(url: string): void {
|
||||
try {
|
||||
this.getUIContext().getRouter().replaceUrl({ url: url }).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${LaunchDiagnostics.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/LaunchLogin",
|
||||
"pages/LaunchNetwork",
|
||||
"pages/LaunchPrivacy",
|
||||
"pages/Index",
|
||||
"pages/CrashHistory"
|
||||
]
|
||||
|
||||
@@ -6,45 +6,302 @@
|
||||
|
||||
## 2. 总体链路
|
||||
|
||||
`APP_LAUNCH` 系统事件提供启动类型和系统耗时;首页完成首次有效绘制后,通过 `reportDrawnCompleted` 标记首帧终点。客户端将两者组合为自定义启动指标,由 APM SDK 本地采样、聚合后上传;后台按模块配置阈值和采样策略,并在 ELK/Grafana 展示趋势与分位数。
|
||||
`APP_LAUNCH` 系统事件提供启动类型和系统耗时;首个有效入口页完成首次绘制后,通过 `reportDrawnCompleted` 标记终点。当前客户端双通道上报:`ElkService` 异步上传结构化追溯明细(11 字段),`HXEventMonitor` 分桶指标记录原始耗时由 SDK 按桶聚合(详见 6.2),后台再按模块配置阈值、聚合和看板。若后续具备 HarmonyOS 自定义 APM SDK,可替换上传层,但保留事件采集和去重逻辑。
|
||||
|
||||
## 3. 客户端接入
|
||||
|
||||
在 `future_entry_demo/src/main/ets/monitor/APMHelper.ets` 中初始化与现有 `HXMonitor` 同体系的 HarmonyOS 自定义 APM 事件能力;不要直接复用 Android 文档中的 Java API。
|
||||
实现位于 `future_entry_demo/src/main/ets/monitor/LaunchMonitor.ets`。`EntryAbility.onCreate` 必须尽早调用 `LaunchMonitor.init()`,先注册 watcher,再允许入口页提交首帧完成事件。`init()` 本身应保持幂等,避免同一进程重复注册 watcher。
|
||||
|
||||
在 `EntryAbility.ets` 中订阅 `APP_LAUNCH`,读取:
|
||||
监听 `APP_LAUNCH` 后读取:
|
||||
|
||||
- `start_type`:启动类型;仅 `0` 纳入冷启动统计。
|
||||
- `extend_time`:从抬手到应用报告首帧完成的耗时。
|
||||
- `animation_finish_time`、`icon_input_time`、`bundle_version`:用于分析和问题追溯。
|
||||
- `extend_time`:从系统记录的 `icon_input_time` 到应用报告绘制完成的耗时。
|
||||
- `time`、`icon_input_time`、`process_name`:标识启动事件并用于去重。
|
||||
- `animation_finish_time`、`bundle_version`:用于分析和问题追溯。
|
||||
|
||||
在首页根节点完成第一次有效绘制时仅上报一次 `reportDrawnCompleted`。隐私页、网络异常页等可能成为首次可见页面的入口也应覆盖,否则对应场景的首帧时间会失真。
|
||||
### 3.1 首帧单次提交
|
||||
|
||||
`LauncherPage`、`NetworkAnomalyPage` 和 `Index` 都应尝试监听根节点首次 `draw`,以最先有效绘制的页面作为 `entry_page`。必须遵循以下约束:
|
||||
|
||||
1. 在调用 `reportDrawnCompleted` **之前**设置进程级已提交状态;异步回调尚未执行时,其他页面也不得再次提交。
|
||||
2. observer 触发后立即注销,页面 `aboutToDisappear` 仅释放与自身 `pageName` 匹配的 observer,避免旧页面销毁新页面的监听。
|
||||
3. 仅同步调用异常表示任务未提交,此时恢复未提交状态,允许后续入口页重试;页面销毁不得重置已经提交的状态。
|
||||
|
||||
### 3.2 事件过滤与去重
|
||||
|
||||
仅处理 `start_type === 0` 且 `extend_time > 0` 的记录。上传前按以下键去重:
|
||||
|
||||
```text
|
||||
time + icon_input_time + start_type + process_name
|
||||
```
|
||||
|
||||
客户端保留最近 20 个事件键,兼容同一回调组内重复记录和系统重新投递,同时避免缓存无限增长。
|
||||
|
||||
## 4. 指标定义
|
||||
|
||||
| 项目 | 建议值 |
|
||||
|---|---|
|
||||
| 模块 | `launch` |
|
||||
| 指标名 | `cold_start_time` |
|
||||
| 指标值 | 冷启动耗时,单位 ms |
|
||||
| 维度 | `start_type`、`entry_page`、`app_version` |
|
||||
| 分桶 | `[500, 800, 1200, 2000, 3000]` |
|
||||
| 项目 | 建议值 |
|
||||
| ---- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 模块 | `launch` |
|
||||
| 指标名 | `cold_start_time` |
|
||||
| 指标值 | 冷启动耗时,单位 ms |
|
||||
| 维度 | `start_type`、`entry_page`、`app_version`、`app_install_status` |
|
||||
| 追溯字段 | `response_latency`、`animation_finish_time`、`startability_processstart_dur`、`appattach_to_appforeground_dur`、`git_commit`、`is_backfill` |
|
||||
| 分桶 | `[500, 600, 700, 800, 1000, 1500, 2000, 3000]` |
|
||||
|
||||
模块名使用小写字母和连字符;指标名不包含连字符。维度控制在必要范围内,避免将用户 ID、完整 URL 等高基数字段作为维度。
|
||||
上传负载为最小必要集:指标 1、维度 4(含 `app_install_status`)、启动生命周期各阶段时间 4(劣化分段定位)、`git_commit`(代码级追溯)、`is_backfill`(补报标识);去重为客户端行为(第 3.2 节),负载不含去重键。其余候选字段按需加回,无需改动采集逻辑。
|
||||
|
||||
模块名小写+连字符,指标名不含连字符;维度避免高基数(用户 ID、完整 URL 等)。
|
||||
|
||||
分桶指标由 `HXEventMonitor` 实现(8 桶、4 维度,与追溯一致),`record(extendTime)` 由 SDK 归桶聚合;桶边界按实测主体(600-800ms)细分、尾部留告警锚点,分桶变更需后台重建并档(历史分布不可比)。维度 `app_install_status` 取值 `normal`/`new_install`/`over_written`(安装状态:常规/全新安装/覆盖安装),与 `StartManager` 安装状态对齐。
|
||||
|
||||
### 4.1 启动时间拆解
|
||||
|
||||
`cold_start_time` 之外,追溯通道已随上传携带各分段耗时(见上表),可拆分冷启动定位劣化归属:
|
||||
|
||||
```text
|
||||
离手 ──response_latency──► 动效开始 ──(animation_finish_time − response_latency)──► 动效完成 ──(extend_time − animation_finish_time)──► 首帧绘制完成
|
||||
系统响应段 系统动效段 应用可优化段
|
||||
```
|
||||
|
||||
- `response_latency`:离手到动效开始的耗时(需 API 22+),反映系统响应快慢。
|
||||
- `startability_processstart_dur`:系统启动 Ability 到进程创建完成(仅冷启动),反映系统侧进程启动。
|
||||
- `appattach_to_appforeground_dur`:进程初始化完成到应用切前台(仅冷启动),反映系统侧应用挂载。
|
||||
- `extend_time − animation_finish_time`:动效完成到应用首帧的耗时,即**应用侧可优化的独占部分**;看板可据此判断劣化发生在系统侧还是应用侧。注意 `animation_finish_time` 在部分机型为 `0`(系统未填充),此时该段等于 `extend_time` 全量,聚合时需兼容。
|
||||
|
||||
未上传的候选字段(按需启用):环境 `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. 后台与看板配置
|
||||
|
||||
按新增 PDF 的自定义 APM 模块流程,在 APM 后台创建 `launch` 模块并启用:全量采样(`sampling_rate: 1000`)、60 秒聚合(`aggre_time: 60`)、累计 100 条触发上报(`aggre_count: 100`)。随后为该模块补充 ELK 索引模板,在 Grafana 建立按版本、启动类型和时间聚合的 P50/P95、超 2 秒占比及分桶分布面板。
|
||||
### 5.1 模块策略下发(链路 B 激活前提)
|
||||
|
||||
链路 B 是**策略驱动**的:SDK 侧 `HXEventMonitorPlugin.parseMonitorStrategy()` 读取后台策略注入 `EventStrategyBean`,若 `launch` 模块无开启策略,`create()/record()` 一律空转并打印 `Monitor.Event cold_start_time eventId has not created!`,应用侧无法绕开。因此必须在 APM 后台为 **module=`launch`** 下发如下策略:
|
||||
|
||||
```json
|
||||
{
|
||||
"module": "launch",
|
||||
"config": {
|
||||
"status": 1,
|
||||
"sampling_rate": 1.0,
|
||||
"interval": 5000,
|
||||
"aggre_time": 60000,
|
||||
"aggre_count": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明(对齐 `EventStrategyBean.fromJson`,仅这些字段生效):
|
||||
|
||||
| 字段 | 取值 | 说明 |
|
||||
|------|------|------|
|
||||
| `module` | `launch` | 必须与代码 `HXEventMonitor.getEventFactory('launch')` 一致 |
|
||||
| `status` | `1` | **必须 `1`(STATUS_OPEN)**;否则 `isOpen()` 为 false,`create/record` 全空转 |
|
||||
| `sampling_rate` | `0~1` | 抽样率,`isOpen()` 需 `status==1` 且抽样通过才生效 |
|
||||
| `interval` | ms | 采集间隔 |
|
||||
| `aggre_time` | ms | 聚合时间阈值,达到即触发一次 push |
|
||||
| `aggre_count` | 条 | 聚合数量阈值,达到即触发一次 push |
|
||||
|
||||
客户端 `record` 后由 SDK 本地聚合,达到聚合周期或条数任一触发条件后自动上报至 APM 平台(发送通道为 `HXMonitor` 注入的 `IElkService`,即 `APMElkService` 所走的 `ElkService` 通道)。
|
||||
|
||||
**判定生效**:后台下发后重启应用,冷启动日志中应消失 `Monitor.Event cold_start_time eventId has not created!`、出现 `Cold start bucket metric recorded: <ms>` 与 SDK 聚合 push 日志,看板出现 `launch` 模块 `cold_start_time` 桶分布。
|
||||
|
||||
### 5.2 策略门控机制(源码级验证,2026-08-07,`@kernel/app_monitor_event`/`app_monitor_lib` 源码)
|
||||
|
||||
策略门控发生在 **SDK 指标层**,插件无法绕过:
|
||||
|
||||
- **注入链**:后台策略 JSON → `HXEventMonitorPlugin.parseMonitorStrategy()` → `EventStrategyBean.fromJson()` → `EventMonitorStrategy.addStrategy(module, bean)`(全局 `configMap`)→ `DataMonitorModule` 构造时 `getStrategy('launch')` 取策略。
|
||||
- **两级门控**:插件级(`AbstractHXBasePlugin.isSupport()`,默认"有策略且开启",`HXLaunchMonitorPlugin` override 为 `true` 强制通过)仅控制插件生命周期;**指标级**(`DataMonitorModule.create/getEventMonitor` 检查 `_strategy?.isOpen()`)独立生效——`launch` 无策略 bean 时 `_strategy=null`,`create`/`getEventMonitor` 返回 `NullDataMonitorImp`,`record` 空转打印 `eventId has not created!`。**插件强制开启绕不过指标级门控,必须后台下发策略。**
|
||||
- **`isOpen()` 语义**:`status == 1(STATUS_OPEN)` 且抽样通过(`isOpenByRandom`,对应 `sampling_rate`)。
|
||||
- **聚合上传触发**:`onRecord → isNeedUpload`——累计条数 ≥ `aggregatorSizeThreshold`(`aggre_count`)或距上次上传 ≥ `aggregatorTimeThreshold`(`aggre_time`)即触发 `push`。
|
||||
- **重启生效机制**:`LaunchMonitor.ensureColdStartMetric()` 的幂等标记在 `create` 被策略拒绝(返回空实现)时仍置 `true`——本进程内策略下发后不会重试创建,**必须重启应用**(与上节"判定生效"一致);策略未下发时链路 A(Elk 追溯)不受影响、正常上传。
|
||||
|
||||
随后为该模块补充 ELK 索引模板,在 Grafana 建立看板面板(建议形态):
|
||||
|
||||
```json
|
||||
{
|
||||
"panels": [
|
||||
{
|
||||
"title": "冷启动 P50/P95",
|
||||
"type": "percentile",
|
||||
"metric": "cold_start_time",
|
||||
"group_by": ["app_version", "start_type"],
|
||||
"window": "1d"
|
||||
},
|
||||
{
|
||||
"title": "超 2 秒占比",
|
||||
"type": "ratio",
|
||||
"condition": "cold_start_time >= 2000",
|
||||
"group_by": ["app_version"],
|
||||
"window": "1d"
|
||||
},
|
||||
{
|
||||
"title": "分桶分布",
|
||||
"type": "bucket_distribution",
|
||||
"metric": "cold_start_time",
|
||||
"buckets": [500, 600, 700, 800, 1000, 1500, 2000, 3000],
|
||||
"group_by": ["entry_page", "app_install_status"],
|
||||
"window": "1d"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
看板按版本、启动类型和时间聚合;`app_install_status` 维度(`new_install`/`over_written`)用于排除安装后首启的偏高数据。
|
||||
|
||||
## 6. 兼容与兜底
|
||||
|
||||
现有 `AppEvent.ets` 的 `ElkService` 可以上传结构化启动日志,但它不具备 PDF 所述的指标聚合语义。优先确认 HarmonyOS 是否提供与 Android 自定义 APM SDK 对应的包和 API;若暂未提供,则先通过 `ElkService` 上传 `cold_start_time`、维度和原始系统字段,并由后端补充索引、聚合规则和看板。
|
||||
链路 A(`ElkService` 追溯)上传 `cold_start_time`、4 维度和阶段时间字段,不具备分桶聚合语义,由链路 B(`HXEventMonitor` 分桶指标,策略驱动见 5.1)承担看板聚合;后台需补充 ELK 索引模板、聚合规则和看板。链路 B 的 `DataMonitorBuilder`/`record` API 形态以 `@kernel/app_monitor_event` 的 `.d.ts` 为准(已随 `HXLaunchMonitorPlugin` 编译验证),不直接复用 Android 文档中的 Java API。
|
||||
|
||||
`APP_LAUNCH` 的事件解析必须与崩溃事件分开处理:崩溃事件依赖异常和日志字段,启动事件不包含这些字段。上传应异步执行,不阻塞首页渲染;以 `time + icon_input_time + start_type + process_name` 去重,兼容系统重新投递。
|
||||
|
||||
### 6.1 启动 5 秒内退出的边界行为
|
||||
|
||||
系统 `extend_time` 的定义:手指离手到 `reportDrawnCompleted` 的耗时,若 5 秒内未调用则该值为 0。真机实测(nova 14 Pro)启动后立即退出的行为链:
|
||||
|
||||
- **退出时首帧已提交**(`reportDrawnCompleted` 已调用,真实 app 首帧通常在 1 秒内):事件正常生成、`extend_time` 有效;若进程在事件送达(约 5 秒延迟)前退出,事件被系统持久化,下次启动 watcher 收到并补报,数据有效。
|
||||
- **退出时首帧未提交**(启动极慢或立即强杀):`reportDrawnCompleted` 从未调用,5 秒窗口过期,系统未生成有效事件(实测无补投递),不产生上报。
|
||||
|
||||
补报事件的 `entry_page` 语义:`capturedEntryPage` 是进程级变量,补报事件到达新进程时该值为新进程的快照(或默认值),不能归因到新启动页面。因此客户端以 watcher 注册时间戳为锚点判定补报事件:`icon_input_time` 早于注册时间超过 2 秒容差(正常启动链间隔为毫秒~秒级,补报至少早 5 秒)即视为补报,`entry_page` 置 `'unknown'` 上报,避免错误归因。同时上报字段新增 `is_backfill` 标识(`true` 为补报事件),看板可据此过滤补报数据或单独统计:补报的耗时数据有效可纳入 P50/P95 统计,但 `entry_page` 分布分析应排除。
|
||||
|
||||
真机实测确认(2026-08-05):部署新版本后先 force-stop 再冷启动,新进程收到旧进程的遗留事件,判定 `补报事件=是`(`icon_input_time` 早于注册时间约 4 秒);随后正常冷启动收到自身事件,判定 `补报事件=否`。双向判定均正确,正常启动无误判。
|
||||
|
||||
### 6.2 双通道上报
|
||||
|
||||
启动指标同时走两条通道,职责分离:
|
||||
|
||||
- **分桶指标(`HXEventMonitor`,module=`launch`)**:`record(extendTime)` 后由 SDK 按桶聚合,负责看板聚合统计(P50/P95、超 2 秒占比、分桶分布)。**策略驱动**:无后台下发策略时 `create()/record()` 空转(见 5.1),`record` 失败/空转不影响启动流程,聚合逻辑在 SDK 侧。实现为 `HXLaunchMonitorPlugin`(仿 `HXCrashMonitorPlugin`,`startPlugin()` 中建指标),在 `APMHelper` 的 `.plugin(...)` 列表注册。
|
||||
- **追溯明细(`ElkService`,biz=`launch`)**:11 字段最小集(指标/4 维度含 `app_install_status`/启动生命周期各阶段时间/`git_commit`/`is_backfill`),负责明细追溯与后端校验;其他字段按需加回。分桶指标同步 4 个维度(`setDimension4Name('app_install_status')`),与追溯维度一致。
|
||||
|
||||
两通道独立失败互不影响;`HXEventMonitor` 的 module 参数为显式传入的 `'launch'`,与文档模块定义一致,不经过 `APMElkService` 固定的 `'apm'` 来源。
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
1. 每次冷启动只产生一条可查询记录,且 `extend_time` 大于 0。
|
||||
2. 本地日志/性能工具的耗时与后台数据量级一致。
|
||||
3. 看板可按版本和冷启动维度查询 P50、P95、超阈值占比及分桶分布。
|
||||
4. 断网、重复回调或上传失败不影响启动流程,网络恢复后按既有上传机制重试。
|
||||
1. 单次冷启动中,隐私页、网络页和首页连续跳转时,`reportDrawnCompleted` 仍只提交一次。
|
||||
2. 未登录启动以登录/启动页为入口;已登录启动以 `Index` 为入口,`entry_page` 不被后续页面覆盖。
|
||||
3. 每次冷启动只产生一条可查询记录,且 `extend_time > 0`;热启动、无效耗时和重复事件不上传。
|
||||
4. 页面在系统异步回调前销毁时,不影响已经提交的上报;旧页面不会释放新页面 observer。
|
||||
5. 本地日志与后台数据量级一致;看板可查询 P50、P95、超 2 秒占比及分桶分布。
|
||||
6. 断网或上传失败不影响启动流程,网络恢复后按既有上传机制重试。
|
||||
7. 双通道上传验证:完整 workspace 编译通过(`HXEventMonitor` 的 `DataMonitorBuilder`/`getEventFactory`/`setBuckets`/`record`/`setDimension_4` 已随 `HXLaunchMonitorPlugin` 编译验证);真机冷启动 hilog 出现 `Cold start metric uploaded`(链路 A)与 `Cold start bucket metric recorded`(链路 B,需后台下发策略,见 5.1);ELK 索引可查 `biz='launch'` 的 11 字段记录,APM 平台 `launch` 模块出现分桶分布。
|
||||
8. 维度与分桶:分桶分布可按 4 个维度(`start_type`/`entry_page`/`app_version`/`app_install_status`)分组查询;分桶边界变更需后台重建或并档(历史分布不可比)。
|
||||
|
||||
## 8. 已验证结果
|
||||
|
||||
### 8.1 首次回归(2026-08-05,`CrashDiagnosticsDemo`,nova 14 Pro)
|
||||
|
||||
未登录冷启动入口为 `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)
|
||||
|
||||
| # | 场景 | 操作 | 结果 |
|
||||
|---|---|---|---|
|
||||
| 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` 清数据后启动 | 入口 `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` 事件产生(系统对运行中单例不视为启动),不产生上报 |
|
||||
|
||||
验证结论:新增字段在真机全部可捕获且数值合理(`response_latency` 反映系统响应,两个 `*_dur` 拆解系统侧进程启动与挂载耗时)。已登录/未登录冷启动耗时(137~152ms)与首次回归(155/158ms)量级一致,字段扩展未引入回归。
|
||||
|
||||
**发现的边界情况**:`animation_finish_time` 在真机上为 `0`(系统未填充该字段),因此计算"应用可优化段(`extend_time − animation_finish_time`)"时需处理 0 值场景——此时该段等于 `extend_time` 全量,应在看板聚合时兼容。
|
||||
|
||||
### 8.3 完整重跑确认(2026-08-05 当天,同设备)
|
||||
|
||||
全部场景重新模拟一遍,结果与 8.2 一致:
|
||||
|
||||
| # | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | 清数据后未登录冷启动 | 入口 `LaunchLogin`,`extend_time=161ms`,单次提交 |
|
||||
| 2 | 登录后同进程跳转 `Index` | `Index` 重复首帧注册被拦截,无重复提交 |
|
||||
| 3 | 已登录冷启动 | 入口 `Index`,`extend_time=138ms`,单次提交 |
|
||||
| 4 | 热启动(进程存活) | 无新 `APP_LAUNCH` 事件,不产生上报 |
|
||||
|
||||
### 8.4 连续 3 次冷启动稳定性验证(2026-08-05 当天,同设备)
|
||||
|
||||
已登录态下连续 3 次 `force-stop` → 启动,每次均为独立进程、单次 draw 回调、单次 `reportDrawnCompleted` 提交与成功回调、独立有效 `APP_LAUNCH` 事件:
|
||||
|
||||
| 次数 | 进程 | 入口 | 提交次数 | `extend_time` | `start_type` |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 43415 | `Index` | 1 | 131ms | 0 |
|
||||
| 2 | 43785 | `Index` | 1 | 132ms | 0 |
|
||||
| 3 | 43914 | `Index` | 1 | 126ms | 0 |
|
||||
|
||||
连续启动耗时稳定在 126~138ms,无事件串扰、无重复上报。
|
||||
|
||||
### 8.5 全场景矩阵 3 组验证(2026-08-05 当天,同设备)
|
||||
|
||||
每组完整链路:清数据 → 未登录冷启动 → 网络异常页跳转(拦截)→ 返回 → 隐私确认页跳转(拦截)→ 返回 → 模拟登录 → `Index` 跳转(拦截)→ 已登录冷启动 → 热启动(无新事件)。3 组结果:
|
||||
|
||||
| 组 | 未登录冷启动(入口 `LaunchLogin`) | 网络页拦截 | 隐私页拦截 | `Index` 拦截 | 已登录冷启动(入口 `Index`) |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | `extend_time=162ms`(进程 44062) | ✓ | ✓ | ✓ | `extend_time=133ms`(进程 44644) |
|
||||
| 2 | `extend_time=157ms`(进程 44927) | ✓ | ✓ | ✓ | `extend_time=131ms`(进程 45200) |
|
||||
| 3 | `extend_time=159ms`(进程 45571) | ✓ | ✓ | ✓ | `extend_time=135ms`(进程 45862) |
|
||||
|
||||
每组 6 个场景全部按预期执行:单次冷启动只提交一次 `reportDrawnCompleted`,后续页面(网络异常页、隐私页、`Index`)的重复首帧注册全部被拦截,热启动不产生新事件。未登录冷启动耗时稳定在 157~162ms,已登录稳定在 131~135ms,组间一致,验证可复现。
|
||||
|
||||
### 8.6 启动 5 秒内退出场景验证(2026-08-05 当天,同设备)
|
||||
|
||||
系统 `extend_time` 语义:手指离手 5 秒内未调用 `reportDrawnCompleted` 则该值为 0。真机实测退出场景:
|
||||
|
||||
| # | 场景 | 操作 | 结果 |
|
||||
|---|---|---|---|
|
||||
| 1 | 启动后 1 秒内退出(首帧已提交) | `aa start` → 1 秒后 `force-stop` | `reportDrawnCompleted` 已在退出前提交,事件正常生成、`extend_time` 有效;事件送达时进程已退出,由后续进程补投递处理 |
|
||||
| 2 | 启动后立即强杀(首帧未提交) | `aa start` → 立即 `force-stop`(无间隔) | 仅有"已监听 draw"日志,`reportDrawnCompleted` 从未调用;5 秒窗口过期,系统未生成有效事件,重启后无补投递,不产生上报 |
|
||||
| 3 | 补报判定(修复验证) | 部署新版本后 `force-stop` → 冷启动 | 新进程收到旧进程遗留事件,`icon_input_time` 早于注册时间约 4 秒,判定 `补报事件=是`,`entry_page` 按 `unknown` 处理 |
|
||||
| 4 | 正常冷启动(修复验证) | 干净 `force-stop` → 启动 | 收到自身事件,判定 `补报事件=否`,无误判 |
|
||||
|
||||
结论:首帧未提交时退出由 `extend_time<=0` 过滤兜底不上报;首帧已提交后退出的补报数据有效,修复后补报事件与正常事件可稳定区分(判定逻辑见 6.1)。
|
||||
|
||||
### 8.7 全场景(含 5 秒退出)3 组验证(2026-08-05 当天,同设备)
|
||||
|
||||
每组 8 步链路:清数据 → 未登录冷启动 → 网络页拦截 → 隐私页拦截 → 登录 `Index` 拦截 → 已登录冷启动 → 热启动 → 立即强杀(首帧未提交)→ 1 秒退出(首帧已提交)→ 补报判定。3 组结果:
|
||||
|
||||
| 步骤 | 组 1 | 组 2 | 组 3 |
|
||||
|---|---|---|---|
|
||||
| 未登录冷启动(`LaunchLogin`) | 158ms / 补报=否 | 180ms / 补报=否 | 180ms / 补报=否 |
|
||||
| 网络页重复注册拦截 | ✓ | ✓ | ✓ |
|
||||
| 隐私页重复注册拦截 | ✓ | ✓ | ✓ |
|
||||
| `Index` 重复注册拦截 | ✓ | ✓ | ✓ |
|
||||
| 已登录冷启动(`Index`) | 129ms / 补报=否 | 132ms / 补报=否 | 131ms / 补报=否 |
|
||||
| 热启动(进程存活) | 无新事件 | 无新事件 | 无新事件 |
|
||||
| 立即强杀(首帧未提交)后重启 | 被强杀进程无有效事件,重启进程正常捕获 | 同左 | 同左 |
|
||||
| 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`(boolean)基于 `preferences` 记录,`bm clean` 清数据后该标记重置(再次显示首启);生产实现为 `app_install_status`(`normal`/`new_install`/`over_written` 三值,见第 4 节),以生产为准。
|
||||
|
||||
### 8.9 真实应用验证(2026-08-06,完整 workspace,nova 14 Pro)
|
||||
|
||||
在完整 workspace 落地(`HXLaunchMonitorPlugin` 插件 + `APMHelper` 注册)后的真机数据:
|
||||
|
||||
| 场景 | 结果 |
|
||||
|------|------|
|
||||
| 全新安装首启(`new_install`) | 链路 A:`cold_start_time=1141ms`,Entry=Index,Elk 上报 ✅ |
|
||||
| 二次冷启动(`normal`) | 链路 A:`cold_start_time=1717ms`,Elk 上报 ✅ |
|
||||
| 链路 B(分桶) | 待后台下发 `launch` 策略(5.1)后复测 |
|
||||
|
||||
编译:`claude_compile.sh → BUILD_SUCCESS`(链路 B API 随插件编译验证通过)。
|
||||
|
||||
真实应用耗时(1.1~1.7s)远高于 demo(127~165ms),符合生产首页复杂度的预期;`app_install_status` 维度可区分首启与常规启动的耗时差异。
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# 启动性能监控(冷启动耗时)接入配置
|
||||
|
||||
> 作者:cheliangzhao
|
||||
> 日期:2026-08-06
|
||||
> 涉及模块:`entry`(HAP 入口)+ `@kernel/app_monitor_event`(APM 事件 SDK)
|
||||
> 对应需求:冷启动耗时(`cold_start_time`)采集与看板聚合
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
统计同花顺期货鸿蒙版**冷启动耗时**,覆盖三条启动入口(`Index` / `LauncherPage` / `NetworkAnomalyPage`),区分冷启动 / 补投递四种场景,实现启动性能的持续观测与劣化告警。
|
||||
|
||||
采集分两条通道:
|
||||
|
||||
- **链路 A(Elk 追溯,已上线验证)**:把冷启动指标结构化 JSON 经 `ElkService` 异步上报,字段最全,可明细定位。
|
||||
- **链路 B(HXEventMonitor 分桶,需后台策略开启后生效)**:由 SDK 按桶自动归集,负责看板聚合曲线。
|
||||
|
||||
> ⚠️ 链路 B 是**策略驱动**的:SDK 侧在 `EventMonitorStrategy` 找不到 `launch` 模块的开启策略时,`create()/record()` 一律空转并打印 `Monitor.Event cold_start_time eventId has not created!`。应用侧无法绕开,必须由 **APM 后台下发对应模块策略**才能激活。
|
||||
|
||||
---
|
||||
|
||||
## 2. 链路 A:Elk 追溯(已生效,无需后台操作)
|
||||
|
||||
上报字段(`LaunchElkBuilder`,业务来源 `launch`):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `cold_start_time` | number | 冷启动耗时 ms = 系统事件 `extend_time` |
|
||||
| `start_type` | number | 启动类型(当前只统计 `0`=冷启动) |
|
||||
| `entry_page` | string | 入口页(`Index`/`LauncherPage`/`NetworkAnomalyPage`;补投递为 `unknown`) |
|
||||
| `app_version` | string | 应用内部版本号 |
|
||||
| `app_install_status` | string | `normal`/`new_install`/`over_written` |
|
||||
| `response_latency` | number | 离手到动效开始(需 API 22+) |
|
||||
| `animation_finish_time` | number | 动效完成耗时 |
|
||||
| `startability_processstart_dur` | number | 进程创建段(仅冷启动) |
|
||||
| `appattach_to_appforeground_dur` | number | 进程挂载段(仅冷启动) |
|
||||
| `git_commit` | string | 代码级追溯,劣化定位用 |
|
||||
| `is_backfill` | boolean | 是否系统补投递的上次启动遗留事件 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 链路 B:HXEventMonitor 分桶指标(需后台开启)
|
||||
|
||||
### 3.1 后台需下发的模块策略
|
||||
|
||||
SDK 由 `HXEventMonitorPlugin.parseMonitorStrategy()` 读取,注入到 `EventStrategyBean` 后按模块打开。请为 **module=`launch`** 下发如下配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"module": "launch",
|
||||
"config": {
|
||||
"status": 1,
|
||||
"sampling_rate": 1.0,
|
||||
"interval": 5000,
|
||||
"aggre_time": 60000,
|
||||
"aggre_count": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明(对齐 `EventStrategyBean.fromJson`,仅这些字段生效):
|
||||
|
||||
| 字段 | 取值 | 说明 |
|
||||
|------|------|------|
|
||||
| `module` | `launch` | 必须与代码 `HXEventMonitor.getEventFactory('launch')` 一致 |
|
||||
| `status` | `1` | **必须 `1`(STATUS_OPEN)**;否则 `isOpen()` 为 false,`create/record` 全空转 |
|
||||
| `sampling_rate` | `0~1` | 抽样率,`isOpen()` 需 `status==1 && 抽样通过` 才生效 |
|
||||
| `interval` | ms | 采集间隔 |
|
||||
| `aggre_time` | ms | 聚合时间阈值,达到即触发一次 push |
|
||||
| `aggre_count` | 条 | 聚合数量阈值,达到即触发一次 push |
|
||||
|
||||
### 3.2 指标定义
|
||||
|
||||
| 项 | 值 |
|
||||
|----|----|
|
||||
| 模块名 `module` | `launch` |
|
||||
| 指标名 `metric` | `cold_start_time` |
|
||||
| 值域 | 冷启动耗时 ms(`extend_time`) |
|
||||
| 分桶 `buckets` | `[500, 600, 700, 800, 1000, 1500, 2000, 3000]`(按实测 600–800ms 主体细分,尾部留劣化锚点) |
|
||||
| 维度1 `start_type` | 冷启动 `0` |
|
||||
| 维度2 `entry_page` | `Index`/`LauncherPage`/`NetworkAnomalyPage`/`unknown`(补投递) |
|
||||
| 维度3 `app_version` | 应用内部版本号 |
|
||||
| 维度4 `app_install_status` | `normal`/`new_install`/`over_written` |
|
||||
|
||||
### 3.3 判定生效
|
||||
|
||||
后台下发后,重启应用,冷启动日志中:
|
||||
|
||||
- ✅ 消失:`Monitor.Event cold_start_time eventId has not created!`
|
||||
- ✅ 出现:`Cold start bucket metric recorded: <ms>` 与 SDK 聚合 push 日志
|
||||
- ✅ 看板出现 `launch` 模块 `cold_start_time` 桶分布
|
||||
|
||||
---
|
||||
|
||||
## 4. 相关代码与文件
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `entry/src/main/ets/monitor/LaunchMonitor.ets` | APP_LAUNCH 订阅、Elk 上报、首帧 `reportDrawnCompleted`、桶指标定义与 record |
|
||||
| `entry/src/main/ets/monitor/LaunchMonitor.ets`(`ensureColdStartMetric`) | 创建 `launch/cold_start_time` 桶指标(幂等) |
|
||||
| `entry/src/main/ets/monitor/plugin/HXLaunchMonitorPlugin.ets` | 仿 `HXCrashMonitorPlugin` 接入的启动插件,在 `startPlugin()` 建指标 |
|
||||
| `entry/src/main/ets/monitor/APMHelper.ets` | 注册 `HXLaunchMonitorPlugin` 及配置类、`.plugin(...)` 列表 |
|
||||
| `entry/src/main/ets/entryability/EntryAbility.ets` | `onCreate` 早注册 `LaunchMonitor.init()` |
|
||||
| `entry/src/main/ets/pages/Index.ets` / `LauncherPage.ets` / `network/NetworkAnomalyPage.ets` | 首帧 `HOME_ROOT` 监听 + `reportFirstFrameOnDraw`/`dispose` |
|
||||
|
||||
### 打点时序
|
||||
|
||||
```
|
||||
EntryAbility.onCreate → LaunchMonitor.init()(注册 APP_LAUNCH watcher)
|
||||
入口页 aboutToAppear → reportFirstFrameOnDraw(HOME_ROOT) → 首帧 draw → reportDrawnCompleted()
|
||||
系统产生 APP_LAUNCH 事件(冷启动)→ onReceive
|
||||
├─ 链路A: ElkService 上传冷启动 JSON(字段见 §2)
|
||||
└─ 链路B: HXEventMonitor record(cold_start_time)(依赖 launch 策略,见 §3)
|
||||
补投递:icon_input_time 早于 watcher 注册 > 2000ms → 判为 backfill,entry_page=unknown
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 真机验证结论(2026-08-06,nova 14 Pro)
|
||||
|
||||
| 场景 | 结果 |
|
||||
|------|------|
|
||||
| 全新安装首启(`new_install`) | 链路 A:`cold_start_time=1141ms`,Entry=Index,Elk 上报 ✅ |
|
||||
| 二次冷启动(`normal`) | 链路 A:`cold_start_time=1717ms`,Elk 上报 ✅ |
|
||||
| 链路 B(分桶) | ❌ 待后台下发 `launch` 策略(§3.1)后复测 |
|
||||
|
||||
编译:`./claude_tool/claude_compile.sh → BUILD_SUCCESS`
|
||||
@@ -44,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 { LaunchMonitor } from '../monitor/LaunchMonitor';
|
||||
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 已就绪)
|
||||
LaunchMonitor.init();
|
||||
|
||||
DebugToolUtil.TARGET_NAME = BuildProfile.TARGET_NAME
|
||||
DebugToolUtil.PRODUCT_NAME = BuildProfile.PRODUCT_NAME
|
||||
DebugToolUtil.BUILD_MODE_NAME = BuildProfile.BUILD_MODE_NAME
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { HXPerformancePlugin } from "@kernel/app_monitor_performance";
|
||||
import { HXWebMonitorPlugin } from '@kernel/app_monitor_web';
|
||||
import { HXUserService } from "biz_hxservice";
|
||||
import { deviceInfo } from "@kit.BasicServicesKit";
|
||||
import { ElkService, ElkUploadMessage, ElkUploadMessageBuilder } from "service_monitor";
|
||||
import {
|
||||
AuthLogMonitor,
|
||||
@@ -20,9 +21,11 @@ import {
|
||||
import { HXAuthLogMonitor } from "biz_auth";
|
||||
import { HXHttpMonitorPlugin } from "@kernel/app_monitor_http";
|
||||
import { HXCrashMonitorPlugin, ICrashMonitorConfig } from "@kernel/app_monitor_crash";
|
||||
import { HXLaunchMonitorPlugin } from "./plugin/HXLaunchMonitorPlugin";
|
||||
import { ILaunchMonitorConfig } from "./plugin/ILaunchMonitorConfig";
|
||||
import { router } from "@kit.ArkUI";
|
||||
import { GIT_COMMIT } from "biz_common/src/main/ets/utils/debug/GitBuildConfig";
|
||||
import { AppConfigManager, getConnectionManager } from "biz_common";
|
||||
import { AppConfigManager, StartManager, AppInstallStatus, getConnectionManager } from "biz_common";
|
||||
import { HxCBASAgentProvider } from "@b2c-spi/hx_cbas_api";
|
||||
import {
|
||||
AppContextLike,
|
||||
@@ -65,6 +68,7 @@ export class APMHelper {
|
||||
.plugin(new HXWebMonitorPlugin())
|
||||
.plugin(new HXHttpMonitorPlugin())
|
||||
.plugin(new HXCrashMonitorPlugin(new HXCrashMonitorConfig()))
|
||||
.plugin(new HXLaunchMonitorPlugin(new HXLaunchMonitorConfig()))
|
||||
.build()
|
||||
.startAllPlugins(new APMMonitorOnStartCallback())
|
||||
APMHelper.initLeakWatcher(context)
|
||||
@@ -192,6 +196,51 @@ class HXMonitorAppConfig extends IMonitorAppConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动监控插件配置:提供冷启动指标固定维度(user/app-version/build)
|
||||
*/
|
||||
class HXLaunchMonitorConfig implements ILaunchMonitorConfig {
|
||||
provideUserId(): string {
|
||||
return HXUserService.getInstance().getUserIdFromLocal()
|
||||
}
|
||||
|
||||
provideDeviceType(): string {
|
||||
return deviceInfo.deviceType
|
||||
}
|
||||
|
||||
provideDeviceModel(): string {
|
||||
// 营销名(设备名称),如 nova 14 Pro;无则回退品牌
|
||||
return deviceInfo.marketName || deviceInfo.brand
|
||||
}
|
||||
|
||||
provideAppVersion(): string {
|
||||
if (APMHelper.isDebugOrTestBuild()) {
|
||||
return DEBUG_VERSION
|
||||
}
|
||||
return AppConfigManager.getInstance().getConfigProvider()?.getInnerVersionFull() ?? DEBUG_VERSION
|
||||
}
|
||||
|
||||
provideBuildType(): string {
|
||||
return BuildProfile.BUILD_MODE_NAME.toString()
|
||||
}
|
||||
|
||||
provideGitCommit(): string {
|
||||
return GIT_COMMIT
|
||||
}
|
||||
|
||||
provideAppInstallStatus(): string {
|
||||
switch (StartManager.getInstance().getAppInstallStatus()) {
|
||||
case AppInstallStatus.NEW:
|
||||
return 'new_install'
|
||||
case AppInstallStatus.OVER_WRITTEN:
|
||||
return 'over_written'
|
||||
case AppInstallStatus.NORMAL:
|
||||
default:
|
||||
return 'normal'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* tcp监控配置
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* author:cheliangzhao
|
||||
* created on 2026/08/06
|
||||
* desc: 启动性能监控 v2,订阅 APP_LAUNCH 采集冷启动耗时,走 ElkService 追溯 + HXEventMonitor 分桶双通道
|
||||
*/
|
||||
import hiAppEvent from '@ohos.hiviewdfx.hiAppEvent';
|
||||
import { HXLog } from 'biz_common/src/main/ets/logger/HXLog';
|
||||
import { ElkService, ElkUploadMessage, ElkUploadMessageBuilder } from 'service_monitor';
|
||||
import { inspector } from '@kit.ArkUI';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { HXEventMonitor, DataMonitorBuilder } from '@kernel/app_monitor_event';
|
||||
import { ILaunchMonitorConfig } from './plugin/ILaunchMonitorConfig';
|
||||
|
||||
const TAG: string = 'LaunchMonitor';
|
||||
|
||||
/**
|
||||
* 启动性能监控:订阅 APP_LAUNCH 事件,采集冷启动耗时(extend_time),
|
||||
* 并通过 ElkService(追溯)与 HXEventMonitor(分桶)双通道上报。
|
||||
*/
|
||||
export class LaunchMonitor {
|
||||
private static readonly MAX_EVENT_KEYS: number = 20;
|
||||
/**
|
||||
* 补报判定容差(ms):正常启动链间隔为毫秒~秒级,补报(上次启动遗留事件)至少早 2 秒
|
||||
*/
|
||||
private static readonly BACKFILL_TOLERANCE_MS: number = 2000;
|
||||
/** watcher 注册时间戳(epoch ms),补报判定的锚点 */
|
||||
private static watcherRegisterTime: number = 0;
|
||||
|
||||
/** 首帧绘制完成时快照的入口页面,在 onDraw 回调中赋值 */
|
||||
private static capturedEntryPage: string = 'unknown';
|
||||
|
||||
/** 固定维度配置(由 HXLaunchMonitorPlugin 注入 user/app-version/build 等) */
|
||||
private static launchConfig: ILaunchMonitorConfig | null = null;
|
||||
|
||||
static setLaunchConfig(config: ILaunchMonitorConfig): void {
|
||||
LaunchMonitor.launchConfig = config
|
||||
}
|
||||
|
||||
private static watcherInitialized: boolean = false;
|
||||
private static bucketMetricInitialized: boolean = false;
|
||||
private static drawReportSubmitted: boolean = false;
|
||||
private static activeObserverOwner: string = '';
|
||||
private static activeDisposer?: () => void;
|
||||
private static processedEventKeys: Array<string> = [];
|
||||
|
||||
/**
|
||||
* 注册入口页根节点 draw 监听:仅触发一次,快照 entryPage 并调用 reportDrawnCompleted。
|
||||
* 页面在 aboutToDisappear 调用 disposeFirstFrame(pageName) 清理。
|
||||
*/
|
||||
static reportFirstFrameOnDraw(
|
||||
createObserver: () => inspector.ComponentObserver,
|
||||
abilityContext: common.UIAbilityContext,
|
||||
pageName: string
|
||||
): void {
|
||||
if (LaunchMonitor.drawReportSubmitted) {
|
||||
HXLog.i(TAG, `Skip duplicate draw registration for page: ${pageName}`)
|
||||
return
|
||||
}
|
||||
|
||||
LaunchMonitor.disposeFirstFrame(LaunchMonitor.activeObserverOwner)
|
||||
|
||||
try {
|
||||
const observer = createObserver();
|
||||
const onDraw = () => {
|
||||
observer.off('draw', onDraw);
|
||||
if (LaunchMonitor.activeObserverOwner === pageName) {
|
||||
LaunchMonitor.activeObserverOwner = ''
|
||||
LaunchMonitor.activeDisposer = undefined
|
||||
}
|
||||
|
||||
// 必须在系统调用前设置进程级提交状态,防止后续页面或重复回调再次提交
|
||||
if (LaunchMonitor.drawReportSubmitted) {
|
||||
HXLog.i(TAG, `Skip duplicate draw callback for page: ${pageName}`)
|
||||
return
|
||||
}
|
||||
LaunchMonitor.drawReportSubmitted = true
|
||||
LaunchMonitor.capturedEntryPage = pageName
|
||||
|
||||
try {
|
||||
abilityContext.reportDrawnCompleted(() => {
|
||||
});
|
||||
HXLog.i(TAG, `reportDrawnCompleted submitted for page: ${pageName}`)
|
||||
} catch (e) {
|
||||
// 同步异常表示系统调用尚未提交,允许后续入口页面重试。
|
||||
LaunchMonitor.drawReportSubmitted = false
|
||||
LaunchMonitor.capturedEntryPage = 'unknown'
|
||||
HXLog.e(TAG, `reportDrawnCompleted failed synchronously: ${JSON.stringify(e)}`)
|
||||
}
|
||||
};
|
||||
|
||||
LaunchMonitor.activeObserverOwner = pageName
|
||||
LaunchMonitor.activeDisposer = () => {
|
||||
observer.off('draw', onDraw);
|
||||
};
|
||||
observer.on('draw', onDraw);
|
||||
HXLog.i(TAG, `Draw listener registered for page: ${pageName}`);
|
||||
} catch (e) {
|
||||
HXLog.e(TAG, `Create draw listener failed for page ${pageName}: ${JSON.stringify(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消入口页面的首帧绘制监听。
|
||||
*/
|
||||
static disposeFirstFrame(pageName: string): void {
|
||||
if (pageName === '' || LaunchMonitor.activeObserverOwner !== pageName) {
|
||||
return
|
||||
}
|
||||
LaunchMonitor.activeDisposer?.();
|
||||
LaunchMonitor.activeObserverOwner = ''
|
||||
LaunchMonitor.activeDisposer = undefined;
|
||||
}
|
||||
|
||||
/** 注册 APP_LAUNCH watcher(幂等)。应在 onCreate 尽早调用,确保事件生成前完成注册 */
|
||||
static init(): void {
|
||||
if (LaunchMonitor.watcherInitialized) {
|
||||
return
|
||||
}
|
||||
LaunchMonitor.watcherRegisterTime = Date.now()
|
||||
hiAppEvent.addWatcher({
|
||||
name: "launchWatcher",
|
||||
appEventFilters: [{
|
||||
domain: hiAppEvent.domain.OS,
|
||||
names: [hiAppEvent.event.APP_LAUNCH]
|
||||
}],
|
||||
onReceive: async (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => {
|
||||
HXLog.i(TAG, `APP_LAUNCH onReceive: domain=${domain}`)
|
||||
LaunchMonitor.processAppLaunchEvents(appEventGroups)
|
||||
}
|
||||
});
|
||||
LaunchMonitor.watcherInitialized = true
|
||||
HXLog.i(TAG, 'LaunchMonitor watcher registered')
|
||||
}
|
||||
|
||||
/** 处理 APP_LAUNCH:过滤冷启动且 extend_time 有效,去重后上传 */
|
||||
private static processAppLaunchEvents(appEventGroups: Array<hiAppEvent.AppEventGroup>): void {
|
||||
try {
|
||||
if (!appEventGroups || appEventGroups.length === 0) {
|
||||
return
|
||||
}
|
||||
appEventGroups.forEach((eventGroup: hiAppEvent.AppEventGroup) => {
|
||||
eventGroup.appEventInfos.forEach((eventInfo: hiAppEvent.AppEventInfo) => {
|
||||
const startType = eventInfo.params['start_type'] as number;
|
||||
if (startType !== 0) {
|
||||
HXLog.i(TAG, `Skip non-cold-start: start_type=${startType}`)
|
||||
return
|
||||
}
|
||||
const extendTime = eventInfo.params['extend_time'] as number;
|
||||
// 未填充说明首帧未完成(如 5 秒内退出)
|
||||
if (!extendTime || extendTime <= 0) {
|
||||
HXLog.w(TAG, `APP_LAUNCH has no valid extend_time, skip`)
|
||||
return
|
||||
}
|
||||
|
||||
const iconInputTime = eventInfo.params['icon_input_time'] as number;
|
||||
const eventTime = eventInfo.params['time'] as number;
|
||||
const processName = (eventInfo.params['process_name'] as string) ?? '';
|
||||
const eventKey = `${eventTime}|${iconInputTime}|${startType}|${processName}`;
|
||||
if (LaunchMonitor.processedEventKeys.indexOf(eventKey) !== -1) {
|
||||
HXLog.i(TAG, `Skip duplicate APP_LAUNCH: ${eventKey}`)
|
||||
return
|
||||
}
|
||||
LaunchMonitor.processedEventKeys.push(eventKey)
|
||||
if (LaunchMonitor.processedEventKeys.length > LaunchMonitor.MAX_EVENT_KEYS) {
|
||||
LaunchMonitor.processedEventKeys.shift()
|
||||
}
|
||||
|
||||
HXLog.i(TAG,
|
||||
`cold_start_time=${extendTime}ms, ` +
|
||||
`icon_input_time=${iconInputTime}, ` +
|
||||
`entry_page=${LaunchMonitor.capturedEntryPage}`)
|
||||
|
||||
LaunchMonitor.uploadColdStartTime(extendTime, startType, eventInfo)
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
HXLog.e(TAG, `Error processing APP_LAUNCH: ${JSON.stringify(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** 上传冷启动指标:ElkService 追溯 + HXEventMonitor 分桶,异步不阻塞首帧 */
|
||||
private static uploadColdStartTime(
|
||||
coldStartTime: number,
|
||||
startType: number,
|
||||
eventInfo: hiAppEvent.AppEventInfo
|
||||
): void {
|
||||
try {
|
||||
const iconInputTime = eventInfo.params['icon_input_time'] as number;
|
||||
// 补投递事件(上次启动遗留)的 entry_page 是当前进程快照,无意义,置 unknown
|
||||
const isBackfillEvent = LaunchMonitor.isBackfillEvent(iconInputTime);
|
||||
// 启动生命周期各阶段时间(ms):response_latency 需 API 22+,两个 *dur 仅冷启动存在
|
||||
const responseLatency = eventInfo.params['response_latency'] as number;
|
||||
const animationFinishTime = eventInfo.params['animation_finish_time'] as number;
|
||||
const startabilityProcessStartDur = eventInfo.params['startability_processstart_dur'] as number;
|
||||
const appattachToAppForegroundDur = eventInfo.params['appattach_to_appforeground_dur'] as number;
|
||||
|
||||
// 负载结构与字段语义见 ColdStartMetricPayload
|
||||
const metricPayload: ColdStartMetricPayload = {
|
||||
cold_start_time: coldStartTime,
|
||||
start_type: startType,
|
||||
entry_page: isBackfillEvent ? 'unknown' : LaunchMonitor.capturedEntryPage,
|
||||
device_type: LaunchMonitor.launchConfig?.provideDeviceType() ?? 'unknown',
|
||||
device_model: LaunchMonitor.launchConfig?.provideDeviceModel() ?? 'unknown',
|
||||
user_id: LaunchMonitor.launchConfig?.provideUserId() ?? 'unknown',
|
||||
app_version: LaunchMonitor.launchConfig?.provideAppVersion() ?? 'unknown',
|
||||
build_type: LaunchMonitor.launchConfig?.provideBuildType() ?? 'unknown',
|
||||
app_install_status: LaunchMonitor.launchConfig?.provideAppInstallStatus() ?? 'unknown',
|
||||
response_latency: responseLatency,
|
||||
animation_finish_time: animationFinishTime,
|
||||
startability_processstart_dur: startabilityProcessStartDur,
|
||||
appattach_to_appforeground_dur: appattachToAppForegroundDur,
|
||||
git_commit: LaunchMonitor.launchConfig?.provideGitCommit() ?? 'unknown',
|
||||
is_backfill: isBackfillEvent,
|
||||
}
|
||||
|
||||
const builder = new LaunchElkBuilder()
|
||||
builder.userid(LaunchMonitor.launchConfig?.provideUserId() ?? 'unknown')
|
||||
builder.messageKey('cold_start_time')
|
||||
builder.messageValue(JSON.stringify(metricPayload))
|
||||
|
||||
const message = new ElkUploadMessage(builder)
|
||||
// 异步上传,避免阻塞启动流程
|
||||
ElkService.getInstance().pushBusinessLog(message, true)
|
||||
HXLog.i(TAG, `Cold start metric uploaded: ${coldStartTime}ms${isBackfillEvent ? ' (backfill)' : ''}`)
|
||||
|
||||
// 分桶指标(链路 B):记录原始耗时,SDK 按桶自动归集,负责看板聚合
|
||||
// 兜底:若 APM 插件尚未执行 startPlugin 创建指标,此处惰性创建
|
||||
LaunchMonitor.ensureColdStartMetric()
|
||||
const factory = HXEventMonitor.getEventFactory('launch')
|
||||
const monitor = factory.getEventMonitor('cold_start_time')
|
||||
|
||||
const dim1 = LaunchMonitor.launchConfig?.provideAppVersion() ?? 'unknown'
|
||||
const dim2 = LaunchMonitor.launchConfig?.provideBuildType() ?? 'unknown'
|
||||
const dim3 = LaunchMonitor.launchConfig?.provideDeviceType() ?? 'unknown'
|
||||
const dim4 = LaunchMonitor.launchConfig?.provideAppInstallStatus() ?? 'unknown'
|
||||
monitor.setDimension1(dim1)
|
||||
monitor.setDimension2(dim2)
|
||||
monitor.setDimension3(dim3)
|
||||
monitor.setDimension4(dim4)
|
||||
monitor.record(coldStartTime)
|
||||
|
||||
HXLog.i(TAG, `Cold start bucket metric recorded: ${coldStartTime}ms`)
|
||||
} catch (e) {
|
||||
HXLog.e(TAG, `uploadColdStartTime failed: ${JSON.stringify(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 launch 分桶指标(幂等):桶 [500,600,700,800,1000,1500,2000,3000],
|
||||
* 按实测主体分布(600-800ms)细分,尾部留劣化告警锚点。
|
||||
* 由 HXLaunchMonitorPlugin.startPlugin() 在 APM 引擎启动后调用,避免 create 早于 SDK 初始化。
|
||||
*/
|
||||
static ensureColdStartMetric(): void {
|
||||
if (LaunchMonitor.bucketMetricInitialized) {
|
||||
return
|
||||
}
|
||||
const builder = new DataMonitorBuilder()
|
||||
builder.setMetricName('cold_start_time')
|
||||
builder.setDimension1Name('app_version')
|
||||
builder.setDimension2Name('build_type')
|
||||
builder.setDimension3Name('device_type')
|
||||
builder.setDimension4Name('app_install_status')
|
||||
builder.setBuckets([500, 600, 700, 800, 1000, 1500, 2000, 3000])
|
||||
HXEventMonitor.getEventFactory('launch').create(builder)
|
||||
LaunchMonitor.bucketMetricInitialized = true
|
||||
HXLog.i(TAG, 'launch bucket metric created: cold_start_time')
|
||||
}
|
||||
|
||||
/** 补投递判定:icon_input_time 早于 watcher 注册时间超过容差(2s)视为上次启动遗留事件 */
|
||||
private static isBackfillEvent(iconInputTime: number): boolean {
|
||||
return iconInputTime < LaunchMonitor.watcherRegisterTime - LaunchMonitor.BACKFILL_TOLERANCE_MS
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** ELK 消息构造器,业务来源标记为 launch 模块 */
|
||||
class LaunchElkBuilder extends ElkUploadMessageBuilder {
|
||||
constructor() {
|
||||
super(Date.now(), 'launch', 'i');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 冷启动指标上报负载(ElkService 追溯通道,messageValue 的 JSON 结构)。
|
||||
*/
|
||||
interface ColdStartMetricPayload {
|
||||
// 核心指标:冷启动耗时(ms),即系统事件的 extend_time
|
||||
cold_start_time: number
|
||||
// 维度:启动类型(0=冷启动,当前仅统计冷启动)
|
||||
start_type: number
|
||||
// 维度:入口页面(Index/LauncherPage 等;补报事件为 unknown)
|
||||
entry_page: string
|
||||
// 维度:设备类型(phone/tablet/2in1)
|
||||
device_type: string
|
||||
// 维度:设备型号(如 nova 14 Pro / MIA-AL00)
|
||||
device_model: string
|
||||
// 维度:用户ID
|
||||
user_id: string
|
||||
// 维度:应用内部版本号
|
||||
app_version: string
|
||||
// 维度:构建类型(debug/release/forTest 等)
|
||||
build_type: string
|
||||
// 维度:安装状态语义名("normal"/"new_install"/"over_written",见 AppInstallStatus;新装耗时显著偏高需区分)
|
||||
app_install_status: string
|
||||
// 系统响应段:离手到动效开始耗时(ms,需 API 22+)
|
||||
response_latency: number
|
||||
// 系统动效段:动效完成耗时(ms)
|
||||
animation_finish_time: number
|
||||
// 进程创建段:系统启动 Ability 到进程创建完成(ms,仅冷启动存在)
|
||||
startability_processstart_dur: number
|
||||
// 进程挂载段:进程初始化完成到应用切前台(ms,仅冷启动存在)
|
||||
appattach_to_appforeground_dur: number
|
||||
// 代码级追溯:Git 提交号,劣化时可定位到具体提交
|
||||
git_commit: string
|
||||
// 补报标识:true 表示系统补投递的上次启动遗留事件(entry_page 无意义)
|
||||
is_backfill: boolean
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* author:cheliangzhao
|
||||
* created on 2026/08/06
|
||||
* desc: 冷启动监控插件,仿 HXCrashMonitorPlugin 接入 APM 插件系统;在插件启动后创建冷启动分桶指标,
|
||||
* 并把固定维度配置(user/app-version/build)注入 LaunchMonitor。
|
||||
* 解决分桶 create 早于 HXEventMonitor SDK 而失效的时序问题
|
||||
*/
|
||||
import { AbstractHXBasePlugin, IMonitorStrategy, StrategyModuleConfig, HXMonitorLogger } from '@kernel/app_monitor_lib';
|
||||
import { LaunchMonitor } from '../LaunchMonitor';
|
||||
import { ILaunchMonitorConfig } from './ILaunchMonitorConfig';
|
||||
|
||||
/**
|
||||
* 冷启动监控插件
|
||||
*
|
||||
* 职责:在 APM 插件引擎启动后,创建 launch 分桶指标(链路B),并把固定维度配置
|
||||
* (user/app-version/build 等)经 LaunchMonitor.setLaunchConfig 注入。APP_LAUNCH
|
||||
* 事件订阅、Elk 追溯上传与首帧埋点仍由 LaunchMonitor 承接(在 onCreate 早注册)。
|
||||
*/
|
||||
export class HXLaunchMonitorPlugin extends AbstractHXBasePlugin {
|
||||
private TAG: string = 'HXLaunchMonitorPlugin';
|
||||
private static MODULE_NAME = 'launch';
|
||||
private readonly config: ILaunchMonitorConfig;
|
||||
|
||||
constructor(config: ILaunchMonitorConfig) {
|
||||
super();
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
getModuleName(): string {
|
||||
return HXLaunchMonitorPlugin.MODULE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新指标无后台策略下发,强制随主配置开启;后续如需开关控制可改读 isSwitchOpen
|
||||
*/
|
||||
isSupport(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
parseMonitorStrategy(config: StrategyModuleConfig): IMonitorStrategy | null {
|
||||
return null
|
||||
}
|
||||
|
||||
isSwitchOpen(gatherSwitch: number): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
initPlugin(): void {
|
||||
// 插件初始化时即把固定维度配置注入 LaunchMonitor(user/app-version/build 等)
|
||||
LaunchMonitor.setLaunchConfig(this.config)
|
||||
}
|
||||
|
||||
startPlugin(): void {
|
||||
// 指标创建延后到 startPlugin(此时 HX 引擎已初始化)
|
||||
LaunchMonitor.ensureColdStartMetric()
|
||||
HXMonitorLogger.d(this.TAG, 'startPlugin: launch cold_start_time metric ready')
|
||||
}
|
||||
|
||||
stopPlugin(): void {
|
||||
// 无长连接需要释放
|
||||
}
|
||||
|
||||
destroyPlugin(): void {
|
||||
// 无需清理
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* author:cheliangzhao
|
||||
* created on 2026/08/06
|
||||
* desc: 冷启动指标固定维度(user/app-version/build 等)的配置提供接口。
|
||||
* 由 HXLaunchMonitorConfig 实现传入,LaunchMonitor 据此注入指标维度。
|
||||
*/
|
||||
export interface ILaunchMonitorConfig {
|
||||
/**
|
||||
* 提供用户ID(Elk 消息用户标识)
|
||||
*/
|
||||
provideUserId(): string;
|
||||
/**
|
||||
* 提供设备类型(如 phone/tablet/2in1)
|
||||
*/
|
||||
provideDeviceType(): string;
|
||||
/**
|
||||
* 提供设备品牌(如 HUAWEI)
|
||||
*/
|
||||
provideDeviceModel(): string;
|
||||
/**
|
||||
* 提供应用内部版本号
|
||||
*/
|
||||
provideAppVersion(): string;
|
||||
/**
|
||||
* 提供构建类型
|
||||
*/
|
||||
provideBuildType(): string;
|
||||
/**
|
||||
* 提供代码级追溯 Git 提交号
|
||||
*/
|
||||
provideGitCommit(): string;
|
||||
/**
|
||||
* 提供安装状态语义名(normal/new_install/over_written)
|
||||
*/
|
||||
provideAppInstallStatus(): string;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { HXNetwork } from './HXNetwork'
|
||||
import { ConnectionSeq } from '@kernel/lib_communication'
|
||||
import { AuthProviderService } from 'biz_auth'
|
||||
import { AdvertisingMonitorService } from 'biz_hxservice'
|
||||
import { LaunchMonitor } from '../monitor/LaunchMonitor'
|
||||
|
||||
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()
|
||||
LaunchMonitor.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
|
||||
this.context, 'NetworkAnomalyPage');
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
LaunchMonitor.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 { LaunchMonitor } from '../monitor/LaunchMonitor';
|
||||
|
||||
|
||||
const TAG: string = 'Struct Index';
|
||||
@@ -309,6 +310,8 @@ struct IndexM {
|
||||
|
||||
aboutToDisappear() {
|
||||
HXLog.d(TAG, 'Index aboutToDisappear triggered at ' + Date.now())
|
||||
// 页面退出时取消首帧绘制监听
|
||||
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)
|
||||
@@ -361,6 +364,11 @@ struct IndexM {
|
||||
|
||||
// 冷启动先初始化一次 drawline 基础能力,保证首次进入行情页即可绘制同步线。
|
||||
DrawLineInit.init()
|
||||
|
||||
// 注册首帧绘制监听,绘制完成时自动调用 reportDrawnCompleted
|
||||
LaunchMonitor.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 { LaunchMonitor } from '../monitor/LaunchMonitor'
|
||||
|
||||
/**
|
||||
* 启动页面
|
||||
@@ -46,6 +47,13 @@ struct LauncherPage {
|
||||
aboutToAppear(): void {
|
||||
this.setSystemBar()
|
||||
this.userPrivacyDialogVisible = !PreferenceService.getBooleanValueSync('Privacy', 'isUserAgree', false);
|
||||
LaunchMonitor.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('HOME_ROOT'),
|
||||
this.context, 'LauncherPage');
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
LaunchMonitor.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%')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user