test(ncm): M10 宿主回归三套(44 断言)+ 构建脚本 + REPORT §42

- bench/retry_storm_sim.c(25 断言):真实 retry_ctl.c/meta_gate.c,100ms tick 断网
  120s→4 次/间隔 ≥30s;状态每 tick 抖动→2 次;时钟回退→不立即重试且间隔仍 ≥30s;
  worker 45s>间隔→不并发;META 空闲 3 帧/60s(旧 600)、播放 13 帧(≤1/5s)、
  状态抖动 ≤4 帧/秒、元数据变化/seek 立即报、超长标题截断不重报、回退不连发。
- bench/home_retry_sim.c(13 断言):真实 app/ui/home.c 的 home_tick 编进宿主
  (--gc-sections + 桩),断网 120s→4 次重试、单飞、全帧重绘 0 次;
  把真机形态的垃圾 64 位 now 喂给 home_tick 仍为 4 次/≥30s;回在线再断网不清基准。
- bench/meta_ipc_sim.c(6 断言):真实 ipc_media 线程 + 假 launcher AF_UNIX socket,
  空闲 2s 只收到 1 帧 META(旧 ~10 帧),状态翻转恰好多 1 帧,帧头合法。
- build_ncm.sh / build_ncm_host.sh:源列表加 retry_ctl.c、meta_gate.c(host 补
  mem_watch.c,否则 player_manager 链接失败);build_ncm_sims.sh 加三个 sim。
- REPORT §42:真机日志证据(时间戳低/高 32 位拆解、21016 帧 META)、根因三链、
  实现、验证(session_gate 88 / http_truncate 13 / core 139 全过)、真机验证建议。
This commit is contained in:
AskaEth committed 2026-11-05 13:00:32 +08:00
1 parent 7f4e18b0ed
commit 294ed6ffc7
7 files changed
+938 -5

No files matched your search

+135
View File
@@ -3591,3 +3591,138 @@ MP3 ≤128k→标准,>128k→极高 HQ。列表显示**实际档**(如 hires
若升级时 MAC 已变化(例如换网卡后首次启动),旧会话仍会解不开 —— 这是旧方案的固有缺陷,
无法事后补救;迁移后再不会因网络状态变化丢会话。
- **未部署/未 push**:设备离线,真机联调后置;本节所有验证均在宿主完成。
## 42. M10:断网重试风暴 / META 洪泛(真机卡死两分钟 + 歌单滑动卡)(2026-11-05)
> 用户原话:「我打开网易云 现在设备卡死了 你帮我抓日志调试一下」→「刚才卡了两分钟 现在恢复了 但是
> 网易云的歌单列表滑动特别卡」。
>
> 现场:ncm CPU 48%、load 3.65,整机交互卡死两分钟(塔台 kill pid 7534 恢复)。
> 红线:不依赖真机即可完成(宿主覆盖计时逻辑);0warning;不 push。
### 42.1 真机证据(只读抓取,未动设备)
**App 日志** `/usr/data/mnt/sd_0/MeowOS/logs/dev.player.ncm/19000100_000000_7534.log`(44 行,
跨度 74s)。稳定循环三个一组:
```
... home: playlists cache hit count=120 ts=1793794687
... home: playlists rc=-4 count=120 uid=408996274
... session: offline
... home: offline retry ← 每轮一次
... home: playlists cache hit count=120 ts=1793794687
... home: playlists rc=-6 count=120 uid=408996274
... home: offline retry ← 又一轮
```
74s 内 **10 次** `home: offline retry`(≈7s 一次;旧代码本意是 30s 一次)。
**launcher 日志** `/usr/data/mnt/sd_0/MeowOS/logs/meow_20261105_122541.log`:单次启动
**21016 条** `ipc: META app=dev.player.ncm state=0 pos=0/0 rc=0 -> ACK`,尾部连续帧时间戳
`1203566 / 1203667 / 1203767 / 1203868 …`——**每 ~100ms 一帧**,且 `state=0 pos=0/0` 完全静止
(没有任何元数据变化)。旧代码本意是 1s 一帧。
**关键新证据:`meow_ui_time_ms()` 在真机上是坏的。** 把上面两个日志里每行的首列(该函数返回值)
按 64 位拆开看:
| 日志行 | 返回的 64 位值 | 低 32 位 | 高 32 位 |
|---|---|---|---|
| `home: offline retry` | 6607363222717682072 | 1135000 | 1538396632 |
| `cache hit` | 8754846870717682072 | 1135000 | 2038396632 |
| `rc=-6` | 18138485921460348192 | 1140000 | 4223195352 |
| `offline retry` | 395341259877737760 | 1140000 | 92047560 |
- 低 32 位 = **开机秒数 × 1000**(1130000 → 1204000 共 74s,与 launcher 的 `up=1129` 吻合),
**整秒粒度、毫秒位恒为 0**:`now_ms()` 里的 `tv_nsec/1e6` 项要么被读成 0 要么被读错;
- 高 32 位**每次调用都变、无任何单调性**(同一秒内 1538396632 → 2038396632 → …)。
对照:同一进程里 core 的 `ncm_now_ms()` 落盘时间戳正确(`ts=1793794687` ≈ 2026-11-05),
HTTP 超时按秒级生效(重试周期 5-7s 正是连接超时量级)——**app/core 侧(`clock_gettime` 走 app
的 TU)的时钟是好的,只有 SDK 侧 `meow_ui_time_ms()` 返回坏值**。反汇编
`patch_v2/sdk/meow_ui.a(meow_ui_gfx.o)` 的 `meow_ui_time_ms`(`meow_ui_gfx.c:175-182 now_ms`)
可见它按 **64 位 `tv_sec`** 的 `struct timespec` 布局取数(`lw 0x10/0x14`),与真机 libc 的
32 位 `time_t` 布局不一致:低字拿到正确的 `tv_sec`,高字拿到 `tv_nsec`(×1000 后表现为随机),
`tv_nsec` 项则从缓冲外读。这与观测到的「低 32 位=整秒×1000、高 32 位随机」完全吻合。
(`patch_v2/sdk/**` 不在本 mission 范围,已作为 finding 上报给塔台。)
### 42.2 根因:100ms/7s 循环与 META 洪泛是同一个根因
**① 离线重试风暴**(`app/ui/home.c` 改前 300-314 行,`app/app.c:172`):
```c
uint64_t now = meow_ui_time_ms(); // app.c:172 —— 高 32 位是垃圾
if (!g_home_thr_on && (!g_offline_retry_ms || now - g_offline_retry_ms >= 30000)) { ... }
```
`now` 与 `g_offline_retry_ms` 各自带随机高 32 位,`now - last` 的 64 位差值几乎总是巨大正数
(≈2^63),**守卫恒真** → 只要 `!g_home_thr_on`(worker 收工后的下一个 tick)就再发一次重试。
真机上「每轮间隔 5-7s」不是定时器的节奏,而是**一次失败请求本身的耗时**(连接超时)——
定时器实际上从未生效。第二条触发链是 `else { g_offline_retry_ms = 0; }`:离线→在线→离线
只要在一个 tick 内抖动,基准就被清零,立刻再试。
**② META 洪泛**(`app/play/ipc_media.c` 改前 200-204 行,`ipc_thread` 主循环):
```c
uint64_t now = meow_ui_time_ms();
if (now - last_meta >= 1000) { send_meta(fd, ...); last_meta = now; }
```
同一个坏时钟 → 条件恒真 → **每个 IPC 轮次(poll 200ms)都发一帧 META**;每次上报在 launcher 侧
要写 2 行日志(`util.c:1674` + `meow_media.c:98`)并做会话归并,SD 写把 launcher 拖到 ~100ms/帧。
而且 `state=0 pos=0/0` 说明这些帧**没有任何新信息**。
**③ 卡顿的直接来源(UI 线程)**:重试路径走的是 `home_reload()`(进页用的完整刷新):
`appstate_playlists_cache_load()` 在 **UI 线程同步读盘**(`playlists.bin`,120 条歌单),随后
`home_apply()` **把列表滚动位置复位为 0 并 `meow_ui_paint_full()` 整帧重绘**。风暴期间每次重试
都把用户正在滑的列表拽回顶部 + 全屏重绘 + UI 线程写 SD 日志 → 「歌单列表滑动特别卡」。
另外 `home_reload()` 每轮 `ncm_thread_create` 后从不 join/detach,线程栈也不回收。
结论:**一个坏时钟打穿了所有「`now - last >= N`」型守卫**(重试节奏 + META 节奏),UI 线程上的
同步读盘/滚动复位/整帧重绘又把风暴放大成整机卡死。
### 42.3 实现
| 文件 | 改动 |
|---|---|
| `app/logic/retry_ctl.h/.c`(新) | 离线重试节奏闸(纯逻辑,零依赖):①时钟由调用方给**可信单调钟**;②回退(`now < last`)不下溢成「巨大已超时」,只重定基准、不重试;③`busy` 期间单飞;④在线/离线抖动**不重置**基准(抖动不得加速)。首次观察到离线立即探测一次,之后严格 ≥ interval。 |
| `app/ui/home.c` | `home_tick`:节奏改用 `ncm_retry_ctl_poll(&g_retry, offline, g_home_thr_on, ncm_monotonic_ms(), 30000)`;入参 `now` 不再参与判定(真机坏时钟无效化)。新增 `home_reload_ex(silent)`:静默重试**不预读盘、不复位滚动、不重绘**;`home_apply_ex(keep_scroll)`:后台刷新保留滚动位置;worker 记 `g_home_changed`(离线态翻转/歌单数量变化/空态才需要重绘);worker 线程 `pthread_detach`(旧代码每轮泄漏栈)。新增 `home_retry_reset()`(页创建/登出/宿主测试)。 |
| `app/play/meta_gate.h/.c`(新) | META 上报闸(纯逻辑):状态/时长/标题/歌手/专辑/封面/歌词标志**变化立即报**;播放中位置偏移 ≥5s 重锚(launcher `meow_media_display_pos` 自己外推);心跳播放中 5s / 非播放 30s;**硬下限 250ms**(任何路径 ≤4 帧/秒);时间同样用单调钟,回退不下溢。字符串比较按发送侧截断长度进行(防超长标题恒判「变了」)。 |
| `app/play/ipc_media.c` | `send_meta()` 改为「组快照 → meta_gate 判定 → 才发」,删除 1s 定时器;主循环时间改 `ncm_monotonic_ms()`;首帧仍必发(注册媒体源);发送时打 DEBUG 日志(默认 INFO 级不落盘)。 |
| `app/ncm_app_internal.h` | 新增 `home_retry_reset()` 声明。 |
| `bench/retry_storm_sim.c`(新) | 节奏闸 + META 闸的纯逻辑回归(真实 `retry_ctl.c`/`meta_gate.c`):100ms tick 断网 120s、状态抖动、时钟回退、单飞;META 空闲/播放/抖动/元数据变化/seek/超长标题/回退。 |
| `bench/home_retry_sim.c`(新) | **真实 `app/ui/home.c`** 编进宿主(`-ffunction-sections --gc-sections` 只保留 `home_tick` 链),用桩替换网络/状态/UI,驱动 `home_tick` 断言重试次数/间隔/重绘次数;其中一个用例把**真机形态的垃圾 64 位 now** 喂进 `home_tick`。 |
| `bench/meta_ipc_sim.c`(新) | **真实 `app/play/ipc_media.c`** 起线程连到临时目录里的「假 launcher」AF_UNIX socket,统计 App 真发出的 `MSG_META` 帧数。 |
| `build_ncm.sh` / `build_ncm_host.sh` | 源列表加 `app/logic/retry_ctl.c`、`app/play/meta_gate.c`(host 构建同时补上漏掉的 `app/play/mem_watch.c`,否则 `player_manager` 链接失败)。 |
| `build_ncm_sims.sh` | 新增三个 sim 的构建/运行(产物落 `out/chk/`,不入库)。 |
### 42.4 验证(宿主,0 真机、0 真实 NCM 请求)
| 项 | 结果 |
|---|---|
| `bench/retry_storm_sim`(新) | **25 PASS / 0 FAIL**:A 断网 120s→4 次(0/30/60/90s,间隔全 ≥30000ms);B 每 tick 抖动→2 次(旧实现随抖动加速);C 时钟回退 30s→回退瞬间不重试、真实间隔仍 ≥30s、闸不失锁;D worker 跑 45s>间隔→不并发;E1 空闲 META 60s→3 帧(旧 600);E2 播放中→13 帧(≤1/5s);E3 状态每 tick 抖动→≤4 帧/秒且最小间隔 ≥250ms;E4 元数据/seek 立即报、超长标题截断后不重报;E5 时钟回退不连发。 |
| `bench/home_retry_sim`(新,真实 home.c) | **13 PASS / 0 FAIL**:断网 120s→4 次重试、每次只起 1 个 worker、**全帧重绘 0 次**(旧实现每轮 1 次);**入参 now 用真机垃圾 64 位值→仍是 4 次/≥30s**;抖动→2 次;回在线再断网→间隔仍 30s(在线不得清零基准)。 |
| `bench/meta_ipc_sim`(新,真实 ipc_media.c + 假 launcher) | **6 PASS / 0 FAIL**:空闲 2s 只收到 **1 帧** META(旧实现 ~10 帧);播放态翻转→恰好多 1 帧,停止→再多 1 帧,其余时间 0 帧;帧头 magic/version/长度合法。 |
| `bench/session_gate_sim`(M9) | **88 PASS / 0 FAIL**(`TMPDIR` 可写;http 桩 34 次) |
| `bench/http_truncate_sim`(M9) | **13 PASS / 0 FAIL** |
| `test/test_ncm_core_host`(全量核心库) | **139 PASS / 0 FAIL** |
| 宿主告警 | 改动/新增的 6 个 TU(home.c、ipc_media.c、retry_ctl.c、meta_gate.c + 2 个 sim)`-Wall -Wextra` **0 warning**;全量宿主 app 链接(`ncm_host`,`NCM_PAGE=home` 出图)通过 |
| mipsel / 真机 | **未跑**(本机无 zig 交叉链;交叉编译 + 热更由塔台做) |
### 42.5 诚实出入 / 遗留
- **根因不止在 app**:`meow_ui_time_ms()` 在真机返回坏值是 SDK/构建侧问题(`patch_v2/sdk/meow_ui_gfx.c`
的 `now_ms` 与真机 libc 的 `struct timespec` 宽度不一致)。本 mission 范围内只能**在 app 侧绕开**:
重试与 META 的节奏全部改用 core 的 `ncm_monotonic_ms()`(同进程实测正确)。**SDK 侧仍需修**,
否则所有依赖「经过时间」的 SDK/App 逻辑(动画、IME 冷却、snack 计时、其他 App 的定时器)在真机
上都会表现异常——已作为 finding 上报塔台。
- **低 32 位只有整秒精度**:即使将来某些代码继续用 `meow_ui_time_ms()`,也拿不到毫秒;本批新代码
已全部改用 `ncm_monotonic_ms()`。
- **META 硬下限 250ms**:极端情况下(播放状态每 100ms 抖动一次,例如缓冲/播放反复切换)仍可能
4 帧/秒;这是刻意的安全上限,正常状态稳定时不会触发。
- **首次进离线立即探测一次**:进离线先探一次(用户体验:恢复即回在线),之后严格 30s;若首次
进离线的同时页刚加载过,会多一次请求。**不合并**是因为「网络刚恢复」是最常见的时刻。
- **静默重试不重绘**:歌单数量/离线态都没变时界面完全不重绘(滑动不受打扰);若服务端返回了
不同的歌单数量,会在下一次 tick 保留滚动位置地重绘一次。
- **真机验证建议**(塔台做热更时):断网启动 → 看 App 日志 `home: offline retry` 应约每 30s 一条
(旧:5-7s);launcher 日志 `ipc: META app=dev.player.ncm` 空闲时应约每 30s 一条(旧:~10/s);
播放中约每 5s 一条、暂停/切歌各多一条;同时观察 ncm CPU 与首屏/列表滑动是否恢复。
- **未部署/未 push**:设备只读访问,未改设备;本节所有验证均在宿主完成。