Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/plpage_sim.c
T
AskaEth 31879f3302 fix(ncm): 缓存命中改单请求出参,移除全局命中计数(评审 P2-2/P2-1)
问题(P2-2):离线判定用 `ncm_apicache_hits()` 前后差值判「本次是否缓存命中」——
①其它 worker 的命中会污染差值 → 误判「已回在线」;②盘缓存分支的 g_hits++ 在 g_mu 锁外,
跨线程自增/64 位读存在竞争与撕裂可能。

修法(出参方案):
- `api_post_cached(..., int *from_cache)`:命中置 1(未走网络),miss/失败为 0;
  四个调用点显式传参。
- 新增 `ncm_api_user_playlist_ex(..., int *out_from_cache)`;out_n 与原接口一致**必填**
  (NULL→NCM_ERR_ARG,修入口容忍 NULL、成功路径却无条件解引用的契约不一致,评审 P2-1);
  out_from_cache 可 NULL。旧 `ncm_api_user_playlist` 为薄包装(调用方零改动)。
- `appstate_load_playlists_ex` 透传该标志;home_worker 用本次请求的 from_cache 判定是否
  回在线;歌单页只在失败时同步离线态(成功可能是缓存命中,不据此清离线)。
- 删除 `ncm_apicache_hits()`/g_hits(无使用者,同时消除数据竞争);盘缓存命中分支两行
  合并为一行(行为不变,重构)。
- 测试:session_gate_sim 新增 D2 五例(miss→0 且真网络 +1 次 / 命中→1 且 0 请求 /
  失败→0 / 兼容入口不崩 / out_n 必填契约);playlist_race_sim、plpage_sim 补 `_ex` 桩。

验证:session_gate_sim 88 PASS/0 FAIL;test_ncm_core_host 139 PASS/0 FAIL;
http_truncate_sim 13 PASS/0 FAIL;核心 13 TU(-Wall -Wextra)0 warning。
2026-11-05 11:59:33 +08:00

284 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* bench/plpage_sim.c — 歌单页「懒加载分页 + 加载后列表刷新」离线回归(0 真实 NCM 请求)。
*
* 驱动真实 app/ui/playlist.c(页面打开 / 滚动触发 / worker / tick 消费全链路)+ 真实
* appstate.c(滑动窗口)+ 真实 songmeta.c(/tmp 盘缓存)+ 宿主 SDK(meow_listview/draw 全真,
* 与 ncm_host 同一批 out/host/sdk_*.o)。网络端点(ncm_api_playlist_detail /
* ncm_api_song_detail)为本地合成 stub(计调用次数、可注入单批失败)。
*
* Bug1(大歌单懒加载分页)验证点:
* ① 打开 400 首歌单:首屏 = 1 detail + 恰 1 批 song_detail(物化 [0,100),第 100 首未物化)
* ——不再首屏连拉多批补满窗口(旧行为 ≤3 批连发,5k+ 歌单阻塞 + 风控);
* ② 顶部小幅滚动(视口底距已分页底部 >20 首):不发任何新请求;
* ③ 滑到已分页底部(余量 ≤20 首):恰拉 1 页;单批失败时 g_paged_hi 不推进(占位保留),
* 再滑到底部重试同一页成功(一次一页,不连拉);
* ④ 逐页到底(400 = 4 页);全部拉完后滑到尾部:不再发请求(停止);
* ⑤ 回滚到顶部:失效槽(窗口挤出)由盘缓存补物化,song_detail 调用数 0 增长(不占网络额度);
* Bug2(加载完列表刷新)验证点:
* ⑥ 首屏/每页 worker 完成经 playlist_tick 消费后,整帧重画计数(--wrap=meow_ui_paint_full)
* 递增,且新物化行经 appstate_song_copy 取到真实歌名/歌手(= pl_src2 将绑定真实数据,
* 不再是「... 加载中」占位)。
* 风控纪律:0 次真实 NCM 请求(离线 stub + /tmp 盘缓存)。
*
* 编译/运行见 build_ncm_sims.sh「plpage_sim」段(宿主,ncm_app 目录下)。
*/
#include "../app/ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
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; /* 真实分页链路(不走 mock 跳网分支) */
/* ---- core 符号最小实现(同 playlist_race_sim 口径) ---- */
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
{
size_t n = 0;
if (sz) {
while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; }
dst[n] = 0;
}
while (src[n]) n++;
return n;
}
int64_t ncm_now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/* ---- cover / 播放 stub(本 sim 不测封面与播放;playlist.c 引用,链接需要) ---- */
void cover_request(const char *url, const char *song_id) { (void)url; (void)song_id; }
meow_img *cover_get(void) { return NULL; }
void cover_put(void) {}
int cover_changed(void) { return 0; }
void cover_ack(void) {}
void cover_cache_reset(void) {}
void player_play_song(int idx) { (void)idx; }
/* ---- core 符号(appstate.c 引用;运行时不到达,仅链接需要) ---- */
int ncm_login_status_check(const char *device_id, const char *cookie, char *uid, size_t uid_sz,
char *nick, size_t nick_sz)
{ (void)device_id;(void)cookie;(void)uid;(void)uid_sz;(void)nick;(void)nick_sz; return NCM_ERR_AUTH; }
int ncm_ctx_clear_cookie(ncm_ctx_t *ctx) { (void)ctx; return NCM_OK; }
int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid,
int limit, int offset, ncm_playlist_t *out, int max, int *out_n)
{ (void)device_id;(void)cookie;(void)uid;(void)limit;(void)offset;(void)out;(void)max;(void)out_n; return NCM_ERR_NET; }
/* M9 出参变体(appstate_load_playlists_ex 引用;本 sim 不到达) */
int ncm_api_user_playlist_ex(const char *device_id, const char *cookie, const char *uid,
int limit, int offset, ncm_playlist_t *out, int max, int *out_n,
int *out_from_cache)
{ if (out_from_cache) *out_from_cache = 0;
return ncm_api_user_playlist(device_id, cookie, uid, limit, offset, out, max, out_n); }
/* ---- 网络端点 stub(离线,合成数据;计调用次数 + 可注入单批失败) ---- */
#define SIM_TOTAL 400 /* 4 页:100 × 4(> NCM_SONG_WINDOW 256,回滚必滑窗) */
static int g_detail_calls;
static int g_song_calls; /* song_detail 成功批次数 */
static int g_song_attempts; /* song_detail 发起次数(含失败批) */
static int g_song_fail_n; /* 接下来 N 批 song_detail 网络失败模拟 */
int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id,
char *name, size_t name_sz, char *cover, size_t cover_sz,
int64_t *ids, int id_cap, int *out_id_n)
{
(void)device_id; (void)cookie; (void)playlist_id; (void)cover_sz;
g_detail_calls++;
if (name) snprintf(name, name_sz, "Sim Playlist");
if (cover) cover[0] = 0;
int n = (SIM_TOTAL < id_cap) ? SIM_TOTAL : id_cap;
for (int i = 0; i < n; i++) ids[i] = 1000000LL + i;
*out_id_n = n;
return NCM_OK;
}
int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t *ids, int n,
ncm_song_t *out, int max, int *out_n)
{
(void)device_id; (void)cookie;
g_song_attempts++;
if (g_song_fail_n > 0) { /* 单批网络失败模拟(失败不推进分页高水位 → 同页重试) */
g_song_fail_n--;
if (out_n) *out_n = 0;
return NCM_ERR_NET;
}
g_song_calls++;
int m = (n < max) ? n : max;
for (int i = 0; i < m; i++) {
snprintf(out[i].id, sizeof out[i].id, "%lld", (long long)ids[i]);
snprintf(out[i].name, sizeof out[i].name, "Song%lld", (long long)ids[i]);
snprintf(out[i].art_name, sizeof out[i].art_name, "Artist%lld", (long long)ids[i]);
snprintf(out[i].album_name, sizeof out[i].album_name, "Album%lld", (long long)ids[i]);
out[i].duration_ms = 200000;
out[i].fee = 0;
}
if (out_n) *out_n = m;
return NCM_OK;
}
/* ---- Bug2 证据:整帧重画计数(链接时 --wrap=meow_ui_paint_full 重定向到此) ---- */
extern void __real_meow_ui_paint_full(void);
static int g_paint_full_calls;
void __wrap_meow_ui_paint_full(void)
{
g_paint_full_calls++;
__real_meow_ui_paint_full();
}
/* ---- 驱动辅助:模拟 app_on_tick 周期性消费 worker 完成 ---- */
static int pump_until(int (*cond)(void), int timeout_ms)
{
for (int t = 0; t < timeout_ms; t += 5) {
playlist_tick(0);
if (cond()) {
/* 再喂若干 tick:确保完成态被消费(g_plthr_on 清零),后续滚动可再触发 */
for (int k = 0; k < 30; k++) { playlist_tick(0); usleep(2000); }
return 1;
}
usleep(5000);
}
return 0;
}
static int g_fail = 0;
#define CHECK(cond, fmt, ...) do { \
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
} while (0)
/* 条件(C 无闭包,用全局目标值) */
static int g_want_calls;
static int cond_calls(void) { return g_song_calls >= g_want_calls; }
static int g_want_attempts;
static int cond_attempts(void) { return g_song_attempts >= g_want_attempts; }
static int cond_first_screen(void)
{
return g_st.track_ids_count == SIM_TOTAL && g_song_calls >= 1;
}
/* 滚动到指定首行(行高 APP_ROW_H px;swipe 距离按当前行差换算) */
static void scroll_to_row(int row)
{
static int cur;
int d = row - cur;
cur = row;
playlist_swipe(d >= 0 ? MEOW_SWIPE_UP : MEOW_SWIPE_DOWN,
(d >= 0 ? d : -d) * APP_ROW_H);
}
int main(void)
{
printf("== plpage_sim: 懒加载分页 + 加载后列表刷新(%d 首,窗口 %d,批 %d,触底余量 20)==\n",
SIM_TOTAL, NCM_SONG_WINDOW, NCM_SONG_BATCH);
/* 宿主 SDK 离屏初始化(与 ncm_host 同口径) */
setenv("MEOW_UI_DUMP", "/tmp/plpage_sim.ppm", 0);
meow_ui_opts o = { APP_W, APP_H };
if (meow_ui_init(&o) != 0) { fprintf(stderr, "init failed\n"); return 1; }
meow_theme_set_mode(MEOW_THEME_DARK);
ncm_on_theme(MEOW_THEME_DARK);
/* 歌单 mock:1 张 SIM_TOTAL 首歌单;songmeta 盘缓存用临时目录 */
char cdt[] = "/tmp/plpage_cache_XXXXXX";
if (!mkdtemp(cdt)) { fprintf(stderr, "mkdtemp failed\n"); return 1; }
snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "%s", cdt);
g_st.playlist_count = 1;
snprintf(g_st.playlists[0].id, sizeof g_st.playlists[0].id, "pl0");
snprintf(g_st.playlists[0].name, sizeof g_st.playlists[0].name, "SimPlaylist");
g_st.playlists[0].track_count = SIM_TOTAL;
ncm_song_t sq;
int paint0;
/* ---- ① Bug1:打开歌单 → 首屏恰 1 detail + 1 批 song_detail(物化 [0,100)) ---- */
printf("== case 1: 首屏只拉一页 ==\n");
paint0 = g_paint_full_calls;
playlist_view_create();
playlist_open(0);
CHECK(pump_until(cond_first_screen, 8000), "first-screen loaded (track_ids=%d calls=%d)",
g_st.track_ids_count, g_song_calls);
CHECK(g_detail_calls == 1, "detail calls=%d (expect 1)", g_detail_calls);
CHECK(g_song_calls == 1, "song_detail calls=%d (expect 1, 不再首屏连拉多批)", g_song_calls);
CHECK(g_st.track_ids_count == SIM_TOTAL, "track_ids_count=%d (expect %d)",
g_st.track_ids_count, SIM_TOTAL);
CHECK(appstate_song_copy(99, &sq) == 0 && strcmp(sq.name, "Song1000099") == 0,
"row 99 materialized name='%s'", sq.name);
CHECK(appstate_song_copy(100, &sq) != 0, "row 100 NOT materialized yet (placeholder)");
/* Bug2:首屏完成 → 重画计数递增 + 行绑定真实歌名/歌手(非占位) */
CHECK(g_paint_full_calls > paint0, "paint_full after first-screen %d -> %d (list repainted)",
paint0, g_paint_full_calls);
CHECK(appstate_song_copy(5, &sq) == 0 && strcmp(sq.art_name, "Artist1000005") == 0,
"row 5 binds real artist='%s' (src2 填真实数据,非「... 加载中」)", sq.art_name);
/* ---- ② 顶部小幅滚动:不触发任何新请求(距已分页底部 >20 首) ---- */
printf("== case 2: 顶部滚动不拉页 ==\n");
scroll_to_row(10);
int a0 = g_song_attempts;
for (int k = 0; k < 40; k++) { playlist_tick(0); usleep(3000); }
CHECK(g_song_attempts == a0, "song_detail attempts=%d (unchanged, 顶部滚动不拉页)", g_song_attempts);
/* ---- ③ 滑到已分页底部(余量 ≤20 首):恰拉 1 页;单批失败不推进,再滑重试同页成功 ---- */
printf("== case 3: 触底拉下一页(一次一页;失败重试同页) ==\n");
g_song_fail_n = 1; /* 注入:下一次 song_detail 网络失败 */
int att_before = g_song_attempts;
scroll_to_row(90); /* 视口底 ≈97 行,余量 ≤20 → 触发第 2 页(失败) */
g_want_attempts = att_before + 1;
CHECK(pump_until(cond_attempts, 8000), "page-2 first attempt fired (attempts=%d)", g_song_attempts);
CHECK(g_song_calls == 1, "page-2 failed -> calls=%d (g_paged_hi 不推进,占位保留)", g_song_calls);
CHECK(appstate_song_copy(199, &sq) != 0, "row 199 still placeholder after failed page");
scroll_to_row(91); /* 再滑(滚动事件)→ 重试同一页 */
g_want_calls = 2;
CHECK(pump_until(cond_calls, 8000), "page 2 retried & loaded (calls=%d attempts=%d)",
g_song_calls, g_song_attempts);
CHECK(g_song_attempts == att_before + 2,
"attempts +%d (1 失败 + 1 重试同页 = 一次一页,未连拉)", g_song_attempts - att_before);
CHECK(appstate_song_copy(199, &sq) == 0 && strcmp(sq.name, "Song1000199") == 0,
"row 199 materialized name='%s'", sq.name);
CHECK(g_paint_full_calls > paint0, "paint_full after page-2 consumed (=%d)", g_paint_full_calls);
/* ---- ④ 逐页到底;全部拉完后滑到尾部:不再发请求 ---- */
printf("== case 4: 逐页到底 + 拉完即止 ==\n");
att_before = g_song_attempts;
scroll_to_row(190); /* 余量 ≤20 → 触发第 3 页 */
g_want_calls = 3;
CHECK(pump_until(cond_calls, 8000), "page 3 loaded (calls=%d)", g_song_calls);
scroll_to_row(290); /* 余量 ≤20 → 触发第 4 页(末页) */
g_want_calls = 4;
CHECK(pump_until(cond_calls, 8000), "page 4 (last) loaded (calls=%d)", g_song_calls);
CHECK(g_song_attempts == att_before + 2, "attempts +%d (第 3、4 页恰各 1 批)",
g_song_attempts - att_before);
CHECK(appstate_song_copy(399, &sq) == 0 && strcmp(sq.name, "Song1000399") == 0,
"tail row 399 materialized name='%s'", sq.name);
att_before = g_song_attempts;
scroll_to_row(390); /* 已在尾部:全部拉完 → 不再发请求 */
for (int k = 0; k < 40; k++) { playlist_tick(0); usleep(3000); }
CHECK(g_song_attempts == att_before, "all loaded -> scroll at tail: attempts=%d (unchanged)",
g_song_attempts);
/* ---- ⑤ 回滚到顶部:失效槽(窗口挤出)盘缓存补物化,song_detail 0 增长 ---- */
printf("== case 5: 回滚补物化走盘缓存(0 网络) ==\n");
att_before = g_song_attempts;
scroll_to_row(0); /* 回顶部:窗口已滑走,行 0-35 失效 → 盘缓存补 */
for (int k = 0; k < 400 && !appstate_range_loaded(0, 30); k++) { playlist_tick(0); usleep(5000); }
CHECK(appstate_range_loaded(0, 30), "rows 0-29 re-materialized after scroll-back");
CHECK(g_song_attempts == att_before, "scroll-back refill: song_detail attempts=%d "
"(unchanged, 盘缓存命中不占网络)", g_song_attempts);
CHECK(appstate_song_copy(0, &sq) == 0 && strcmp(sq.name, "Song1000000") == 0,
"row 0 re-materialized name='%s'", sq.name);
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
/* 清理临时盘缓存(≤400 个小文件) */
{
char cmd[600];
snprintf(cmd, sizeof cmd, "rm -rf %s", cdt);
if (system(cmd) != 0) fprintf(stderr, "cleanup failed: %s\n", cdt);
}
meow_ui_shutdown();
return g_fail ? 1 : 0;
}