From 66e598cb622f9520423ca19195f6ee5fb93d9478 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 5 Nov 2026 12:31:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E5=BA=93:=20=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E7=BC=A9=E7=95=A5=E5=9B=BE=20LRU=20=E8=B7=B3=E8=BF=87=E8=A7=A3?= =?UTF-8?q?=E5=8E=8B=E9=A1=B5=E7=BC=93=E5=AD=98=EF=BC=88M11=20P2-1=20?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=EF=BC=89=EF=BC=8C=E8=A1=A5=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E7=A9=BA=E5=88=97=E8=A1=A8/prune=20=E5=9B=9E=E5=BD=92=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=EF=BC=88M13=20=E8=AF=84=E5=AE=A1=20P2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ui/library/library.c cache_prune:rebase 时丢失了 M11 c775f85 的必修保护, 恢复 `if (mr_cache_is_page_name(de->d_name)) continue;`(在 .bmp 过滤之前)。 解压页缓存与缩略图同目录,mrpage_* 归 page_cache 自己的 64 页/32MiB LRU 管理; 否则档案页被缩略图 LRU 统计并删除 → 每翻页重解压,且「touch 命中→解码」窗口内 可能被删导致解码失败。 - 回归测试(MR_LIBRARY_UI_TEST_HOOKS 接缝):test_browse_ui_host 新增 U6—— 预置 1 个最旧 mrpage_*.bmp + 65 个较新缩略图,调用 library_test_cache_prune(), 断言 mrpage_* 保留且不计入 64 张额度、缩略图被裁到 64;已验证去掉修复时该用例 会失败(防止再次在 rebase 中丢失)。 - 顺手补齐/校正: ① test_browse_ui_host 的启动路径改为 create 前就把根指向不存在的目录,真实覆盖 「启动根不可读仍建空列表」(断言 has_list/rows==0/横幅/当前目录),不再只是声明; ② tuku_internal.h 的 library_at_root 注释改为「书库根(生产=MR_ROOT_DIR,测试可改根)」。 - 复验:run_tests.sh 全 PASS(73/69/90/18/108/19/85 + settings 集成 + browse UI 集成 + zip 106 + cbz 325);run_webtoon_tests.sh 全 PASS(76/36/71/57 + 集成); sdk/build_sdk.sh + build_tuku.sh 零警告 MIPS32r2 静态 ELF;UI host 测试 ASan/UBSan 全绿。 --- tuku/test/test_browse_ui_host.c | 99 ++++++++++++++++++++++++++++++++- tuku/tuku_internal.h | 2 +- tuku/ui/library/library.c | 25 +++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/tuku/test/test_browse_ui_host.c b/tuku/test/test_browse_ui_host.c index b1bbe73..f278098 100644 --- a/tuku/test/test_browse_ui_host.c +++ b/tuku/test/test_browse_ui_host.c @@ -6,7 +6,10 @@ * U2 行语义:书行显示 "N pages"(无进入箭头),空目录「空目录」,无图目录「无图片」; * U3 导航:可逐级上溯父目录;文件系统 "/" 处 library_go_up 返回 0(app.c 据此退出); * 「TF卡」回根动作回到书库根; - * U4 不可读目录:load 失败 → 停留原目录 + 顶部提示横幅。 + * U4 不可读目录:load 失败 → 停留原目录 + 顶部提示横幅; + * U5 启动根不可读:create 时仍建空列表(不是只有横幅); + * U6 缩略图 LRU 跳过解压页缓存(M11 P2-1 回归):cache_prune 只统计/删除缩略图, + * mrpage_*(page_cache 的解压页)必须保留且不计入 64 张额度。 * * 编译:见 test/run_tests.sh(与 test_settings_lock_host 同口径:链 SDK 源码树 + 打桩; * 只发布 meow_ui.a 的交付环境自动 SKIP)。ui/library/library.c 以 @@ -19,6 +22,8 @@ #include #include #include +#include +#include #include #include @@ -34,6 +39,10 @@ int library_test_hint_active(void); const char *library_test_hint(void); int library_test_go_root(void); int library_test_go_up(void); +int library_test_has_list(void); +const char *library_test_cache_dir(void); +int library_test_cache_on(void); +void library_test_cache_prune(void); static int g_fail; #define CHECK(cond, ...) do { \ @@ -114,6 +123,33 @@ static int row_index_for(const char *name) return -1; } +static void set_mtime(const char *path, time_t t) +{ + struct timespec ts[2]; + ts[0].tv_sec = t; + ts[0].tv_nsec = 0; + ts[1] = ts[0]; + utimensat(AT_FDCWD, path, ts, 0); +} + +/* 统计缓存目录里的缩略图(*.bmp 且非 mrpage_*)与解压页(mrpage_*.bmp)。 */ +static int count_bmp(const char *dir, int *pages) +{ + DIR *d = opendir(dir); + if (!d) return -1; + int thumbs = 0, pg = 0; + struct dirent *de; + while ((de = readdir(d)) != NULL) { + size_t len = strlen(de->d_name); + if (len <= 4 || strcmp(de->d_name + len - 4, ".bmp") != 0) continue; + if (strncmp(de->d_name, "mrpage_", 7) == 0) pg++; + else thumbs++; + } + closedir(d); + if (pages) *pages = pg; + return thumbs; +} + static void wait_ready(int ms) { for (int t = 0; t < ms / 10; t++) { @@ -130,6 +166,55 @@ static void wait_ready(int ms) } } +/* U6:缩略图 LRU 必须跳过解压页缓存(M11 P2-1 回归;MR_CACHE_MAX_FILES=64)。 */ +static void test_cache_prune_skips_pages(void) +{ + const char *dir = library_test_cache_dir(); + CHECK(library_test_cache_on() && dir && dir[0], "thumbnail cache enabled: %s", + dir ? dir : ""); + if (!dir || !dir[0]) return; + + /* 清空之前可能写入的 bmp(本测试独占 MEOW_DATA_DIR)。 */ + DIR *d = opendir(dir); + if (d) { + struct dirent *de; + while ((de = readdir(d)) != NULL) { + size_t len = strlen(de->d_name); + if (len > 4 && strcmp(de->d_name + len - 4, ".bmp") == 0) { + char full[MR_PATH_MAX + 96]; + snprintf(full, sizeof full, "%s/%s", dir, de->d_name); + unlink(full); + } + } + closedir(d); + } + + char page[MR_PATH_MAX + 96], thumb[MR_PATH_MAX + 96]; + snprintf(page, sizeof page, "%s/mrpage_0123456789abcdef.bmp", dir); + CHECK(touch_file(page) == 0, "create page cache file"); + set_mtime(page, time(NULL) - 7200); /* 最旧:若被误删一定先删它 */ + + time_t base = time(NULL) - 3600; + for (int i = 0; i < 65; i++) { /* 65 张缩略图 > MR_CACHE_MAX_FILES */ + snprintf(thumb, sizeof thumb, "%s/%016x_%ld_%d.bmp", dir, + (unsigned)i, (long)(base + i), i); + CHECK(touch_file(thumb) == 0, "create thumbnail %d", i); + set_mtime(thumb, base + i); + } + + int pages_before = 0; + CHECK(count_bmp(dir, &pages_before) == 65, "precondition 65 thumbnails"); + CHECK(pages_before == 1, "precondition 1 page (got %d)", pages_before); + + library_test_cache_prune(); + + int pages_after = 0; + int thumbs_after = count_bmp(dir, &pages_after); + CHECK(access(page, R_OK) == 0, "mrpage_* survives thumbnail LRU"); + CHECK(pages_after == 1, "mrpage_* not counted as thumbnail (got %d)", pages_after); + CHECK(thumbs_after == 64, "thumbnails trimmed to 64 (got %d)", thumbs_after); +} + int main(void) { const char *base = getenv("TMPDIR"); @@ -157,7 +242,17 @@ int main(void) meow_ui_opts o = { 480, 720 }; if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; } + /* U5:先指向一个不存在的根,再 create —— 真实走「启动时根目录不可读」分支。 */ + const char *bad_root = "/no/such/tuku/m13_root"; + library_test_set_root(bad_root); + CHECK(strcmp(library_test_root(), bad_root) == 0, "bad root installed"); meow_view *lib = library_view_create(); + CHECK(library_test_hint_active(), "startup unreadable root shows hint: %s", + library_test_hint()); + CHECK(library_test_rows() == 0, "startup empty list rows=%d", library_test_rows()); + CHECK(library_test_has_list(), "startup built listview for empty list"); + CHECK(strcmp(library_test_dir(), bad_root) == 0, "startup dir=%s", library_test_dir()); + library_test_set_root(root); CHECK(strcmp(library_test_root(), root) == 0, "test root installed"); meow_view_page_push(ACT_LIBRARY); @@ -260,6 +355,8 @@ int main(void) printf(" SKIP unreadable-dir case (running as root)\n"); } + test_cache_prune_skips_pages(); + meow_view_free(lib); meow_ui_shutdown(); rm_rf(data); diff --git a/tuku/tuku_internal.h b/tuku/tuku_internal.h index dffae61..b23d6d5 100644 --- a/tuku/tuku_internal.h +++ b/tuku/tuku_internal.h @@ -37,7 +37,7 @@ typedef struct mr_page_ctrl { /* ---- 书库页(ui/library/library.c) ---- */ meow_view *library_view_create(void); -int library_at_root(void); /* 1=当前在 MR_ROOT_DIR */ +int library_at_root(void); /* 1=当前在书库根(生产=MR_ROOT_DIR,宿主测试可改根) */ int library_go_up(void); /* 上溯父目录:1=已上移/提示停留,0=已在文件系统 "/"(app.c 据此退出) */ void library_on_tap(int x, int y); void library_on_swipe(int dir, int dist); /* dist=SDK on_swipe 的位移量(UP/DOWN 用) */ diff --git a/tuku/ui/library/library.c b/tuku/ui/library/library.c index 0332796..a58f8a3 100644 --- a/tuku/ui/library/library.c +++ b/tuku/ui/library/library.c @@ -186,6 +186,10 @@ static void cache_prune(void) time_t oldest_t = 0; struct dirent *de; while ((de = readdir(d)) != NULL) { + /* 解压页缓存(M11)与缩略图同目录:mrpage_* 归 page_cache 自己的 + * 64 页/32MiB LRU 管,这里绝不能把它们当缩略图统计或删除——否则 + * 档案页会被缩略图 LRU 清掉(P2-1)。判据与 page_cache 共用。 */ + if (mr_cache_is_page_name(de->d_name)) continue; const char *dot = strrchr(de->d_name, '.'); if (!dot || strcmp(dot, ".bmp") != 0) continue; char full[MR_PATH_MAX + 96]; @@ -977,6 +981,11 @@ int library_test_rows(void) return g_lib.count + (g_lib.truncated ? 1 : 0); } +int library_test_has_list(void) +{ + return g_lv != NULL; +} + int library_test_src(int idx, meow_list_item *item) { return lib_src(idx, item, NULL); @@ -1002,4 +1011,20 @@ int library_test_go_up(void) { return library_go_up(); } + +/* 缩略图 LRU 接缝(M11 P2-1 回归):指向当前缓存目录并同步跑一次 prune。 */ +const char *library_test_cache_dir(void) +{ + return g_cache_dir; +} + +int library_test_cache_on(void) +{ + return g_cache_on; +} + +void library_test_cache_prune(void) +{ + cache_prune(); +} #endif