# Dino English · 崩溃 & 性能（#4）规则

| 项 | 值 |
| --- | --- |
| 看板编号 | #4，`DASHBOARDS.md` §6.1.4 |
| 负责人 | @lee.winqi |
| 文档依据 | Lark《关键链路数据 & 维度定义（v0.1 草案）》**2026-08-12 版** §3.4 |
| 实测依据 | BigQuery，自埋点窗口 2026-07-14 → 08-12（30 天）；Firebase Performance 08-06 → 08-11；Crashlytics 08-13 → 08-14 |
| 建立日期 | 2026-08-14 |
| 状态 | **已上线** · [health-performance.html](./health-performance.html)（数据 2026-08-14，三窗口可切；7 月窗口数据不足已标为不可用）|

**这份文件是「崩溃 & 性能」这块板的唯一标准。**口径有分歧以本文件为准；与 Lark 文档冲突时先改 Lark 再改这里。

页面做法见 [`PAGE-STANDARD.md`](./PAGE-STANDARD.md)，代码写法见 [`DEVELOPMENT.md`](./DEVELOPMENT.md)，通用取数纪律见 [`DASHBOARDS.md`](./DASHBOARDS.md)。

---

## 0. 先说三条颠覆现有文档的实测

`DASHBOARDS.md` 对这块板的判断写于 2026-08-03，**三条都已过期**：

| 文档现在写的 | 2026-08-14 实测 |
| --- | --- |
| 「`area='performance'` 仅 363 条，样本不足以出结论」 | **17,034 条**（30 天，双端齐全）—— 埋点阻塞早已解除 |
| 「**崩溃 / ANR 走 Crashlytics，未入 BQ**」 | **`firebase_crashlytics` dataset 已存在**，2026-08-13 刚开始导 —— 但只有 Android **42 行**、iOS **1 行** |
| （完全没提） | **多出一个 `firebase_performance` dataset，41 万行** —— 自动采集，含**卡顿率**这个自埋点完全没有的维度 |
| （完全没提） | **这四个 `firebase_*` dataset 都在 `US`**，主数据在 `asia-southeast1` —— 查它们要显式传 location，而且 **BigQuery 不允许跨区域查询，它们与 GA4 日表永远没法写进同一条 SQL**（`tools/bq.py` 已加 location 参数） |

**所以这块板现在的难点不是「没数据」，而是「三个数据源测同一件事，会打架」。**见 §2。

---

## 1. 边界：三个数据源 × 三块板，先划清再动手

### 1.1 与其它板的分工

| 不管 | 归属 | 为什么 |
| --- | --- | --- |
| **单个接口的成功率、失败归因** | #5 网络 & 三方 | Firebase Performance 里有 `NETWORK_REQUEST`（Android 就占了 12 万行），**与 #5 的 `api_result` 大量重叠** —— 本板一律不碰，见 §1.2 |
| 「某一步有没有发生、发生后去了哪」 | 用户路线板 | 本板只回答「慢不慢、崩不崩」 |
| 教室 WebView / RTC 本身 | #3 进入教室 | 本板只出教室的**内存**与**卡顿** |
| 登录、支付各段成功率 | #1 / #2 | 同上 |

**与用户路线板的硬规矩（`USER-ROUTE.md` §1.3 已定，本文件确认）：**

> `cold_start_result` / `page_ready_result` 两块板都要用，但**耗时分布只在 #4 出，用户路线板不出耗时排行榜；路径分布只在用户路线板出，#4 不画路径图**。

### 1.2 `NETWORK_REQUEST` 明确划给 #5，本板不用

Firebase Performance 的 `NETWORK_REQUEST` 事件（Android 12 万 / iOS 大部分）看起来很诱人 —— 它有 `response_code`、`request_payload_bytes`、各阶段时间戳，比自埋点还细。**但本板不用它**：

1. **会和 #5 打架。**同一个接口两个数据源两套成功率，读者不知道信哪个。
2. **口径不同。**Firebase 按 URL pattern 聚合（`app.dinoenglish.ai/**`），自埋点按业务调用聚合（一次调用一条，含重试）—— 分母根本不是一回事。
3. 它只有 **08-06 → 08-11 六天**，比自埋点短得多。

**如果将来要用它，应该是 #5 拿去做「客户端视角 vs 网络层视角」的交叉验证，而不是本板另起炉灶。**

---

## 2. 三个数据源，各测什么，为什么会打架

| 数据源 | 位置 | 覆盖 | 本板用它测什么 |
| --- | --- | --- | --- |
| **自埋点** | `analytics_538991439` · `area='performance'` | 30 天，双端 | **主数据源**：冷启动、首屏就绪、内存快照 |
| **Firebase Performance** | `firebase_performance.…{ANDROID,IOS}` · **在 `US`** | **08-05 → 08-11，7 天** | **只取 `SCREEN_TRACE` 的卡顿率**（自埋点没有这个维度，两端都有数据）；`NETWORK_REQUEST` 不用（§1.2） |
| **Crashlytics** | `firebase_crashlytics.…{ANDROID,IOS}_REALTIME` · **在 `US`** | **08-13 起，Android 42 行 / iOS 1 行** | 崩溃、ANR、非致命异常 |

### 2.1 冷启动有两个来源，值不一样 —— 必须只认一个

| 来源 | 口径 | 实测（Android） |
| --- | --- | --- |
| 自埋点 `cold_start_result` | 进程启动 → **首个可见 Activity 首帧**（当前 `SplashActivity`） | p50 730ms（高端机）/ 1,470ms（低端机） |
| Firebase `_app_start` | Firebase SDK 自己的口径（进程创建 → 首个 Activity `onResume`） | p50 **796.8ms**，p95 3,253.9ms |

两个数量级接近但**不是同一个数**，而且 Firebase 那份不分内存档。

**裁定：冷启动一律用自埋点 `cold_start_result`。**理由：① 它有 `mem_tier`，能按机型档下钻（这是本板最有价值的维度，见 §4.1）；② 它覆盖 30 天，Firebase 只有 6 天；③ 口径写在 Lark 文档里，可追溯。
**Firebase `_app_start` 不上页面**，只在文档里留一句「另一个来源测出来是 796.8ms，量级吻合」作为交叉验证。

### 2.2 Crashlytics 只有两天数据，只有 `_REALTIME` 表，且**不跟随时间窗口**

- Android `..._ANDROID_REALTIME` 建于 **2026-08-13 01:17**，42 行，最后更新 08-14 02:00 —— **在持续写入**
- iOS `..._IOS_REALTIME` 建于 08-13 23:33，**1 行**（08-14 才出现第一条）
- **没有每日批表**（`com_prime_dino_english_ANDROID`，不带 `_REALTIME`）—— Crashlytics 的批表通常在开启导出后次日才生成

**裁定一：崩溃这一块先做，但整节标「数据 2026-08-13 起，样本极小」，不算比例、不做趋势、不上色。**只列「有哪些崩溃、影响多少台设备」的事实清单。等批表出现、攒够两周再谈 crash-free 率。

**裁定二：崩溃块不跟随页面上方的时间窗口。**主窗口终点是最新已结算日表（08-12），而崩溃数据从 08-13 才有 —— 跟随窗口的话**一条都取不到**。数据总共两天，切窗口对它没有任何意义。所以固定取全部，页面上明写「这一块不随上方时间窗口变化」并显示它自己的真实区间。

---

## 3. 自埋点三个事件的字段与口径

### 3.1 `cold_start_result`

| 字段 | 说明 |
| --- | --- |
| `duration_ms` | 进程启动 → 首帧。**`result='rejected'` 时不带**（后台预拉起） |
| `target` | 首个可见 Activity。**Android 20 个取值**，iOS 7 个 —— 两端语义完全不同，见 §4.3 |
| `mem_tier` | `high` / `mid` / `low`，**本板最重要的下钻维度** |
| `result` | Android `success` / `rejected`；**iOS 只有 `success`** |
| `reason` | 仅 `background_start`（98 条），配 `result='rejected'` |
| `network_type` | 冷启动时的网络类型 |
| `perf_schema_version` | Android 3,655/3,667 有；**iOS 只有 3/3,728** —— 见 §5 |

**口径：耗时只取 `result='success'` 的 `duration_ms`。**后台预拉起（`rejected` + `background_start`，98 条）不带耗时，单独计数，**不进任何分母**。

**`target` 里混进了调试页**（`DinoClassDebugActivity` 15 条、`DebugActivity` 6 条）—— 内部测试机的数据。量小，但按机型/版本下钻时要注意。

### 3.2 `page_ready_result`

| 字段 | Android | iOS |
| --- | --- | --- |
| `duration_ms` | ✅ 4,208 | ✅ 2,587 |
| `data_ready_ms` | ✅ 4,196 | **❌ 只有 7 条** |
| `rendered` | ✅ 4,196（**08-04 才上线**） | **❌ 无** |
| `mem_tier` | ✅ 4,196 | **❌ 无** |
| `target` | `home` / `login` | **只有 `home`** |
| `result` | `success` / **`tech_fail`** | `success` / **`fail`** |

**四个问题：**

1. **失败词两端又不一致**（第五次撞上）：Android `tech_fail`、iOS `fail`。取数必须双取。
2. **`rendered` 是 2026-08-04 才上线的**，30 天窗口里前三周没有 —— 按 `rendered` 切片必须标首报日，不能拿 30 天总量做分母。
3. **iOS 没有 `mem_tier`** → 本板最有价值的「按内存档下钻」在 iOS 的首屏就绪上做不了。
4. **iOS 没有 `login` 页的就绪数据**，只有 `home`。

**`rendered=false` 时的耗时含义（实测修正 Lark 口径）：**Lark 写「`rendered=false` 时 `duration_ms` 退化为 `data_ready_ms`」，但实测 Android `tech_fail`+`timeout` 的 58 条 **`duration_ms` p50 = 15,007ms**（正好卡在 15 秒超时阈值）而不是 `data_ready_ms` 的值。**做页面前必须逐条比对这两个字段，别照抄文档那句话。**见 §7 拍板项 ③。

### 3.3 `mem_snapshot` —— 两端差异最大的一个

| 字段 | Android | iOS |
| --- | --- | --- |
| `total_pss_kb` | ✅ 5,844 | ✅ 2,513 |
| `java_heap_kb` / `native_heap_kb` | ✅ 5,844 | **❌ 无**（iOS 没有 Java 堆，合理） |
| `phase` | ✅ 5,832（`enter` / `exit`） | **❌ 只有 2 条** |
| `target` | `home` / `classroom` | **只有 `home`** |
| `app_foreground` | **string** | **int** |
| `result` | ❌ 无 | ✅ `success` |

**四条硬规矩：**

1. **两端内存绝对值不可比。**Android `total_pss_kb` p50 = **542,929 KB（约 530 MB）**，iOS = **152,897 KB（约 149 MB）**。差 3.5 倍**不是 iOS 省内存**，是 PSS（Linux 的按比例分摊内存，含共享库）和 iOS 的口径根本不是一回事。**页面上两端内存必须分开画，并明确写「两端口径不同，不可比大小」。**
2. **`app_foreground` 两端类型不同**（Android string / iOS int）—— 取数必须双取，否则静默丢一端。这是本项目第 N 次撞上布尔字段类型不一致（`DASHBOARDS.md` §6.1.1 已记录 `is_new_user` 等三个）。
3. **iOS 没有 `phase`、没有 `classroom`** → Lark §3.4 要求的「classroom `enter`/`exit` 配对分析」**只有 Android 能做**。
4. **Android 的 `enter`/`exit` 本身就配不齐**：classroom `enter` 2,124 条 vs `exit` 1,516 条，**exit 缺失约 29%** —— 系统直接杀进程时没机会报 exit。Lark 已写「配对分析要允许缺失」，页面要把「未配对」单列，**不能当成 0 处理**。

---

## 4. 四条地基级发现

### 4.1 内存档是最有价值的下钻维度：低端机慢一倍

| 事件 · 平台 | high | mid | low |
| --- | --- | --- | --- |
| 冷启动 Android（`SplashActivity`） | **730ms** / p95 2,045 | 1,218ms / p95 4,366 | **1,470ms** / p95 5,099 |
| 冷启动 iOS（`ExperimentAssignmentPlaceholder…`） | **420ms** / p95 728 | 634ms / p95 1,073 | **1,290ms** / p95 2,982 |
| 首屏就绪 Android（`home`） | **705ms** / p95 2,154 | — | **1,306ms** / p95 4,806 |

**低端机的 p95 是高端机的 2.2 ~ 4.1 倍。**一个只看总体 p50 的页面会把这件事完全盖掉。

**裁定：本板所有耗时指标默认按 `mem_tier` 三档拆开，总体值只作为副指标。**这是本板区别于「随便画个 P95」的核心。

#### 4.1.1 `mem_tier` 到底按什么分的（2026-08-14 反推）

Lark 文档只列了 `high` / `mid` / `low` 三个取值，**没写分档规则**。分档是客户端算好了才上报，BigQuery 只看得到结果。从数据反推：

**① 不是按当前内存占用分的。**如果是，两档的 `total_pss_kb` 就不该重叠。实测大量重叠：

| | 机型 | 该机型 PSS 中位数 |
| --- | --- | --- |
| `high` 档里最低的 | Samsung SM-A556E | **188 MB** |
| `low` 档里最高的 | Samsung SM-A165F | **657 MB** |

**② 是按设备总内存（RAM）分的。**机型分布与 RAM 档次高度吻合，iOS 尤其干净：

| 档 | iOS 机型（按设备数排） |
| --- | --- |
| `high` | iPhone 17 Pro Max / 15 Pro Max / 16 Pro Max / 16 Pro / 17 Pro / 17 / 15 Pro / 16 / 16 Plus / 16e、iPad Air 5·7·8th、iPad Pro |
| `low` | iPhone 13 / 11 / 11 Pro Max / 12 / XS Max / 7 Plus / 8 Plus / 11 Pro / X / XR / 6s Plus / 7 / XS、iPad 5–10th gen、iPad mini 6、iPad Air 2·4 |

对上各代 RAM：**iPhone 11 / 12 / 13 全是 4GB → `low`；iPhone 15 是 6GB、16 / 17 是 8GB → `high`**。
所以**阈值落在 4GB 与 6GB 之间，大概率是「≥ 6GB 算 high」** —— 但中间那批 6GB 机型（12 Pro、13 Pro、14）样本太少，卡不死这条线。

Android 同样吻合：`high` 是三星 S 系列旗舰、Honor 高端；`low` 是三星 A0xx 入门系列、itel A6610L、Infinix、Redmi 入门款。

**③ 但确切阈值数据侧证明不了。**大概率是 Android 的 `ActivityManager.MemoryInfo.totalMem`、iOS 的 `ProcessInfo.physicalMemory`，**这是推测**。见 §8 拍板项 ⑩。

**④ 一个容易踩的坑：平板大量落在 `low` 档。**iPad 5–10th gen、iPad mini 6、iPad Air 2·4、Galaxy Tab A9（`SM-X110`）、Honor 平板（`NDL-W09`）都在低端档。
**所以「低端机慢一倍」这个结论里混着平板**，不全是廉价手机 —— 按这个档做优化决策前，要先想清楚照顾的是谁。页面上已注明。

### 4.2 卡顿率：Android 比 iOS 差一倍，冻结帧差 50 倍

> **2026-08-14 修正。**本节初稿写「iOS 一条 `SCREEN_TRACE` 都没有」，**是错的** ——
> 当时只看了按量排序的前 30 行，被 Android 占满了。实际 iOS 有 **57 个页面**在报，只是单页量少。

Firebase `SCREEN_TRACE` 的 `slow_frame_ratio`（渲染超过 16ms 的帧占比）与 `frozen_frame_ratio`（超过 700ms）：

**Android**

| 页面 | slow | frozen | 样本 |
| --- | --- | --- | --- |
| `_st_DinoClassLessonEntryActivity` | **92.66%** | 2.78% | 134 |
| `_st_SplashActivity` | **88.24%** | **5.25%** | 1,377 |
| `_st_WinBackActivity` | 76.36% | 0.99% | 317 |
| `_st_CommonWebActivity` | 69.19% | 0.81% | 1,676 |
| `_st_MainActivity` | 64.88% | 0.79% | 3,192 |
| 对照：`_st_DinoClassTeacherSelectActivity` | 12.55% | 0.18% | 488 |

**iOS**

| 页面 | slow | frozen | 样本 |
| --- | --- | --- | --- |
| `_st_OnboardingNavigationController` | 45.89% | 0.14% | 2,771 |
| `_st_AppNavigationController` | 40.14% | 0.01% | 2,408 |
| `_st_DinoClassViewController` | 38.16% | 0.01% | 1,550 |
| `_st_DinoChatViewController` | 36.62% | 0.02% | 2,146 |
| `_st_MainTabViewController` | 36.21% | 0.01% | 3,086 |

**两端对照是这一节最有价值的东西：**

| | Android 主界面 | iOS 主界面 |
| --- | --- | --- |
| 慢帧率 | **64.88%** | 36.21% |
| **冻结帧率** | **0.79%** | **0.01%** |

慢帧率差近一倍，**冻结帧（卡死超过 0.7 秒）差约 50 倍**。行业基线一般认为慢帧应低于 5% —— 两端都远超，但 Android 是另一个量级。

**三条必须写在页面上：**

1. **只有 6 天**（实测分区 08-05 → 08-11），比自埋点短三周。
2. **这是平均值不是分位数**，且未按机型档拆 —— Android 低端机占比更高，会抬高它。**先当「哪个页面最卡」的排序用，别当绝对值汇报。**
3. 两端页面名不同（Activity vs ViewController），**只能各自排序，不能逐行对齐比**。

### 4.3 两端的 `target` 语义完全不同，不能并排比

| | Android | iOS |
| --- | --- | --- |
| 冷启动落地页 | `SplashActivity`（3,226 条） | `ExperimentAssignmentPlaceholderViewController`（1,871）、`SupplementResumeLoadingViewController`（1,819） |

iOS 报的是**占位/加载控制器**，Android 报的是真实的 Splash。**两端的「冷启动到哪里」根本不是同一个位置**，耗时并排放会误导。

**裁定：冷启动耗时两端各画各的，页面明确写出两端 `target` 的实际取值。**Lark 已写「不含 Splash → MainActivity 路由时间，端到端需结合 `page_ready_result`」—— 本板照此，**不自己造一个「端到端冷启动」的合成指标**。

### 4.4 崩溃数据虽少，但已经指出一个明确的 bug

08-13 一天的 42 条里，**最集中的一个是可以直接修的**：

> **`PlaceholderView.showContentViews` → `CalledFromWrongThreadException`**
> 「Only the original thread that created a view hierarchy can touch its views. **Expected: main Calling: `dino-media-preload`**」
> **13 台不同设备、13 个不同机型**（华为 ALT-NX1 / OPPO CPH27xx / 三星 SM-S908N·S916N·A156L / vivo V2346 …），全部 1.5.1

`dino-media-preload` 这个后台线程直接碰了 UI。机型分散说明**不是某款机器的兼容问题，是代码 bug**。

其余：

| 类型 | 问题 | 说明 |
| --- | --- | --- |
| FATAL | `[libutils.so]` SIGSEGV | 3 台，原生崩溃 |
| FATAL | `IndexOutOfBoundsException`（`Index -1 out of bounds for length 0`） | 1 台 |
| **ANR** | `DinoEnglishApp.initRouter`（"main thread waiting for too long" / "slow operations in main thread"） | **印证 #5 的发现** —— `onCreate` 里同步启了一串网络活儿（见 `NETWORK-THIRDPARTY.md` §3.4） |
| ANR | `MessageQueue.nativePollOnce`（"Root cause unknown"） | 6 条，Crashlytics 自己也定位不到 |
| NON_FATAL | `MicrosoftRecognizer.releaseRecognizerLocked` → `TimeoutException` | 11 条 / 5 台 —— **属语音链路**，做 ASR/TTS 板（#11）时要一起看 |

---

## 5. 已知缺口

| 缺口 | 影响 |
| --- | --- |
| **iOS 的 Crashlytics 几乎没有**（Android 42 条 / iOS **1 条**） | 崩溃这一节实质是 Android 单端 |
| **iOS `page_ready_result` 没有 `mem_tier`** | 按内存档看首屏就绪，只有 Android 能做 |
| **iOS `mem_snapshot` 没有 `phase` / 没有 `classroom`** | 教室内存的 enter/exit 配对只有 Android 能做 |
| **iOS `perf_schema_version` 几乎不报**（cold_start 3/3,728，page_ready 0） | schema 一变，iOS 侧无法区分新旧格式 |
| **Crashlytics 没有每日批表** | 只有 `_REALTIME`，历史数据补不回来 |
| **Firebase Performance 分区停在 08-11** | 卡顿数据比自埋点少三周，且是否持续在导要确认 |
| **`rendered` 2026-08-04 才上线** | 按它切片的分母只能从 08-04 起算 |
| **ANR 主因 "Root cause unknown"** | 6 条 ANR 里 Crashlytics 自己也定位不到根因 |

---

## 6. 取数条件与实测量

### 6.1 成本闸门

按 `DASHBOARDS.md` §4.4。三个数据源都要带日期条件：

| 表 | 必须写的过滤 |
| --- | --- |
| `analytics_538991439.events_*` | `_TABLE_SUFFIX BETWEEN` |
| `firebase_performance.*` | **`_PARTITIONTIME BETWEEN`**（分区表，不写会扫全表 41 万行） |
| `firebase_crashlytics.*_REALTIME` | `event_timestamp BETWEEN` |

### 6.2 自埋点量（窗口 2026-07-14 → 08-12，30 天）

| 事件 | Android | iOS | Android 首报 | iOS 首报 |
| --- | --- | --- | --- | --- |
| `mem_snapshot` | 5,844 | 2,513 | 07-27 | 07-28 |
| `page_ready_result` | 4,208 | 2,587 | 07-27 | 07-28 |
| `cold_start_result` | 3,667 | 3,728 | 07-27 | 07-28 |

三个事件**首报日都在 07-27/28**，30 天窗口里只有 16 天有数据 —— **窗口标事件自己的首报日**（`DASHBOARDS.md` §4.2 硬规矩）。

### 6.3 另两个源

| 源 | Android | iOS | 覆盖 |
| --- | --- | --- | --- |
| Firebase Performance（全部） | 196,202 | 215,405 | 08-06 → **08-11** |
| ├ `SCREEN_TRACE` | ✅ 61 个页面 | ✅ **57 个页面** | 同上 |
| └ `NETWORK_REQUEST` | 约 12 万 | 约 21 万 | **本板不用**（§1.2） |
| Crashlytics | **42**（8 类问题） | **1** | 08-13 → 08-14 |

---

## 7. 页面结构（DoD）

按 `PAGE-STANDARD.md` §6。**建议页面标题叫「崩溃 & 性能」**，但崩溃那一节必须整节标「数据从 2026-08-13 起，样本极小」。

**2026-08-14 拍板（@lee.winqi）：iOS 缺数据不阻塞开发,照现状先做。**理由是那多半是埋点没埋好,不是数据管道问题。
但**页面必须把「哪一端没埋」显式列出来,让双端同事自己能看见**,不许只写在「这页有哪些坑」里 —— 所以有了下面第 1 块。

| # | 块 | 内容 | 备注 |
| --- | --- | --- | --- |
| 0 | 主指标 | **不设单一主指标** —— 这块板没有「一个数」能概括。结论卡出：冷启动 p50/p95（按内存档）+ 首屏就绪 + 崩溃条数 | |
| **1** | **埋点体检** | **逐字段列出两端实际上报量,缺的标出来 + 写清「缺了它哪个分析做不了」+ 该找谁。**放在结论之后、正文之前,是本页要传达的第一件事 | **本板专属,其它板没有这一块** |
| 2 | 冷启动 | `duration_ms` P50/P95，**按 `mem_tier` 三档拆**；两端各画各的（§4.3）；后台预拉起单列不进分母 | 主数据源 |
| 3 | 首屏就绪 | 同上按档拆；`rendered=false` 与失败单列；**iOS 无 `mem_tier`，那一格标缺口** | 注意 `rendered` 首报日 |
| 4 | 内存 | `total_pss_kb` 分布按档；Android 另出 java/native 堆；**两端分开画 + 明写不可比**；classroom `enter`/`exit` 配对（Android only），未配对单列 | §3.3 |
| 5 | 卡顿 | Firebase `SCREEN_TRACE` 的 slow/frozen 排行，两端各自排序 | **7 天，平均值不是分位数，两端页面名不同不能逐行对齐** |
| 6 | 崩溃 & ANR | 事实清单：issue_title × 影响设备数 × 机型；**不算比例、不上色、不做趋势**；**这一块不随上方时间窗口变化**（见 §2.2） | 实质 Android 单端，2 天 |
| 7 | 这页有哪些坑 | | |

### 7.0 埋点体检块的硬要求

这一块是给**双端同事**看的,不是给数据分析看的,所以：

- **逐项写「缺了它，哪个分析做不了」** —— 只说「iOS 没有 `mem_tier`」没人会动,要说「按内存档看首屏就绪，iOS 整块做不了」
- **覆盖量从数据里现算,不写死** —— 某端补埋了,这一行要自己变绿
- **判定用「有没有量」不是「有没有字段」**：字段存在但只有 2 条（iOS 的 `phase`）**同样算缺**,门槛写进常量
- **不上健康度的红黄绿**（那是给成功率用的）,用「有 / 缺 / 少」三态 + 灰底,避免和 §5.4 的分档色混淆

### 7.1 验收清单

- [ ] 所有耗时按 `mem_tier` 三档拆开，总体值只作副指标
- [ ] 两端内存分开画，页面明写「口径不同，不可比大小」
- [ ] 双端失败词双取（`tech_fail` + `fail`）
- [ ] `app_foreground` 双取 string / int
- [ ] 后台预拉起（`rejected` + `background_start`）不进任何分母
- [ ] classroom 内存的「未配对 exit」单列，不按 0 处理
- [ ] 卡顿一节标明真实覆盖区间与「平均值不是分位数」，两端各自排序不逐行对齐
- [ ] 崩溃一节标明「不随上方窗口变化」+ 真实覆盖区间 + 不算比例
- [ ] iOS 缺失的四项（`mem_tier` / `phase` / `classroom` / `rendered`）在对应位置标缺口，不留空白
- [ ] 窗口标各事件自己的首报日（07-27/28，不是查询区间）
- [ ] 不出「端到端冷启动」这种跨事件合成指标
- [ ] 页面不出现任何 `NETWORK_REQUEST` 的数（那是 #5 的）

---

## 8. 待拍板

| # | 事项 | 卡住什么 | 找谁 |
| --- | --- | --- | --- |
| ① | **`PlaceholderView.showContentViews` 的 `CalledFromWrongThreadException`** —— `dino-media-preload` 线程碰 UI，13 台机型全中 | 不卡看板，但**这是个现成的可修 bug**，应立刻提给 Android | Android |
| ② | **Crashlytics 的每日批表什么时候有？iOS 为什么 0 行？** | 决定崩溃这一节能不能做 crash-free 率，还是只能列清单 | 客户端 / Firebase 配置 |
| ③ | **`rendered=false` 时 `duration_ms` 到底是什么** | Lark 说退化为 `data_ready_ms`，实测 timeout 那批是 15 秒（超时阈值）。归因表会写错 | 客户端 |
| ④ | **iOS 补 `mem_tier` / `phase` / `rendered`** | 本板最有价值的按档下钻，iOS 侧大面积做不了 | iOS |
| ⑤ | ~~iOS 为什么没有 `SCREEN_TRACE`~~ —— **已查明是误判，iOS 有 57 个页面在报** | — | 已解决 |
| ⑥ | **Firebase Performance 分区停在 08-11，还在导吗** | 决定卡顿一节是一次性快照还是常规指标 | 数据侧 |
| ⑦ | 冷启动认自埋点还是 Firebase `_app_start`（本文件裁定自埋点） | 两个源都在，先说定一个，免得日后各引各的 | PM / 数据侧 |
| ⑧ | `cold_start_result` 的 `target` 混进调试页（`DebugActivity` 等 21 条） | 内部测试机数据，按机型下钻时要不要剔 | Android |
| ⑨ | ANR 主因 `MessageQueue.nativePollOnce` 定位不到根因 | 6 条 ANR 无法归因 | Android |
| ⑩ | **`mem_tier` 的分档阈值是多少、按哪个 API 取的** | 数据侧只能从机型反推出「大概 ≥6GB 算 high」（§4.1.1）。**这个阈值直接决定「低端机慢一倍」的分界线在哪** —— 阈值一变，整页的分档结论跟着变 | 双端 |

**②③ 影响页面怎么做，建议开发前问掉；其余可以带着标注先上。**

---

## 9. 与其他文档的改动清单

| 文件 | 改什么 | 状态 |
| --- | --- | --- |
| `DASHBOARDS.md` §6.1.4 | 删掉「Crashlytics 未入 BQ」的判断，改为「08-13 起已导，样本极小」 | ✅ 2026-08-14 |
| `DASHBOARDS.md` §4.1 | dataset 表补四个 `firebase_*`，并写明它们在 `US`、跨区域不能 JOIN | ✅ |
| `DASHBOARDS.md` §4.4 | 成本闸门补两行：`firebase_performance` 分区表必须写 `_PARTITIONTIME`；Crashlytics 要写 `event_timestamp` | ✅ |
| `USER-ROUTE.md` §1.3 | 确认耗时/路径的分工（本文件 §1.1 已复述，无需改动） | ✅ 无冲突 |
| `NETWORK-THIRDPARTY.md` | 加一行：Firebase Performance 的 `NETWORK_REQUEST` 可作 #5 的交叉验证源 | ✅ |
| `README.md` / `index.html` / GitHub Actions | 页面表、文件表、导航卡片、自动刷数据登记 | ✅ |
| `tools/bq.py` | `query()` 加 `location` 参数（firebase_* 在 `US`，不传会 404） | ✅ |
