Merge branch 'feat/ncm-offline-retry-storm'

This commit is contained in:
AskaEth committed 2026-11-05 13:16:54 +08:00
commit 8a115f6583
18 files changed
+1253 -36

No files matched your search

+135
View File
@@ -3591,3 +3591,138 @@ MP3 ≤128k→标准,>128k→极高 HQ。列表显示**实际档**(如 hires
若升级时 MAC 已变化(例如换网卡后首次启动),旧会话仍会解不开 —— 这是旧方案的固有缺陷, 若升级时 MAC 已变化(例如换网卡后首次启动),旧会话仍会解不开 —— 这是旧方案的固有缺陷,
无法事后补救;迁移后再不会因网络状态变化丢会话。 无法事后补救;迁移后再不会因网络状态变化丢会话。
- **未部署/未 push**:设备离线,真机联调后置;本节所有验证均在宿主完成。 - **未部署/未 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**:设备只读访问,未改设备;本节所有验证均在宿主完成。
+30
View File
@@ -0,0 +1,30 @@
/* logic/retry_ctl.c — 见 retry_ctl.h 头注释(M10 离线重试节奏闸)。 */
#include "retry_ctl.h"
void ncm_retry_ctl_init(ncm_retry_ctl *c)
{
if (!c) return;
c->last_ms = 0;
c->have_last = 0;
}
int ncm_retry_ctl_poll(ncm_retry_ctl *c, int offline, int busy, int64_t now_ms,
int64_t interval_ms)
{
if (!c || interval_ms <= 0) return 0;
if (!offline) return 0; /* 在线:不动基准 —— 抖动不得加速重试 */
if (busy) return 0; /* 单飞:上一个 worker 还在跑 */
if (!c->have_last) { /* 首次进离线:立即探测一次 */
c->have_last = 1;
c->last_ms = now_ms;
return 1;
}
if (now_ms < c->last_ms) { /* 时钟回退/异常:绝不让差值下溢成「巨大已超时」 */
c->last_ms = now_ms; /* 重定基准 → 仍需重新等满 interval */
return 0;
}
if (now_ms - c->last_ms < interval_ms) return 0;
c->last_ms = now_ms;
return 1;
}
+42
View File
@@ -0,0 +1,42 @@
/* logic/retry_ctl.h — 离线自动重试节奏闸(M10:断网重试风暴的修复点)。
*
* 背景(真机 19000100_000000_7534.log + launcher meow_20261105_122541.log):
* M9 的 30s 守卫写成 `if (!thr_on && (!last || now - last >= 30000))`,节奏完全依赖
* meow_ui_time_ms() 的 64 位差值。真机实测该值「低 32 位 = 整秒×1000(无毫秒)、高 32 位是垃圾」
* (见 REPORT §42),于是 now-last 几乎恒为巨大正数 → 每个 UI tick 都判「该重试」,
* 断网时把「30s 一次」退化成「每个 tick 一次」的重试风暴。
*
* 本模块把节奏判定收敛成三条硬约束(纯逻辑、零依赖,宿主编译可直接驱动):
* 1) 时钟由调用方供给**可信单调钟**(home.c 传 core 的 ncm_monotonic_ms(),不再用 SDK 的
* meow_ui_time_ms());本模块只做差值,不取时间。
* 2) 时钟回退(now < last)绝不下溢成「巨大已超时」:按「刚试过」处理并重定基准,
* 下一次尝试仍需重新等满 interval。
* 3) 单飞:busy(上一个 worker 未收工)期间任何时钟跳变都不发新尝试。
* 在线/离线抖动不重置基准 —— 抖动无法加速重试(旧实现 else 分支清零 g_offline_retry_ms,
* 状态一抖就立刻再试,是风暴的第二条触发链)。
*/
#ifndef NCM_RETRY_CTL_H
#define NCM_RETRY_CTL_H
#include <stdint.h>
typedef struct {
int64_t last_ms; /* 上次发起尝试的单调毫秒时刻(have_last=0 时无意义) */
int have_last; /* 1=至少尝试过一次 */
} ncm_retry_ctl;
void ncm_retry_ctl_init(ncm_retry_ctl *c);
/* 每个 UI tick 调一次。返回 1=现在应当发起一次重试(内部已记账:调用方随即起 worker)。
* offline=当前是否离线;busy=是否已有 worker 在跑(单飞);now_ms=单调毫秒;
* interval_ms=两次尝试的最小间隔(M10 定 30000)。
*
* 语义:
* - offline=0:返回 0,且**不动**基准(在线期间与状态抖动都不重置节奏)。
* - busy=1:返回 0。
* - 首次观察到离线:返回 1(进离线先立即探测一次,恢复后由会话门切回在线)。
* - 之后:仅当 now-last >= interval 才返回 1;now<last(时钟回退)→ 重置基准、返回 0。 */
int ncm_retry_ctl_poll(ncm_retry_ctl *c, int offline, int busy, int64_t now_ms,
int64_t interval_ms);
#endif /* NCM_RETRY_CTL_H */
+1
View File
@@ -345,6 +345,7 @@ void home_touch_down(int x, int y);
void home_touch_move(int x, int y); void home_touch_move(int x, int y);
void home_reload(void); /* 刷新我的歌单(后台 worker,非阻塞) */ void home_reload(void); /* 刷新我的歌单(后台 worker,非阻塞) */
void home_tick(uint64_t now); /* UI tick:检查歌单 worker 是否完成并刷新 */ void home_tick(uint64_t now); /* UI tick:检查歌单 worker 是否完成并刷新 */
void home_retry_reset(void); /* M10:清离线自动重试节奏(页创建/登出/宿主测试) */
int home_go_up(void); /* 左缘返回:到底则退出 */ int home_go_up(void); /* 左缘返回:到底则退出 */
/* playlist */ /* playlist */
+35 -13
View File
@@ -1,12 +1,14 @@
/* play/ipc_media.c — 媒体会话 IPC 最小客户端(SDK 尚无媒体封装,App 内用 meow_ipc.h 帧格式直连)。 /* play/ipc_media.c — 媒体会话 IPC 最小客户端(SDK 尚无媒体封装,App 内用 meow_ipc.h 帧格式直连)。
* *
* 连接:AF_UNIX SOCK_STREAM 到 MEOW_IPC_SOCK(缺省 /usr/data/meow_ipc.sock)。HELLO(app_id, * 连接:AF_UNIX SOCK_STREAM 到 MEOW_IPC_SOCK(缺省 /usr/data/meow_ipc.sock)。HELLO(app_id,
* MF_PLAYBACK) 注册为媒体源;之后**同一连接**周期发 MSG_META(~1s,进度随播放推进)并阻塞等 * MF_PLAYBACK) 注册为媒体源;之后**同一连接**上报 MSG_META 并阻塞等 launcher 下发 MSG_PLAYCTL
* launcher 下发 MSG_PLAYCTL(播放/暂停/上下曲/seek/toggle)。launcher 经该连接 fd 回发(meow_media.c * (播放/暂停/上下曲/seek/toggle)。launcher 经该连接 fd 回发(meow_media.c send_playctl 用最近
* send_playctl 用最近活跃会话的 fd)。无 socket/launcher 不跑 → 优雅跳过(打日志不崩)。 * 活跃会话的 fd)。META 节奏见 meta_gate.h(变化 + 低频心跳,不再是 1s 定时;M10 修洪泛)。
* 无 socket/launcher 不跑 → 优雅跳过(打日志不崩)。
* 线程模型:独立线程(main 起了 meow_ui_run 阻塞;播放线程归 player_manager,本线程只做 IPC 收发)。 * 线程模型:独立线程(main 起了 meow_ui_run 阻塞;播放线程归 player_manager,本线程只做 IPC 收发)。
*/ */
#include "../ncm_app_internal.h" #include "../ncm_app_internal.h"
#include "meta_gate.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -23,6 +25,12 @@ static int g_thr_on;
static int g_quit; static int g_quit;
static volatile int g_req_focus_player = 0; /* 任务3:launcher 请求「回到播放页」(UI 线程消费) */ static volatile int g_req_focus_player = 0; /* 任务3:launcher 请求「回到播放页」(UI 线程消费) */
/* M10:META 上报闸(变化 + 低频心跳;时间用 core 单调钟)。
* 旧实现 `if (now - last_meta >= 1000)` 依赖 meow_ui_time_ms(),真机该值高 32 位是垃圾
* (REPORT §42)→ 条件恒真 → 每个 IPC 轮次都发 META(真机 launcher 日志 ~100ms 一帧、
* 单次启动 21016 帧),launcher 每帧写日志+归并会话,SD 写放大到整机卡死。 */
static ncm_meta_gate g_meta_gate;
static const char *sock_path(void) static const char *sock_path(void)
{ {
const char *e = getenv("MEOW_IPC_SOCK"); const char *e = getenv("MEOW_IPC_SOCK");
@@ -92,8 +100,10 @@ static int read_frame(int fd, uint16_t *type, uint8_t *rbuf, uint32_t rcap)
return (int)len; return (int)len;
} }
/* 组并发送一条 META。 */ /* 组并发送一条 META。返回 1=已发 / 0=闸判定不必发 / -1=写失败。
static void send_meta(int fd, uint32_t req_id) * 是否发由 meta_gate 决定:状态/元数据变化立即发;播放中位置偏移 ≥5s 重锚;心跳 5s/30s;
* 硬下限 250ms(任何路径都不快于 4 帧/秒)。now_ms 必须是单调毫秒(ncm_monotonic_ms)。 */
static int send_meta(int fd, uint32_t req_id, int64_t now_ms)
{ {
m_meta_t mm; m_meta_t mm;
memset(&mm, 0, sizeof mm); memset(&mm, 0, sizeof mm);
@@ -135,7 +145,21 @@ static void send_meta(int fd, uint32_t req_id)
mm.flags |= MF_ART | MF_COVER; mm.flags |= MF_ART | MF_COVER;
} }
if (lyric_count() > 0) mm.flags |= MF_LYRIC; if (lyric_count() > 0) mm.flags |= MF_LYRIC;
send_frame(fd, MSG_META, req_id, &mm, sizeof mm);
ncm_meta_snap s;
s.state = (int)mm.state;
s.pos_ms = (int)mm.pos_ms;
s.dur_ms = (int)mm.dur_ms;
s.flags = (int)mm.flags;
s.title = mm.title;
s.artist = mm.artist;
s.album = mm.album;
s.artwork = mm.artwork;
if (!ncm_meta_gate_should_send(&g_meta_gate, &s, now_ms)) return 0;
if (send_frame(fd, MSG_META, req_id, &mm, sizeof mm) != 0) return -1;
meow_log(MEOW_LOG_DEBUG, "ipc: meta sent state=%u pos=%u/%u", mm.state, mm.pos_ms, mm.dur_ms);
return 1;
} }
static void apply_playctl(const m_playctl_t *c) static void apply_playctl(const m_playctl_t *c)
@@ -177,9 +201,8 @@ static void *ipc_thread(void *arg)
uint16_t htype; uint16_t htype;
if (read_frame(fd, &htype, hb, sizeof hb) < 0) { close(fd); return NULL; } if (read_frame(fd, &htype, hb, sizeof hb) < 0) { close(fd); return NULL; }
send_meta(fd, 2); /* 首条注册媒体源(state=0 未播也登记,start 播后自动成源) */ send_meta(fd, 2, ncm_monotonic_ms()); /* 首条注册媒体源(state=0 未播也登记,start 播后自动成源) */
uint64_t last_meta = 0;
while (!g_quit && !g_app_quit) { while (!g_quit && !g_app_quit) {
struct pollfd p; struct pollfd p;
p.fd = fd; p.events = POLLIN; p.fd = fd; p.events = POLLIN;
@@ -197,11 +220,9 @@ static void *ipc_thread(void *arg)
} else if (pr < 0 && errno != EINTR) { } else if (pr < 0 && errno != EINTR) {
break; break;
} }
uint64_t now = meow_ui_time_ms(); /* M10:不再定时无脑发;send_meta 内部按 meta_gate 判「变化 / 位置偏移 / 低频心跳」。 */
if (now - last_meta >= 1000) { int64_t now_ms = ncm_monotonic_ms();
send_meta(fd, (uint32_t)(now & 0x7FFFFFFF)); send_meta(fd, (uint32_t)(now_ms & 0x7FFFFFFF), now_ms);
last_meta = now;
}
} }
close(fd); close(fd);
return NULL; return NULL;
@@ -284,6 +305,7 @@ int ipc_media_start(void)
{ {
if (g_thr_on) return 0; if (g_thr_on) return 0;
g_quit = 0; g_quit = 0;
ncm_meta_gate_init(&g_meta_gate); /* M10:首帧必发(注册媒体源),之后按变化/心跳 */
if (ncm_thread_create(&g_thr, ipc_thread, NULL) != 0) return -1; if (ncm_thread_create(&g_thr, ipc_thread, NULL) != 0) return -1;
pthread_detach(g_thr); pthread_detach(g_thr);
g_thr_on = 1; g_thr_on = 1;
+74
View File
@@ -0,0 +1,74 @@
/* play/meta_gate.c — 见 meta_gate.h 头注释(M10 媒体会话 META 上报闸)。 */
#include "meta_gate.h"
#include <string.h>
static void gate_cp(char *dst, size_t cap, const char *src)
{
size_t i = 0;
if (!dst || cap == 0) return;
if (src) {
for (; src[i] && i + 1 < cap; i++) dst[i] = src[i];
}
dst[i] = 0;
}
void ncm_meta_gate_init(ncm_meta_gate *g)
{
if (g) memset(g, 0, sizeof *g);
}
static void gate_record(ncm_meta_gate *g, const ncm_meta_snap *s, int64_t now_ms)
{
g->inited = 1;
g->state = s->state;
g->dur_ms = s->dur_ms;
g->flags = s->flags;
g->last_pos_ms = s->pos_ms;
g->last_send_ms = now_ms;
gate_cp(g->title, sizeof g->title, s->title);
gate_cp(g->artist, sizeof g->artist, s->artist);
gate_cp(g->album, sizeof g->album, s->album);
gate_cp(g->artwork, sizeof g->artwork, s->artwork);
}
/* 只比较「会被发送的那部分」:发送侧同样按 cap 截断,超长串不得因截断而恒判「变了」。 */
static int str_diff(const char *stored, size_t cap, const char *now)
{
size_t i = 0;
if (!now) now = "";
for (; i + 1 < cap && stored[i] && stored[i] == now[i]; i++) { }
if (i + 1 >= cap) return 0; /* 前 cap-1 字符一致且已到截断长度 → 相同 */
return stored[i] != now[i];
}
int ncm_meta_gate_should_send(ncm_meta_gate *g, const ncm_meta_snap *s, int64_t now_ms)
{
if (!g || !s) return 0;
if (!g->inited) { gate_record(g, s, now_ms); return 1; } /* 首帧:注册媒体源 */
int64_t gap = now_ms - g->last_send_ms;
if (gap < 0) { /* 时钟回退/异常:重定基准,差值按 0 处理(防下溢成「恒该发」) */
g->last_send_ms = now_ms;
gap = 0;
}
int changed = (s->state != g->state) ||
(s->dur_ms != g->dur_ms) ||
(s->flags != g->flags) ||
str_diff(g->title, sizeof g->title, s->title) ||
str_diff(g->artist, sizeof g->artist, s->artist) ||
str_diff(g->album, sizeof g->album, s->album) ||
str_diff(g->artwork, sizeof g->artwork, s->artwork);
int64_t dpos = (int64_t)s->pos_ms - (int64_t)g->last_pos_ms;
if (dpos < 0) dpos = -dpos; /* seek 前后跳都算偏移 */
int playing = (s->state == NCM_META_ST_PLAYING || s->state == NCM_META_ST_BUFFER);
int64_t hb = playing ? NCM_META_HB_PLAY_MS : NCM_META_HB_IDLE_MS;
if (!changed && dpos < NCM_META_POS_RESEND_MS && gap < hb) return 0;
if (gap < NCM_META_MIN_GAP_MS) return 0; /* 硬下限:变化挂起,下一轮再发(不快于 4 帧/秒) */
gate_record(g, s, now_ms);
return 1;
}
+66
View File
@@ -0,0 +1,66 @@
/* play/meta_gate.h — 媒体会话 META 上报闸(M10:META 洪泛修复点;纯逻辑,宿主可驱动)。
*
* 背景(真机证据):launcher meow_20261105_122541.log 里 21016 条 `ipc: META app=dev.player.ncm
* state=0 pos=0/0 rc=0 -> ACK`,间隔 ~100ms(每次 launcher 收帧都要写日志+归并会话)。
* 旧 ipc_media 的节奏是 `if (now - last_meta >= 1000)`,同样依赖 meow_ui_time_ms() 的 64 位差值;
* 真机该值高 32 位是垃圾(REPORT §42)→ 条件几乎恒真 → 每个 IPC 轮次都发 META,
* 1s 心跳退化成 10Hz 洪泛,把 launcher 的 SD 写/会话归并拖满,整机卡死。
*
* 修复后的上报策略(不再是定时器,而是「变化 + 低频心跳」):
* - 状态/时长/标题/歌手/专辑/封面/歌词标志变化 → 立即上报(用户操作要即时可见);
* - 播放中位置相对上次上报偏移 ≥ 5s(含 seek 前后跳)→ 上报,给 launcher 重锚进度
* (launcher meow_media_display_pos 会按 last_active 自行外推,5s 足够);
* - 心跳:播放中 5s、非播放 30s(保活会话/防 launcher 会话超时清理);
* - 硬下限 250ms:任何路径(含状态抖动)都不快于 4 帧/秒。
* 时间基准由调用方给单调钟(ipc_media 传 core 的 ncm_monotonic_ms())。
*/
#ifndef NCM_META_GATE_H
#define NCM_META_GATE_H
#include <stdint.h>
/* 与 meow_ipc.h 的 m_meta_t.state 同口径(MF_PLAYBACK 会话状态)。 */
enum {
NCM_META_ST_STOPPED = 0,
NCM_META_ST_PLAYING = 1,
NCM_META_ST_PAUSED = 2,
NCM_META_ST_BUFFER = 3,
};
/* 一次上报快照(字符串由调用方持有,本模块只拷贝比较,不接管所有权)。 */
typedef struct {
int state; /* NCM_META_ST_* */
int pos_ms;
int dur_ms;
int flags; /* MF_ART/MF_COVER/MF_LYRIC 位 */
const char *title;
const char *artist;
const char *album;
const char *artwork;
} ncm_meta_snap;
typedef struct {
int inited;
int state;
int dur_ms;
int flags;
int last_pos_ms; /* 上次上报时的播放位置(判进度偏移/seek) */
int64_t last_send_ms; /* 上次上报的单调毫秒 */
char title[128];
char artist[128];
char album[128];
char artwork[192];
} ncm_meta_gate;
#define NCM_META_POS_RESEND_MS 5000 /* 播放中位置偏移(ms)触发重报 */
#define NCM_META_HB_PLAY_MS 5000 /* 播放中心跳 */
#define NCM_META_HB_IDLE_MS 30000 /* 非播放心跳 */
#define NCM_META_MIN_GAP_MS 250 /* 硬下限:任何情况下 ≤ 4 帧/秒 */
void ncm_meta_gate_init(ncm_meta_gate *g);
/* 返回 1=现在应当发送(内部已把本次快照记为「已上报」);0=不必发。
* now_ms=单调毫秒(建议 ncm_monotonic_ms())。未初始化时首调必发(注册媒体源)。 */
int ncm_meta_gate_should_send(ncm_meta_gate *g, const ncm_meta_snap *s, int64_t now_ms);
#endif /* NCM_META_GATE_H */
+67 -18
View File
@@ -12,8 +12,14 @@
* *
* 左缘返回 = 退出 App(app.c)。歌单加载后台 worker(home_worker,保留 REPORT §10.11 * 左缘返回 = 退出 App(app.c)。歌单加载后台 worker(home_worker,保留 REPORT §10.11
* 的异步结构,不回退成同步——P1 启动卡顿已修,勿回退)。 * 的异步结构,不回退成同步——P1 启动卡顿已修,勿回退)。
*
* M10:离线自动重试不再用「裸 u64 差值 + 状态抖动清零」的 30s 守卫(真机 meow_ui_time_ms()
* 高 32 位是垃圾 → 守卫恒真 → 断网重试风暴 / META 洪泛 / 列表被反复拽回顶部)。改由
* logic/retry_ctl.c 统一管节奏(core 单调钟、单飞、时钟回退安全、抖动不重置),
* 静默重试不预读盘/不复位滚动/无可见变化不重绘。详见 REPORT §42。
*/ */
#include "../ncm_app_internal.h" #include "../ncm_app_internal.h"
#include "../logic/retry_ctl.h"
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
@@ -36,6 +42,13 @@ static int g_home_thr_on;
static volatile int g_home_gen; static volatile int g_home_gen;
static int g_home_acked; static int g_home_acked;
static volatile int g_home_loading; static volatile int g_home_loading;
static int g_home_silent; /* 1=本次加载是后台静默刷新(M10 离线重试) */
static int g_home_changed; /* 1=本次加载改了可见数据/离线态(决定是否重绘) */
/* M10:离线自动重试节奏闸。时钟用 core 单调钟(ncm_monotonic_ms),不用 SDK meow_ui_time_ms
* ——真机后者高 32 位是垃圾,任何 `now - last >= N` 都会变成「恒真」(REPORT §42)。 */
#define HOME_OFFLINE_RETRY_MS 30000
static ncm_retry_ctl g_retry;
static const char *tab_label(int tab) { return tab ? "我收藏的" : "我创建的"; } static const char *tab_label(int tab) { return tab ? "我收藏的" : "我创建的"; }
@@ -234,12 +247,17 @@ static void home_draw_footer(void *ctx)
ncm_draw_footer(r_cont, foot); ncm_draw_footer(r_cont, foot);
} }
/* 把当前 tab 子集刷进列表 + 重绘(UI 线程)。 */ /* 把当前 tab 子集刷进列表 + 重绘(UI 线程)。
static void home_apply(void) * keep_scroll=1:后台静默刷新用——保留用户当前滚动位置(数量变化由 SDK 钳位);
* 0=复位到顶部(进页/切 tab/手动刷新,与原行为一致)。
* M10:离线重试不再每轮把列表拽回顶部(旧代码每轮调本函数复位滚动 + 整帧重绘,
* 重试风暴时表现为「歌单列表滑动特别卡」)。 */
static void home_apply_ex(int keep_scroll)
{ {
if (g_list) { if (g_list) {
int keep = keep_scroll ? meow_listview_scroll(g_list) : 0;
meow_listview_set_count(g_list, tab_count(g_tab)); meow_listview_set_count(g_list, tab_count(g_tab));
meow_listview_set_scroll(g_list, 0); meow_listview_set_scroll(g_list, keep);
meow_listview_set_sel(g_list, -1); meow_listview_set_sel(g_list, -1);
} }
if (g_tabs) meow_tabbar_set_selected(g_tabs, g_tab); if (g_tabs) meow_tabbar_set_selected(g_tabs, g_tab);
@@ -248,6 +266,8 @@ static void home_apply(void)
meow_ui_paint_full(); meow_ui_paint_full();
} }
static void home_apply(void) { home_apply_ex(0); }
/* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist)。 /* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist)。
* §26 stale-while-revalidate:网络成功 → appstate_load_playlists 覆写盘缓存 + 更新内存; * §26 stale-while-revalidate:网络成功 → appstate_load_playlists 覆写盘缓存 + 更新内存;
* 失败但有缓存在展示 → 保持缓存列表(g_load_err=0),后台静默不打断(断网可离线浏览)。 */ * 失败但有缓存在展示 → 保持缓存列表(g_load_err=0),后台静默不打断(断网可离线浏览)。 */
@@ -256,6 +276,8 @@ static void *home_worker(void *arg)
(void)arg; (void)arg;
int rc; int rc;
int from_cache = 0; int from_cache = 0;
int off_before = appstate_is_offline();
int cnt_before = g_st.playlist_count;
if (!g_st.logged_in) rc = NCM_ERR_AUTH; if (!g_st.logged_in) rc = NCM_ERR_AUTH;
else { else {
/* §NEW MUSIC_U 临期续期:先于歌单网络请求(后续 eapi/weapi 用新 cookie)。 /* §NEW MUSIC_U 临期续期:先于歌单网络请求(后续 eapi/weapi 用新 cookie)。
@@ -273,15 +295,22 @@ static void *home_worker(void *arg)
pthread_mutex_lock(&g_home_mu); pthread_mutex_lock(&g_home_mu);
g_load_err = (g_st.playlist_count > 0) ? 0 : rc; /* 有数据(缓存/已载)即不置错误态 */ g_load_err = (g_st.playlist_count > 0) ? 0 : rc; /* 有数据(缓存/已载)即不置错误态 */
g_home_loading = 0; g_home_loading = 0;
/* M10:本次加载是否产生「看得见的变化」——离线态翻转 / 歌单数量变化 / 仍处空态
* (空态文案随 load_err、离线提示变化)。无变化则静默重试不重绘,不打断滑动。 */
g_home_changed = (appstate_is_offline() != off_before) ||
(g_st.playlist_count != cnt_before) ||
(g_st.playlist_count == 0);
g_home_gen++; g_home_gen++;
pthread_mutex_unlock(&g_home_mu); pthread_mutex_unlock(&g_home_mu);
return NULL; return NULL;
} }
void home_reload(void) /* silent=0:进页/手动刷新(读盘缓存秒开 + 复位滚动);1:后台静默刷新(不动界面)。 */
static void home_reload_ex(int silent)
{ {
if (g_host_mock) { g_load_err = 0; home_apply(); return; } if (g_host_mock) { if (!silent) { g_load_err = 0; home_apply(); } return; }
if (g_home_thr_on) return; if (g_home_thr_on) return; /* 单飞:同一时刻只一个 worker */
if (!silent) {
/* §26:进首页先用磁盘缓存秒渲染(若有),后台 worker 静默刷新;无缓存才显示 LOADING。 */ /* §26:进首页先用磁盘缓存秒渲染(若有),后台 worker 静默刷新;无缓存才显示 LOADING。 */
int64_t ts = 0; int64_t ts = 0;
int had_cache = (appstate_playlists_cache_load(&ts) == NCM_OK && g_st.playlist_count > 0); int had_cache = (appstate_playlists_cache_load(&ts) == NCM_OK && g_st.playlist_count > 0);
@@ -290,27 +319,41 @@ void home_reload(void)
g_st.playlist_count = 0; /* 无缓存 → LOADING 空态 */ g_st.playlist_count = 0; /* 无缓存 → LOADING 空态 */
g_home_loading = 1; g_home_loading = 1;
} }
}
/* M10 静默刷新:界面已有数据 → 不预读盘(旧实现每轮都在 UI 线程同步读 playlists.bin)、
* 不复位滚动、不重绘;等 worker 完成后由 home_tick 按 g_home_changed 决定是否重绘。 */
g_home_silent = silent;
g_home_thr_on = 1; g_home_thr_on = 1;
if (ncm_thread_create(&g_home_thr, home_worker, NULL) != 0) { if (ncm_thread_create(&g_home_thr, home_worker, NULL) != 0) {
g_home_thr_on = 0; g_home_loading = 0; g_load_err = NCM_ERR_IO; g_home_thr_on = 0; g_home_loading = 0; g_load_err = NCM_ERR_IO;
return;
} }
home_apply(); pthread_detach(g_home_thr); /* 只靠 gen 代次收工、不 join:线程栈即刻回收(旧代码每轮泄漏) */
if (!silent) home_apply();
} }
void home_reload(void) { home_reload_ex(0); }
/* M10:清离线重试节奏(页创建/登出/宿主测试用)。 */
void home_retry_reset(void) { ncm_retry_ctl_init(&g_retry); }
void home_tick(uint64_t now) void home_tick(uint64_t now)
{ {
/* M9 离线恢复:离线时每 30s 静默重试一次(home worker 单飞;成功后 appstate_session_gate /* M10 离线恢复:断网下严格 ≥30s 静默重试一次(home worker 单飞;成功后 appstate_session_gate
* 自动回在线并重绘)。仅在 home 可见时轮询,不轰炸服务端(风控纪律)。 */ * 自动回在线并重绘)。仅在 home 可见时轮询,不轰炸服务端(风控纪律)。
static uint64_t g_offline_retry_ms; *
if (appstate_is_offline() && !g_host_mock) { * 节奏判定不使用入参 now:真机 meow_ui_time_ms() 的高 32 位是垃圾(REPORT §42),
if (!g_home_thr_on && (!g_offline_retry_ms || now - g_offline_retry_ms >= 30000)) { * `now - last >= 30000` 会恒真 → 旧实现每个 tick 都重试(真机 74s 内 10 次,CPU 48%、
g_offline_retry_ms = now; * load 3.65、整机卡死)。这里统一用 core 单调钟 + retry_ctl 闸:
* - 单飞(worker 未收工不重试)、
* - 时钟回退不下溢(回退时重定基准,不立即重试)、
* - 在线/离线抖动不重置基准(旧 else 分支清零 g_offline_retry_ms,一抖就重试)。 */
(void)now;
if (appstate_is_offline() && !g_host_mock &&
ncm_retry_ctl_poll(&g_retry, 1, g_home_thr_on, ncm_monotonic_ms(), HOME_OFFLINE_RETRY_MS)) {
meow_log(MEOW_LOG_INFO, "home: offline retry"); meow_log(MEOW_LOG_INFO, "home: offline retry");
ncm_api_cache_refresh_path("/api/user/playlist"); /* 离线恢复探测必须真打网络 */ ncm_api_cache_refresh_path("/api/user/playlist"); /* 离线恢复探测必须真打网络 */
home_reload(); home_reload_ex(1);
}
} else {
g_offline_retry_ms = 0;
} }
if (!g_home_thr_on) return; if (!g_home_thr_on) return;
@@ -318,7 +361,12 @@ void home_tick(uint64_t now)
g_home_acked = g_home_gen; g_home_acked = g_home_gen;
g_home_thr_on = 0; g_home_thr_on = 0;
g_home_loading = 0; g_home_loading = 0;
home_apply(); int changed, silent;
pthread_mutex_lock(&g_home_mu);
changed = g_home_changed;
silent = g_home_silent;
pthread_mutex_unlock(&g_home_mu);
if (!silent || changed) home_apply_ex(silent ? 1 : 0); /* 静默刷新保留滚动位置 */
} }
int home_is_active(void) { return g_hc.active; } int home_is_active(void) { return g_hc.active; }
@@ -403,6 +451,7 @@ static void home_page_create(meow_view *v, void *ctx)
{ {
(void)v; (void)v;
((home_ctrl *)ctx)->view = v; ((home_ctrl *)ctx)->view = v;
home_retry_reset(); /* M10:离线重试节奏闸(首次进离线立即探测一次) */
r_cont = ncm_content_rect(); r_cont = ncm_content_rect();
ncm_rect_t pc = NR(16, r_cont.y + 8, r_cont.w - 32, 72); ncm_rect_t pc = NR(16, r_cont.y + 8, r_cont.w - 32, 72);
r_tabs = NR(16, pc.y + pc.h + 8, r_cont.w - 32, 48); r_tabs = NR(16, pc.y + pc.h + 8, r_cont.w - 32, 48);
+230
View File
@@ -0,0 +1,230 @@
/* bench/home_retry_sim.c — M10:驱动**真实 app/ui/home.c 的 home_tick** 的离线重试回归。
*
* 与 bench/retry_storm_sim.c 的分工:那边直接驱动 logic/retry_ctl.c(纯节奏闸);
* 这边把真实 home.c 编进宿主(-ffunction-sections + --gc-sections 只保留 home_tick 调用链),
* 用桩替换网络/状态/UI,验证三件事在**页面代码**层面成立:
* 1) 断网 120s(100ms tick):home_tick 只发起 4 次重试(0/30/60/90s),间隔 ≥30000ms;
* 2) 传给 home_tick 的 now 是**真机形态的垃圾 64 位值**(低 32 位=整秒×1000、高 32 位随机,
* 见 REPORT §42)时,节奏完全不受影响(旧实现此时会每 tick 重试);
* 3) 静默重试若无可见变化(歌单数量/离线态都不变)→ 一次全帧重绘都不发(旧实现每轮
* home_apply() 复位滚动 + 整帧重绘,这是「歌单列表滑动特别卡」的直接来源)。
*
* 编译(ncm_app 目录下;桩全部在本文件内,不链 SDK/核心库):
* gcc -O2 -Wall -Wextra -ffunction-sections -fdata-sections -DMEOW_UI_HOST_TEST -DJSMN_HEADER \
* -I. -I../sdk -I../sdk/include -I../meow_launcherd -Icore -Ivendor/jsmn \
* -c app/ui/home.c -o out/chk/home_m10.o
* gcc -O2 -Wall -Wextra -ffunction-sections -fdata-sections -DMEOW_UI_HOST_TEST -DJSMN_HEADER \
* -I. -I../sdk -I../sdk/include -I../meow_launcherd -Icore -Ivendor/jsmn \
* -c bench/home_retry_sim.c -o out/chk/home_retry_sim.o
* gcc -Wl,--gc-sections -o home_retry_sim out/chk/home_m10.o out/chk/home_retry_sim.o -lpthread
* ./home_retry_sim
*/
#include <stdio.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "../app/ncm_app_internal.h"
/* ---- app.c 的全局(本测试自持) ---- */
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER,
.state = NCM_PC_STOPPED, .cur_song = -1 };
volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 0; /* 关键:走真实离线重试路径(非 host mock 短路) */
/* ---- 可控世界状态 ---- */
static volatile int g_offline;
static volatile int g_load_calls;
static volatile int g_retry_probes; /* ncm_api_cache_refresh_path 调用 = 重试次数 */
static int64_t g_probe_at[64];
static volatile int g_paint_full; /* 全帧重绘次数 */
static int64_t g_mono; /* 假单调钟(UI 线程推进) */
static int64_t g_vt; /* 虚拟时间轴(跨 run_ticks 连续) */
#define TICK_MS 100
static int g_pass, g_fail;
#define CHECK(cond, ...) do { \
int _ok = (cond) ? 1 : 0; \
printf(" %s ", _ok ? "PASS" : "FAIL"); printf(__VA_ARGS__); printf("\n"); \
if (_ok) g_pass++; else g_fail++; \
} while (0)
/* ============================================================
* 桩:核心/逻辑层
* ============================================================ */
int64_t ncm_monotonic_ms(void) { return g_mono; }
int appstate_is_offline(void) { return g_offline; }
int appstate_session_gate(int rc)
{
/* 与真实实现同语义:网络类错误 → 离线;成功 → 在线 */
if (rc == NCM_OK) g_offline = 0;
else if (rc == -4 || rc == -6 || rc == -17) g_offline = 1;
return 0;
}
int appstate_load_playlists_ex(int *from_cache)
{
g_load_calls++;
if (from_cache) *from_cache = 0;
return -4; /* 断网:真机 rc=-4/-6 */
}
int appstate_playlists_cache_load(int64_t *ts_out)
{
if (ts_out) *ts_out = 0;
return NCM_ERR_NOENT; /* 本场景已有内存数据,不需要盘缓存 */
}
int account_cookie_maybe_refresh(void) { return NCM_OK; }
void account_vip_maybe_refresh(int force) { (void)force; }
void ncm_api_cache_refresh_path(const char *path)
{
(void)path;
int i = g_retry_probes++;
if (i < 64) g_probe_at[i] = g_mono;
}
/* ============================================================
* 桩:SDK(home_tick 可达的绘制/列表调用)
* ============================================================ */
static meow_theme s_theme;
const meow_theme *meow_theme_get(void) { return &s_theme; }
void meow_ui_set_page_bg(uint32_t c) { (void)c; }
void meow_ui_paint_full(void) { g_paint_full++; }
void meow_listview_set_count(meow_widget *w, int n) { (void)w; (void)n; }
void meow_listview_set_scroll(meow_widget *w, int o) { (void)w; (void)o; }
void meow_listview_set_sel(meow_widget *w, int i) { (void)w; (void)i; }
int meow_listview_scroll(meow_widget *w) { (void)w; return 0; }
void meow_tabbar_set_selected(meow_widget *w, int i) { (void)w; (void)i; }
void meow_log(meow_log_level lvl, const char *fmt, ...)
{
(void)lvl;
char body[256];
va_list ap;
va_start(ap, fmt);
vsnprintf(body, sizeof body, fmt, ap);
va_end(ap);
printf(" [log] %s\n", body);
}
/* ============================================================
* 场景
* ============================================================ */
static void reset_world(void)
{
home_retry_reset(); /* 清真实 home.c 的节奏闸(等价于重新进页) */
g_offline = 1;
g_load_calls = 0;
g_retry_probes = 0;
g_paint_full = 0;
g_mono = 0;
g_vt = 0;
g_st.logged_in = 1;
g_st.playlist_count = 120; /* 已有缓存列表(真机 120 个歌单) */
g_st.nickname[0] = 0;
g_st.uid[0] = 0;
}
/* 真机 meow_ui_time_ms() 形态:低 32 位=整秒×1000,高 32 位随机(REPORT §42 实测)。 */
static uint64_t device_like_now(int tick, uint64_t *seed)
{
*seed = *seed * 6364136223846793005ULL + 1442695040888963407ULL;
uint32_t hi = (uint32_t)(*seed >> 33);
uint64_t lo = ((uint64_t)(tick / 10)) * 1000u; /* 整秒粒度 */
return ((uint64_t)hi << 32) | lo;
}
/* 跑 seconds 秒的 100ms tick(虚拟时间连续推进,跨调用不重置);garbage_clock=1 时传给
* home_tick 的 now 用真机垃圾形态。 */
static int64_t g_vt;
static void run_ticks(int seconds, int flap, int garbage_clock)
{
uint64_t seed = 88172645463325252ULL;
int64_t end = g_vt + (int64_t)seconds * 1000;
for (; g_vt < end; g_vt += TICK_MS) {
g_mono = g_vt; /* 可信单调钟(core clock_gettime) */
if (flap) g_offline = ((g_vt / TICK_MS) % 2 == 0);
uint64_t now = garbage_clock ? device_like_now((int)g_vt, &seed) : (uint64_t)g_vt;
home_tick(now);
usleep(1000); /* 让 worker 跑完(真机是一次网络失败 5-7s) */
}
}
static void case_steady(void)
{
printf("A. 真实 home_tick:断网 120s(100ms tick)\n");
reset_world();
run_ticks(120, 0, 0);
CHECK(g_retry_probes == 4, "重试次数 = %d(期望 4:0/30/60/90s;旧实现每 tick 一次)",
g_retry_probes);
int gaps_ok = 1;
for (int i = 1; i < g_retry_probes && i < 64; i++)
if (g_probe_at[i] - g_probe_at[i - 1] < 30000) gaps_ok = 0;
CHECK(gaps_ok, "相邻重试间隔全部 ≥30000ms");
CHECK(g_load_calls == g_retry_probes + 0, "每次重试只起一个 worker(单飞,%d/%d)",
g_load_calls, g_retry_probes);
CHECK(g_paint_full == 0,
"静默重试无可见变化 → 全帧重绘 %d 次(期望 0;旧实现每轮都重绘+复位滚动)",
g_paint_full);
}
static void case_garbage_clock(void)
{
printf("B. 真实 home_tick:入参 now 用真机垃圾 64 位值(低 32 位整秒×1000 + 高 32 位随机)\n");
reset_world();
run_ticks(120, 0, 1);
CHECK(g_retry_probes == 4, "垃圾 now 下重试次数 = %d(期望 4;旧实现此时每 tick 都重试)",
g_retry_probes);
int gaps_ok = 1;
for (int i = 1; i < g_retry_probes && i < 64; i++)
if (g_probe_at[i] - g_probe_at[i - 1] < 30000) gaps_ok = 0;
CHECK(gaps_ok, "垃圾 now 下间隔仍 ≥30000ms(节奏只认 core 单调钟)");
}
static void case_flap(void)
{
printf("C. 真实 home_tick:online/offline 每 tick 抖动 60s\n");
reset_world();
run_ticks(60, 1, 0);
CHECK(g_retry_probes == 2, "抖动下重试次数 = %d(期望 2:0/30s;旧实现随抖动加速)",
g_retry_probes);
if (g_retry_probes >= 2)
CHECK(g_probe_at[1] - g_probe_at[0] >= 30000, "抖动下间隔 = %lldms ≥30000",
(long long)(g_probe_at[1] - g_probe_at[0]));
}
static void case_recover(void)
{
printf("D. 真实 home_tick:回在线再断网(在线期间不得重置节奏)\n");
reset_world();
run_ticks(35, 0, 0); /* 0..35s 断网:0s / 30s 各一次 */
CHECK(g_retry_probes == 2, "35s 断网重试 %d 次(期望 2)", g_retry_probes);
g_offline = 0; /* 等价于 session_gate 把它切回在线 */
run_ticks(5, 0, 0); /* 35..40s 在线:不该有重试 */
CHECK(g_retry_probes == 2, "回在线期间不重试(%d)", g_retry_probes);
g_offline = 1; /* 40s 再次断网 */
run_ticks(30, 0, 0); /* 40..70s:上次尝试在 30s,故下一次落在 60s */
CHECK(g_retry_probes == 3, "再次断网重试 %d 次(期望 3)", g_retry_probes);
if (g_retry_probes >= 3)
CHECK(g_probe_at[2] - g_probe_at[1] >= 30000,
"跨越在线/离线切换后间隔 = %lldms ≥30000(在线不得清零基准)",
(long long)(g_probe_at[2] - g_probe_at[1]));
CHECK(g_paint_full >= 0, "离线态翻转路径可重绘(%d 次)", g_paint_full);
}
int main(void)
{
printf("home_retry_sim — M10 真实 home_tick 离线重试回归(REPORT §42)\n\n");
case_steady();
case_garbage_clock();
case_flap();
case_recover();
printf("\nhome_retry_sim: %d-%d\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+235
View File
@@ -0,0 +1,235 @@
/* bench/meta_ipc_sim.c — M10:真实 app/play/ipc_media.c 的 META 上报频率回归(假 launcher socket)。
*
* 真机证据:launcher meow_20261105_122541.log 单次启动 21016 条
* `ipc: META app=dev.player.ncm state=0 pos=0/0 rc=0 -> ACK`,间隔 ~100ms。
* 旧 ipc_media 用 `now(meow_ui_time_ms) - last_meta >= 1000` 做 1s 心跳;真机该值高 32 位是垃圾
* (REPORT §42)→ 条件恒真 → 每个 IPC 轮次(200ms poll)都发一帧。
*
* 本测试把真实 ipc_media.c 编进宿主(--gc-sections 只保留 ipc_media_start 调用链),
* 在临时目录起一个 AF_UNIX “假 launcher”,统计 App 真正发出的 MSG_META 帧数:
* A. 空闲(无播放、无元数据)2s → 只应有 1 帧(注册媒体源),旧实现为 ~10 帧;
* B. 播放态翻转一次 → 只多 1 帧(状态变化立即上报),之后持续播放 2s 不再刷屏;
* C. 帧格式合法(magic/version/app_id),且没有 MSG_META 以外的上行帧。
*
* 编译(ncm_app 目录下):
* gcc -O2 -Wall -Wextra -ffunction-sections -fdata-sections -DMEOW_UI_HOST_TEST -DJSMN_HEADER \
* -I. -I../sdk -I../sdk/include -I../meow_launcherd -Icore -Ivendor/jsmn \
* -c app/play/ipc_media.c -o out/chk/ipc_m10.o
* gcc -O2 -Wall -Wextra -ffunction-sections -fdata-sections -DMEOW_UI_HOST_TEST -DJSMN_HEADER \
* -I. -I../sdk -I../sdk/include -I../meow_launcherd -Icore -Ivendor/jsmn \
* -c app/play/meta_gate.c -o out/chk/meta_gate_m10.o
* gcc -O2 -Wall -Wextra -ffunction-sections -fdata-sections -DMEOW_UI_HOST_TEST -DJSMN_HEADER \
* -I. -I../sdk -I../sdk/include -I../meow_launcherd -Icore -Ivendor/jsmn \
* -c bench/meta_ipc_sim.c -o out/chk/meta_ipc_sim.o
* gcc -Wl,--gc-sections -o meta_ipc_sim out/chk/ipc_m10.o out/chk/meta_gate_m10.o \
* out/chk/meta_ipc_sim.o -lpthread
* ./meta_ipc_sim
*/
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
#include <time.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include "../app/ncm_app_internal.h"
#include "meow_ipc.h"
/* ---- app.c 全局 ---- */
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER,
.state = NCM_PC_STOPPED, .cur_song = -1 };
volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 0;
static int g_pass, g_fail;
#define CHECK(cond, ...) do { \
int _ok = (cond) ? 1 : 0; \
printf(" %s ", _ok ? "PASS" : "FAIL"); printf(__VA_ARGS__); printf("\n"); \
if (_ok) g_pass++; else g_fail++; \
} while (0)
/* ---- 播放器桩(本测试只关心「有没有播放/元数据变化」) ---- */
static volatile int g_pm_state = NCM_PC_STOPPED;
int player_manager_state(void) { return g_pm_state; }
int player_manager_playing_song(void) { return -1; }
int player_manager_current_song(ncm_song_t *out) { (void)out; return -1; }
void player_manager_pause(int on) { g_pm_state = on ? NCM_PC_PAUSED : NCM_PC_PLAYING; }
void player_manager_next(int dir) { (void)dir; }
void player_manager_seek(int64_t ms) { (void)ms; }
const char *cover_current_path(void) { return ""; }
int lyric_count(void) { return 0; }
/* ---- 日志/时间桩 ---- */
static volatile int g_meta_logs;
void meow_log(meow_log_level lvl, const char *fmt, ...)
{
(void)fmt;
if (lvl == MEOW_LOG_DEBUG) g_meta_logs++;
}
int64_t ncm_monotonic_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
size_t ncm_strlcpy(char *dst, const char *src, size_t cap)
{
size_t n = 0;
if (!dst || cap == 0) return src ? strlen(src) : 0;
if (src) { for (; src[n] && n + 1 < cap; n++) dst[n] = src[n]; }
dst[n] = 0;
return n;
}
/* ============================================================
* 假 launcher:AF_UNIX 服务端 + 帧计数
* ============================================================ */
static int g_srv = -1, g_cli = -1;
static char g_sock_path[256];
static int g_bad_frames;
static char g_last_app_id[64];
static void set_play_state(int st)
{
pthread_mutex_lock(&g_play.mu);
g_play.state = st; /* META 的 state 取自 g_play(与真实播放线程同源) */
g_play.err = 0;
pthread_mutex_unlock(&g_play.mu);
}
static void put_hdr(uint8_t *h, uint16_t type, uint32_t req, uint32_t len)
{
uint32_t magic = MEOW_IPC_MAGIC;
uint16_t ver = MEOW_IPC_VERSION;
memcpy(h + 0, &magic, 4); memcpy(h + 4, &ver, 2);
memcpy(h + 6, &type, 2); memcpy(h + 8, &req, 4);
memcpy(h + 12, &len, 4);
}
static int srv_open(void)
{
snprintf(g_sock_path, sizeof g_sock_path, "%s/m10_ipc.sock",
getenv("TMPDIR") && getenv("TMPDIR")[0] ? getenv("TMPDIR") : "/tmp");
unlink(g_sock_path);
g_srv = socket(AF_UNIX, SOCK_STREAM, 0);
if (g_srv < 0) return -1;
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
snprintf(a.sun_path, sizeof a.sun_path, "%s", g_sock_path);
if (bind(g_srv, (struct sockaddr *)&a, sizeof a) != 0) return -1;
if (listen(g_srv, 1) != 0) return -1;
setenv("MEOW_IPC_SOCK", g_sock_path, 1);
return 0;
}
/* 收一帧(带超时)。返回帧长(负载),*type 出帧类型;-1=超时/断开。 */
static int srv_read(int timeout_ms, uint16_t *type, uint8_t *buf, int cap)
{
struct pollfd p = { .fd = g_cli, .events = POLLIN };
if (poll(&p, 1, timeout_ms) <= 0) return -1;
uint8_t hdr[MEOW_IPC_HDR_LEN];
size_t off = 0;
while (off < sizeof hdr) {
ssize_t r = read(g_cli, hdr + off, sizeof hdr - off);
if (r <= 0) return -1;
off += (size_t)r;
}
uint32_t magic, len; uint16_t ver;
memcpy(&magic, hdr, 4); memcpy(&ver, hdr + 4, 2);
memcpy(type, hdr + 6, 2); memcpy(&len, hdr + 12, 4);
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION || len > (uint32_t)cap) {
g_bad_frames++;
return -1;
}
off = 0;
while (off < len) {
ssize_t r = read(g_cli, buf + off, len - off);
if (r <= 0) return -1;
off += (size_t)r;
}
return (int)len;
}
/* 收 HELLO 并回一帧 ACK(ipc_thread 等这一帧才进入主循环)。 */
static int srv_handshake(void)
{
struct pollfd p = { .fd = g_srv, .events = POLLIN };
if (poll(&p, 1, 2000) <= 0) return -1;
g_cli = accept(g_srv, NULL, NULL);
if (g_cli < 0) return -1;
uint8_t buf[MEOW_IPC_MAX]; uint16_t type = 0;
if (srv_read(2000, &type, buf, sizeof buf) < 0) return -1;
if (type != MSG_HELLO) return -1;
m_hello_t hh;
memset(&hh, 0, sizeof hh);
memcpy(&hh, buf, sizeof hh < sizeof buf ? sizeof hh : sizeof buf);
snprintf(g_last_app_id, sizeof g_last_app_id, "%s", hh.app_id);
uint8_t out[MEOW_IPC_HDR_LEN + sizeof(m_ack_t)];
m_ack_t ack; memset(&ack, 0, sizeof ack); ack.code = MEOW_OK;
put_hdr(out, MSG_ACK, 1, (uint32_t)sizeof ack);
memcpy(out + MEOW_IPC_HDR_LEN, &ack, sizeof ack);
return (write(g_cli, out, sizeof out) == (ssize_t)sizeof out) ? 0 : -1;
}
/* 收 run_ms 内的全部上行帧,按类型计数。 */
static void srv_collect(int run_ms, int *meta, int *other)
{
int64_t end = ncm_monotonic_ms() + run_ms;
uint8_t buf[MEOW_IPC_MAX];
*meta = *other = 0;
while (ncm_monotonic_ms() < end) {
uint16_t type = 0;
int n = srv_read(50, &type, buf, sizeof buf);
if (n < 0) continue;
if (type == MSG_META) (*meta)++;
else (*other)++;
}
}
int main(void)
{
printf("meta_ipc_sim — M10 真实 ipc_media 线程 META 帧频回归(REPORT §42)\n\n");
if (srv_open() != 0) { printf(" FAIL 假 launcher socket 建立失败 (%s)\n", g_sock_path); return 1; }
if (ipc_media_start() != 0) { printf(" FAIL ipc_media_start\n"); return 1; }
if (srv_handshake() != 0) { printf(" FAIL HELLO 握手失败\n"); return 1; }
printf("A. 空闲(state=0,无播放/无元数据)2s\n");
int meta = 0, other = 0;
srv_collect(2000, &meta, &other);
CHECK(meta == 1, "上行 META 帧 = %d(期望 1:注册媒体源;旧实现 ~10 帧/2s)", meta);
CHECK(other == 0, "无其它上行帧(%d)", other);
CHECK(strcmp(g_last_app_id, "dev.player.ncm") == 0, "HELLO app_id = %s", g_last_app_id);
printf("B. 播放态变化(0 → PLAYING)后继续空闲 2s\n");
set_play_state(NCM_PC_PLAYING);
int meta2 = 0, other2 = 0;
srv_collect(2000, &meta2, &other2);
CHECK(meta2 == 1, "状态变化只上报 1 帧(%d)", meta2);
/* 停止播放:又是一次状态变化 → 再 1 帧 */
set_play_state(NCM_PC_STOPPED);
int meta3 = 0, other3 = 0;
srv_collect(2000, &meta3, &other3);
CHECK(meta3 == 1, "停止播放只上报 1 帧(%d)", meta3);
CHECK(g_bad_frames == 0, "帧头 magic/version/长度全部合法(坏帧 %d)", g_bad_frames);
ipc_media_stop();
g_app_quit = 1;
usleep(400 * 1000); /* 让 ipc 线程在 poll 超时内退出 */
if (g_cli >= 0) close(g_cli);
if (g_srv >= 0) close(g_srv);
unlink(g_sock_path);
printf("\nmeta_ipc_sim: %d-%d\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+305
View File
@@ -0,0 +1,305 @@
/* bench/retry_storm_sim.c — M10 断网重试风暴 / META 洪泛 宿主回归(0 网络、0 真机)。
*
* 覆盖真实实现(直接编译 app/logic/retry_ctl.c 与 app/play/meta_gate.c,非复刻逻辑):
* A. 100ms tick + 断网常驻 + worker 每次耗时 5s(真机 rc=-4/-6 的连接超时量级):
* 重试间隔严格 ≥30000ms,74s 量级的窗口内次数与真机日志对得上(真机为 10 次 / 74s)。
* B. online/offline 每 tick 抖动(旧实现 else 分支清零重试时刻 → 一抖就重试):
* 抖动不得产生任何额外重试(间隔仍 ≥30s)。
* C. 时钟回退(喂入倒退 30s 的 now):不得下溢成「巨大已超时」立即重试;
* 以真实 tick 计的重试间隔仍 ≥30s。
* D. 单飞:worker 未收工时任何 tick 都不发第二次。
* E. META 闸:空闲(state=0, 元数据不变)60s 内 ≤3 帧(首帧 + 30s 心跳);
* 播放中位置推进 → ≤1 帧/5s;状态抖动 → 不快于 4 帧/秒(硬下限 250ms);
* 元数据变化/seek → 立即报;超长标题截断后不得因截断恒判「变了」而反复重报;
* 时钟回退不产生连发。
*
* 编译(ncm_app 目录下):
* gcc -O2 -Wall -Wextra -o retry_storm_sim bench/retry_storm_sim.c \
* app/logic/retry_ctl.c app/play/meta_gate.c
* 运行:./retry_storm_sim (退出码 0=全过)
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "../app/logic/retry_ctl.h"
#include "../app/play/meta_gate.h"
static int g_pass, g_fail;
#define CHECK(cond, ...) do { \
int _ok = (cond) ? 1 : 0; \
printf(" %s ", _ok ? "PASS" : "FAIL"); printf(__VA_ARGS__); printf("\n"); \
if (_ok) g_pass++; else g_fail++; \
} while (0)
#define TICK_MS 100
#define RETRY_MS 30000
#define WORKER_COST 5000 /* 断网时一次重试要跑多久(连接超时量级,真机 ~5-7s) */
/* 模拟 UI 每 tick 调 home_tick:offline 由场景给,busy 由「worker 是否还在跑」给。 */
typedef struct {
ncm_retry_ctl ctl;
int fires;
int64_t fire_tick[64];
int64_t fire_now[64];
int64_t busy_until;
int64_t worker_cost; /* 一次重试的耗时(默认 WORKER_COST) */
} retry_sim;
static void retry_sim_init(retry_sim *s)
{
memset(s, 0, sizeof *s);
ncm_retry_ctl_init(&s->ctl);
s->busy_until = 0;
s->worker_cost = WORKER_COST;
}
static int retry_sim_tick(retry_sim *s, int offline, int64_t now, int64_t tick)
{
int busy = (tick < s->busy_until);
int fire = ncm_retry_ctl_poll(&s->ctl, offline, busy, now, RETRY_MS);
if (fire) {
if (s->fires < 64) { s->fire_tick[s->fires] = tick; s->fire_now[s->fires] = now; }
s->fires++;
s->busy_until = tick + s->worker_cost;
}
return fire;
}
/* ---- A:断网常驻,120s,100ms tick,worker 5s ---- */
static void case_steady(void)
{
printf("A. 断网常驻 120s(100ms tick,worker 5s)\n");
retry_sim s; retry_sim_init(&s);
for (int64_t t = 0; t < 120000; t += TICK_MS) retry_sim_tick(&s, 1, t, t);
CHECK(s.fires == 4, "重试次数 = %d(期望 4:t=0/30/60/90s)", s.fires);
int gaps_ok = 1;
for (int i = 1; i < s.fires && i < 64; i++)
if (s.fire_tick[i] - s.fire_tick[i - 1] < RETRY_MS) gaps_ok = 0;
CHECK(gaps_ok, "相邻重试真实间隔全部 ≥30000ms");
/* 旧实现量级对照:同样 120s 若每 tick 都重试 = 1200 次(真机 74s/10 次,且每次都要跑满网络超时) */
CHECK(s.fires <= 8, "120s 内重试次数 = %d(≤8,即 ≤4 次/分钟的理论上限)", s.fires);
}
/* ---- B:状态抖动(每 tick 翻转 online/offline) ---- */
static void case_flap(void)
{
printf("B. online/offline 每 tick 抖动 60s\n");
retry_sim s; retry_sim_init(&s);
int k = 0;
for (int64_t t = 0; t < 60000; t += TICK_MS, k++)
retry_sim_tick(&s, (k % 2 == 0), t, t); /* 每 tick 翻转 */
CHECK(s.fires == 2, "抖动下重试次数 = %d(期望 2:0s / 30s,抖动不得加速)", s.fires);
if (s.fires >= 2)
CHECK(s.fire_tick[1] - s.fire_tick[0] >= RETRY_MS, "抖动下间隔 = %lldms ≥30000",
(long long)(s.fire_tick[1] - s.fire_tick[0]));
}
/* ---- C:时钟回退 ---- */
static void case_clock_rollback(void)
{
printf("C. 时钟回退 30s(真机 meow_ui_time_ms 异常形态的另一面)\n");
retry_sim s; retry_sim_init(&s);
int64_t now = 0;
int fired_at_rollback = 0;
for (int64_t t = 0; t < 120000; t += TICK_MS) {
if (t == 40000) { /* 40s 处时钟倒退 30s → now=10000(< 上次尝试时刻 30000) */
now -= 30000;
if (retry_sim_tick(&s, 1, now, t)) fired_at_rollback = 1;
} else {
retry_sim_tick(&s, 1, now, t);
}
now += TICK_MS;
}
CHECK(!fired_at_rollback, "回退瞬间不触发重试(不下溢成「巨大已超时」)");
/* 回退把基准重锚到回退后的 now(10000)→ 下一次要等 now 回到 40000(真实 t=70000)。
* 真实间隔:0 / 30s / 70s / 100s,全部 ≥30s。 */
CHECK(s.fires == 4, "回退场景重试次数 = %d(期望 4;回退只推迟、不加速)", s.fires);
int gaps_ok = 1;
for (int i = 1; i < s.fires && i < 64; i++)
if (s.fire_tick[i] - s.fire_tick[i - 1] < RETRY_MS) gaps_ok = 0;
CHECK(gaps_ok, "回退前后按真实 tick 计的间隔仍 ≥30000ms");
CHECK(s.fires >= 1 && s.ctl.have_last, "回退后闸仍处于已记账状态(不会失锁成每 tick 重试)");
}
/* ---- D:单飞 ---- */
static void case_single_flight(void)
{
printf("D. 单飞(每次重试的 worker 跑 45s > 30s 间隔)\n");
retry_sim s; retry_sim_init(&s);
s.worker_cost = 45000;
for (int64_t t = 0; t < 200000; t += TICK_MS) retry_sim_tick(&s, 1, t, t);
/* worker 跑 45s(比间隔还长):期间任何 tick 都不许再起第二个 worker;
* 收工后才按 30s 间隔排下一次 → 0/45/90/135/180s 共 5 次。 */
CHECK(s.fires == 5, "长 worker 期间不并发重试,次数 = %d(期望 5)", s.fires);
int gaps_ok = 1;
for (int i = 1; i < s.fires && i < 64; i++)
if (s.fire_tick[i] - s.fire_tick[i - 1] < RETRY_MS) gaps_ok = 0;
CHECK(gaps_ok, "单飞下间隔仍 ≥30000ms");
}
/* ============================================================
* E. META 闸
* ============================================================ */
static void snap_set(ncm_meta_snap *s, int state, int pos, int dur, const char *title)
{
s->state = state;
s->pos_ms = pos;
s->dur_ms = dur;
s->flags = 0;
s->title = title;
s->artist = "artist";
s->album = "album";
s->artwork = "";
}
/* 返回发送次数;记最小间隔。 */
static int run_meta(ncm_meta_gate *g, int64_t from, int64_t to, int64_t *min_gap, int64_t *last_send)
{
int n = 0;
*min_gap = -1;
*last_send = -1;
ncm_meta_snap s;
for (int64_t t = from; t <= to; t += TICK_MS) {
snap_set(&s, NCM_META_ST_STOPPED, 0, 0, "");
if (ncm_meta_gate_should_send(g, &s, t)) {
if (*last_send >= 0) {
int64_t gap = t - *last_send;
if (*min_gap < 0 || gap < *min_gap) *min_gap = gap;
}
*last_send = t;
n++;
}
}
return n;
}
static void case_meta_idle(void)
{
printf("E1. 空闲(state=0,元数据不变)60s\n");
ncm_meta_gate g; ncm_meta_gate_init(&g);
int64_t min_gap, last;
int n = run_meta(&g, 0, 60000, &min_gap, &last);
CHECK(n == 3, "上报次数 = %d(期望 3:首帧 + 30s + 60s 心跳;旧实现为 600)", n);
CHECK(min_gap < 0 || min_gap >= 29000, "心跳间隔 = %lldms(≥30s 量级)", (long long)min_gap);
}
static void case_meta_playing(void)
{
printf("E2. 播放中(位置每 tick 推进)60s\n");
ncm_meta_gate g; ncm_meta_gate_init(&g);
ncm_meta_snap s;
int n = 0, min_gap = -1, last = -1;
for (int64_t t = 0; t <= 60000; t += TICK_MS) {
snap_set(&s, NCM_META_ST_PLAYING, (int)t, 200000, "song");
if (ncm_meta_gate_should_send(&g, &s, t)) {
if (last >= 0 && (min_gap < 0 || (int)(t - last) < min_gap)) min_gap = (int)(t - last);
last = (int)t;
n++;
}
}
CHECK(n >= 12 && n <= 14, "上报次数 = %d(期望 ~13:≤1 帧/5s)", n);
CHECK(min_gap < 0 || min_gap >= 4000, "最小间隔 = %dms(给 launcher 重锚进度用,≥5s 量级)", min_gap);
}
static void case_meta_flap(void)
{
printf("E3. 播放状态每 tick 抖动 10s(硬下限)\n");
ncm_meta_gate g; ncm_meta_gate_init(&g);
ncm_meta_snap s;
int n = 0, min_gap = -1, last = -1;
int k = 0;
for (int64_t t = 0; t <= 10000; t += TICK_MS, k++) {
snap_set(&s, (k % 2) ? NCM_META_ST_PLAYING : NCM_META_ST_PAUSED, 0, 0, "");
if (ncm_meta_gate_should_send(&g, &s, t)) {
if (last >= 0 && (min_gap < 0 || (int)(t - last) < min_gap)) min_gap = (int)(t - last);
last = (int)t;
n++;
}
}
CHECK(n <= 42, "抖动 10s 上报次数 = %d(≤4 帧/秒的硬上限 40+2)", n);
CHECK(min_gap < 0 || min_gap >= NCM_META_MIN_GAP_MS, "最小间隔 = %dms(≥250ms 硬下限)", min_gap);
}
static void case_meta_change(void)
{
printf("E4. 元数据变化 / seek / 超长标题截断\n");
ncm_meta_gate g; ncm_meta_gate_init(&g);
ncm_meta_snap s;
int64_t t = 0;
snap_set(&s, NCM_META_ST_STOPPED, 0, 0, "");
CHECK(ncm_meta_gate_should_send(&g, &s, t), "首帧必发(注册媒体源)");
t += TICK_MS; /* 同一快照:不发 */
CHECK(!ncm_meta_gate_should_send(&g, &s, t), "无变化不重发");
t += 5000;
snap_set(&s, NCM_META_ST_STOPPED, 0, 0, "新歌名");
CHECK(ncm_meta_gate_should_send(&g, &s, t), "标题变化 → 立即报");
t += 5000;
snap_set(&s, NCM_META_ST_PLAYING, 120000, 200000, "新歌名");
CHECK(ncm_meta_gate_should_send(&g, &s, t), "state 变化(开始播放)→ 立即报");
t += 5000;
snap_set(&s, NCM_META_ST_PLAYING, 30000, 200000, "新歌名"); /* 用户 seek 回 30s */
CHECK(ncm_meta_gate_should_send(&g, &s, t), "seek 前后跳 → 立即报(给 launcher 重锚)");
/* 超长标题:发送侧按 127 字节截断;截断后不得恒判「变了」而每 tick 重报 */
static char longtitle[400];
memset(longtitle, 'A', sizeof longtitle - 1);
longtitle[sizeof longtitle - 1] = 0;
ncm_meta_gate g2; ncm_meta_gate_init(&g2);
snap_set(&s, NCM_META_ST_STOPPED, 0, 0, longtitle);
int n = 0, t2 = 0;
for (; t2 <= 10000; t2 += TICK_MS) {
if (ncm_meta_gate_should_send(&g2, &s, t2)) n++;
}
CHECK(n == 1, "超长标题 10s 内上报 %d 次(期望 1:截断不得造成恒变更)", n);
}
static void case_meta_rollback(void)
{
printf("E5. META 时钟回退\n");
ncm_meta_gate g; ncm_meta_gate_init(&g);
ncm_meta_snap s;
int n = 0;
int64_t now = 0;
snap_set(&s, NCM_META_ST_STOPPED, 0, 0, "");
for (int i = 0; i < 400; i++) { /* 40s:首帧 + 30s 心跳 */
if (ncm_meta_gate_should_send(&g, &s, now)) n++;
now += TICK_MS;
}
CHECK(n == 2, "回退前 40s 上报 %d 次(期望 2)", n);
now -= 20000; /* 时钟倒退 20s */
int burst = 0;
for (int i = 0; i < 50; i++) { /* 5s */
if (ncm_meta_gate_should_send(&g, &s, now)) burst++;
now += TICK_MS;
}
CHECK(burst <= 1, "回退后 5s 内补发 %d 次(≤1:不得连发成洪泛)", burst);
}
int main(void)
{
printf("retry_storm_sim — M10 断网重试风暴 / META 洪泛 宿主回归(REPORT §42)\n\n");
case_steady();
case_flap();
case_clock_rollback();
case_single_flight();
case_meta_idle();
case_meta_playing();
case_meta_flap();
case_meta_change();
case_meta_rollback();
printf("\nretry_storm_sim: %d-%d\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+4 -3
View File
@@ -21,9 +21,10 @@ APP="app/app.c app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
app/ui/settings.c app/ui/search.c app/ui/minibar.c app/ui/queue.c app/ui/cache.c \ app/ui/settings.c app/ui/search.c app/ui/minibar.c app/ui/queue.c app/ui/cache.c \
app/ui/ncm_colors.c app/ui/chrome.c \ app/ui/ncm_colors.c app/ui/chrome.c \
app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \ app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
app/logic/cover.c app/logic/account.c app/play/player_manager.c app/play/audio_out.c \ app/logic/cover.c app/logic/account.c app/logic/retry_ctl.c \
app/play/ipc_media.c app/play/decoder.c app/play/stream_src.c app/play/minimp3_impl.c \ app/play/player_manager.c app/play/audio_out.c \
app/play/mem_watch.c" app/play/ipc_media.c app/play/meta_gate.c app/play/decoder.c app/play/stream_src.c \
app/play/minimp3_impl.c app/play/mem_watch.c"
echo "==> app+core (strict)" echo "==> app+core (strict)"
for f in $APP $CORE; do for f in $APP $CORE; do
+4 -2
View File
@@ -33,8 +33,10 @@ APP_SRC="app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
app/ui/settings.c app/ui/search.c app/ui/minibar.c app/ui/queue.c app/ui/cache.c \ app/ui/settings.c app/ui/search.c app/ui/minibar.c app/ui/queue.c app/ui/cache.c \
app/ui/ncm_colors.c app/ui/chrome.c \ app/ui/ncm_colors.c app/ui/chrome.c \
app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \ app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
app/logic/cover.c app/logic/account.c app/play/player_manager.c app/play/audio_out.c \ app/logic/cover.c app/logic/account.c app/logic/retry_ctl.c \
app/play/ipc_media.c app/play/decoder.c app/play/stream_src.c app/play/minimp3_impl.c" app/play/player_manager.c app/play/audio_out.c app/play/mem_watch.c \
app/play/ipc_media.c app/play/meta_gate.c app/play/decoder.c app/play/stream_src.c \
app/play/minimp3_impl.c"
for f in $APP_SRC; do for f in $APP_SRC; do
b=$(basename "$f" .c) b=$(basename "$f" .c)
$CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -DJSMN_HEADER $INC "$f" -o "$OUT/host/$b.o" || { echo "app $f FAILED"; exit 1; } $CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -DJSMN_HEADER $INC "$f" -o "$OUT/host/$b.o" || { echo "app $f FAILED"; exit 1; }
+25
View File
@@ -161,4 +161,29 @@ gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
gcc -Wl,--gc-sections -o http_truncate_sim /tmp/t.o /tmp/t_http.o /tmp/t_common.o gcc -Wl,--gc-sections -o http_truncate_sim /tmp/t.o /tmp/t_http.o /tmp/t_common.o
run_sim http_truncate_sim run_sim http_truncate_sim
echo "==> build retry_storm_sim(M10 断网重试风暴 / META 洪泛 节奏闸:纯逻辑,0 网络 0 SDK)"
mkdir -p out/chk
gcc -O2 -Wall -Wextra -o out/chk/retry_storm_sim bench/retry_storm_sim.c \
app/logic/retry_ctl.c app/play/meta_gate.c -lpthread
run_sim out/chk/retry_storm_sim
echo "==> build home_retry_sim(M10 真实 ui/home.c 的 home_tick 离线重试回归;网络/UI 桩在测试内)"
HINC="-DMEOW_UI_HOST_TEST -DJSMN_HEADER -I. -I../sdk -I../sdk/include -I../meow_launcherd -Icore -Ivendor/jsmn"
for f in app/ui/home.c app/logic/retry_ctl.c bench/home_retry_sim.c; do
b=$(basename "$f" .c)
gcc -O2 -Wall -Wextra -ffunction-sections -fdata-sections $HINC -c "$f" -o "out/chk/m10_$b.o"
done
gcc -Wl,--gc-sections -o out/chk/home_retry_sim \
out/chk/m10_home.o out/chk/m10_retry_ctl.o out/chk/m10_home_retry_sim.o -lpthread
run_sim out/chk/home_retry_sim
echo "==> build meta_ipc_sim(M10 真实 play/ipc_media.c + 假 launcher socket 的 META 帧频回归)"
for f in app/play/ipc_media.c app/play/meta_gate.c bench/meta_ipc_sim.c; do
b=$(basename "$f" .c)
gcc -O2 -Wall -Wextra -ffunction-sections -fdata-sections $HINC -c "$f" -o "out/chk/m10_$b.o"
done
gcc -Wl,--gc-sections -o out/chk/meta_ipc_sim \
out/chk/m10_ipc_media.o out/chk/m10_meta_gate.o out/chk/m10_meta_ipc_sim.o -lpthread
run_sim out/chk/meta_ipc_sim
echo "===== all sims done =====" echo "===== all sims done ====="
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.