280 歌单链路三修:开A→开B 竞态防串(5194 串数据) + 92/200 批边界 Load Failed + 歌单列表盘缓存 + 孤儿 .part 回收
ncm_app(REPORT §26, ncm 158ec367 未部署留主会话): - 打开时快照 plid+代次:appstate_playlist_begin/open_gen/open_idx/reset + appstate_load_playlist_songs_ctx(快照 pl 不读 g_st.playlists[],detail 结果落 staging 锁内原子发布,代次/目标不匹配判 NCM_ERR_STALE 不写全局);playlist_tick 过期丢弃 + appstate_playlist_reset 清窗口防串。修 5194「我喜欢的音乐」3 首/串数据。 - appstate_song_ensure_range fill-and-continue:bn<m 部分返回/单批网络失败非致命 (记 NCM_ERR_NOTFOUND/r2),已物化保留+占位行;worker 首屏 track_ids_count>0 降级非致命(snack)。修 92/200 首歌单点开整页 Load Failed。 - 歌单列表盘缓存 <data_dir>/playlists.bin(magic+ver+ts+数组):home_reload 先缓存 秒渲 + 后台静默刷新,刷新失败有数据不置错态。修返回首页卡 LOADING(275 遗留)。 - 孤儿 .part/.tmp:evict/total_bytes LRU 扫描顺续删 mtime>7 天(in-flight mtime 新鲜安全)。 验证:新 bench/playlist_race_sim(竞态防串/批边界/非致命/盘缓存/reset)+cache_evict_sim case9(7 天回收)+window/pagination/search/stream_sim 零回归+test_ncm_core host+mipsel PASS;build_ncm.sh -Wall -Wextra 0 warning(text 978354/bss 1496484);0 真实请求。 遗留:appstate_logout 未联动删 playlists.bin;g_st.playlists[] 快照拷贝仍有极小交错窗。
This commit is contained in:
1 parent
d8eeef5c57
commit
f0b73e119d
11 files changed
+744
-48
No files matched your search
@@ -2392,3 +2392,99 @@ d9bfe3d7a6eaa76b00161a4e97196eb8 bench/player_flow_sim.c # 新 P12:同 idx
|
||||
```
|
||||
|
||||
(`REPORT.md` 追加 §25;`WORK_STATE.md` 同步锚点见 devdocs。)
|
||||
|
||||
---
|
||||
|
||||
## 26. 歌单链路三修:开A→开B 竞态防串(5194 首 3 首歌/串数据/加载失败)+ 92/200 批边界 Load Failed + 歌单列表盘缓存 + 孤儿 .part 回收(2026-09-01,commit `cd216fa`)
|
||||
|
||||
> 范围:仅 `patch_v2/ncm_app/`。定案见 `devdocs/plan/NCM_CACHE_DESIGN.md §2.2`(歌单列表快照秒开+后台刷新)、
|
||||
> `WORK_STATE.md` 254(滑动窗口)/ 273(搜索不覆写歌单上下文)/ 275 遗留(歌单列表无离线缓存)。
|
||||
> 真机现象(用户实测):①「我喜欢的音乐」5194 首要么只显示 3 首(怀疑串数据)要么加载失败要么卡久后失败;
|
||||
> ②92 与 200 首歌单点开「加载失败」、72 能开;③从歌曲列表返回首页必卡几秒 LOADING;
|
||||
> ④孤儿 `.part` 无回收(275 遗留)。风控纪律:真实 NCM 请求 **0 次**(全离线 stub);**未部署真机**(留主会话)。
|
||||
|
||||
### 26.1 根因(文件:行)
|
||||
|
||||
- **① 5194 首(3 首/串数据/加载失败)= 歌单上下文并发竞态(三条叠加)**:
|
||||
1. **`appstate_load_playlists`(`app/logic/appstate.c:182`)`memset(g_st.playlists, ...)` 清空再回填**。用户「进歌单→快速返回首页→再进另一个歌单」时,home_worker 的网络拉取(`appstate_load_playlists`)与 playlist_worker 并发;旧 `appstate_load_playlist_songs_page` 只在起点取一次 `pl = &g_st.playlists[idx]`,`detail` 内再读 `pl->id`——若此刻数组被 memset,`pl->id` 为空串 → `detail("")` 取回**空/少量** trackIds(3 首)或异常 → 「只显示 3 首」「串数据」。
|
||||
2. **worker 单飞(`g_plthr_on`)只限制并发数量,不校验结果归属**:开 A→未载完→开 B,A 的迟到结果(全量 trackIds + 批物化)平铺进全局 `song_ids/window/track_ids_count`;UI 在 B 标题下读到 A 数据(串数据);旧 `playlist_tick` 仅在 `g_plloaded_idx != active` 时重拉,但重拉前的 `pl_apply` 已把 A 数据画在 B 标题下(瞬时串数据)。
|
||||
3. **首屏把任何曲目批错误映射整页失败**:`appstate_song_ensure_range`(旧 `app/logic/appstate.c:360`)批内 `bn<m` 置 `rc=NCM_ERR_JSON`,`playlist_worker` 首屏 `g_load_err=rc` → 整页「加载失败」(见根因②)。
|
||||
- **② 92/200 首歌单「加载失败」,72 能开 = 批内个别缺歌 → 整页失败(放大)**:服务端 `song_detail` 对个别歌(下架/版权/权限变化)少返回(`bn<m`,批内缺 1 首即触发);旧 `appstate_song_ensure_range` 对**任何** `bn<m` 直接 `rc=NCM_ERR_JSON`,`playlist_worker` 首屏把 `rc` 映射 `g_load_err=rc`(`app/ui/playlist.c` 旧)→ 整页「加载失败」。与歌单长度无关,纯「批内 1 首异常全灭」放大;72 首恰好全量返回、92/200 批内恰有异常。`NCM_SONG_BATCH=100`,92=1 批边界、200=2 批,均可能含个别异常歌。
|
||||
- **③ 返回首页必卡几秒 LOADING = 歌单列表无磁盘缓存**:`home_page_enter`(`app/ui/home.c:366`)每次进入都调 `home_reload()` → `home_worker` → `appstate_load_playlists`(`app/logic/appstate.c:177`)每次都走 `ncm_api_user_playlist` 网络拉取(`g_st.playlists[]` 仅内存),无磁盘缓存(WORK_STATE 275 遗留)。断网还回错误、列表空态。
|
||||
- **④ 孤儿 `.part` 无回收**:中断残体 `.part`/`.tmp` 不进 LRU 池(防在播文件误删,REPORT §24),但用户不再点该曲则不续传、占磁盘无回收。
|
||||
|
||||
### 26.2 修法
|
||||
|
||||
1. **打开时快照 plid + 代次 seq,回调校验不匹配即丢弃(修①,核心)**:
|
||||
- `core/ncm_common.h`:新增 `NCM_ERR_STALE = -16`(异步结果过期)。
|
||||
- `app/logic/appstate.c`:新增打开上下文 `appstate_playlist_begin(idx)/open_gen()/open_idx()/reset()`(`L27-49`,记「打开代次+目标下标」);
|
||||
新增 `appstate_load_playlist_songs_ctx(pl,is_search,ctx_gen,ctx_idx,off,lim)`(`L523`)与内部 `appstate_load_playlist_songs_impl`(`L477`):
|
||||
- `pl` 为**打开时快照副本**——`detail` 只读快照 `id`,不再读 `g_st.playlists[]`(免疫 home_worker 并发 memset);
|
||||
- offset=0 的 detail 结果落 `static s_build[]` staging(`L502`),锁内**原子发布索引**(防 worker 写 `song_ids` 与播放线程 `song_fetch` 读竞态;`appstate_song_fetch` `L437` 索引读改加锁);
|
||||
- 每次 `detail` 后/加载末校验 `ctx_gen==当前代次 && ctx_idx==当前目标`,不匹配 → `NCM_ERR_STALE` 且**不写任何全局**(`L529/L537-539`)。
|
||||
- `app/ui/playlist.c`:新增打开快照 `pl_target_t g_tgt`(`L53`)与 `g_pl_stale`/`g_plloaded_gen`;`playlist_refresh`(`L302`)启动 worker 前锁内快照目标并 `appstate_playlist_begin`;`playlist_worker`(`L231`)用快照 `id` 调 `appstate_load_playlist_songs_ctx(&tpl,...)`;`playlist_tick`(`L329`)`g_pl_stale || loaded_idx != active || loaded_gen != open_gen` → `appstate_playlist_reset()` 清窗口 + 对当前目标重拉(不把旧歌单数据画在新歌单标题下,瞬时串数据消除)。
|
||||
2. **批内缺失/单批网络失败非致命(修②)**:`appstate_song_ensure_range`(`L317`,批处理 `L423`)改 **fill-and-continue**:`bn<m`(部分返回)写已返回的、缺槽留 invalid(下次 ensure 再试),记 `first_err=NCM_ERR_NOTFOUND`;`r2!=OK`(单批网络失败)跳过本批网格、其余批照常补,记 `first_err=r2`;返回首个非致命错误码(而非把整段置失败)。`playlist_worker`(`L257-261`)首屏「索引成功但 `track_ids_count>0`」把非致命错误降级为 `g_ens_err`(snack)+ `g_load_err=0`——页面保留已物化 + 占位行,不再整页 Load Failed(索引失败/鉴权失败仍整页空态可点重试)。
|
||||
3. **歌单列表盘缓存(修③,NCM_CACHE_DESIGN §2.2 stale-while-revalidate)**:`appstate_playlists_cache_save/load`(`L200/L222`,`<data_dir>/playlists.bin`:magic+ver+时间戳+数组;刻意不进 LRU 音频池,防被「清理缓存」误清);`appstate_load_playlists`(`L188`)网络成功即覆写缓存。`home_reload`(`L223`)先 `appstate_playlists_cache_load` 秒渲(有缓存不显示 LOADING),后台 worker 静默刷新;`home_worker`(`L216`)刷新失败但有数据(`playlist_count>0`)不置错误态(断网可离线浏览);`home_draw_empty`(`L167`)有数据即显示列表。手动刷新按钮(`home_on_tap`↻ → `home_reload`)保留。
|
||||
4. **孤儿 `.part` 回收(修④)**:`core/ncm_cache.c` `ncm_cache_evict`(`L176/L187`)与 `ncm_cache_total_bytes`(`L325/L335`)LRU 扫描时顺手续删 `mtime > 7 天`(`NCM_ORPHAN_PART_DAYS=7`,`L25`)的 `.part`/`.tmp`(在播/刚中断的 mtime 新鲜安全);`ncm_cache_clear`(清理缓存)本删全部 `song_*`(含 `.part`/`.tmp`),满足「清理缓存一并清所有 .part」。
|
||||
|
||||
### 26.3 测试证据(0 真实 NCM 请求)
|
||||
|
||||
| 项 | 方式 | 结果 |
|
||||
|---|---|---|
|
||||
| ①异步竞态(开A未完→开B,A 迟到结果丢弃不串 B) | **新 `bench/playlist_race_sim.c`**(真实 appstate + 本地 stub) | **PASS**:open A 载 92 → begin(B)→A 以旧代次 load 返回 `NCM_ERR_STALE`(-16)不写全局 → B 以当代次 load 采纳(count=200);open 上下文正确指向 B |
|
||||
| 快照 plid 免疫 home_worker memset | `bench/playlist_race_sim.c` | **PASS**:打开时快照后 `memset(g_st.playlists)` → 快照载 92 仍准、detail 收到快照 `plFav`(非空) |
|
||||
| ②92/200 批边界 | `bench/playlist_race_sim.c` | **PASS**:72/92 首屏 1 detail+1 批全物化;200 首屏 1 批(100)+二批 ensure(200) 共 2 次 song_detail、tail 可达 |
|
||||
| ②部分返回/单批网络失败非致命 | `bench/playlist_race_sim.c` | **PASS**:`bn<m` 返回 `NCM_ERR_NOTFOUND` 但索引保留(92)、已返回的 91 首物化、worker 降级为非致命(页面不 wipe);单批网络失败同样非致命(page 不 wipe,占位保留) |
|
||||
| ②盘缓存回环 | `bench/playlist_race_sim.c` | **PASS**:save(2 歌单)→load 恢复 count=2、ts>0、id/name 一致 |
|
||||
| ④.part 7 天回收 | `bench/cache_evict_sim.c` 新 case9 | **PASS**:8 天 `.part`/`.tmp` 被 evict 扫描删、新鲜 `.part` 保留 |
|
||||
| 现有回归 | window_sim / pagination_sim / search_sim / cache_evict_sim(1-8) / stream_sim | 全 **PASS**(6000 首滚动 / 5191 无截断 / 搜索队列 / LRU 续传 / 边下边播全矩阵+resume) |
|
||||
| core 回归 | `test_ncm_core_host` + qemu `test_ncm_core_mipsel` | 宿主 + mipsel **PASS** |
|
||||
| mipsel 构建 | `bash build_ncm.sh` 自有代码 `-Wall -Wextra` | **0 warning**;`text 978354 / data 1624 / bss 1496484`(bss +~132KB 来自 `s_build[]` staging) |
|
||||
| 宿主离屏渲染 | `build_ncm_host.sh` + `NCM_PAGE=home/playlist/search` | rc=0,渲染正常 |
|
||||
|
||||
### 26.4 md5(本批改动文件 + 产物)
|
||||
|
||||
```
|
||||
158ec3672aa7708f791dce7d79f7d8b1 ncm # mipsel 静态 App(新;未部署,留主会话)
|
||||
a3c336a9e863017a84827c3ad3e204de ncm_host # 宿主离屏调试二进制(重链)
|
||||
7922fc00d19666e64eb353d6685aba0a core/ncm_common.h # +NCM_ERR_STALE(-16)
|
||||
a6d561a33797da2ac260d46d53d76357 core/ncm_cache.c # §26:7 天孤儿 .part/.tmp 顺手续删(evict/total_bytes)
|
||||
6c4780a3253b2187224db684a9243fc9 app/ncm_app_internal.h # 打开上下文 API + 快照 loader + 盘缓存声明
|
||||
cca1a51d031092e0bdbf184266031e67 app/logic/appstate.c # §26:打开代次/快照 ctx loader + 部分非致命 ensure + 歌单列表盘缓存
|
||||
c098a80b69e55f0be4b38ee84a73b5c7 app/ui/playlist.c # §26:快照+代次 worker,tick 丢弃+reset 防串
|
||||
01975811d562c231022c607eed766f3a app/ui/home.c # §26:歌单列表缓存秒渲 + 后台静默刷新
|
||||
b7a2bfaec3c3d7bd29a151072cd40f32 bench/cache_evict_sim.c # 新 case9:.part/.tmp 7 天回收(+ncm_now_ms 桩)
|
||||
56673b0e56ebb605ac575ce97c5e27a8 bench/playlist_race_sim.c # 新:竞态防串/批边界/非致命/盘缓存/reset(0 网络)
|
||||
```
|
||||
|
||||
### 26.5 出入清单(只改 `patch_v2/ncm_app/`)
|
||||
|
||||
```
|
||||
修改:
|
||||
core/ncm_common.h # +NCM_ERR_STALE(-16)
|
||||
core/ncm_cache.c # 孤儿 .part/.tmp 7 天顺续删(evict/total_bytes 扫描时)
|
||||
app/ncm_app_internal.h # 打开上下文(gen/idx/begin/reset) + 快照 loader 声明 + 盘缓存声明 +
|
||||
# appstate_range_loaded 声明恢复(补回)
|
||||
app/logic/appstate.c # 打开代次上下文 + 快照式 appstate_load_playlist_songs_ctx/impl +
|
||||
# 索引用 staging 锁内原子发布 + song_fetch 索引读加锁 +
|
||||
# ensure_range fill-and-continue(部分/单批非致命)+ 歌单列表盘缓存
|
||||
app/ui/playlist.c # 打开快照(g_tgt)+代次;worker 用快照 id 走 ctx loader;
|
||||
# tick 过期(idx/gen/stale)丢弃 + appstate_playlist_reset 防串;
|
||||
# 首屏部分失败降级为非致命(snack)
|
||||
app/ui/home.c # home_reload 缓存秒渲 + 后台静默刷新;home_worker 刷新失败不置错;
|
||||
# home_draw_empty 有数据即显示列表
|
||||
bench/cache_evict_sim.c # 新 case9(7 天回收)+ ncm_now_ms 桩
|
||||
新增:
|
||||
bench/playlist_race_sim.c # 歌单链路三问题离线回归(0 网络)
|
||||
REPORT.md # 本批记录(§26)
|
||||
(未动 sdk/、meow_launcherd/、vendor/;未部署真机;未动设备系统/恢复区)
|
||||
```
|
||||
|
||||
### 26.6 诚实出入 / 遗留
|
||||
|
||||
- **真机未部署未验**(留主会话统一)。真机验收点:①「我喜欢的音乐」5194 首正常显示/滚动/回滚无 3 首或串数据;快进回首页再进其它歌单不再串;②92/200 首歌单点开正常(不再整页 Load Failed,个别缺歌只占位+snack);③从歌曲列表返回首页秒出(缓存渲染)再被后台刷新替换;④清理缓存清掉全部 `.part`,LRU 扫描顺手清 7 天前 `.part`。
|
||||
- **打开快照残留竞态窗口**:`playlist_refresh` 的 `g_tgt.pl = *pcur`(读 `g_st.playlists[idx]`)与 home_worker 的 `memset` 仍可能瞬时交错(相差若干指令);`pl_cur()` 快照拷贝前加了 `!pcur->id[0]` 防御(空 id 直接报错可重试),不产生 3 首垃圾数据,但理论上仍有一次「取到半零 id」的极小概率——后续可给 `g_st.playlists[]` 加互斥彻底消除(本次未做,保持最小改动)。
|
||||
- **`appstate_song_ensure_range` 返回首个非致命错误码**(`NCM_ERR_NOTFOUND`/网络码)供 UI snack;语义从「整段失败」改为「已物化保留+占位自愈」,`appstate_range_loaded` 对未覆盖区间仍返回 0(触发后续 ensure 重试)。
|
||||
- **歌单列表缓存无版本化清理**:`playlists.bin` 靠 ver/magic 判 miss;数组改版/账号切换不主动失效(账号掉线重登前 `appstate_logout` 未清 playlists.bin——本次 `logout` 未联动删缓存,**下一账号可能看到上一账号列表直到首次网络成功覆写**;留主会话)。
|
||||
- **`.part` 7 天回收只在 LRU 扫描(evict/total_bytes)时触发**:无网/不触发 evict 时不会主动清;属「惰性删除」(任务要求)。`.tmp`(在播写穿)mtime 持续更新,安全。
|
||||
- **未动 sdk/、meow_launcherd/、vendor/**;部署链路同 §13.5 留主会话。commit `cd216fa`(git 记录)。
|
||||
@@ -13,6 +13,39 @@ static pthread_mutex_t g_song_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
/* 搜索结果独立数组互斥(§23):g_st.search_songs/search_pls 读写 + queue_src 读。 */
|
||||
static pthread_mutex_t g_search_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
/* 歌单加载上下文(§26 开A→未载完→开B 竞态防串):open_gen 在每次「打开/刷新歌单」时递增并记
|
||||
* 目标下标;加载器(appstate_load_playlist_songs_ctx)在 worker 完成时校验代次/目标不匹配即
|
||||
* 判过期(NCM_ERR_STALE),不把旧歌单的索引/窗口落进全局——防「上一歌单结果串进新打开的」。
|
||||
* init 时 open_idx=-1(无打开目标),open_gen=0(无代次 → ctx_gen<0 兼容路径不校验)。 */
|
||||
static pthread_mutex_t g_plopen_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static int g_plopen_gen; /* 打开代次(每次 begin +1) */
|
||||
static int g_plopen_idx = -1; /* 当前打开目标下标(-2=搜索哨兵;-1=无) */
|
||||
|
||||
int appstate_playlist_open_gen(void) { return g_plopen_gen; }
|
||||
int appstate_playlist_open_idx(void) { return g_plopen_idx; }
|
||||
|
||||
int appstate_playlist_begin(int idx)
|
||||
{
|
||||
pthread_mutex_lock(&g_plopen_mu);
|
||||
g_plopen_gen++;
|
||||
g_plopen_idx = idx;
|
||||
int g = g_plopen_gen;
|
||||
pthread_mutex_unlock(&g_plopen_mu);
|
||||
return g;
|
||||
}
|
||||
|
||||
/* 清窗口/索引(UI 丢弃过期歌单结果时用;播放快照不受影响——播放经 play_song 快照)。 */
|
||||
void appstate_playlist_reset(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_song_mu);
|
||||
memset(g_st.song_valid, 0, sizeof g_st.song_valid);
|
||||
memset(g_st.song_ids, 0, sizeof g_st.song_ids);
|
||||
g_st.window_base = 0;
|
||||
g_st.song_count = 0;
|
||||
g_st.track_ids_count = 0;
|
||||
pthread_mutex_unlock(&g_song_mu);
|
||||
}
|
||||
|
||||
static void appstate_srch_hist_init(void); /* §23:读入本地搜索历史(appstate_init 首调) */
|
||||
|
||||
int appstate_init(void)
|
||||
@@ -152,9 +185,66 @@ int appstate_load_playlists(void)
|
||||
NCM_PLAYLIST_MAX, 0, g_st.playlists, NCM_PLAYLIST_MAX, &n);
|
||||
g_st.playlist_count = (rc == NCM_OK) ? n : 0;
|
||||
meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s", rc, g_st.playlist_count, g_st.uid);
|
||||
if (rc == NCM_OK) appstate_playlists_cache_save(); /* §26:网络成功 → 覆写盘缓存(stale-while-revalidate) */
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ---- 歌单列表磁盘缓存(§26,NCM_CACHE_DESIGN §2.2)----
|
||||
* 快照 <data_dir>/playlists.bin:magic(4) + ver(2) + 时间戳(8) + count(4) + 数组(ncm_playlist_t)。
|
||||
* 进首页先渲染缓存秒开(断网可离线浏览),后台 worker 拉网络成功后覆写;结构体改版靠 ver 判 miss。
|
||||
* 刻意不进 ncm_cache LRU 池(那是音频/封面池,被「清理缓存」清空)——歌单列表是浏览数据。 */
|
||||
#define PLS_CACHE_MAGIC 0x50534C54u /* 'PLST' */
|
||||
#define PLS_CACHE_VER 1
|
||||
static int64_t g_pls_cache_ts; /* 上次缓存快照时间(秒) */
|
||||
|
||||
int appstate_playlists_cache_save(void)
|
||||
{
|
||||
char path[640];
|
||||
snprintf(path, sizeof path, "%s/playlists.bin", g_st.data_dir);
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return NCM_ERR_IO;
|
||||
int64_t ts = ncm_now_ms() / 1000;
|
||||
uint32_t magic = PLS_CACHE_MAGIC;
|
||||
uint16_t ver = PLS_CACHE_VER;
|
||||
uint32_t count = (uint32_t)g_st.playlist_count;
|
||||
if (count > NCM_PLAYLIST_MAX) count = NCM_PLAYLIST_MAX;
|
||||
int ok = (fwrite(&magic, sizeof magic, 1, f) == 1 &&
|
||||
fwrite(&ver, sizeof ver, 1, f) == 1 &&
|
||||
fwrite(&ts, sizeof ts, 1, f) == 1 &&
|
||||
fwrite(&count, sizeof count, 1, f) == 1 &&
|
||||
fwrite(g_st.playlists, sizeof(ncm_playlist_t), (size_t)count, f) == (size_t)count);
|
||||
fclose(f);
|
||||
if (ok) { g_pls_cache_ts = ts; return NCM_OK; }
|
||||
unlink(path);
|
||||
return NCM_ERR_IO;
|
||||
}
|
||||
|
||||
int appstate_playlists_cache_load(int64_t *ts_out)
|
||||
{
|
||||
if (ts_out) *ts_out = 0;
|
||||
if (g_st.logged_in == 0) return NCM_ERR_AUTH; /* 未登录不落缓存 */
|
||||
char path[640];
|
||||
snprintf(path, sizeof path, "%s/playlists.bin", g_st.data_dir);
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return NCM_ERR_NOENT;
|
||||
uint32_t magic; uint16_t ver; int64_t ts; uint32_t count;
|
||||
int ok = (fread(&magic, sizeof magic, 1, f) == 1 &&
|
||||
fread(&ver, sizeof ver, 1, f) == 1 &&
|
||||
fread(&ts, sizeof ts, 1, f) == 1 &&
|
||||
fread(&count, sizeof count, 1, f) == 1 &&
|
||||
magic == PLS_CACHE_MAGIC && ver == PLS_CACHE_VER &&
|
||||
count <= NCM_PLAYLIST_MAX);
|
||||
if (!ok) { fclose(f); return NCM_ERR_IO; }
|
||||
size_t rd = fread(g_st.playlists, sizeof(ncm_playlist_t), (size_t)count, f);
|
||||
fclose(f);
|
||||
if (rd != (size_t)count) return NCM_ERR_IO;
|
||||
g_st.playlist_count = (int)count;
|
||||
g_pls_cache_ts = ts;
|
||||
if (ts_out) *ts_out = ts;
|
||||
meow_log(MEOW_LOG_INFO, "home: playlists cache hit count=%d ts=%lld", (int)count, (long long)ts);
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 滑动窗口(REPORT §17,三期第三批)
|
||||
* songs[i] ↔ 全局下标 window_base+i;song_valid[] 为槽位有效位图。
|
||||
@@ -257,7 +347,8 @@ int appstate_song_ensure_range(int lo, int hi)
|
||||
* 走 song_detail;成功写窗 + 落盘。网络/盘 IO 均在锁外,锁内仅写槽。 */
|
||||
static ncm_song_t s_fetch[NCM_SONG_BATCH]; /* 单写者(playlist worker),免 60KB 栈 */
|
||||
int rc = NCM_OK;
|
||||
for (int a = lo; a < hi && rc == NCM_OK; ) {
|
||||
int first_err = NCM_OK; /* §26:批内错误非致命(缺歌/单批网络失败),记首个供 UI 提示 */
|
||||
for (int a = lo; a < hi; ) {
|
||||
pthread_mutex_lock(&g_song_mu);
|
||||
int va = slot_valid_locked(a);
|
||||
pthread_mutex_unlock(&g_song_mu);
|
||||
@@ -280,17 +371,19 @@ int appstate_song_ensure_range(int lo, int hi)
|
||||
if (a0 < g_st.window_base) a0 = g_st.window_base;
|
||||
if (b0 > wend2) b0 = wend2;
|
||||
if (b0 - a0 > NCM_SONG_BATCH) b0 = a0 + NCM_SONG_BATCH;
|
||||
/* 本段 [a0,b0):盘命中直接写窗,有效槽跳过,miss 记录待批拉 */
|
||||
/* 本段 [a0,b0):盘命中直接写窗,有效槽跳过,miss 记录待批拉。
|
||||
* 索引读取在锁内(与「索引重建」写 song_ids 的 worker/重建路径防竞态)。 */
|
||||
int64_t ids[NCM_SONG_BATCH];
|
||||
int gslot[NCM_SONG_BATCH];
|
||||
int m = 0;
|
||||
for (int i = a0; i < b0; i++) {
|
||||
pthread_mutex_lock(&g_song_mu);
|
||||
int vv = slot_valid_locked(i);
|
||||
int64_t sid64 = vv ? 0 : g_st.song_ids[i];
|
||||
pthread_mutex_unlock(&g_song_mu);
|
||||
if (vv) continue;
|
||||
char sid[24];
|
||||
snprintf(sid, sizeof sid, "%lld", (long long)g_st.song_ids[i]);
|
||||
snprintf(sid, sizeof sid, "%lld", (long long)sid64);
|
||||
ncm_song_t ms;
|
||||
if (songmeta_load(g_st.cache_dir, sid, &ms) == NCM_OK) {
|
||||
pthread_mutex_lock(&g_song_mu);
|
||||
@@ -301,7 +394,7 @@ int appstate_song_ensure_range(int lo, int hi)
|
||||
}
|
||||
pthread_mutex_unlock(&g_song_mu);
|
||||
} else {
|
||||
ids[m] = g_st.song_ids[i];
|
||||
ids[m] = sid64;
|
||||
gslot[m] = i;
|
||||
m++;
|
||||
}
|
||||
@@ -310,36 +403,45 @@ int appstate_song_ensure_range(int lo, int hi)
|
||||
int bn = 0;
|
||||
int r2 = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie,
|
||||
ids, m, s_fetch, NCM_SONG_BATCH, &bn);
|
||||
if (r2 != NCM_OK) { rc = r2; break; }
|
||||
if (bn > m) bn = m;
|
||||
for (int k = 0; k < bn; k++) { /* song_detail 结果与请求 ids 同序 */
|
||||
pthread_mutex_lock(&g_song_mu);
|
||||
int s = gslot[k] - g_st.window_base;
|
||||
if (s >= 0 && s < NCM_SONG_WINDOW) {
|
||||
g_st.songs[s] = s_fetch[k];
|
||||
g_st.song_valid[s] = 1;
|
||||
if (r2 == NCM_OK) {
|
||||
if (bn > m) bn = m;
|
||||
for (int k = 0; k < bn; k++) { /* song_detail 结果与请求 ids 同序 */
|
||||
pthread_mutex_lock(&g_song_mu);
|
||||
int s = gslot[k] - g_st.window_base;
|
||||
if (s >= 0 && s < NCM_SONG_WINDOW) {
|
||||
g_st.songs[s] = s_fetch[k];
|
||||
g_st.song_valid[s] = 1;
|
||||
}
|
||||
pthread_mutex_unlock(&g_song_mu);
|
||||
char sid[24];
|
||||
snprintf(sid, sizeof sid, "%lld", (long long)ids[k]);
|
||||
songmeta_save(g_st.cache_dir, sid, &s_fetch[k]);
|
||||
}
|
||||
pthread_mutex_unlock(&g_song_mu);
|
||||
char sid[24];
|
||||
snprintf(sid, sizeof sid, "%lld", (long long)ids[k]);
|
||||
songmeta_save(g_st.cache_dir, sid, &s_fetch[k]);
|
||||
/* §26:部分返回(bn<m,服务端缺歌/权限变化)非致命——已物化槽保留,缺槽留
|
||||
* invalid(下次 ensure 再试),不再整段置失败。修【92/200 首真机「加载失败」】:
|
||||
* 原 NCM_ERR_JSON 会把整页打成 Load Failed(批内一首缺歌即全灭)。 */
|
||||
if (bn < m && first_err == NCM_OK) first_err = NCM_ERR_NOTFOUND;
|
||||
} else {
|
||||
/* 单批网络失败:非致命;跳过本批网格,其余批照常补(首屏尽量多物化)。 */
|
||||
if (first_err == NCM_OK) first_err = r2;
|
||||
}
|
||||
if (bn < m) rc = NCM_ERR_JSON; /* 部分返回:缺槽留 invalid,下次 ensure 再试 */
|
||||
}
|
||||
a = b;
|
||||
a = (b0 > a) ? b0 : b; /* 推进到本批网格末尾:失败批不在同一次调用内被重复重发 */
|
||||
}
|
||||
pthread_mutex_lock(&g_song_mu);
|
||||
win_recount_locked();
|
||||
pthread_mutex_unlock(&g_song_mu);
|
||||
return rc;
|
||||
return (first_err != NCM_OK) ? first_err : rc;
|
||||
}
|
||||
|
||||
int appstate_song_fetch(int gidx, ncm_song_t *out)
|
||||
{
|
||||
if (!out) return NCM_ERR_ARG;
|
||||
if (appstate_song_copy(gidx, out) == 0) return NCM_OK; /* 窗内直接命中 */
|
||||
if (gidx < 0 || gidx >= g_st.track_ids_count) return NCM_ERR_ARG;
|
||||
if (gidx < 0) return NCM_ERR_ARG;
|
||||
pthread_mutex_lock(&g_song_mu);
|
||||
if (gidx >= g_st.track_ids_count) { pthread_mutex_unlock(&g_song_mu); return NCM_ERR_ARG; }
|
||||
int64_t id = g_st.song_ids[gidx];
|
||||
pthread_mutex_unlock(&g_song_mu);
|
||||
if (id == 0) return NCM_ERR_ARG; /* 无索引(如搜索上下文窗外) */
|
||||
char sid[24];
|
||||
snprintf(sid, sizeof sid, "%lld", (long long)id);
|
||||
@@ -368,13 +470,14 @@ void appstate_adopt_songs(int n)
|
||||
pthread_mutex_unlock(&g_song_mu);
|
||||
}
|
||||
|
||||
int appstate_load_playlist_songs_page(int idx, int offset, int limit)
|
||||
/* 核心加载(无代次校验):pl 为打开时的快照副本(已拷贝,之后不读 g_st.playlists[]——
|
||||
* 防 home_worker 刷新歌单列表 memset 旧数组时把正在加载的歌单 id 污染成空、导致
|
||||
* detail("") 取回 3 首歌/串数据/加载失败,§26)。offset==0 重头用 staging 缓冲收编
|
||||
* detail 结果、锁内原子发布索引(防 worker 写 song_ids 与播放线程 song_fetch 读竞态)。 */
|
||||
static int appstate_load_playlist_songs_impl(const ncm_playlist_t *pl, int is_search,
|
||||
int offset, int limit)
|
||||
{
|
||||
/* 搜索歌单详情(§23):idx==NCM_PL_SEARCH 时读 g_st.search_pl_open(复用本链路)。 */
|
||||
if (idx != NCM_PL_SEARCH && (idx < 0 || idx >= g_st.playlist_count)) return NCM_ERR_ARG;
|
||||
/* 不在此处写 g_st.active_playlist:调用方(playlist_open/UI)已在 UI 线程设定;
|
||||
* 若后台 worker 在此改写,会覆盖用户已切换到的新歌单,造成“打开 B 却显示 A”。 */
|
||||
const ncm_playlist_t *pl = (idx == NCM_PL_SEARCH) ? &g_st.search_pl_open : &g_st.playlists[idx];
|
||||
(void)is_search;
|
||||
if (limit <= 0 || limit > NCM_SONG_BATCH) limit = NCM_SONG_BATCH;
|
||||
if (offset < 0) offset = 0;
|
||||
|
||||
@@ -384,7 +487,8 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit)
|
||||
* 歌曲对象只物化视口附近的 NCM_SONG_WINDOW 首(offset 为「要确保的区间」起点)。 */
|
||||
|
||||
if (offset == 0) {
|
||||
/* 重头:清窗口 + 重建 trackIds 索引(song_count/track_ids_count 无论成败都归零)。 */
|
||||
/* 重头:清窗口 + 重建 trackIds 索引(song_count/track_ids_count 无论成败都归零)。
|
||||
* 封面兜底值取自快照 pl->cover(不读 g_st.playlists[],防污染)。 */
|
||||
pthread_mutex_lock(&g_song_mu);
|
||||
memset(g_st.song_valid, 0, sizeof g_st.song_valid);
|
||||
memset(g_st.song_ids, 0, sizeof g_st.song_ids);
|
||||
@@ -392,13 +496,19 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit)
|
||||
g_st.song_count = 0;
|
||||
g_st.track_ids_count = 0;
|
||||
pthread_mutex_unlock(&g_song_mu);
|
||||
ncm_strlcpy(g_st.cover_url, pl->cover, sizeof g_st.cover_url);
|
||||
/* detail 为网络调用(不能持锁);结果落 staging 缓冲,锁内一次性发布索引(原子)。
|
||||
* 静态缓冲:本加载函数单写者(playlist worker 单飞),免 128KB 栈。 */
|
||||
static int64_t s_build[NCM_TRACKID_MAX];
|
||||
int idn = 0;
|
||||
int rc = ncm_api_playlist_detail(g_st.ctx.device_id, g_st.ctx.cookie, pl->id,
|
||||
NULL, 0, NULL, 0,
|
||||
g_st.song_ids, NCM_TRACKID_MAX, &idn);
|
||||
NULL, 0, NULL, 0, s_build, NCM_TRACKID_MAX, &idn);
|
||||
if (rc != NCM_OK) return rc;
|
||||
if (idn > NCM_TRACKID_MAX) idn = NCM_TRACKID_MAX;
|
||||
pthread_mutex_lock(&g_song_mu);
|
||||
if (idn > 0) memcpy(g_st.song_ids, s_build, (size_t)idn * sizeof(int64_t));
|
||||
g_st.track_ids_count = idn;
|
||||
appstate_apply_playlist_cover(idx);
|
||||
pthread_mutex_unlock(&g_song_mu);
|
||||
} else if (g_st.track_ids_count == 0) {
|
||||
/* 防御:offset>0 但没有索引(不应发生;主路径先走 offset=0)。 */
|
||||
return NCM_ERR_ARG;
|
||||
@@ -410,6 +520,38 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit)
|
||||
return appstate_song_ensure_range(offset, hi);
|
||||
}
|
||||
|
||||
int appstate_load_playlist_songs_ctx(const ncm_playlist_t *pl, int is_search,
|
||||
int ctx_gen, int ctx_idx, int offset, int limit)
|
||||
{
|
||||
if (!pl || !pl->id[0]) return NCM_ERR_ARG;
|
||||
/* §26 打开上下文校验:代次/目标不匹配 → 判过期(不写全局),防「开 A→开 B 的迟到结果串 B」。
|
||||
* ctx_gen<0 = 兼容调用方(既有单测/autoplay)不校验,永远采纳。 */
|
||||
if (ctx_gen >= 0 && (ctx_gen != g_plopen_gen || ctx_idx != g_plopen_idx))
|
||||
return NCM_ERR_STALE;
|
||||
int rc = appstate_load_playlist_songs_impl(pl, is_search, offset, limit);
|
||||
/* 加载跨网络(detail 一次 + song_detail 分批),期间用户可能已切到新歌单(开 B):
|
||||
* 再验一次代次,过期即弃(清窗口,避免旧歌单数据被画在新歌单标题下)。 */
|
||||
if (ctx_gen >= 0 && (ctx_gen != g_plopen_gen || ctx_idx != g_plopen_idx)) {
|
||||
appstate_playlist_reset();
|
||||
return NCM_ERR_STALE;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int appstate_load_playlist_songs_page(int idx, int offset, int limit)
|
||||
{
|
||||
/* 搜索歌单详情(§23):idx==NCM_PL_SEARCH 时读 g_st.search_pl_open(复用本链路)。 */
|
||||
if (idx != NCM_PL_SEARCH && (idx < 0 || idx >= g_st.playlist_count)) return NCM_ERR_ARG;
|
||||
/* 不在此处写 g_st.active_playlist:调用方(playlist_open/UI)已在 UI 线程设定;
|
||||
* 若后台 worker 在此改写,会覆盖用户已切换到的新歌单,造成“打开 B 却显示 A”。 */
|
||||
ncm_playlist_t snap;
|
||||
int is_search = (idx == NCM_PL_SEARCH);
|
||||
if (is_search) snap = g_st.search_pl_open;
|
||||
else snap = g_st.playlists[idx];
|
||||
/* ctx_gen=-1:兼容入口(单测/autoplay)不做代次校验——由调用方协调并发。 */
|
||||
return appstate_load_playlist_songs_ctx(&snap, is_search, -1, idx, offset, limit);
|
||||
}
|
||||
|
||||
int appstate_load_playlist_songs(int idx)
|
||||
{
|
||||
/* 兼容入口:载索引 + 物化首个窗口。 */
|
||||
|
||||
@@ -337,6 +337,8 @@ int appstate_song_ensure_range(int lo, int hi);
|
||||
* 窗口未覆盖先滑窗(重叠区保留,出窗槽位失效);
|
||||
* 缺失槽位先读盘缓存(<cache_dir>/meta/<id>.bin),
|
||||
* miss 攒批 ≤NCM_SONG_BATCH 走 song_detail,成功落盘。
|
||||
* 批内部分返回/单批网络失败不再整段置失败——已物化槽保留,
|
||||
* 缺槽留 invalid(下次 ensure 再试),返回仍为 NCM_OK(§26)。
|
||||
* worker 线程调用(含网络);返回 NCM_OK/负错误码。 */
|
||||
int appstate_range_loaded(int lo, int hi); /* 1=[lo,hi) 全部在窗内且有效(UI 判是否需 ensure) */
|
||||
int appstate_song_copy(int gidx, ncm_song_t *out); /* 锁内读窗:0=已物化拷出;-1=窗外/未物化 */
|
||||
@@ -345,6 +347,32 @@ int appstate_song_fetch(int gidx, ncm_song_t *out); /* 任意全局下标物化
|
||||
void appstate_adopt_songs(int n); /* 收编外部已填的 songs[0..n)(搜索结果)为窗口 base=0,
|
||||
* 同步 song_ids/track_ids_count(播放上下文) */
|
||||
|
||||
/* ---- 歌单加载上下文(§26 开A→未载完→开B 竞态防串)----
|
||||
* UI(playlist.c)在每次打开/刷新歌单时调用 appstate_playlist_begin 记录「代次+目标」,
|
||||
* worker 用同一代次调 appstate_load_playlist_songs_ctx;加载完成时若代次/目标已不匹配,
|
||||
* 结果判过期丢弃(NCM_ERR_STALE),防「前一次加载的 worker 结果落进新打开的歌单」。 */
|
||||
int appstate_playlist_open_gen(void); /* 当前打开代次(0=从未打开) */
|
||||
int appstate_playlist_open_idx(void); /* 当前打开目标下标(-2=搜索歌单哨兵;-1=无) */
|
||||
int appstate_playlist_begin(int idx); /* 递增代次并记目标;返回新代次(UI 打开/刷新时调) */
|
||||
void appstate_playlist_reset(void); /* 清窗口/索引(UI 丢弃过期结果、在播安全)。加锁。 */
|
||||
int appstate_load_playlist_songs_ctx(const ncm_playlist_t *pl, int is_search,
|
||||
int ctx_gen, int ctx_idx, int offset, int limit);
|
||||
/* 快照式加载(歌单详情/曲目物化):
|
||||
* pl = 打开时快照(不读 g_st.playlists[],防
|
||||
* home_worker 并发 memset 污染 id);
|
||||
* is_search = 1=搜索歌单(NCM_PL_SEARCH 哨兵);
|
||||
* ctx_gen/ctx_idx = begin() 返回的代次与目标,加载中/末
|
||||
* 验不匹配 → NCM_ERR_STALE(不写全局);
|
||||
* ctx_gen < 0 = 不校验(兼容既有调用方/测试)。
|
||||
* offset=0 重头(detail 全量 trackIds 重建索引+清窗口);
|
||||
* offset>0 = 物化 [offset,offset+limit)(滑窗+盘缓存+补载)。 */
|
||||
|
||||
/* ---- 歌单列表磁盘缓存(§26,NCM_CACHE_DESIGN §2.2)----
|
||||
* 快照 <data_dir>/playlists.bin(magic/ver/时间戳 + 数组)。进首页先渲染缓存秒开,
|
||||
* 后台 worker 拉网络成功后覆写缓存(stale-while-revalidate);断网时列表可离线浏览。 */
|
||||
int appstate_playlists_cache_load(int64_t *ts_out); /* 0=命中(g_st.playlists/playlist_count 已填) */
|
||||
int appstate_playlists_cache_save(void); /* 把当前 g_st.playlists/playlist_count 落盘 */
|
||||
|
||||
/* ---- 播放队列来源(§23)---- */
|
||||
void appstate_set_queue(int src); /* NCM_QUEUE_PLAYLIST / NCM_QUEUE_SEARCH */
|
||||
int appstate_get_queue(void);
|
||||
|
||||
@@ -168,7 +168,7 @@ static void home_draw_empty(void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (meow_ui_activity_active() != ACT_HOME) return;
|
||||
if (g_load_err == 0 && g_st.playlist_count != 0) return;
|
||||
if (g_st.playlist_count != 0) return; /* 有数据(缓存/已载)即显示列表,后台刷新失败不置空 */
|
||||
if (g_home_loading) { ncm_draw_empty(r_list, "LOADING", "正在加载歌单..."); return; }
|
||||
if (g_st.logged_in == 0) ncm_draw_empty(r_list, "NOT SIGNED IN", "请先登录");
|
||||
else if (g_load_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "SESSION EXPIRED", "请重新登录");
|
||||
@@ -201,7 +201,9 @@ static void home_apply(void)
|
||||
meow_ui_paint_full();
|
||||
}
|
||||
|
||||
/* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist)。 */
|
||||
/* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist)。
|
||||
* §26 stale-while-revalidate:网络成功 → appstate_load_playlists 覆写盘缓存 + 更新内存;
|
||||
* 失败但有缓存在展示 → 保持缓存列表(g_load_err=0),后台静默不打断(断网可离线浏览)。 */
|
||||
static void *home_worker(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
@@ -211,7 +213,7 @@ static void *home_worker(void *arg)
|
||||
if (rc == NCM_OK)
|
||||
account_vip_maybe_refresh(0); /* §20:超 1 天/到期后各补查一次(worker 线程,不卡 UI) */
|
||||
pthread_mutex_lock(&g_home_mu);
|
||||
g_load_err = rc;
|
||||
g_load_err = (g_st.playlist_count > 0) ? 0 : rc; /* 有数据(缓存/已载)即不置错误态 */
|
||||
g_home_loading = 0;
|
||||
g_home_gen++;
|
||||
pthread_mutex_unlock(&g_home_mu);
|
||||
@@ -222,9 +224,14 @@ void home_reload(void)
|
||||
{
|
||||
if (g_host_mock) { g_load_err = 0; home_apply(); return; }
|
||||
if (g_home_thr_on) return;
|
||||
/* §26:进首页先用磁盘缓存秒渲染(若有),后台 worker 静默刷新;无缓存才显示 LOADING。 */
|
||||
int64_t ts = 0;
|
||||
int had_cache = (appstate_playlists_cache_load(&ts) == NCM_OK && g_st.playlist_count > 0);
|
||||
g_load_err = 0;
|
||||
g_st.playlist_count = 0;
|
||||
g_home_loading = 1;
|
||||
if (!had_cache) {
|
||||
g_st.playlist_count = 0; /* 无缓存 → LOADING 空态 */
|
||||
g_home_loading = 1;
|
||||
}
|
||||
g_home_thr_on = 1;
|
||||
if (pthread_create(&g_home_thr, NULL, home_worker, NULL) != 0) {
|
||||
g_home_thr_on = 0; g_home_loading = 0; g_load_err = NCM_ERR_IO;
|
||||
|
||||
@@ -35,10 +35,23 @@ static volatile int g_plgen;
|
||||
static int g_placked;
|
||||
static volatile int g_plloading;
|
||||
static volatile int g_plloaded_idx;
|
||||
static volatile int g_plloaded_gen; /* worker 启动时代次(§26:过期结果丢弃判据) */
|
||||
static volatile int g_pl_stale; /* 1=本次结果被判过期(未写全局,需重拉当前目标) */
|
||||
static volatile int g_ens_lo, g_ens_hi; /* 滑窗补载目标区间(全局下标) */
|
||||
static volatile int g_ens_first; /* 1=首屏重头(offset=0) */
|
||||
static volatile int g_ens_err; /* 最近一次滑窗补载错误(0=ok;首屏错误走 g_load_err) */
|
||||
|
||||
/* 打开快照(§26 开A→未载完→开B 防串):refresh 在启动 worker 前把「目标歌单」拷进 g_tgt,
|
||||
* worker 启动时再拷到本地——detail 加载不再依赖 g_st.playlists[](会被回家时 home_worker 的
|
||||
* appstate_load_playlists 并发 memset 污染)。tgen = appstate_playlist_begin 返回的打开代次。 */
|
||||
typedef struct {
|
||||
ncm_playlist_t pl; /* 目标快照(id/name/cover/track_count) */
|
||||
int is_search; /* 1=搜索歌单(active_playlist==NCM_PL_SEARCH) */
|
||||
int idx; /* 目标下标(-2=搜索哨兵) */
|
||||
int gen; /* 打开代次(appstate_playlist_begin 返回) */
|
||||
} pl_target_t;
|
||||
static pl_target_t g_tgt;
|
||||
|
||||
static void pl_drag_reset(void)
|
||||
{
|
||||
g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0;
|
||||
@@ -218,19 +231,45 @@ static void pl_apply(int reset_scroll)
|
||||
static void *playlist_worker(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
int my_idx = g_st.active_playlist;
|
||||
int first = g_ens_first;
|
||||
int lo = g_ens_lo, hi = g_ens_hi;
|
||||
/* 拷贝打开快照 + 任务参数:单飞(g_plthr_on)保证 worker 运行期间 g_tgt 不被改写。 */
|
||||
ncm_playlist_t tpl;
|
||||
int tsearch, tidx, tgen, first, lo, hi;
|
||||
pthread_mutex_lock(&g_plmu);
|
||||
tpl = g_tgt.pl; tsearch = g_tgt.is_search;
|
||||
tidx = g_tgt.idx; tgen = g_tgt.gen;
|
||||
first = g_ens_first; lo = g_ens_lo; hi = g_ens_hi;
|
||||
pthread_mutex_unlock(&g_plmu);
|
||||
int cur_active = g_st.active_playlist; /* ensure 路径结束校验用(当前目标) */
|
||||
int rc;
|
||||
if (g_host_mock) rc = 0;
|
||||
else if (my_idx < 0) rc = NCM_ERR_ARG;
|
||||
else if (first) rc = appstate_load_playlist_songs_page(my_idx, 0, NCM_SONG_BATCH);
|
||||
else rc = appstate_song_ensure_range(lo, hi);
|
||||
else if (first) {
|
||||
/* 首屏:快照式加载(detail 全量 trackIds + 物化首段)。ctx 校验代次/目标,过期自动丢弃。 */
|
||||
if (tidx < 0) rc = NCM_ERR_ARG;
|
||||
else rc = appstate_load_playlist_songs_ctx(&tpl, tsearch, tgen, tidx, 0, NCM_SONG_BATCH);
|
||||
} else {
|
||||
/* 滑窗补载:物化视口区间(盘缓存优先,批网格补载)。 */
|
||||
rc = (lo < hi) ? appstate_song_ensure_range(lo, hi) : NCM_OK;
|
||||
}
|
||||
pthread_mutex_lock(&g_plmu);
|
||||
if (first) g_load_err = rc; /* 首屏失败 → 整页空态(点按重试) */
|
||||
else g_ens_err = rc; /* 补载失败 → snack + 占位行保留(滚动/点按再试) */
|
||||
if (first) {
|
||||
if (rc == NCM_ERR_STALE) {
|
||||
/* 打开 A 未完→已切到 B:本次结果过期(ctx 未写全局),丢弃,tick 对当前目标重拉。 */
|
||||
g_pl_stale = 1; g_load_err = 0;
|
||||
} else if (rc != NCM_OK && rc != NCM_ERR_AUTH && g_st.track_ids_count > 0) {
|
||||
/* 索引成功但曲目批部分失败(缺歌/单批网络):非致命——保留已物化 + snack,
|
||||
* 不整页 Load Failed(修【92/200 首真机点开即「加载失败」】)。 */
|
||||
g_ens_err = rc; g_load_err = 0;
|
||||
} else {
|
||||
g_load_err = rc; /* 索引失败/鉴权失败 → 整页空态(点按重试) */
|
||||
}
|
||||
g_plloaded_idx = tidx;
|
||||
g_plloaded_gen = tgen;
|
||||
} else {
|
||||
g_ens_err = rc; /* 补载失败 → snack + 占位行保留(滚动/点按再试) */
|
||||
g_plloaded_idx = cur_active;
|
||||
g_plloaded_gen = tgen;
|
||||
}
|
||||
g_plloading = 0;
|
||||
g_plloaded_idx = my_idx;
|
||||
g_plgen++;
|
||||
pthread_mutex_unlock(&g_plmu);
|
||||
return NULL;
|
||||
@@ -266,7 +305,19 @@ void playlist_refresh(void)
|
||||
if (g_plthr_on) return;
|
||||
g_load_err = 0;
|
||||
g_ens_err = 0;
|
||||
/* 打开快照 + 代次(§26):锁内拷目标歌单,detail 加载不依赖 g_st.playlists[](会被
|
||||
* 回家时 home_worker 并发 memset 污染成空 id → 3 首歌/串数据);同时记录打开代次,
|
||||
* 过期结果在 worker 完成时判 NCM_ERR_STALE 丢弃(防「开 A→开 B 的迟到结果串 B」)。 */
|
||||
const ncm_playlist_t *pcur = pl_cur();
|
||||
if (!pcur || !pcur->id[0]) { g_load_err = NCM_ERR_ARG; pl_apply(1); return; }
|
||||
pthread_mutex_lock(&g_plmu);
|
||||
g_tgt.pl = *pcur;
|
||||
g_tgt.is_search = (g_st.active_playlist == NCM_PL_SEARCH);
|
||||
g_tgt.idx = g_st.active_playlist;
|
||||
g_tgt.gen = appstate_playlist_begin(g_tgt.idx);
|
||||
pthread_mutex_unlock(&g_plmu);
|
||||
g_ens_first = 1; /* 首屏:重建索引 + 物化首段 */
|
||||
g_pl_stale = 0;
|
||||
g_plloading = 1;
|
||||
g_plthr_on = 1;
|
||||
if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) {
|
||||
@@ -283,7 +334,15 @@ void playlist_tick(uint64_t now)
|
||||
g_placked = g_plgen;
|
||||
g_plthr_on = 0;
|
||||
g_plloading = 0;
|
||||
if (g_plloaded_idx != g_st.active_playlist) { playlist_refresh(); return; }
|
||||
/* §26 防串:结果过期(代次不匹配)/已切到别的歌单 → 先清窗口(不把旧歌单数据画在新歌单
|
||||
* 标题下),再对当前 active_playlist 重新发起。 */
|
||||
if (g_pl_stale || g_plloaded_idx != g_st.active_playlist ||
|
||||
g_plloaded_gen != appstate_playlist_open_gen()) {
|
||||
g_pl_stale = 0;
|
||||
appstate_playlist_reset();
|
||||
playlist_refresh();
|
||||
return;
|
||||
}
|
||||
if (g_load_err != 0) {
|
||||
g_st.track_ids_count = 0; /* 首屏失败 → 整页空态(点按重试) */
|
||||
pl_apply(1);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
/* ---- ncm_cache.c 依赖的符号(核心库其他 TU 提供;此处最小内联/桩,避免链 mbedtls) ---- */
|
||||
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
||||
@@ -59,6 +60,12 @@ int ncm_store_mkdir_p(const char *path)
|
||||
return 0;
|
||||
}
|
||||
int ncm_store_read_cfg_int(const char *dir, const char *key, int def) { (void)dir; (void)key; return def; }
|
||||
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;
|
||||
}
|
||||
|
||||
/* ---- 工具 ---- */
|
||||
static char g_dir[512];
|
||||
@@ -285,6 +292,44 @@ int main(void)
|
||||
CHECK(stat(p, &st2) != 0, "part_remove deleted .part");
|
||||
}
|
||||
|
||||
/* ⑨ §26 孤儿半成品回收:LRU 扫描顺手续删超过 7 天的 .part/.tmp(用户不再点该曲、不续传),
|
||||
* 新鲜的在途/刚中断残体保留(写端 mtime 持续更新,不会被误删)。 */
|
||||
printf("== case 9: orphan .part/.tmp older than 7d swept by LRU scan (stage3 §26.4) ==\n");
|
||||
cleanup();
|
||||
{
|
||||
char op[600], ot[600], lp[600];
|
||||
int64_t now = ncm_now_ms() / 1000;
|
||||
int64_t old = now - 8 * 86400; /* 8 天前 */
|
||||
struct stat st;
|
||||
struct timespec ts[2];
|
||||
ts[0].tv_sec = old; ts[0].tv_nsec = 0; ts[1].tv_sec = old; ts[1].tv_nsec = 0;
|
||||
|
||||
/* 8 天前的 .part:应被 evict 扫描顺手删 */
|
||||
ncm_cache_path(&g_c, "orph", ".mp3", op, sizeof op);
|
||||
snprintf(op + strlen(op), sizeof op - strlen(op), ".part");
|
||||
int fd = open(op, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd >= 0) { (void)!write(fd, "old", 3); close(fd); }
|
||||
utimensat(AT_FDCWD, op, ts, 0);
|
||||
|
||||
/* 8 天前的 .tmp:也应被删 */
|
||||
ncm_cache_path(&g_c, "orcht", ".mp3", ot, sizeof ot);
|
||||
snprintf(ot + strlen(ot), sizeof ot - strlen(ot), ".tmp");
|
||||
fd = open(ot, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd >= 0) { (void)!write(fd, "tm", 2); close(fd); }
|
||||
utimensat(AT_FDCWD, ot, ts, 0);
|
||||
|
||||
/* 新鲜的 .part(现在):应保留(可能正在进行/刚中断,mtime 新) */
|
||||
ncm_cache_path(&g_c, "live", ".mp3", lp, sizeof lp);
|
||||
snprintf(lp + strlen(lp), sizeof lp - strlen(lp), ".part");
|
||||
fd = open(lp, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd >= 0) { (void)!write(fd, "live", 4); close(fd); }
|
||||
|
||||
ncm_cache_evict(&g_c);
|
||||
CHECK(stat(op, &st) != 0, "8-day .part swept by evict scan");
|
||||
CHECK(stat(ot, &st) != 0, "8-day .tmp swept by evict scan");
|
||||
CHECK(stat(lp, &st) == 0, "fresh .part kept (inflight/resumable)");
|
||||
}
|
||||
|
||||
cleanup();
|
||||
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
|
||||
return g_fail ? 1 : 0;
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
/* bench/playlist_race_sim.c — 歌单链路三问题 离线验证(0 真实 NCM 请求,全本地 stub)。
|
||||
*
|
||||
* 驱动真实 appstate.c(歌单详情 + 滑动窗口 + 打开上下文 + 歌单列表盘缓存),网络端点为本地
|
||||
* 可配置 stub,验证 REPORT §26 的三类修复:
|
||||
* A. 批边界:72 / 92 / 200 首歌单首屏 + 二批 ensure(≤100/批 网格)正常物化;
|
||||
* B. 部分响应/单批失败不再整页 Load Failed:歌单索引(全量 trackIds)成功即保留 + 已物化槽
|
||||
* + snack,缺槽留占位(修【92/200 首真机点开即「加载失败」】);
|
||||
* C. 异步竞态防串:开 A(未载完)→开 B,A 的迟到结果判 NCM_ERR_STALE 丢弃,不把 A 的索引/
|
||||
* 窗口写进全局(修【5194 首 3 首歌/串数据/加载失败】);打开时快照 plid 免疫 home_worker
|
||||
* 对 g_st.playlists[] 的并发 memset 污染;
|
||||
* D. 歌单列表磁盘缓存:appstate_playlists_cache_save/load 回环(断网离线浏览秒开);
|
||||
* E. 打开上下文 reset 清窗口(UI 丢弃过期结果时不留旧数据)。
|
||||
* 风控纪律:0 次真实 NCM 请求(离线 stub;songmeta 全 miss/不落盘)。
|
||||
*
|
||||
* 编译(宿主,ncm_app 目录下):
|
||||
* GCC -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn \
|
||||
* -c bench/playlist_race_sim.c -o /tmp/pr.o
|
||||
* GCC -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn \
|
||||
* -c app/logic/appstate.c -o /tmp/pr_app.o
|
||||
* GCC -O2 -Wl,--gc-sections -o playlist_race_sim /tmp/pr.o /tmp/pr_app.o -lpthread
|
||||
* 运行:./playlist_race_sim (0 网络;PASS/FAIL 退出码)
|
||||
*/
|
||||
#include "../app/ncm_app_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.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;
|
||||
|
||||
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;
|
||||
}
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||||
int meow_config_get(const char *key, char *out, int outsz) { (void)key; (void)out; (void)outsz; return MEOW_CFG_ERR_NOENT; }
|
||||
int meow_config_set(const char *key, const char *val) { (void)key; (void)val; return MEOW_CFG_OK; }
|
||||
void meow_config_remove(const char *key) { (void)key; }
|
||||
int meow_config_int(const char *key, int def) { (void)key; return def; }
|
||||
|
||||
/* songmeta 全 miss / 不落盘(本 sim 不测盘缓存歌曲元数据,只测歌单列表上下文)。 */
|
||||
int songmeta_load(const char *cd, const char *id, ncm_song_t *out) { (void)cd;(void)id;(void)out; return NCM_ERR_IO; }
|
||||
int songmeta_save(const char *cd, const char *id, const ncm_song_t *s){ (void)cd;(void)id;(void)s; return NCM_ERR_IO; }
|
||||
|
||||
/* ---- 可配置网络 stub ---- */
|
||||
static int g_pl_total = 92; /* 当前歌单返回的 trackIds 总数 */
|
||||
static int64_t g_id_base = 1000000;
|
||||
static int g_detail_calls, g_song_calls;
|
||||
static int g_song_drop; /* song_detail 每次少返回条数(部分响应模拟;0=全量) */
|
||||
static int g_song_fail_n; /* 前 N 次 song_detail 网络失败模拟 */
|
||||
static char g_last_plid[64]; /* detail 收到的 playlist_id(验证快照不被 memset 污染) */
|
||||
|
||||
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 (playlist_id) { snprintf(g_last_plid, sizeof g_last_plid, "%s", playlist_id); }
|
||||
if (name) snprintf(name, name_sz, "Sim Playlist");
|
||||
if (cover) cover[0] = 0;
|
||||
int n = (g_pl_total < id_cap) ? g_pl_total : id_cap;
|
||||
for (int i = 0; i < n; i++) ids[i] = g_id_base + 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;
|
||||
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;
|
||||
int want = m - g_song_drop; if (want < 0) want = 0;
|
||||
for (int i = 0; i < want; 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[i].fee = 0;
|
||||
}
|
||||
if (out_n) *out_n = want;
|
||||
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)
|
||||
|
||||
static void set_pl(int idx, const char *id, const char *name, int total)
|
||||
{
|
||||
ncm_strlcpy(g_st.playlists[idx].id, id, sizeof g_st.playlists[idx].id);
|
||||
ncm_strlcpy(g_st.playlists[idx].name, name, sizeof g_st.playlists[idx].name);
|
||||
snprintf(g_st.playlists[idx].cover, sizeof g_st.playlists[idx].cover, "http://p1.music.126.net/%s.jpg", id);
|
||||
g_st.playlists[idx].track_count = total;
|
||||
}
|
||||
static void reset_ns(void)
|
||||
{
|
||||
g_detail_calls = 0; g_song_calls = 0; g_song_drop = 0; g_song_fail_n = 0;
|
||||
g_last_plid[0] = 0;
|
||||
}
|
||||
|
||||
/* 模拟 playlist.c 首屏 worker 的非致命降级逻辑(§26):索引成功但曲目批部分失败 → snack,不置 load_err。 */
|
||||
static int worker_first_downgrade(int rc)
|
||||
{
|
||||
if (rc != NCM_OK && rc != NCM_ERR_AUTH && rc != NCM_ERR_STALE && g_st.track_ids_count > 0)
|
||||
return 0; /* 非致命:等价 g_load_err=0(页面保留已物化 + snack) */
|
||||
return (rc != NCM_OK); /* 致命:g_load_err=rc(整页空态) */
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
if (system("rm -rf /tmp/ncm_plrace_data /tmp/ncm_plrace_cache") != 0) {
|
||||
printf("FAIL cannot clean /tmp data dir\n");
|
||||
return 1;
|
||||
}
|
||||
memset(&g_st, 0, sizeof g_st);
|
||||
snprintf(g_st.data_dir, sizeof g_st.data_dir, "/tmp/ncm_plrace_data");
|
||||
snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "/tmp/ncm_plrace_cache");
|
||||
if (system("mkdir -p /tmp/ncm_plrace_data /tmp/ncm_plrace_cache") != 0) {
|
||||
printf("FAIL cannot mkdir /tmp dirs\n");
|
||||
return 1;
|
||||
}
|
||||
g_st.logged_in = 1;
|
||||
ncm_strlcpy(g_st.uid, "408996274", sizeof g_st.uid);
|
||||
g_st.playlist_count = 2;
|
||||
set_pl(0, "plFav", "我喜欢的音乐", 92); /* A:92 首 */
|
||||
set_pl(1, "plDoc", "通勤歌单", 200); /* B:200 首 */
|
||||
ncm_strlcpy(g_st.ctx.device_id, "dev", sizeof g_st.ctx.device_id);
|
||||
ncm_strlcpy(g_st.ctx.cookie, "ck", sizeof g_st.ctx.cookie);
|
||||
|
||||
/* ---- A. 批边界 ---- */
|
||||
printf("== A. batch boundary: 72 / 92 / 200 ==\n");
|
||||
int totals[3] = {72, 92, 200};
|
||||
for (int k = 0; k < 3; k++) {
|
||||
reset_ns();
|
||||
g_pl_total = totals[k];
|
||||
g_st.playlists[0].track_count = totals[k];
|
||||
ncm_playlist_t snap = g_st.playlists[0];
|
||||
int rc = appstate_load_playlist_songs_ctx(&snap, 0, -1, 0, 0, NCM_SONG_BATCH);
|
||||
CHECK(rc == NCM_OK, "[%d] first load rc=%d", totals[k], rc);
|
||||
CHECK(g_st.track_ids_count == totals[k], "[%d] track_ids_count=%d (expect %d)",
|
||||
totals[k], g_st.track_ids_count, totals[k]);
|
||||
int first = (totals[k] < NCM_SONG_BATCH) ? totals[k] : NCM_SONG_BATCH;
|
||||
CHECK(g_st.song_count == first, "[%d] first batch valid=%d (expect %d)",
|
||||
totals[k], g_st.song_count, first);
|
||||
CHECK(g_detail_calls == 1 && g_song_calls == 1,
|
||||
"[%d] detail=%d song_detail=%d (expect 1+1)", totals[k], g_detail_calls, g_song_calls);
|
||||
ncm_song_t s;
|
||||
CHECK(appstate_song_copy(first - 1, &s) == 0, "[%d] first-batch last song copy rc=0", totals[k]);
|
||||
if (totals[k] > NCM_SONG_BATCH) { /* 二批 ensure:200 首 → [100,200) */
|
||||
int rc2 = appstate_song_ensure_range(NCM_SONG_BATCH, totals[k]);
|
||||
CHECK(rc2 == NCM_OK && g_st.song_count == totals[k],
|
||||
"[%d] second batch ensure rc=%d valid=%d (expect %d)",
|
||||
totals[k], rc2, g_st.song_count, totals[k]);
|
||||
CHECK(g_song_calls == 2, "[%d] song_detail calls=%d (expect 2 batches)", totals[k], g_song_calls);
|
||||
CHECK(appstate_song_copy(totals[k] - 1, &s) == 0,
|
||||
"[%d] tail song copy rc=0 after 2nd batch", totals[k]);
|
||||
}
|
||||
appstate_playlist_reset(); /* 下一次干净 */
|
||||
}
|
||||
|
||||
/* ---- B. 部分响应/单批失败非致命(92/200 点开不再整页 Load Failed) ---- */
|
||||
printf("== B. partial response / single-batch failure non-fatal ==\n");
|
||||
reset_ns();
|
||||
g_pl_total = 92; g_song_drop = 1; /* 首批少返回 1 首(服务端缺歌) */
|
||||
g_st.playlists[0].track_count = 92;
|
||||
{
|
||||
ncm_playlist_t snap = g_st.playlists[0];
|
||||
int rc = appstate_load_playlist_songs_ctx(&snap, 0, -1, 0, 0, NCM_SONG_BATCH);
|
||||
CHECK(rc == NCM_ERR_NOTFOUND || rc == NCM_OK, "partial batch rc=%d (NOTFOUND=非致命)", rc);
|
||||
CHECK(g_st.track_ids_count == 92, "partial keeps index: track_ids_count=%d (expect 92)", g_st.track_ids_count);
|
||||
CHECK(g_st.song_count == 91, "partial keeps 91 materialized: valid=%d", g_st.song_count);
|
||||
int page_err = worker_first_downgrade(rc);
|
||||
CHECK(page_err == 0, "worker downgrades partial to non-fatal (page NOT wiped)");
|
||||
}
|
||||
reset_ns();
|
||||
g_pl_total = 200; g_song_fail_n = 1; /* 首批网络失败一次(index 已建,曲目批失败) */
|
||||
g_st.playlists[0].track_count = 200;
|
||||
{
|
||||
ncm_playlist_t snap = g_st.playlists[0];
|
||||
int rc = appstate_load_playlist_songs_ctx(&snap, 0, -1, 0, 0, NCM_SONG_BATCH);
|
||||
CHECK(g_st.track_ids_count == 200, "network-fail keeps index: track_ids_count=%d (expect 200)",
|
||||
g_st.track_ids_count);
|
||||
int page_err = worker_first_downgrade(rc);
|
||||
CHECK(page_err == 0, "single-batch network fail non-fatal (page not wiped; placeholders remain)");
|
||||
}
|
||||
|
||||
/* ---- C. 异步竞态防串 + 快照免疫 memset ---- */
|
||||
printf("== C. async race: open A unfinished -> open B (A late result discarded) ==\n");
|
||||
reset_ns();
|
||||
g_pl_total = 92;
|
||||
g_st.playlists[0].track_count = 92;
|
||||
{ /* 打开 A:begin(A) 记代次/目标 → A 正常加载 */
|
||||
int genA = appstate_playlist_begin(0);
|
||||
ncm_playlist_t snapA = g_st.playlists[0];
|
||||
int rc = appstate_load_playlist_songs_ctx(&snapA, 0, genA, 0, 0, NCM_SONG_BATCH);
|
||||
CHECK(rc == NCM_OK && g_st.track_ids_count == 92, "open A loads (rc=%d count=%d)", rc, g_st.track_ids_count);
|
||||
}
|
||||
{ /* 开 A 未完 → 开 B:begin(B) 记 B 代次/目标;A 的迟到结果(genA)被判过期丢弃 */
|
||||
g_pl_total = 200;
|
||||
g_st.playlists[1].track_count = 200;
|
||||
int genB = appstate_playlist_begin(1);
|
||||
/* A 的快照 + A 的旧代次 = 过期 → 立即 STALE,不写任何全局 */
|
||||
int genA_stale = genB - 1;
|
||||
ncm_playlist_t snapA = g_st.playlists[0];
|
||||
int rA = appstate_load_playlist_songs_ctx(&snapA, 0, genA_stale, 0, 0, NCM_SONG_BATCH);
|
||||
CHECK(rA == NCM_ERR_STALE, "A late load marked STALE (rc=%d)", rA);
|
||||
CHECK(appstate_playlist_open_gen() == genB && appstate_playlist_open_idx() == 1,
|
||||
"open context now B (gen=%d idx=%d)", appstate_playlist_open_gen(), appstate_playlist_open_idx());
|
||||
/* B 以 B 的当前代次加载 → 采纳,全局内容为 B */
|
||||
ncm_playlist_t snapB = g_st.playlists[1];
|
||||
int rB = appstate_load_playlist_songs_ctx(&snapB, 0, genB, 1, 0, NCM_SONG_BATCH);
|
||||
CHECK(rB == NCM_OK && g_st.track_ids_count == 200,
|
||||
"B loads (rc=%d track_ids_count=%d expect 200)", rB, g_st.track_ids_count);
|
||||
CHECK(g_detail_calls == 2 && g_last_plid[0] != 0, "two detail calls total=%d", g_detail_calls);
|
||||
}
|
||||
/* 快照免疫 home_worker memset:打开时快照 plid,之后清空 g_st.playlists[] 也不影响加载 */
|
||||
printf("== C2. snapshot plid immune to home_worker memset(g_st.playlists[]) ==\n");
|
||||
reset_ns();
|
||||
g_pl_total = 92; g_st.playlists[0].track_count = 92;
|
||||
{
|
||||
ncm_playlist_t snap = g_st.playlists[0]; /* playlist_refresh 在打开时拷的快照 */
|
||||
memset(g_st.playlists, 0, sizeof g_st.playlists);/* home_worker appstate_load_playlists 的 memset */
|
||||
g_st.playlist_count = 0;
|
||||
int rc = appstate_load_playlist_songs_ctx(&snap, 0, -1, 0, 0, NCM_SONG_BATCH);
|
||||
CHECK(rc == NCM_OK && g_st.track_ids_count == 92,
|
||||
"snapshot load survives memset (rc=%d count=%d)", rc, g_st.track_ids_count);
|
||||
CHECK(strcmp(g_last_plid, "plFav") == 0, "detail received snapshot plid='%s' (expect plFav)", g_last_plid);
|
||||
}
|
||||
|
||||
/* ---- D. 歌单列表磁盘缓存 ---- */
|
||||
printf("== D. playlists disk cache (save -> load roundtrip) ==\n");
|
||||
{
|
||||
g_st.logged_in = 1;
|
||||
g_st.playlist_count = 2;
|
||||
set_pl(0, "plFav", "我喜欢的音乐", 92);
|
||||
set_pl(1, "plDoc", "通勤歌单", 200);
|
||||
int rc = appstate_playlists_cache_save();
|
||||
CHECK(rc == NCM_OK, "cache_save rc=%d", rc);
|
||||
memset(g_st.playlists, 0, sizeof g_st.playlists);
|
||||
g_st.playlist_count = 0;
|
||||
int64_t ts = 0;
|
||||
rc = appstate_playlists_cache_load(&ts);
|
||||
CHECK(rc == NCM_OK, "cache_load rc=%d", rc);
|
||||
CHECK(g_st.playlist_count == 2, "cache_load count=%d (expect 2)", g_st.playlist_count);
|
||||
CHECK(ts > 0, "cache timestamp=%lld (expect >0)", (long long)ts);
|
||||
CHECK(strcmp(g_st.playlists[1].id, "plDoc") == 0 &&
|
||||
strcmp(g_st.playlists[1].name, "通勤歌单") == 0,
|
||||
"roundtrip keeps content (id/name)");
|
||||
}
|
||||
|
||||
/* ---- E. 打开上下文 reset 清窗口 ---- */
|
||||
printf("== E. appstate_playlist_reset clears window/index ==\n");
|
||||
{
|
||||
g_pl_total = 92;
|
||||
ncm_playlist_t snap = g_st.playlists[0];
|
||||
appstate_load_playlist_songs_ctx(&snap, 0, -1, 0, 0, NCM_SONG_BATCH);
|
||||
CHECK(g_st.track_ids_count == 92 && g_st.song_count > 0, "prior: count=%d valid=%d",
|
||||
g_st.track_ids_count, g_st.song_count);
|
||||
appstate_playlist_reset();
|
||||
CHECK(g_st.track_ids_count == 0 && g_st.song_count == 0 && g_st.window_base == 0,
|
||||
"after reset: count=%d valid=%d base=%d", g_st.track_ids_count, g_st.song_count, g_st.window_base);
|
||||
}
|
||||
|
||||
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -20,6 +20,10 @@
|
||||
#define MAX_LIMIT_GB 100
|
||||
#define MAX_ENTRIES 4096
|
||||
|
||||
/* §26 孤儿半成品回收:.tmp/.part 不进 LRU 池(防在播文件误删),但长期不续传的残体占磁盘。
|
||||
* LRU 扫描时顺手判 mtime——超过此天数的孤儿 .part/.tmp 惰性删除(in-flight 下载 mtime 新鲜安全)。 */
|
||||
#define NCM_ORPHAN_PART_DAYS 7
|
||||
|
||||
typedef struct entry {
|
||||
char name[256];
|
||||
uint64_t size;
|
||||
@@ -169,10 +173,21 @@ int ncm_cache_evict(const ncm_cache_t *c) {
|
||||
if (!ents) { closedir(d); return NCM_ERR_OOM; }
|
||||
int n = 0;
|
||||
uint64_t total = 0;
|
||||
int64_t orphan_cutoff = (int64_t)time(NULL) - (int64_t)NCM_ORPHAN_PART_DAYS * 86400;
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d)) && n < MAX_ENTRIES) {
|
||||
if (strncmp(e->d_name, "song_", 5) != 0) continue;
|
||||
if (is_partial_name(e->d_name)) continue; /* 阶段3:.tmp/.part 归写者管理域,不淘汰 */
|
||||
if (is_partial_name(e->d_name)) { /* 阶段3:.tmp/.part 归写者管理域,不进 LRU 池 */
|
||||
/* §26 顺手续删:超过 7 天的孤儿 .part/.tmp(用户不再点该曲、不续传)惰性删除。
|
||||
* 在播/刚中断的 .tmp/.part mtime 新鲜(写端持续更新),不会被误删。 */
|
||||
char pb[NCM_CACHE_DIR_MAX + NAME_MAX + 8];
|
||||
snprintf(pb, sizeof pb, "%s/%s", c->dir, e->d_name);
|
||||
struct stat sb;
|
||||
if (stat(pb, &sb) == 0 && S_ISREG(sb.st_mode) &&
|
||||
(int64_t)sb.st_mtim.tv_sec < orphan_cutoff)
|
||||
unlink(pb);
|
||||
continue;
|
||||
}
|
||||
char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8];
|
||||
snprintf(p, sizeof p, "%s/%s", c->dir, e->d_name);
|
||||
struct stat st;
|
||||
@@ -307,10 +322,20 @@ uint64_t ncm_cache_total_bytes(const ncm_cache_t *c) {
|
||||
DIR *d = opendir(c->dir);
|
||||
if (!d) return 0;
|
||||
uint64_t total = 0;
|
||||
int64_t orphan_cutoff = (int64_t)time(NULL) - (int64_t)NCM_ORPHAN_PART_DAYS * 86400;
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (strncmp(e->d_name, "song_", 5) != 0) continue;
|
||||
if (is_partial_name(e->d_name)) continue; /* 阶段3:半成品不计入池占用 */
|
||||
if (is_partial_name(e->d_name)) { /* 阶段3:半成品不计入池占用 */
|
||||
/* §26 顺手续删:超过 7 天的孤儿 .part/.tmp 惰性删除。 */
|
||||
char pb[NCM_CACHE_DIR_MAX + NAME_MAX + 8];
|
||||
snprintf(pb, sizeof pb, "%s/%s", c->dir, e->d_name);
|
||||
struct stat sb;
|
||||
if (stat(pb, &sb) == 0 && S_ISREG(sb.st_mode) &&
|
||||
(int64_t)sb.st_mtim.tv_sec < orphan_cutoff)
|
||||
unlink(pb);
|
||||
continue;
|
||||
}
|
||||
char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8];
|
||||
snprintf(p, sizeof p, "%s/%s", c->dir, e->d_name);
|
||||
struct stat st;
|
||||
|
||||
@@ -31,6 +31,7 @@ enum {
|
||||
NCM_ERR_AUTH = -13, /* 未登录/鉴权失败 */
|
||||
NCM_ERR_LIMIT = -14, /* 超出上限级 */
|
||||
NCM_ERR_VIP = -15, /* VIP 专享(fee 门槛命中,未发 song_url 请求,§20) */
|
||||
NCM_ERR_STALE = -16, /* 异步结果过期(打开歌单 A 未完→打开 B,A 的迟到结果被丢弃,§26) */
|
||||
};
|
||||
|
||||
/* ---------- 常用尺寸 ---------- */
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user