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
@@ -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)
|
||||
{
|
||||
/* 兼容入口:载索引 + 物化首个窗口。 */
|
||||
|
||||
Reference in new issue
Block a user