/* 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 #include #include #include #include #include #include #include #include /* ---- 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_.(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)); cleanup(); printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS"); return g_fail ? 1 : 0; }