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… 未变。
72 lines
4.9 KiB
C
72 lines
4.9 KiB
C
/* app/shelf_index.h — 书架索引落盘(M32):把一次全量扫描的结果 + **Books 根与一级目录**
|
||
* 的 mtime 存到 `MeowData/os.meow.reader/index/shelf.idx`,冷启动时只做 4 次左右的
|
||
* stat 校验,命中就完全跳过 readdir。
|
||
*
|
||
* 为什么只校验一级(M32 r2,真机冷缓存实测后按任务 1 的判据改):
|
||
* 真机 exFAT(drop_caches 后)量到 434 个目录逐个 stat = **3.15s(7.26ms/次)**,
|
||
* 与全量 readdir 的 3.0s 同量级;全目录校验的索引命中冷启动仍要 3.06s,只比全扫快 0.2s,
|
||
* 任务目标不成立。而 Books 根 + 3 个一级目录共 4 次 stat,冷缓存只有 **~20ms**。
|
||
* 真实数字(真机 drop_caches 实测,2026-11-10):
|
||
* - 冷缓存:level-1 索引命中首开 ≈0.13–0.16s;全量扫描首开 ≈3.43–3.48s;
|
||
* - 热 dentry:索引命中 ≈0.03–0.11s;全量扫描始终 ≈3.1–3.3s(readdir 不吃 page cache)。
|
||
* - 探针分项(冷):索引载入 20–30ms;载入+rdprog×595 70–120ms;4 个目录 stat 0–30ms。
|
||
*
|
||
* 校验范围 = Books 根 + 每个一级目录(rel 为空或不含 '/')。**level-2 及更深的目录不落盘、
|
||
* 不 stat**(代价见下)。载入路径仍然完全没有 opendir/readdir。
|
||
*
|
||
* 能自动失效 vs 必须点「刷新」:
|
||
* 自动(Books 根 mtime 变):Books 根下直接放书/删书/改名;一级目录新增/删除/改名。
|
||
* 自动(一级目录 mtime 变):一级目录里**直接**放书/删书/改名/新建子目录。
|
||
* 必须刷新:level-2 及更深处的任何变化(新增/删除/改名书籍或目录)——这些只改更深层
|
||
* 目录的 mtime,一级目录与 Books 根都不变。这与 M29「新书靠刷新」是同一 UX。
|
||
* 刷新 = shelf_index_remove() + 全量重扫(之后写回新索引)。
|
||
*
|
||
* mtime 精度(真机 exFAT 实测):**1 秒**(tm_sec 有值、tm_nsec=0);同一秒内连续两次改动,
|
||
* 目录 mtime 完全不变,30 轮「扫描后同秒内新增」有 29 轮索引仍命中。因此:**不在同一 mtime
|
||
* 秒刻度内的变更靠索引失效;同秒二次变更需刷新**。FAT32 是 2 秒。
|
||
*
|
||
* 安全优先(fail-open):版本不符 / 文件缺失 / 截断 / CRC 错 / 根或一级目录 mtime 变化 /
|
||
* 任何 IO 错误 → shelf_index_load() 返回 0,调用方照常全量重扫。**绝不**用陈旧索引冒充
|
||
* 真实书库,也绝不因为索引写失败而让书架打不开。
|
||
*
|
||
* 索引里**不存阅读进度 ts**:ts 每次由调用方的 ts_fn(生产=rdprog_load)现读,否则
|
||
* 「刚读完回书架」会显示旧的置顶顺序。索引只存与进度无关的扫描事实。
|
||
*/
|
||
#ifndef SHELF_INDEX_H
|
||
#define SHELF_INDEX_H
|
||
|
||
#include "shelf_model.h"
|
||
|
||
/* 落盘格式版本:**扫描语义变了必须 +1**(新增格式扩展名/改深度/改分组/改校验范围等),
|
||
* 版本不符时旧索引直接作废重扫。v2 = 只存根 + 一级目录的 mtime。 */
|
||
#define SHELF_INDEX_VERSION 2
|
||
|
||
/* 载入并校验索引。成功返回 1(books/rep 填好、已按 ts 排序,等价于一次全量扫描);
|
||
* 任何异常返回 0(fail-open,books 内容不可用,调用方必须全量重扫)。
|
||
* 成功路径**不做 opendir/readdir**:只 open/read 索引 + stat 根/一级目录 + 每本书 ts_fn。 */
|
||
int shelf_index_load(const char *root, shelf_book *books, int cap,
|
||
shelf_ts_fn ts_fn, shelf_scan_report *rep);
|
||
|
||
/* 增量写索引(扫描时逐目录上报 → 扫描结束提交)。
|
||
* dir 回调会**过滤掉 level-2+**(只落根 + 一级目录的 mtime);书籍记录照旧全量。
|
||
* open 失败返回 NULL(卡满/写保护/无 MeowData):调用方传 NULL 即可,扫描照常。
|
||
* dir 回调签名与 shelf_dir_fn 一致,可直接传给 shelf_scan_books_ex。
|
||
* commit 成功返回 1 并原子发布(临时文件 + rename);失败返回 0 且清理临时文件。
|
||
* commit 总是接管 writer(成功/失败都会释放),之后不得再用。 */
|
||
typedef struct shelf_idx_writer shelf_idx_writer;
|
||
|
||
shelf_idx_writer *shelf_index_writer_open(const char *root);
|
||
void shelf_index_writer_dir(void *ctx, const char *rel, int64_t mtime_sec,
|
||
long mtime_nsec, int books_here);
|
||
int shelf_index_writer_commit(shelf_idx_writer *w, const shelf_book *books,
|
||
int nbooks, const shelf_scan_report *rep);
|
||
void shelf_index_writer_abort(shelf_idx_writer *w);
|
||
|
||
/* 删除索引(顶栏「刷新」= 清索引 + 全量重扫;root 只用于定位,不存在也静默)。 */
|
||
void shelf_index_remove(const char *root);
|
||
|
||
/* 索引文件路径(测试/诊断用)。0=成功;负值=无数据目录/路径超长。 */
|
||
int shelf_index_path(const char *root, char *out, int outsz);
|
||
|
||
#endif /* SHELF_INDEX_H */
|