- 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 全过)、真机验证建议。
231 lines
9.8 KiB
C
231 lines
9.8 KiB
C
/* 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;
|
||
}
|