Files
MeowOS 997f89f879 ncm 三期三批: 歌曲对象瘦身(1324→608B)+滑动窗口(NCM_SONG_WINDOW=256, 取消1000截断支撑任意长度歌单)+播放快照解耦 — agent-251
- core/ncm_api.h: ncm_song_t 瘦身 id24/name128/art96/al96/pic256/dt int32 (1324B→608B)
- appstate.c 滑动窗口: song_ids 全量 trackIds 索引(16384) + songs[256] 窗口感知
  (song_valid 位图/window_base/滑窗 memmove 保留重叠), ensure_range 批网格(100)对齐
  预取, songmeta.c 盘缓存 <cache_dir>/meta/<id>.bin (回滚/二次进入 0 网络请求)
- playlist.c 触底分页退役→视口 ensure(±24 行), 占位行可直点(播放线程 fetch 物化),
  footer 改视口区间/总数
- 播放兼容: g_play.play_song 快照(mu), run_song 起播物化后全链路(下载/歌词/IPC META/
  播放页 UI)读快照——窗口滑出当前播放曲不崩; 边界 song_count→track_ids_count
- 验证(0 真实请求): pagination_sim 全 PASS(5191 无截断/52 批); window_sim 6000 首
  全程下滚 750 步视口全物化/song_detail 恰 60 次/回滚+0 请求/播放 fetch 不滑窗;
  core 宿主+qemu 全 PASS; mipsel -Wall -Wextra 0 warning, bss 2052068→1080900(-47%)
- 未部署真机(设备音频线排查中, 部署留主会话)
2026-09-01 04:03:01 +08:00

232 lines
11 KiB
C
Raw Permalink 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/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 <stdio.h>
#include <string.h>
#include <stdlib.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;
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;
}