Files
MeowOS-For-R3P2/patch_v2/reader_app/app/shelf_index.h
T
AskaEth 58f2d9bff4 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… 未变。
2026-11-10 18:49:48 +08:00

72 lines
4.9 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 */