Compare commits
2 Commits
d0949704ed
...
84063edcb2
| Author | SHA1 | Date | |
|---|---|---|---|
| 84063edcb2 | |||
| 10bbfc4c82 |
@@ -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/StartupMonitor.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.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { inspector } from '@kit.ArkUI';
|
||||
import { hiAppEvent, hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { deviceInfo } from '@kit.BasicServicesKit';
|
||||
|
||||
enum DrawReportState {
|
||||
IDLE,
|
||||
@@ -19,11 +20,27 @@ enum DrawReportState {
|
||||
*
|
||||
* 监听系统 APP_LAUNCH 事件,并确保一个进程生命周期内只提交一次
|
||||
* reportDrawnCompleted。检测结果同时输出到 hilog 和模拟页面。
|
||||
*
|
||||
* 与 future_entry_demo 的 StartupMonitor.ets 对应:本文件为无依赖演示实现
|
||||
* (仅 hilog + 页面展示,不做 ELK 上传),追溯字段已同步系统时间拆解字段
|
||||
* 与 deviceInfo 环境字段;git_commit / is_first_install / build_mode 依赖
|
||||
* biz_common 与 BuildProfile,演示工程不适用,故未同步。
|
||||
*/
|
||||
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;
|
||||
/**
|
||||
* 补报事件判定容差(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;
|
||||
private static watcherInitialized: boolean = false;
|
||||
private static drawReportState: DrawReportState = DrawReportState.IDLE;
|
||||
private static capturedEntryPage: string = '未捕获';
|
||||
@@ -59,6 +76,9 @@ export class StartupDiagnostics {
|
||||
}
|
||||
};
|
||||
|
||||
// 记录注册时间戳,用于区分本次启动事件与系统补投递的上次启动遗留事件
|
||||
StartupDiagnostics.watcherRegisterTime = Date.now();
|
||||
|
||||
try {
|
||||
hiAppEvent.addWatcher(watcher);
|
||||
StartupDiagnostics.watcherInitialized = true;
|
||||
@@ -230,15 +250,52 @@ export class StartupDiagnostics {
|
||||
StartupDiagnostics.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 = StartupDiagnostics.isBackfillEvent(iconInputTime);
|
||||
|
||||
StartupDiagnostics.launchEventCount++;
|
||||
StartupDiagnostics.lastLaunchDetail =
|
||||
`extend_time=${extendTime}ms, icon_input_time=${iconInputTime}, ` +
|
||||
`start_type=${startType}, process_name=${processName}`;
|
||||
`start_type=${startType}, process_name=${processName}\n` +
|
||||
`bundle_name=${bundleName}, bundle_version=${bundleVersion}\n` +
|
||||
`animation_finish_time=${animationFinishTime}ms, response_latency=${responseLatency}ms\n` +
|
||||
`startability_processstart_dur=${startabilityProcessStartDur}ms, ` +
|
||||
`appattach_to_appforeground_dur=${appattachToAppForegroundDur}ms\n` +
|
||||
`device_type=${StartupDiagnostics.getDeviceType()}, ` +
|
||||
`system_version=${StartupDiagnostics.getSystemVersion()}\n` +
|
||||
`补报事件=${isBackfill ? '是(entry_page 不归因,按 unknown 处理)' : '否'}`;
|
||||
StartupDiagnostics.lastAction = '已捕获有效冷启动 APP_LAUNCH';
|
||||
StartupDiagnostics.logInfo(StartupDiagnostics.lastLaunchDetail);
|
||||
StartupDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断事件是否为系统补投递的上次启动遗留事件。
|
||||
* 本次启动链中 icon_input_time 与 watcher 注册时间间隔在容差内;
|
||||
* 补报事件的 icon_input_time 早于注册时间超过容差(至少 5 秒的送达延迟)。
|
||||
*/
|
||||
private static isBackfillEvent(iconInputTime: number): boolean {
|
||||
return iconInputTime < StartupDiagnostics.watcherRegisterTime - StartupDiagnostics.BACKFILL_TOLERANCE_MS;
|
||||
}
|
||||
|
||||
private static getDeviceType(): string {
|
||||
return deviceInfo.deviceType;
|
||||
}
|
||||
|
||||
private static getSystemVersion(): string {
|
||||
return deviceInfo.osFullName;
|
||||
}
|
||||
|
||||
private static getStateName(): string {
|
||||
switch (StartupDiagnostics.drawReportState) {
|
||||
case DrawReportState.IDLE:
|
||||
|
||||
@@ -45,11 +45,27 @@ time + icon_input_time + start_type + process_name
|
||||
| 指标名 | `cold_start_time` |
|
||||
| 指标值 | 冷启动耗时,单位 ms |
|
||||
| 维度 | `start_type`、`entry_page`、`app_version` |
|
||||
| 追溯字段 | `event_time`、`icon_input_time`、`process_name`、`extend_time`、`animation_finish_time`、`bundle_version` |
|
||||
| 追溯字段 | `event_time`、`icon_input_time`、`process_name`、`extend_time`、`animation_finish_time`、`bundle_version`、`bundle_name`、`response_latency`、`startability_processstart_dur`、`appattach_to_appforeground_dur`、`device_type`、`system_version`、`build_mode`、`target_name`、`git_commit`、`is_first_install`、`is_backfill` |
|
||||
| 分桶 | `[500, 800, 1200, 2000, 3000]` |
|
||||
|
||||
模块名使用小写字母和连字符;指标名不包含连字符。维度控制在必要范围内,避免将用户 ID、完整 URL 等高基数字段作为维度。
|
||||
|
||||
### 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`:动效完成到应用首帧的耗时,即**应用侧可优化的独占部分**;看板可据此判断劣化发生在系统侧还是应用侧。
|
||||
|
||||
环境追溯字段说明:`build_mode`/`target_name` 区分 debug/forTest/Official 环境,避免测试包污染线上看板;`is_first_install`(`StartManager.getAppInstallStatus()`)标记安装后首次启动,该次耗时显著偏高,不标记会拉高 P50/P95;`git_commit` 精确到代码提交,配合内部版本号做代码级追溯。
|
||||
|
||||
## 5. 后台与看板配置
|
||||
|
||||
按新增 PDF 的自定义 APM 模块流程,在 APM 后台创建 `launch` 模块并启用:全量采样(`sampling_rate: 1000`)、60 秒聚合(`aggre_time: 60`)、累计 100 条触发上报(`aggre_count: 100`)。随后为该模块补充 ELK 索引模板,在 Grafana 建立按版本、启动类型和时间聚合的 P50/P95、超 2 秒占比及分桶分布面板。
|
||||
@@ -60,6 +76,17 @@ time + icon_input_time + start_type + process_name
|
||||
|
||||
`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 秒);随后正常冷启动收到自身事件,判定 `补报事件=否`。双向判定均正确,正常启动无误判。
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
1. 单次冷启动中,隐私页、网络页和首页连续跳转时,`reportDrawnCompleted` 仍只提交一次。
|
||||
@@ -71,4 +98,86 @@ time + icon_input_time + start_type + process_name
|
||||
|
||||
## 8. 已验证结果
|
||||
|
||||
在 `CrashDiagnosticsDemo` 的 nova 14 Pro 真机回归中:未登录冷启动入口为 `StartupLogin`,`extend_time` 约 `158ms`;持久化登录后冷启动入口为 `Index`,`extend_time` 约 `155ms`。网络异常、网络恢复、隐私确认及页面返回场景均保持一次 draw 回调和一次系统提交,后续页面注册被全局保护拦截;`start_type=1` 的热启动事件被正确忽略。
|
||||
### 8.1 首次回归(2026-08-05,`CrashDiagnosticsDemo`,nova 14 Pro)
|
||||
|
||||
未登录冷启动入口为 `StartupLogin`,`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` 清数据后启动 | 入口 `StartupLogin`,`extend_time=152ms`,单次提交 |
|
||||
| 4 | 多入口连续跳转 | 登录页 → 网络异常页 → 返回 → 隐私确认页 | `StartupNetwork`、`StartupPrivacy` 重复首帧注册均被拦截(`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 | 清数据后未登录冷启动 | 入口 `StartupLogin`,`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 组结果:
|
||||
|
||||
| 组 | 未登录冷启动(入口 `StartupLogin`) | 网络页拦截 | 隐私页拦截 | `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 |
|
||||
|---|---|---|---|
|
||||
| 未登录冷启动(`StartupLogin`) | 158ms / 补报=否 | 180ms / 补报=否 | 180ms / 补报=否 |
|
||||
| 网络页重复注册拦截 | ✓ | ✓ | ✓ |
|
||||
| 隐私页重复注册拦截 | ✓ | ✓ | ✓ |
|
||||
| `Index` 重复注册拦截 | ✓ | ✓ | ✓ |
|
||||
| 已登录冷启动(`Index`) | 129ms / 补报=否 | 132ms / 补报=否 | 131ms / 补报=否 |
|
||||
| 热启动(进程存活) | 无新事件 | 无新事件 | 无新事件 |
|
||||
| 立即强杀(首帧未提交)后重启 | 被强杀进程无有效事件,重启进程正常捕获 | 同左 | 同左 |
|
||||
| 1 秒退出(首帧已提交)后重启 | 先收遗留事件补报=是,再收自身事件补报=否 | 同左 | 同左 |
|
||||
|
||||
24 个断言(3 组 × 8 步)全部通过,组间一致。补报判定在每组中均双向正确:遗留事件稳定判定为补报(`entry_page` 不归因),自身事件稳定判定为非补报(`entry_page` 正常归因)。
|
||||
|
||||
@@ -2,9 +2,12 @@ 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 { AppConfigManager, StartManager } from 'biz_common';
|
||||
import { GIT_COMMIT } from 'biz_common/src/main/ets/utils/debug/GitBuildConfig';
|
||||
import { inspector } from '@kit.ArkUI';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { deviceInfo } from '@kit.BasicServicesKit';
|
||||
import BuildProfile from 'BuildProfile';
|
||||
|
||||
const TAG: string = 'StartupMonitor';
|
||||
|
||||
@@ -21,6 +24,17 @@ const TAG: string = 'StartupMonitor';
|
||||
*/
|
||||
export class StartupMonitor {
|
||||
private static readonly MAX_EVENT_KEYS: number = 20;
|
||||
/**
|
||||
* 补报事件判定容差(ms)。
|
||||
* 本次启动链中 icon_input_time(点击图标)与 watcher 注册时间间隔在毫秒~秒级;
|
||||
* 系统补投递的上次启动遗留事件,其 icon_input_time 至少早于注册时间 5 秒(事件送达延迟),
|
||||
* 因此容差取 2s 可稳定区分两者。
|
||||
*/
|
||||
private static readonly BACKFILL_TOLERANCE_MS: number = 2000;
|
||||
/**
|
||||
* watcher 注册时间戳(epoch ms),用于识别系统补投递的上次启动遗留事件。
|
||||
*/
|
||||
private static watcherRegisterTime: number = 0;
|
||||
|
||||
/**
|
||||
* 首帧绘制完成时快照的入口页面名称,在 onDraw 回调中赋值。
|
||||
@@ -115,6 +129,8 @@ export class StartupMonitor {
|
||||
if (StartupMonitor.watcherInitialized) {
|
||||
return
|
||||
}
|
||||
// 记录注册时间戳,用于区分本次启动事件与系统补投递的上次启动遗留事件
|
||||
StartupMonitor.watcherRegisterTime = Date.now()
|
||||
hiAppEvent.addWatcher({
|
||||
name: "startupWatcher",
|
||||
appEventFilters: [{
|
||||
@@ -192,22 +208,46 @@ export class StartupMonitor {
|
||||
): void {
|
||||
try {
|
||||
const bundleVersion = (eventInfo.params['bundle_version'] as string) ?? '';
|
||||
const bundleName = (eventInfo.params['bundle_name'] as string) ?? '';
|
||||
const animationFinishTime = eventInfo.params['animation_finish_time'] as number;
|
||||
// 时间拆解字段(详见接入方案文档第 4 节):
|
||||
// response_latency 需要 API 22+;startability_processstart_dur / appattach_to_appforeground_dur 仅冷启动存在
|
||||
const responseLatency = eventInfo.params['response_latency'] as number;
|
||||
const startabilityProcessStartDur = eventInfo.params['startability_processstart_dur'] as number;
|
||||
const appattachToAppForegroundDur = eventInfo.params['appattach_to_appforeground_dur'] as number;
|
||||
const iconInputTime = eventInfo.params['icon_input_time'] as number;
|
||||
// 补投递事件(上次启动提交绘制但进程在事件送达前退出)的 entry_page 无意义,
|
||||
// capturedEntryPage 是当前进程的快照,不能错误归因到本次启动的页面。
|
||||
const isBackfillEvent = StartupMonitor.isBackfillEvent(iconInputTime);
|
||||
|
||||
const metricPayload: Record<string, Object> = {
|
||||
// 核心指标
|
||||
cold_start_time: coldStartTime,
|
||||
// 维度
|
||||
start_type: startType,
|
||||
entry_page: StartupMonitor.capturedEntryPage,
|
||||
entry_page: isBackfillEvent ? 'unknown' : StartupMonitor.capturedEntryPage,
|
||||
app_version: StartupMonitor.getAppVersion(),
|
||||
bundle_version: bundleVersion,
|
||||
// 补报标识:true 表示系统补投递的上次启动遗留事件(entry_page 无意义),看板可据此过滤或单独统计
|
||||
is_backfill: isBackfillEvent,
|
||||
// 原始系统字段,用于后端校验和去重
|
||||
event_time: eventInfo.params['time'],
|
||||
icon_input_time: eventInfo.params['icon_input_time'],
|
||||
process_name: eventInfo.params['process_name'],
|
||||
extend_time: eventInfo.params['extend_time'],
|
||||
animation_finish_time: animationFinishTime,
|
||||
// 追溯字段:系统时间拆解
|
||||
bundle_name: bundleName,
|
||||
response_latency: responseLatency,
|
||||
startability_processstart_dur: startabilityProcessStartDur,
|
||||
appattach_to_appforeground_dur: appattachToAppForegroundDur,
|
||||
// 追溯字段:客户端环境
|
||||
device_type: StartupMonitor.getDeviceType(),
|
||||
system_version: StartupMonitor.getSystemVersion(),
|
||||
build_mode: StartupMonitor.getBuildMode(),
|
||||
target_name: StartupMonitor.getTargetName(),
|
||||
git_commit: StartupMonitor.getGitCommit(),
|
||||
is_first_install: StartupMonitor.getIsFirstInstall(),
|
||||
}
|
||||
|
||||
const builder = new StartupElkBuilder('i')
|
||||
@@ -218,15 +258,51 @@ export class StartupMonitor {
|
||||
const message = new ElkUploadMessage(builder)
|
||||
// 异步上传,避免阻塞启动流程
|
||||
ElkService.getInstance().pushBusinessLog(message, true)
|
||||
HXLog.i(TAG, `Cold start metric uploaded: ${coldStartTime}ms`)
|
||||
HXLog.i(TAG, `Cold start metric uploaded: ${coldStartTime}ms${isBackfillEvent ? ' (backfill)' : ''}`)
|
||||
} catch (e) {
|
||||
HXLog.e(TAG, `uploadColdStartTime failed: ${JSON.stringify(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断事件是否为系统补投递的上次启动遗留事件。
|
||||
* 本次启动链中 icon_input_time 与 watcher 注册时间间隔在容差内;
|
||||
* 补报事件的 icon_input_time 早于注册时间超过容差(至少 5 秒的送达延迟)。
|
||||
*/
|
||||
private static isBackfillEvent(iconInputTime: number): boolean {
|
||||
return iconInputTime < StartupMonitor.watcherRegisterTime - StartupMonitor.BACKFILL_TOLERANCE_MS
|
||||
}
|
||||
|
||||
private static getAppVersion(): string {
|
||||
return AppConfigManager.getInstance().getConfigProvider()?.getInnerVersionFull() ?? 'unknown'
|
||||
}
|
||||
|
||||
private static getDeviceType(): string {
|
||||
return deviceInfo.deviceType
|
||||
}
|
||||
|
||||
private static getSystemVersion(): string {
|
||||
return deviceInfo.osFullName
|
||||
}
|
||||
|
||||
private static getBuildMode(): string {
|
||||
return BuildProfile.BUILD_MODE_NAME.toString()
|
||||
}
|
||||
|
||||
private static getTargetName(): string {
|
||||
return BuildProfile.TARGET_NAME.toString()
|
||||
}
|
||||
|
||||
private static getGitCommit(): string {
|
||||
return GIT_COMMIT
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否安装后首次进入(StartManager 在 EntryAbility.onCreate 中已 init)
|
||||
*/
|
||||
private static getIsFirstInstall(): boolean {
|
||||
return StartManager.getInstance().getAppInstallStatus()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user