perf(reader): M32 r2 书架索引改 level-1 校验 — 真机冷缓存首开 3.47s→0.13–0.16s(r1 全目录校验被真机证伪)

r1 用宿主 1.1µs/stat + M28 的**热** stat 150µs 外推「434 次≈65ms」,真机 drop_caches 后
实测是 3.15s(7.26ms/次),与 readdir 的 3.0s 同量级:全目录校验的索引命中冷启动仍要
3.06s,只比全扫快 0.2s。「3.0s→0.15s」在冷启动不成立。按任务 1 自己定的判据
(stat 接近 1s 就退回 level-1)改为只校验 Books 根 + 每个一级目录。

真机冷缓存实测(exFAT /usr/data/mnt/sd_0,434 目录/646 文件/595 本;sync + drop_caches;
探针走 /proc/uptime,UI 走 meow_ui_time_ms):
- 索引命中首开:**127 / 138 / 157ms**(复测 147ms),热 dentry 107–120ms;
- 全量扫描首开:**3428 / 3477ms**(复测 3711ms),热 dentry 不变(readdir 不吃 page cache);
- 探针冷分项:索引载入 20–30ms;载入+rdprog×595 70–120ms;4 个目录 stat 0–30ms;
- 从阅读页返回书架 29–31ms(M29 语义未变)。
→ 冷启动 22–26 倍提升,方案值得做;索引实测 **63,579B**(v2;r1 全目录 97,490B)。

实现(格式 v2,magic MEOWIDX2 / version 2):
- writer 回调过滤 rel 含 '/' 的目录:只落 root + 一级目录的 mtime;书/大小/格式记录仍全量;
  level-2+ 不落盘、载入不 stat。载入仍然 0 次 opendir/readdir(strace:0 getdents64,
  真实库 4 次 stat;宿主模拟库 11 次)。
- 校验记录里出现 level-2+ rel 视为坏档;sum(books_here) 改为「≤ nbooks」合理性上界。
- fail-open/原子写/CRC/安全加固/ts 不缓存全部保留(r1 已过评审的部分未动)。

失效矩阵(报文逐条写清):
自动(根 mtime):Books 根下直接增/删/改名书;一级目录新增/删除/改名。
自动(一级目录 mtime):一级目录里直接增/删/改名书、新建子目录。
必须刷新:level-2 及更深处的任何变化(只改深层目录 mtime)。刷新=删索引+全扫。
真机 exFAT mtime 实测 **1 秒**(tm_nsec=0;同秒连续增删 mtime 不变)。我复测 10 轮
「目录刚改过就扫描、同秒再改」→ 9/10 仍命中(陈旧),与评审 29/30 一致;sleep 跨秒后
全部正常失效。头注释已把「10ms」改为「1 秒」,并写明「不在同一秒刻度的变更靠索引失效,
同秒二次变更需刷新」。

bench 修复(P3-3):MEOW_BENCH_IDX=only —— **不预先 load/scan**,直接进书架量生产首开;
miss —— 先删索引再进书架。两者配合 drop_caches 即冷启动口径。旧行为(先 bench_index/
bench_scan 焐热)保留为默认模式。新增 test/shelf_index_probe_mipsel(write/scan/load/
loadts/statpaths/mtime/size)用于真机分项与 mtime 粒度验证,不进发行包。

测试:shelf_index_smoke 170 pass(新增:level-3/level-2 内变更「索引仍命中且先看到旧
列表 → 刷新后可见」、新建 level-2 目录自动失效、一级目录直接删书自动失效、v2 ndirs=
根+一级数);ASan+UBSan 干净;build_reader.sh 0 warning,core 6846 / seg 19 / chapter 71 /
shelf_group 63 / index 170 / font 14 / loaders PASS。真机 6 场景逐项验证:一级直接增/
新建子目录/一级改名/根下删书/一级直接删书 = 自动失效;level-3 深处新增 = 仍命中(需刷新)。

交付:app.json 0.1.5/code6 → **0.1.6/code7**;reader md5 5c848c99… 1,711,988B;
mpk md5 10d676f5… 814,574B;未签名不 push。设备验证后已清理 /tmp 与 m32probe,
真实 MeowData 的 shelf.idx 已删(app 自己的章节 .idx 未动),Books 434/646 不变,
已装 0.1.4 md5 4036b889… 未变。
This commit is contained in:
AskaEth committed 2026-11-10 18:49:48 +08:00
1 parent e0dfab95c3
commit 58f2d9bff4
10 files changed
+309 -57

No files matched your search

+77 -16
View File
@@ -3,8 +3,9 @@
* 覆盖:
* I1 全量扫描 → 写索引 → 载入:书数/书名/分组/path/fmt/size/ts 全部与扫描一致
* I2 载入不缓存阅读进度:同一索引换 ts_fn 再载入,ts 立刻反映新值(含真实 rdprog 文件)
* I3 四种书库变更都必须失效(fail-open 重扫):
* level-2 新增 / level-3 深处新增 / level-1 目录改名 / 删书
* I3 v2 校验范围 = Books 根 + 一级目录:
* 自动失效:一级目录直接增/删书、新建 level-2 目录、一级目录改名、根下增删书;
* 必须刷新:level-3 深处新增、已有 level-2 目录内新增(索引仍命中,先看到旧列表)
* I4 坏档 fail-open:截断 / 中间字节翻转 / 垃圾内容 / 文件缺失 / 索引路径是目录 /
* 版本不符 / Books 根路径不符 —— 一律返回 0,不崩
* I5 truncated / depth_skipped 如实重建(索引里存了这两个值)
@@ -86,6 +87,21 @@ static int idx_path(const char *root, char *out, int outsz)
return shelf_index_path(root, out, outsz);
}
/* 读索引 trailer 的 ndirs(v2 应 = 1 + 一级目录数)。 */
static uint32_t idx_trailer_ndirs(const char *root)
{
char ip[500];
if (idx_path(root, ip, sizeof ip) != 0) return 0;
FILE *f = fopen(ip, "rb");
if (!f) return 0;
unsigned char tr[24];
if (fseek(f, -(long)sizeof tr, SEEK_END) != 0 ||
fread(tr, 1, sizeof tr, f) != sizeof tr) { fclose(f); return 0; }
fclose(f);
return (uint32_t)tr[0] | ((uint32_t)tr[1] << 8) | ((uint32_t)tr[2] << 16) |
((uint32_t)tr[3] << 24);
}
static int scan_and_save(const char *root, shelf_book *books, int cap, shelf_scan_report *rep)
{
shelf_idx_writer *w = shelf_index_writer_open(root);
@@ -134,6 +150,9 @@ static void test_roundtrip(const char *root)
struct stat st;
CHECK(stat(ip, &st) == 0 && st.st_size > 0, "索引文件已落盘");
printf("index: %s (%lld bytes)\n", ip, (long long)st.st_size);
/* v2:只落根 + 一级目录(本树一级 = 轻小说/玄幻/B/D/G/400+合集/空目录 → 7,加根 = 8)。
* 这个数直接决定载入时的 stat 次数(真机冷缓存 4 次≈20ms,434 次=3.15s)。 */
CHECK(idx_trailer_ndirs(root) == 8, "v2 目录记录 = Books 根 + 7 个一级目录");
int n2 = shelf_index_load(root, loadb, SHELF_MAX_BOOKS, ts_of, &r2);
CHECK(n2 == 1, "索引命中");
@@ -191,8 +210,21 @@ static void test_ts_not_cached(const char *root)
}
}
/* I3:四种变更都必须让索引失效。每次变更后:先断言 load=0(不静默用陈旧索引),
* 再全量重扫 + 写新索引,断言变化可见。 */
/* I3(v2 = 只校验 Books 根 + 一级目录):
* 自动失效(load 返回 0 → 全扫):
* ① 一级目录里直接新增书(一级目录 mtime 变)
* ② 一级目录里新建子目录(一级目录 mtime 变)
* ③ 一级目录改名(Books 根 mtime 变 + 旧目录 stat 失败)
* ④ Books 根下删书(根 mtime 变)
* ⑤ 一级目录里直接删书(一级目录 mtime 变)
* 必须点「刷新」(load 仍命中,先看到旧列表):
* ⑥ level-3 深处新增(只改更深目录的 mtime)
* ⑦ 已有 level-2 目录里新增(只改 level-2 的 mtime)
*
* 注意:宿主 ext4/f2fs 的 mtime 有纳秒精度,usleep(20000) 足以拉开刻度;**真机 exFAT 的
* 目录 mtime 只有 1 秒精度**(实测 tm_nsec=0),同秒内二次改动不会变——本测试无法模拟
* 那个真机窗口(见 shelf_index.h 的 mtime 精度说明)。
*/
static void test_invalidation(const char *root)
{
static shelf_book b[SHELF_MAX_BOOKS];
@@ -201,22 +233,22 @@ static void test_invalidation(const char *root)
CHECK(scan_and_save(root, b, SHELF_MAX_BOOKS, &rep) > 0, "基线扫描写索引");
/* ① level-2 直接新增 */
/* ① 一级目录里直接新增(level-2 相对 Books 是直接子项) */
usleep(20000);
snprintf(p, sizeof p, "%s/轻小说/新书.txt", root); write_file(p, "new");
CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0,
"level-2 新增 → 索引失效");
"一级目录直接新增 → 索引失效");
int n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep);
CHECK(find_title(b, n, "新书") != NULL, "重扫后新书可见");
/* ② level-3 深处新增(Books/400+.../01、玄幻/01、斗破/) */
/* ② 一级目录里新建 level-2 子目录(一级目录 mtime 变)→ 自动失效 */
usleep(20000);
snprintf(p, sizeof p, "%s/400+本高质量完本合集/01、玄幻/01、斗破/新书2.txt", root);
write_file(p, "new2");
snprintf(p, sizeof p, "%s/轻小说/新子目录", root); mkdirs(p);
snprintf(p, sizeof p, "%s/轻小说/新子目录/深书.txt", root); write_file(p, "deep");
CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0,
"level-3 深处新增 → 索引失效");
"新建 level-2 目录 → 索引失效");
n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep);
CHECK(find_title(b, n, "新书2") != NULL, "重扫后深处新书可见");
CHECK(find_title(b, n, "深书") != NULL, "重扫后新子目录里的书可见");
/* ③ 一级目录改名 */
usleep(20000);
@@ -230,14 +262,43 @@ static void test_invalidation(const char *root)
CHECK(m && strcmp(m->top, "玄幻改") == 0 && strcmp(m->group, "玄幻改") == 0,
"重扫后分组键跟随新目录名");
/* ④ 删书 */
/* ④ Books 根下删书(根 mtime 变) */
usleep(20000);
snprintf(p, sizeof p, "%s/根下.txt", root);
CHECK(unlink(p) == 0, "删除一本书");
CHECK(unlink(p) == 0, "删除 Books 根下的一本书");
CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0,
"删书 → 索引失效");
"根下删书 → 索引失效");
n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep);
CHECK(find_title(b, n, "根下") == NULL, "重扫后删掉的书消失");
/* ⑤ 一级目录里直接删书(一级目录 mtime 变) */
usleep(20000);
snprintf(p, sizeof p, "%s/轻小说/2.epub", root);
CHECK(unlink(p) == 0, "删除一级目录里直接放的书");
CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0,
"一级目录直接删书 → 索引失效");
n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep);
CHECK(find_title(b, n, "2") == NULL, "重扫后该书消失");
/* ⑥ level-3 深处新增:**不自动失效**(level-1 与根 mtime 都没变)→ 需刷新 */
snprintf(p, sizeof p, "%s/400+本高质量完本合集/01、玄幻/01、斗破/新书2.txt", root);
write_file(p, "new2");
CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 1,
"level-3 深处新增 → 索引仍命中(预期:需刷新)");
CHECK(find_title(b, rep.nbooks, "新书2") == NULL, "命中时看不到深处新书(陈旧直到刷新)");
shelf_index_remove(root); /* 顶栏「刷新」= 删索引 + 全扫 */
n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep);
CHECK(find_title(b, n, "新书2") != NULL, "刷新后深处新书可见");
/* ⑦ 已有 level-2 目录里新增:**不自动失效** → 需刷新 */
snprintf(p, sizeof p, "%s/B/sub/新书3.txt", root);
write_file(p, "new3");
CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 1,
"level-2 目录内新增 → 索引仍命中(预期:需刷新)");
CHECK(find_title(b, rep.nbooks, "新书3") == NULL, "命中时看不到 level-2 新书(陈旧直到刷新)");
shelf_index_remove(root);
n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep);
CHECK(find_title(b, n, "新书3") != NULL, "刷新后 level-2 新书可见");
}
static void test_bad_index(const char *root)
@@ -299,12 +360,12 @@ static void test_bad_index(const char *root)
CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 1, ".tmp 残渣不影响载入");
unlink(tmpip);
/* 版本不符:先把版本字段改成 2 */
/* 版本不符:先把版本字段改成 3 */
CHECK(scan_and_save(root, b, SHELF_MAX_BOOKS, &rep) > 0, "重写索引");
f = fopen(ip, "r+b");
CHECK(f != NULL, "打开索引改版本");
if (f) {
unsigned char v[4] = { 2, 0, 0, 0 };
unsigned char v[4] = { 3, 0, 0, 0 };
CHECK(fseek(f, 8, SEEK_SET) == 0, "定位版本字段");
fwrite(v, 1, sizeof v, f);
fclose(f);