[agent-239] 仅改 patch_v2/ncm_app/。 1. 歌单分页(REPORT §14.1): track/all 全 404(§13.1) 弃用, 回 detail(n=1)全量 trackIds + song_detail 分批(≤NCM_SONG_BATCH=100) 触底逐页加载。appstate_load_playlist_songs_page offset=0 重建 song_ids[]/track_ids_count 载第0页, offset>0 追加(失败保留已载); playlist.c g_pl_more=(song_count<track_ids_count && <NCM_SONG_MAX), 触底 g_ploffset=song_count 起下一页(保留滚动), 首屏失败整页空态+点按重试, 追加失败 snack+保留已载。NCM_SONG_MAX 256→1000(单页渲染上限, bss +~1MB), NCM_SONG_BATCH 64→100, 删 NCM_PLAYLIST_PAGE(track/all 遗留)。 2. 专辑封面(§14.2): cover.c 封面改走 ncm_cache(LRU, 键=djb2(url)→song_cov<h>.jpg 同池), 缓存命中免下载/未命中下载→open/write/commit 落盘, 仍写 cover_current.jpg 供 launcher; 顺带修真 bug: save_cover_raw 在已持 g_cm 临界区内再 lock(非递归死锁)→封面永不刷新, 改调用方持锁。 歌单列表/详情头块封面保持现状。 3. 歌词后台加载(§14.3): lyric_worker 已异步(§11)未改线程模型; 补 lyric.c g_lyric_mu 数据竞争锁, 写侧 lyric_parse 整段持锁(清空用 lyric_clear_nolock 免二次取锁), 读侧 lyric_count/line_at/ line_text/line_tl 逐次取锁, 消除 UI/IPC 与 worker 间数据竞争(原 §10.9 FM4 良性竞态)。 验证(0 真实 NCM 请求): bench/pagination_sim.c 离线 stub 驱动真实 appstate 分页函数全 PASS (130首读全程/5191首封顶1000/空/越界无操作); mipsel build_ncm.sh 0 warning(text 842820/bss 2052068); 宿主 build_ncm_host.sh+MEOW_UI_DUMP home/playlist/player/lands 渲染正常无崩溃; host SDK_SRC 补 meow_pinyin(拼音IME py_predict 漏编致链接断)。ncm md5 b34bc1ed, ncm_host md5 1a2327e0。 未动 sdk/(主会话改 meow_ui_ime.c)/meow_launcherd/meow_amsd/vendor/core; 未 commit 它们。
145 lines
6.9 KiB
C
145 lines
6.9 KiB
C
/* bench/pagination_sim.c — 三期第一批 歌单分页 离线状态机验证(0 网络请求)。
|
||
*
|
||
* 直接驱动 App 的真实函数 appstate_load_playlist_songs_page()(app/logic/appstate.c),
|
||
* 仅把两个网络端点(ncm_api_playlist_detail / ncm_api_song_detail)替换成本地 stub
|
||
* (合成 trackIds / 合成歌曲对象),验证 detail 后按页 song_detail 的累加/封顶/尾部语义:
|
||
* ① offset=0 重建索引(song_ids[]←全量 trackIds,track_ids_count)并载第 0 页;
|
||
* ② 逐页 offset 前进,song_count 单调累加;
|
||
* ③ 尾部(song_count==track_ids_count)不再载下一页;④ NCM_SONG_MAX 封顶不越界。
|
||
* 风控纪律:本验证 0 次真实 NCM 请求(离线 stub)。歌曲上限 = NCM_SONG_MAX(内存封顶);
|
||
* 本模拟用两种歌单规模:(a) 130 首(< 上限,读全程);(b) 5191 首(> 上限,读满 NCM_SONG_MAX 截断)。
|
||
*
|
||
* 编译(宿主):$CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||
* -Icore -I../vendor/jsmn -c bench/pagination_sim.c -o /tmp/ps.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/ps_app.o
|
||
* $CC_HOST -O2 -Wl,--gc-sections -o pagination_sim /tmp/ps.o /tmp/ps_app.o -lpthread
|
||
* 运行:./pagination_sim (0 网络;PASS/FAIL 退出码)
|
||
*/
|
||
#include "../app/ncm_app_internal.h"
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
|
||
/* appstate.c 依赖的全局(app.c 提供;本 bench 只测分页路径,仅 g_st 被用到) */
|
||
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;
|
||
|
||
/* ncm_strlcpy:appstate_apply_playlist_cover 用到(从 core ncm_common 借;此处最小内联实现) */
|
||
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(离线,合成数据) ---- */
|
||
static int g_pid_total; /* detail 应返回的 trackIds 总数(供测试注入) */
|
||
static int g_detail_calls;
|
||
static int g_song_calls;
|
||
|
||
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;
|
||
g_detail_calls++;
|
||
if (name) snprintf(name, name_sz, "Sim Playlist");
|
||
if (cover) cover[0] = 0;
|
||
int n = (g_pid_total < id_cap) ? g_pid_total : id_cap;
|
||
for (int i = 0; i < n; i++) ids[i] = 1000000LL + i; /* 合成连续 id */
|
||
*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;
|
||
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].al_pic, sizeof out[i].al_pic, "http://p.music.126.net/cov%lld.jpg", (long long)ids[i]);
|
||
}
|
||
*out_n = m;
|
||
return NCM_OK;
|
||
}
|
||
|
||
/* ---- 驱动:整批载入到尾部/上限,返回最终 song_count ---- */
|
||
static int load_all(int pid_total, int *detail_calls, int *song_calls)
|
||
{
|
||
memset(&g_st, 0, sizeof g_st);
|
||
g_st.playlist_count = 1;
|
||
snprintf(g_st.playlists[0].id, sizeof g_st.playlists[0].id, "pl0");
|
||
g_st.playlists[0].track_count = pid_total;
|
||
g_pid_total = pid_total;
|
||
g_detail_calls = 0; g_song_calls = 0;
|
||
|
||
int rc = appstate_load_playlist_songs_page(0, 0, NCM_SONG_BATCH);
|
||
if (rc != NCM_OK) { printf(" page0 rc=%d\n", rc); return -1; }
|
||
int off = g_st.song_count;
|
||
while (off < g_st.track_ids_count && off < NCM_SONG_MAX) {
|
||
rc = appstate_load_playlist_songs_page(0, off, NCM_SONG_BATCH);
|
||
if (rc != NCM_OK) { printf(" page(%d) rc=%d\n", off, rc); return -1; }
|
||
int nxt = g_st.song_count;
|
||
if (nxt <= off) break;
|
||
off = nxt;
|
||
}
|
||
if (detail_calls) *detail_calls = g_detail_calls;
|
||
if (song_calls) *song_calls = g_song_calls;
|
||
return g_st.song_count;
|
||
}
|
||
|
||
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)
|
||
|
||
int main(void)
|
||
{
|
||
/* (a) 130 首(< NCM_SONG_MAX):应读全程,track_ids_count=130,song_count=130,尾部停。 */
|
||
printf("== case A: 130 songs (reads to tail) ==\n");
|
||
int dc = 0, sc = 0;
|
||
int n = load_all(130, &dc, &sc);
|
||
CHECK(n == 130, "song_count=%d (expect 130)", n);
|
||
CHECK(g_st.track_ids_count == 130, "track_ids_count=%d (expect 130)", g_st.track_ids_count);
|
||
CHECK(dc == 1, "detail calls=%d (expect 1, index fetched once)", dc);
|
||
CHECK(sc == 2, "song_detail calls=%d (expect 2 = 100 + 30 pages)", sc);
|
||
CHECK(strcmp(g_st.songs[129].name, "Song1000129") == 0, "last song name='%s'", g_st.songs[129].name);
|
||
CHECK(g_st.songs[129].al_pic[0] != 0, "last song has al_pic='%s'", g_st.songs[129].al_pic);
|
||
/* 越过尾部再 load:应无操作(不越界) */
|
||
int rc = appstate_load_playlist_songs_page(0, 130, NCM_SONG_BATCH);
|
||
CHECK(rc == NCM_OK && g_st.song_count == 130, "load past tail: rc=%d song_count=%d", rc, g_st.song_count);
|
||
|
||
/* (b) 5191 首(> NCM_SONG_MAX):索引封顶 NCM_SONG_MAX,读满封顶截断。 */
|
||
printf("== case B: 5191 songs (capped at NCM_SONG_MAX) ==\n");
|
||
dc = sc = 0;
|
||
n = load_all(5191, &dc, &sc);
|
||
CHECK(n == NCM_SONG_MAX, "song_count=%d (expect NCM_SONG_MAX=%d)", n, NCM_SONG_MAX);
|
||
CHECK(g_st.track_ids_count == NCM_SONG_MAX, "track_ids_count=%d (index capped)", g_st.track_ids_count);
|
||
CHECK(dc == 1, "detail calls=%d", dc);
|
||
CHECK(sc == NCM_SONG_MAX / NCM_SONG_BATCH, "song_detail calls=%d (expect %d full pages)", sc, NCM_SONG_MAX / NCM_SONG_BATCH);
|
||
/* 越界再 load:应 NCM_OK 但不动 song_count */
|
||
rc = appstate_load_playlist_songs_page(0, NCM_SONG_MAX, NCM_SONG_BATCH);
|
||
CHECK(rc == NCM_OK && g_st.song_count == NCM_SONG_MAX, "load past cap: rc=%d song_count=%d", rc, g_st.song_count);
|
||
|
||
/* (c) 空歌单(0 首) */
|
||
printf("== case C: empty playlist ==\n");
|
||
n = load_all(0, &dc, &sc);
|
||
CHECK(n == 0 && g_st.track_ids_count == 0, "empty: song_count=%d track_ids_count=%d", n, g_st.track_ids_count);
|
||
|
||
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
|
||
return g_fail ? 1 : 0;
|
||
}
|