Files
MeowOS-For-R3P2/patch_v2/reader_app/app/shelf_model.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

102 lines
6.1 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_model.h — 书架数据模型:扫描 Books/ + 生成行序列(纯 C,不依赖 SDK/设备)。
*
* 从 shelf.c 拆出,供 shelf.c(控件装配)、宿主单测(test/shelf_group_smoke.c)与
* 扫描计时(test/shelf_scan_bench.c)共用。
*
* 目录归属(M27 r2):folder 模式按「Books/ 下的第一级目录」分组(如 "轻小说"、
* "400+本高质量完本合集");更深的相对子路径("01、玄幻/01、斗破")进该书的副题,
* 不再每组一本书把书架炸成上百个段头。Books/ 根下的书 top="",显示段头 SHELF_GROUP_ROOT。
* 分组键是「第一级目录名」,完整相对路径保留在 group[] 供副题/调试用。
* a/b 与 a/c 同属 a 组;a/b 与 b 仍是两个组(不会因为只取末级目录名而混)。
*
* 硬约束:path 永远是书的完整路径(打开书 + 进度键 rdprog_hash(path)),分组只写 top/group,
* 绝不改写 path;两个分组模式间切换不影响进度。
*/
#ifndef SHELF_MODEL_H
#define SHELF_MODEL_H
#include <stdint.h>
/* 书籍上限:真机真实书库 646 文件/约 595 本候选,2048 给 3×+ 余量。
* 超限不再静默停扫:report->truncated=1,由 UI 出提示。 */
#define SHELF_MAX_BOOKS 2048
#define SHELF_GROUP_MAX 96 /* 目录字段容量(含 NUL):95 字节 ≈ 31 个汉字 */
/* 扫描深度上限(root=0;文件所在目录 depth ≤ 该值才会被收录)。
* 真机真实布局 Books/400+本高质量完本合集/<分类>/<作品>/<书名>.txt 最深 4 层,取 6 留余量。
* 超过上限的书籍文件计入 report->depth_skipped(不静默跳过)。 */
#define SHELF_SCAN_MAX_DEPTH 6
/* 行数上限:每本书最多贡献 1 行书 + 1 行段头(段头数 ≤ 书数),故 2N 必够。 */
#define SHELF_MAX_ROWS (SHELF_MAX_BOOKS * 2)
#define SHELF_GROUP_ROOT "未分类" /* Books/ 根下书籍的段头 */
typedef struct shelf_book {
char path[400]; /* 完整路径:打开书 + 进度键;分组不得改写 */
char title[128];
char sub[160]; /* 默认(format/宫格)副题:格式 · 大小 [· 读],与 M27 前逐字一致 */
char sub_folder[160]; /* folder 模式副题:更深子路径 · 格式 · 大小 [· 读] */
char group[SHELF_GROUP_MAX]; /* 相对 Books/ 的完整目录路径(供副题);根下="" */
char top[SHELF_GROUP_MAX]; /* 分组键:相对路径的第一级目录名;根下="" */
int fmt; /* 0 txt,1 fb2,2 epub,3 mobi,4 cbz */
long size;
uint64_t ts; /* 最近阅读 unix;无=0 */
} shelf_book;
typedef struct shelf_row {
int section; /* 1=段头行 */
int fmt; /* 段头/书行格式;段头无 label 时显示格式名 */
int book; /* 书行=books 下标;段头=-1 */
const char *label; /* 段头标题;NULL=格式名(指向 books[].top 或静态串) */
} shelf_row;
/* 进度时间戳查询(生产=rdprog_load;测试/基线=NULL,全部 ts=0)。 */
typedef uint64_t (*shelf_ts_fn)(const char *path);
/* M32:扫描时逐目录回调(书架索引的失效校验数据源)。
* rel = 相对 root 的目录路径(root 自身为 ""),books_here = 该目录里**实际收录**的
* 书籍文件数,mtime = 该目录的 stat 结果(秒 + 纳秒)。
* 回调**不能中止扫描**(返回 void):索引写盘失败只应放弃索引,书架必须照常出书。 */
typedef void (*shelf_dir_fn)(void *ctx, const char *rel, int64_t mtime_sec,
long mtime_nsec, int books_here);
/* 最近阅读排序:ts 降序,同 ts 按书名(不区分大小写)。扫描后与「刚读完置顶」共用。 */
void shelf_sort_recent(shelf_book *books, int nbooks);
typedef struct shelf_scan_report {
int nbooks;
int truncated; /* 1=有书因 SHELF_MAX_BOOKS 上限被丢弃 */
int depth_skipped; /* 因目录深度 > SHELF_SCAN_MAX_DEPTH 未收录的书籍文件数 */
int root_missing; /* 1=root 打不开(无卡/无 Books 目录) */
} shelf_scan_report;
/* 递归扫描 root(目录深度 ≤ SHELF_SCAN_MAX_DEPTH,超限文件计数但不出书),
* 按 ts 降序/书名升序排序后写 books(容量 cap)。
* 返回写入本数。空目录不产生任何记录(段头由 build_rows 按书生成,天然无空段头)。
* dir_fn 非 NULL 时,每进入并成功扫描一个目录回调一次(见 shelf_dir_fn)。 */
int shelf_scan_books_ex(const char *root, shelf_book *books, int cap,
shelf_ts_fn ts_fn, shelf_scan_report *rep,
shelf_dir_fn dir_fn, void *dir_ctx);
/* 同 shelf_scan_books_ex(不采集目录 mtime),供旧调用方/基准复用。 */
int shelf_scan_books(const char *root, shelf_book *books, int cap,
shelf_ts_fn ts_fn, shelf_scan_report *rep);
/* 生成行序列:
* folder_mode=0:按格式分段(TXT/FB2/EPUB/MOBI/CBZ,空格式不出段头)——与 M27 前逐行一致;
* folder_mode=1:按 books[].top(Books/ 第一级目录)分组,根组「未分类」置顶,
* 其余组按组名排序;组内沿用 books[] 全局顺序(最近阅读优先、其次书名)。
* 返回行数;超过 rowcap 时只写 rowcap 行并返回 rowcap。 */
int shelf_build_rows(const shelf_book *books, int nbooks, int folder_mode,
shelf_row *rows, int rowcap);
/* 工具(单测直接调用)。 */
int shelf_fmt_from_ext(const char *name);
const char *shelf_fmt_name(int fmt);
void shelf_fmt_size(char *out, int outsz, long size);
/* 按 fmt/size/ts/group 重建 b->sub 与 b->sub_folder(扫描后与「刚读完置顶」共用)。 */
void shelf_fmt_sub(shelf_book *b);
/* UTF-8 安全截断:最多写 cap-1 字节;若截断点落在多字节序列中间,整段丢弃该残缺字符。
* 返回写入字节数(不含 NUL)。 */
int shelf_copy_utf8(char *dst, int cap, const char *src);
#endif /* SHELF_MODEL_H */