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:
1 parent
7f4e18b0ed
commit
294ed6ffc7
7 files changed
+938
-5
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user