Files
MeowOS-For-R3P2/patch_v2/reader_app/app/shelf_index.h
T
AskaEth e0dfab95c3 perf(reader): M32 书架索引落盘 — 冷启动只 stat 434 个目录 mtime,跳过 readdir(3.0s→~0.15s)
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;未签名不推卡。
2026-11-10 18:14:52 +08:00

56 lines
3.5 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):把一次全量扫描的结果 + 每个目录的 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 */