Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/cache_evict_sim.c
T
MeowOS 19e8fc93ed feat(ncm §24): 流媒体客户端边下边播阶段3——.part断点续传(Range/206/200/416)+LRU防淘汰(.tmp/.part跳过)+失败重试1次退避
按 devdocs/plan/NCM_STREAM_PLAY.md §8 阶段3:
- .part 断续传:中断留 .part,续传带 Range,206 追加 / 200 丢头段 / 416 删残留整首重下;URL 每次新拉
- LRU 防淘汰:evict/total_bytes 跳过 .tmp/.part(在播文件不被动过)
- 失败重试:预缓冲前网络失败重试 1 次,退避 800ms,重试拿新签名 URL(风控:0 真实请求,未部署)
- 顺手修:gb_to_bytes 返回 int 溢出致默认 10GB 上限失效 -> uint64_t
- 验证:stream_sim(续传 PCM 金标准+全矩阵)/player_flow_sim(P9/P10/P11)/cache_evict_sim(case7/8)/test_ncm_core 宿主+qemu-mipsel 全 PASS;build_ncm.sh 0 warning;ncm_host 离屏正常

md5 ncm f8f316a1e6f3bdd8f6b5921fdbb3f3a2
2026-09-01 20:00:14 +08:00

292 lines
13 KiB
C
Raw 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/cache_evict_sim.c — 缓存池 LRU 淘汰 / 清空 离线验证(0 网络,仅本地文件系统)。
*
* 直接驱动 core 的真实函数 ncm_cache_init / ncm_cache_set_limit_gb / ncm_cache_evict /
* ncm_cache_total_bytes / ncm_cache_clear / ncm_cache_lookup,验证三期第二批接进生产路径的
* 淘汰语义(REPORT §15):
* ① 超上限按 mtime 升序删最旧,直到 <= 上限;
* ② 未超上限无操作;
* ③ ncm_cache_lookup 刷新 mtime(LRU:被访问的最近优先存活);
* ④ ncm_cache_set_limit_gb 越界钳制(1~100GB);
* ⑤ ncm_cache_clear 删除 song_* 全部(mp3/封面/残留 .tmp)。
* 文件用 ftruncate 生成稀疏文件(逻辑大小可控,磁盘零消耗)。
*
* 编译(宿主,链接 core/ncm_cache.c + 本文件):
* $CC_HOST -O2 -DJSMN_HEADER -I. -Icore -c bench/cache_evict_sim.c -o /tmp/ce.o
* $CC_HOST -O2 -DJSMN_HEADER -I. -Icore -c core/ncm_cache.c -o /tmp/ce_cache.o
* $CC_HOST -O2 -o cache_evict_sim /tmp/ce.o /tmp/ce_cache.o -lpthread
* 运行:./cache_evict_sim (0 网络;PASS/FAIL 退出码)
*/
#include "../core/ncm_cache.h"
#include "../core/ncm_common.h"
#include "../core/ncm_store.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/time.h>
/* ---- ncm_cache.c 依赖的符号(核心库其他 TU 提供;此处最小内联/桩,避免链 mbedtls) ---- */
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;
}
int ncm_store_mkdir_p(const char *path)
{
if (!path || !*path) return -1;
char tmp[512];
size_t len = strlen(path);
if (len >= sizeof tmp) return -1;
memcpy(tmp, path, len + 1);
if (tmp[len - 1] == '/') tmp[len - 1] = '\0';
for (size_t i = 1; i <= strlen(tmp); i++) {
if (tmp[i] == '/' || tmp[i] == '\0') {
char ch = tmp[i];
tmp[i] = '\0';
if (tmp[0] && mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1;
tmp[i] = ch;
}
}
return 0;
}
int ncm_store_read_cfg_int(const char *dir, const char *key, int def) { (void)dir; (void)key; return def; }
/* ---- 工具 ---- */
static char g_dir[512];
static ncm_cache_t g_c;
static int g_fail;
#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)
/* 按真实生产命名建缓存文件:路径经 ncm_cache_path(与 app/play/cover 同约定,ext 带前导点)。
* 结果文件名 = song_<id>.<ext>(ext 带点时即双点,如 song_a..mp3 —— 与 core 现状一致)。 */
static void make_cached(const char *id, const char *ext, unsigned long long size, long mtime)
{
char p[600];
if (ncm_cache_path(&g_c, id, ext, p, sizeof p) != 0) { printf(" path err %s\n", id); g_fail = 1; return; }
int fd = open(p, O_WRONLY | O_CREAT, 0644);
if (fd < 0) { printf(" open %s failed\n", id); g_fail = 1; return; }
if (ftruncate(fd, (off_t)size) != 0) printf(" truncate %s err\n", id);
close(fd);
struct timespec ts[2];
ts[0].tv_sec = mtime; ts[0].tv_nsec = 0;
ts[1].tv_sec = mtime; ts[1].tv_nsec = 0;
utimensat(AT_FDCWD, p, ts, 0);
}
static int cached_exists(const char *id, const char *ext)
{
char p[600];
if (ncm_cache_path(&g_c, id, ext, p, sizeof p) != 0) return 0;
struct stat st;
return stat(p, &st) == 0;
}
static unsigned long long mb(unsigned long long n) { return n * 1024ULL * 1024ULL; }
static void cleanup(void)
{
/* 递归删 g_dir 下 song_*(恢复干净池) */
DIR *d = opendir(g_dir);
if (d) {
struct dirent *e;
while ((e = readdir(d))) {
if (strncmp(e->d_name, "song_", 5) != 0) continue;
char p[800];
snprintf(p, sizeof p, "%s/%s", g_dir, e->d_name);
unlink(p);
}
closedir(d);
}
}
int main(void)
{
snprintf(g_dir, sizeof g_dir, "/tmp/ncm_cache_sim_%d", (int)getpid());
cleanup();
ncm_cache_init(&g_c, g_dir);
ncm_cache_set_limit_gb(&g_c, 1); /* 1GB 最小上限(淘汰只在 total > limit 时触发) */
/* ① 超上限删最旧。A=700MB(最旧), B=300MB, C=400MB(最新) → 删 A 后 700MB<=1GB 停。 */
printf("== case 1: evict oldest over-limit ==\n");
cleanup();
make_cached("a", ".mp3", mb(700), 1000);
make_cached("b", ".mp3", mb(300), 1100);
make_cached("c", ".mp3", mb(400), 1200);
CHECK(ncm_cache_total_bytes(&g_c) == mb(1400), "total=%llu (expect 1400MB)",
(unsigned long long)ncm_cache_total_bytes(&g_c));
ncm_cache_evict(&g_c);
CHECK(!cached_exists("a", ".mp3"), "oldest A evicted");
CHECK(cached_exists("b", ".mp3") && cached_exists("c", ".mp3"), "B/C kept");
CHECK(ncm_cache_total_bytes(&g_c) == mb(700), "total=%llu after evict (expect 700MB)",
(unsigned long long)ncm_cache_total_bytes(&g_c));
/* ② 4×500MB 超 1GB:删 2 个最旧到 <=1GB。 */
printf("== case 2: evict two oldest ==\n");
cleanup();
make_cached("a", ".mp3", mb(500), 1000);
make_cached("b", ".mp3", mb(500), 1100);
make_cached("c", ".mp3", mb(500), 1200);
make_cached("d", ".mp3", mb(500), 1300);
ncm_cache_evict(&g_c);
CHECK(!cached_exists("a", ".mp3") && !cached_exists("b", ".mp3"), "A/B (two oldest) evicted");
CHECK(cached_exists("c", ".mp3") && cached_exists("d", ".mp3"), "C/D kept");
CHECK(ncm_cache_total_bytes(&g_c) == mb(1000), "total=%llu (expect 1000MB)",
(unsigned long long)ncm_cache_total_bytes(&g_c));
/* ③ 未超上限:无删除。 */
printf("== case 3: under limit no-op ==\n");
cleanup();
make_cached("a", ".mp3", mb(300), 1000);
make_cached("b", ".mp3", mb(200), 1100);
ncm_cache_evict(&g_c);
CHECK(cached_exists("a", ".mp3") && cached_exists("b", ".mp3"), "all kept under limit");
/* ④ lookup 刷新 mtime(LRU):A 被访问 → 变最新,淘汰先删 B/C。 */
printf("== case 4: lookup refreshes LRU mtime ==\n");
cleanup();
make_cached("a", ".mp3", mb(600), 1000);
make_cached("b", ".mp3", mb(600), 1100);
make_cached("c", ".mp3", mb(600), 1200);
char path[600];
CHECK(ncm_cache_lookup(&g_c, "a", ".mp3", path, sizeof path) == NCM_OK, "lookup A hit");
ncm_cache_evict(&g_c); /* A mtime 已被刷新为最新 → 删 B、C */
CHECK(cached_exists("a", ".mp3"), "A (recently accessed) survives");
CHECK(!cached_exists("b", ".mp3") && !cached_exists("c", ".mp3"), "B/C evicted");
/* ⑤ set_limit_gb 钳制。 */
printf("== case 5: limit clamping ==\n");
ncm_cache_set_limit_gb(&g_c, 0);
CHECK(ncm_cache_limit_gb(&g_c) == 1, "limit(0)->%d (expect 1)", ncm_cache_limit_gb(&g_c));
ncm_cache_set_limit_gb(&g_c, 200);
CHECK(ncm_cache_limit_gb(&g_c) == 100, "limit(200)->%d (expect 100)", ncm_cache_limit_gb(&g_c));
/* ⑥ clear 删 song_* 全部(mp3 + 封面 + 残留 .tmp)。 */
printf("== case 6: clear removes all (mp3+cover+tmp) ==\n");
cleanup();
make_cached("x", ".mp3", mb(10), 1000);
make_cached("y", ".mp3", mb(10), 1001);
make_cached("cov123", ".jpg", mb(1), 1002);
/* 残留 .tmp:取 mp3 生产路径后追加 .tmp(ncm_cache_open 的临时名)。 */
{
char tp[600];
ncm_cache_path(&g_c, "z", ".mp3", tp, sizeof tp);
strncat(tp, ".tmp", sizeof tp - strlen(tp) - 1);
int fd = open(tp, O_WRONLY | O_CREAT, 0644);
if (ftruncate(fd, (off_t)mb(5)) != 0) printf(" truncate tmp err\n");
close(fd);
}
CHECK(ncm_cache_clear(&g_c) == NCM_OK, "clear rc=0");
CHECK(!cached_exists("x", ".mp3") && !cached_exists("y", ".mp3") &&
!cached_exists("cov123", ".jpg") && ncm_cache_total_bytes(&g_c) == 0,
"all song_* (mp3+cover+tmp) removed; total=0");
CHECK(ncm_cache_total_bytes(&g_c) == 0, "total=%llu after clear (expect 0)",
(unsigned long long)ncm_cache_total_bytes(&g_c));
/* ⑦ 阶段3:播放中的 .tmp / 中断残体 .part 不被 LRU 淘汰,且不计入池占用。
* 「正在下载播放的文件」= .tmp(写穿缓存态);「上次中断的可续传残体」= .part。
* 二者在 evict/total_bytes 中被跳过——防正在听的歌被缓存上限挤掉(§5.1)。 */
printf("== case 7: evict skips .tmp/.part (playing file not evicted; stage3 §5.1) ==\n");
cleanup();
ncm_cache_set_limit_gb(&g_c, 1); /* 1GB(int 内,避免既有 gb_to_bytes int 溢出 bug 干扰本用例) */
make_cached("a", ".mp3", mb(600), 1000); /* 600MB 最旧 */
make_cached("b", ".mp3", mb(600), 1100); /* 600MB */
/* 在途 .tmp(边下边播写穿缓存态):song_b.mp3.tmp */
{
char p[600];
ncm_cache_path(&g_c, "b", ".mp3", p, sizeof p);
strncat(p, ".tmp", sizeof p - strlen(p) - 1);
int fd = open(p, O_WRONLY | O_CREAT, 0644);
if (ftruncate(fd, (off_t)mb(300)) != 0) printf(" truncate tmp err\n");
close(fd);
}
/* 中断残体 .part(可续传):song_d.mp3.part */
{
char p[600];
ncm_cache_path(&g_c, "d", ".mp3", p, sizeof p);
strncat(p, ".part", sizeof p - strlen(p) - 1);
int fd = open(p, O_WRONLY | O_CREAT, 0644);
if (ftruncate(fd, (off_t)mb(300)) != 0) printf(" truncate part err\n");
close(fd);
}
CHECK(ncm_cache_total_bytes(&g_c) == mb(1200),
"total excludes .tmp/.part (=%lluMB, expect 1200MB)",
(unsigned long long)(ncm_cache_total_bytes(&g_c) / (1024*1024)));
ncm_cache_evict(&g_c);
CHECK(!cached_exists("a", ".mp3"), "oldest committed a evicted");
CHECK(cached_exists("b", ".mp3"), "b kept (<=limit)");
/* .tmp/.part 文件仍在(未被打包进 LRU 淘汰) */
{
char tp[600], pp[600];
ncm_cache_path(&g_c, "b", ".mp3", tp, sizeof tp); strncat(tp, ".tmp", sizeof tp-strlen(tp)-1);
ncm_cache_path(&g_c, "d", ".mp3", pp, sizeof pp); strncat(pp, ".part", sizeof pp-strlen(pp)-1);
struct stat st;
CHECK(stat(tp, &st) == 0, "playing .tmp survives evict");
CHECK(stat(pp, &st) == 0, "resume .part survives evict");
}
CHECK(ncm_cache_total_bytes(&g_c) == mb(600),
"total after evict excludes partials (=%lluMB, expect 600MB)",
(unsigned long long)(ncm_cache_total_bytes(&g_c) / (1024*1024)));
/* ⑧ 阶段3:部分文件 API 快检——part_size 检测 / open_resume 追加 / abort_part 保留 /
* abort 删除 的命名契约(供 player_manager 续传链路)。 */
printf("== case 8: .part resume API contract (stage3 §5.2) ==\n");
cleanup();
{
/* 用 ncm_cache_open_resume 打开上一步遗留 .part 无(已 cleanup)——先造一个真 .part */
char p[600];
ncm_cache_path(&g_c, "r", ".mp3", p, sizeof p);
snprintf(p + strlen(p), sizeof p - strlen(p), ".part");
int fd = open(p, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { (void)!write(fd, "ABCDEF", 6); close(fd); }
uint64_t sz = 0;
CHECK(ncm_cache_part_size(&g_c, "r", ".mp3", &sz) == NCM_OK && sz == 6,
"part_size reports 6 (got %llu)", (unsigned long long)sz);
ncm_cache_writer_t *w = NULL;
CHECK(ncm_cache_open_resume(&g_c, "r", ".mp3", &w) == NCM_OK, "open_resume ok");
CHECK(ncm_cache_write(w, (const unsigned char *)"GH", 2) == NCM_OK, "resume append write");
CHECK(ncm_cache_commit(w) == NCM_OK, "resume commit (rename .part->final)");
/* commit 后 .part 转正:lookup 命中且内容=ABCDEFGH,.part 消失 */
char path[600];
CHECK(ncm_cache_lookup(&g_c, "r", ".mp3", path, sizeof path) == NCM_OK, "resume commit lookup hit");
FILE *f = fopen(path, "rb");
char buf[16] = {0}; int n = 0;
if (f) { n = (int)fread(buf, 1, sizeof buf - 1, f); fclose(f); }
CHECK(n == 8 && memcmp(buf, "ABCDEFGH", 8) == 0, "committed bytes=%.8s (expect ABCDEFGH)", buf);
CHECK(ncm_cache_part_size(&g_c, "r", ".mp3", &sz) == NCM_ERR_NOENT, ".part gone after commit");
/* abort_part 保留残体 / abort 删除:造两个 .part 分别走两路 */
ncm_cache_path(&g_c, "s", ".mp3", p, sizeof p);
snprintf(p + strlen(p), sizeof p - strlen(p), ".part");
fd = open(p, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { (void)!write(fd, "1234", 4); close(fd); }
CHECK(ncm_cache_open_resume(&g_c, "s", ".mp3", &w) == NCM_OK, "s open_resume");
CHECK(ncm_cache_abort_part(w) == NCM_OK, "s abort_part");
struct stat st2;
CHECK(stat(p, &st2) == 0, "abort_part keeps .part (resumable)");
CHECK(ncm_cache_open_resume(&g_c, "s", ".mp3", &w) == NCM_OK, "s reopen_resume");
CHECK(ncm_cache_abort(w) == NCM_OK, "s abort (delete)");
CHECK(stat(p, &st2) != 0, "abort deletes .part");
/* part_remove 显式删除 */
fd = open(p, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { (void)!write(fd, "x", 1); close(fd); }
CHECK(ncm_cache_part_remove(&g_c, "s", ".mp3") == NCM_OK, "part_remove ok");
CHECK(stat(p, &st2) != 0, "part_remove deleted .part");
}
cleanup();
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
return g_fail ? 1 : 0;
}