M29 只解决了「回书架不重扫」;首次进书架仍要全量扫 434 目录(真机 3.0–3.4s, 其中 opendir+readdir 占 96%)。本单把扫描结果 + 每个扫描目录的 mtime 存到 MeowData/os.meow.reader/index/shelf.idx,冷启动逐目录 stat 比对 mtime,命中即 完全跳过 readdir;任何异常一律 fail-open 全量重扫。 先量再选(任务 1):宿主 434 目录/595 本/51 rar 虚拟库(贴真机量级) - 434 次目录 stat = 0.48ms/pass(1.1µs/次,不含 readdir); - 读 72KB 索引 = 0.009ms;全量扫描 3.54ms;索引命中 1.29ms(ts_fn=NULL)。 真机按 M28 实测 1079 次 stat=160–170ms(≈150µs/次)外推:434 次 ≈ 65ms, 远低于「可能不可行」的 1s 阈值 → 选「校验全部扫描目录 mtime」(非只 level-1)。 strace 断言:indexonly 载入 0 次 getdents64、恰好 434 次 stat(真的没 readdir)。 生产路径 ≈ 65ms(stat) + <20ms(索引读) + 40–50ms(rdprog×595) + <20ms(build_rows) ≈ 0.13–0.15s(对比 3.0s)。 索引格式 v1(手工小端序列化,不依赖结构体布局): header(magic/version/header_size/root_len) + root + dir 记录×(rel,mtime_s, mtime_ns,books_here) + book 记录×(rel,size,fmt) + trailer(ndirs,nbooks,flags, depth_skipped,crc32,magic)。truncated/depth_skipped 存在 flags/字段里,载入时 如实重建提示条;**不存阅读进度 ts**,载入后每本由 rdprog_load 现读再排序。 失效语义(存每个扫描过的目录,不只 level-1): - level-2 新增 / level-3 深处新增 / 删书 → 该目录 mtime 变 → 重扫; - 一级目录改名 → 父目录 mtime 变 + 旧目录 stat ENOENT → 重扫; - 超深(>6 层)未扫描子树不在索引内,其内部变化不自动失效(本来就不显示), 点「刷新」更新 depth_skipped 计数;mtime 精度受限(exFAT 10ms/FAT32 2s)的 同刻度变更也可能漏判,点「刷新」必可纠正。 M29 语义不变:首次进书架=索引或全扫;回书架=不扫;刷新=删索引+全扫(之后原子 写回新索引)。 安全/健壮(宁可慢不可错): - 写:shelf.idx.tmp 全量写完(尽力 fsync)后 rename;中途被杀只剩 tmp,正式索引 要么旧要么完整新;rename 覆盖失败先删旧再改名;fflush/写错即放弃,绝不影响书架。 - 读:O_NOFOLLOW|O_NONBLOCK + S_ISREG(FIFO/符号链接直接拒绝不阻塞);CRC32 覆盖 全部记录;rel 做绝对路径/“..”/内嵌 NUL/长度校验;版本/根路径/计数/逐目录 mtime 任一不符即重扫;载入崩溃路径全部 goto 清理,不改书架可用性。 - writer_open 失败(卡满/写保护/无 MeowData)→ 扫描不采集目录、照常出书; commit 失败(含只读目录 rename 失败)不留正式索引。 测试:test/shelf_index_smoke 154 pass(含四种变更场景失效、截断/字节翻转/垃圾/ 缺失/FIFO/符号链接/版本不符/根路径不符、ts 现读、truncated/depth_skipped 重建、 只读目录写失败、空 Books、root 缺失、.tmp 残渣),ASan+UBSan 干净; test/shelf_index_bench 模拟 434 目录/595 本/51 rar:scan/load 均 595 本、 truncated=0/depth_skipped=0、索引 72,156B。build_reader.sh 全绿 0 warning (core 6846 / seg 19 / chapter 71 / shelf_group 63 / index 154 / font 14 / loaders PASS)。shelf_bench_ondev 增加 index_load/scan_save 计时与 MEOW_BENCH_IDX=miss(真机首开命中 vs 未命中对比,待有卡复核)。 交付:app.json 0.1.4/code5 → 0.1.5/code6;reader md5 df4cadcf 1,711,924B;mpk md5 fec7b1bb 814,535B;未签名不推卡。
56 lines
3.5 KiB
C
56 lines
3.5 KiB
C
/* app/shelf_index.h — 书架索引落盘(M32):把一次全量扫描的结果 + 每个目录的 mtime
|
||
* 存到 `MeowData/os.meow.reader/index/shelf.idx`,冷启动时用「逐个 stat 目录 mtime」校验,
|
||
* 命中就完全跳过 readdir(真机全量扫描 3.0s 的大头是 434 次 opendir+readdir)。
|
||
*
|
||
* 安全优先(fail-open):版本不符 / 文件缺失 / 截断 / CRC 错 / 任一目录 mtime 变化 /
|
||
* 任何 IO 错误 → shelf_index_load() 返回 0,调用方照常全量重扫。**绝不**用陈旧索引
|
||
* 冒充真实书库,也绝不因为索引写失败而让书架打不开。
|
||
*
|
||
* 索引里**不存阅读进度 ts**:ts 每次由调用方的 ts_fn(生产=rdprog_load)现读,
|
||
* 否则「刚读完回书架」会显示旧的置顶顺序。索引只存与进度无关的扫描事实。
|
||
*
|
||
* 目录 mtime 失效语义(存「扫描过的每个目录」的 mtime,非只存 level-1):
|
||
* - 任意目录里新增/删除/改名文件 → 该目录 mtime 变 → 校验失败 → 重扫(含 level-3 深处);
|
||
* - 一级目录改名 → 父目录 mtime 变 + 旧目录 stat 失败 → 重扫;
|
||
* - 超深(> SHELF_SCAN_MAX_DEPTH)未扫描的子树不在索引里,其内部变化不会自动失效
|
||
* (那些书本来就不显示),需点「刷新」更新 depth_skipped 计数。
|
||
* mtime 精度受文件系统限制(exFAT 约 10ms / FAT32 2s):与上次扫描落在同一时间刻度的
|
||
* 变更可能漏判,点一次「刷新」必可纠正(刷新=清索引+全量重扫)。
|
||
*/
|
||
#ifndef SHELF_INDEX_H
|
||
#define SHELF_INDEX_H
|
||
|
||
#include "shelf_model.h"
|
||
|
||
/* 落盘格式版本:**扫描语义变了必须 +1**(新增格式扩展名/改深度/改分组成因等),
|
||
* 版本不符时旧索引直接作废重扫。 */
|
||
#define SHELF_INDEX_VERSION 1
|
||
|
||
/* 载入并校验索引。成功返回 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);
|
||
|
||
/* 增量写索引(扫描时逐目录上报 → 扫描结束提交)。
|
||
* 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 */
|