feat(ncm): 三期第二批 — 中文搜索接拼音IME+设置页做好(音质全档/缓存上限可设数字+清理缓存/关于)+缓存LRU淘汰接生产路径(写前写后+手动调低上限)

- 搜索页输入框 MEOW_IME_ASCII→MEOW_IME_PINYIN: 候选上屏UTF-8经 on_change 实时回写 g_st.search_query, 键盘「拼/英」「中」切换, 英文零回归; 搜索仍仅在点「搜索」时发 cloudsearch(风控)
- 设置页 5 行: 音质(Hi-Res 默认,Lossless/Exhigh/Higher/Standard 全档,core 既有降级链)/缓存上限(1~100GB 预设+「自定义」NUM IME 数字输入,默认10上限100)/清理缓存(显示当前占用,ncm_cache_clear)/退出登录/关于行
- 缓存淘汰接线: ncm_cache_evict 进生产路径 — player_manager(下载写前/写后)+cover(写前/写后)+appstate_save_settings(手动调低上限立即淘汰); 封面与 mp3 同池; 上限读设置值
- core 新增 ncm_cache_clear; 设置持久化走 meow_config_* → MeowData/dev.player.ncm/data
- bench/cache_evict_sim.c 离线回归(0网络) 6 组 PASS; pagination_sim/core(host+mipsel/qemu)/6页宿主渲染 全过; mipsel -Wall -Wextra 0 warning
(未动 sdk/、meow_launcherd/、vendor/;0 真实 NCM 请求;真机部署留用户)
This commit is contained in:
MeowOS committed 2026-08-31 20:15:31 +08:00
1 parent 1a239c5eab
commit f23720f934
13 files changed
+469 -33

No files matched your search

+201
View File
@@ -0,0 +1,201 @@
/* 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));
cleanup();
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
return g_fail ? 1 : 0;
}