/* 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 #include #include #include #include #include #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; }