/* bench/window_sim.c — 三期第三批 滑动窗口 6000 首滚动全程离线验证(0 网络请求)。 * * 驱动真实 appstate.c(滑动窗口)+ 真实 songmeta.c(盘缓存,/tmp 临时目录), * 网络端点(ncm_api_playlist_detail / ncm_api_song_detail)为本地合成 stub。 * 场景(对齐用户「我喜欢」6000+ 首): * ① 重头:detail 取全量 6000 trackIds,物化首段(1 detail + 1 song_detail 批); * ② 全程下滚:视口 12 行、步进 8 行(模拟跟手拖动),每步 pl_maybe_ensure 等价物 * ensure(pos-24, pos+36);每步断言视口 12 行全部物化(列表无占位破窗); * 断言 song_detail 调用数 ≈ 6000/批网格(批对齐预取,无小请求放大); * ③ 全程回滚到 0:盘缓存(songmeta)全命中,song_detail 调用数 0 增长; * ④ 播放兼容:窗口感知的取歌——播放线程路径 appstate_song_fetch 在窗口远离时 * 仍能物化当前播放曲(不滑窗、不崩、盘缓存命中免请求); * ⑤ 尾部/越界:ensure 尾部区间 + 越过尾部无操作; * ⑥ 内存账:6000 首场景改前(1000 截断 × 1324B)vs 改后(窗口 256×608B + 索引)。 * 风控纪律:0 次真实 NCM 请求(离线 stub + /tmp 盘缓存)。 * * 编译(宿主):$CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * -Icore -I../vendor/jsmn -c bench/window_sim.c -o /tmp/ws.o * $CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * -Icore -I../vendor/jsmn -c app/logic/appstate.c -o /tmp/ws_app.o * $CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * -Icore -I../vendor/jsmn -c app/logic/songmeta.c -o /tmp/ws_meta.o * $CC_HOST -O2 -Wl,--gc-sections -o window_sim /tmp/ws.o /tmp/ws_app.o /tmp/ws_meta.o -lpthread * 运行:./window_sim (0 网络;PASS/FAIL 退出码) */ #include "../app/ncm_app_internal.h" #include #include #include 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; 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; } /* ---- 网络端点 stub(离线,合成数据) ---- */ #define SIM_TOTAL 6000 static int g_detail_calls; static int g_song_calls; static int g_song_served; /* 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)cover_sz; (void)playlist_id; 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] = 2000000LL + 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_calls++; int m = (n < max) ? n : max; g_song_served += m; 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]); snprintf(out[i].al_pic, sizeof out[i].al_pic, "http://p.music.126.net/cov%lld.jpg", (long long)ids[i]); out[i].duration_ms = 200000; } *out_n = m; return NCM_OK; } 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) /* 模拟 playlist.c pl_maybe_ensure:视口 12 行 ±24 行余量。 */ static int ui_ensure(int pos) { int lo = pos - 24, hi = pos + 12 + 24; if (lo < 0) lo = 0; if (hi > SIM_TOTAL) hi = SIM_TOTAL; if (lo >= hi) return NCM_OK; if (appstate_range_loaded(lo, hi)) return NCM_OK; return appstate_song_ensure_range(lo, hi); } int main(void) { if (system("rm -rf /tmp/ncm_window_sim_cache") != 0) { /* 清上次运行残留(盘缓存) */ printf("FAIL cannot clean /tmp cache dir\n"); return 1; } memset(&g_st, 0, sizeof g_st); snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "/tmp/ncm_window_sim_cache"); g_st.playlist_count = 1; snprintf(g_st.playlists[0].id, sizeof g_st.playlists[0].id, "pl6000"); g_st.playlists[0].track_count = SIM_TOTAL; size_t win_bytes = sizeof g_st.songs + sizeof g_st.song_valid; size_t idx_bytes = sizeof g_st.song_ids; size_t old_bytes = (size_t)1000 * 1324; /* §14:NCM_SONG_MAX=1000 × ~1324B/首 */ printf("== memory model ==\n"); printf(" ncm_song_t=%zuB (old 1324B) window=%zuB(%d首) trackIds idx=%zuB(%d首)\n", sizeof(ncm_song_t), win_bytes, NCM_SONG_WINDOW, idx_bytes, NCM_TRACKID_MAX); printf(" 6000首场景:旧模型 %zuKB(且 1000 截断)→ 新模型 %zuKB(任意长度恒定)\n", old_bytes / 1024, (win_bytes + idx_bytes) / 1024); /* ① 重头 */ printf("== phase 1: open 6000-song playlist ==\n"); int rc = appstate_load_playlist_songs_page(0, 0, NCM_SONG_BATCH); CHECK(rc == NCM_OK, "page0 rc=%d", rc); CHECK(g_st.track_ids_count == SIM_TOTAL, "track_ids_count=%d (expect %d, 无截断)", g_st.track_ids_count, SIM_TOTAL); CHECK(g_st.song_count == NCM_SONG_BATCH, "valid slots=%d (expect first batch %d)", g_st.song_count, NCM_SONG_BATCH); CHECK(g_st.window_base == 0, "window_base=%d", g_st.window_base); CHECK(g_detail_calls == 1 && g_song_calls == 1, "detail=%d song_detail=%d (expect 1+1)", g_detail_calls, g_song_calls); /* ② 全程下滚(0→6000,步进 8 行):视口每步全部物化 */ printf("== phase 2: scroll down 0 -> %d (step 8 rows) ==\n", SIM_TOTAL); int step_checks = 0; for (int pos = 0; pos < SIM_TOTAL; pos += 8) { rc = ui_ensure(pos); if (rc != NCM_OK) { printf(" FAIL ensure(pos=%d) rc=%d\n", pos, rc); g_fail = 1; break; } int vhi = pos + 12; if (vhi > SIM_TOTAL) vhi = SIM_TOTAL; if (!appstate_range_loaded(pos, vhi)) { printf(" FAIL viewport [%d,%d) not fully materialized\n", pos, vhi); g_fail = 1; break; } step_checks++; } CHECK(step_checks == (SIM_TOTAL + 7) / 8, "scroll steps verified=%d (expect %d)", step_checks, (SIM_TOTAL + 7) / 8); CHECK(g_st.song_count <= NCM_SONG_WINDOW, "valid slots=%d ≤ window %d (全程不超窗)", g_st.song_count, NCM_SONG_WINDOW); printf(" INFO song_detail calls=%d served=%d (grid prefetch; 下限 %d)\n", g_song_calls, g_song_served, SIM_TOTAL / NCM_SONG_BATCH); CHECK(g_song_calls <= SIM_TOTAL / NCM_SONG_BATCH + 6, "song_detail calls=%d ≤ %d (批网格对齐,无小请求放大)", g_song_calls, SIM_TOTAL / NCM_SONG_BATCH + 6); ncm_song_t sq; CHECK(appstate_song_copy(SIM_TOTAL - 1, &sq) == 0 && strcmp(sq.name, "Song2005999") == 0, "tail song name='%s'", sq.name); CHECK(appstate_song_copy(0, &sq) != 0, "head evicted after scrolling to tail"); /* ③ 全程回滚到 0:盘缓存全命中,0 新增 song_detail */ printf("== phase 3: scroll back up %d -> 0 (disk cache) ==\n", SIM_TOTAL); int calls_before = g_song_calls; step_checks = 0; for (int pos = SIM_TOTAL - 8; pos >= 0; pos -= 8) { rc = ui_ensure(pos); if (rc != NCM_OK) { printf(" FAIL ensure(pos=%d) rc=%d\n", pos, rc); g_fail = 1; break; } int vhi = pos + 12; if (vhi > SIM_TOTAL) vhi = SIM_TOTAL; if (!appstate_range_loaded(pos, vhi)) { printf(" FAIL viewport [%d,%d) not materialized on way back\n", pos, vhi); g_fail = 1; break; } step_checks++; } CHECK(g_song_calls == calls_before, "scroll-back song_detail calls +%d (expect 0,盘缓存全命中)", g_song_calls - calls_before); CHECK(appstate_song_copy(0, &sq) == 0 && strcmp(sq.name, "Song2000000") == 0, "head re-materialized from disk: name='%s'", sq.name); /* ④ 播放兼容:窗口滑出当前播放曲 → fetch 不滑窗、不崩、盘命中免请求 */ printf("== phase 4: playback survives window slide ==\n"); rc = appstate_song_ensure_range(3000 - 128, 3000 + 128); /* 窗口移到 3000 附近 */ int base_at_play = g_st.window_base; CHECK(rc == NCM_OK, "ensure around 3000 rc=%d", rc); /* 模拟 run_song:cur=3050 在窗内 → song_copy;然后窗口滑回 0,再 fetch(3050) */ CHECK(appstate_song_copy(3050, &sq) == 0, "song_copy(3050) in window"); rc = appstate_song_ensure_range(0, 100); /* 用户滚回顶部(窗口滑走) */ CHECK(rc == NCM_OK && g_st.window_base != base_at_play, "window slid away base %d -> %d", base_at_play, g_st.window_base); CHECK(appstate_song_copy(3050, &sq) != 0, "playing song evicted from window"); calls_before = g_song_calls; rc = appstate_song_fetch(3050, &sq); /* 播放线程路径:不滑窗 */ CHECK(rc == NCM_OK && strcmp(sq.id, "2003050") == 0 && sq.duration_ms == 200000, "fetch(3050) ok id=%s dur=%d", sq.id, sq.duration_ms); CHECK(g_st.window_base != base_at_play && g_st.song_count <= NCM_SONG_WINDOW, "fetch did not move window (base=%d)", g_st.window_base); CHECK(g_song_calls == calls_before, "fetch hit disk cache (0 new requests)"); /* next/prev 边界语义:cur 可到 SIM_TOTAL-1,不越界不崩 */ CHECK(appstate_song_fetch(SIM_TOTAL - 1, &sq) == NCM_OK, "fetch(tail) ok"); CHECK(appstate_song_fetch(SIM_TOTAL, &sq) != NCM_OK, "fetch(out of range) rejected"); /* ⑤ 尾部/越界 ensure */ printf("== phase 5: tail & out-of-range ==\n"); rc = appstate_song_ensure_range(SIM_TOTAL - 12, SIM_TOTAL); CHECK(rc == NCM_OK && appstate_range_loaded(SIM_TOTAL - 12, SIM_TOTAL), "tail ensure rc=%d", rc); CHECK(g_st.window_base <= SIM_TOTAL - NCM_SONG_WINDOW || SIM_TOTAL < NCM_SONG_WINDOW, "base=%d clamped at tail", g_st.window_base); rc = appstate_song_ensure_range(SIM_TOTAL, SIM_TOTAL + 100); CHECK(rc == NCM_OK, "ensure past tail no-op rc=%d", rc); rc = appstate_song_ensure_range(-50, 0); CHECK(rc == NCM_OK, "ensure empty range no-op rc=%d", rc); /* ⑥ 盘缓存规模核对 */ printf("== phase 6: disk cache ==\n"); { char cmd[256]; snprintf(cmd, sizeof cmd, "ls /tmp/ncm_window_sim_cache/meta | wc -l"); FILE *p = popen(cmd, "r"); int files = -1; if (p) { if (fscanf(p, "%d", &files) != 1) files = -1; pclose(p); } CHECK(files == SIM_TOTAL, "songmeta files=%d (expect %d, 全程拉过即落盘)", files, SIM_TOTAL); } printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS"); return g_fail ? 1 : 0; }