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;未签名不推卡。
This commit is contained in:
1 parent
3f09f7c137
commit
e0dfab95c3
11 files changed
+1388
-29
No files matched your search
@@ -22,11 +22,17 @@
|
||||
* M30:M29 的「刚读完不置顶」缺口补上(只刷一本书的 ts + 内存重排,不重扫全盘)。
|
||||
* M31:置顶时一并重算这本的副题(shelf_fmt_sub),「· 读」立即出现,不等下一次全量刷新;
|
||||
* 顶栏「刷新」同时丢弃字体路径缓存(新拷入的字库此时生效)。
|
||||
* M32:书架索引落盘(app/shelf_index.c)。冷启动先试 `MeowData/os.meow.reader/index/shelf.idx`:
|
||||
* 版本/CRC/root 路径/全部目录 mtime 校验通过 → 直接出书,**完全跳过 readdir**
|
||||
* (真机全量扫描 3.0s 的 96% 是 434 个目录的 opendir+readdir;校验只做 434 次 stat);
|
||||
* 任何异常 → 照常全量重扫(fail-open)。扫描完成后原子写回索引(写失败只丢缓存,不影响书架)。
|
||||
* M29 语义不变:首次进书架=索引或全扫;从阅读页返回=不扫;顶栏「刷新」=清索引+全扫。
|
||||
*/
|
||||
#include "../reader_internal.h"
|
||||
#include "font_resolve.h"
|
||||
#include "progress.h"
|
||||
#include "reader_conf.h"
|
||||
#include "shelf_index.h"
|
||||
#include "shelf_model.h"
|
||||
#include "meow_ui_internal.h" /* meow_gfx_fill_round(宫格卡圆角填充) */
|
||||
#include <stdio.h>
|
||||
@@ -77,40 +83,69 @@ static void notice_set(const char *line1, const char *line2)
|
||||
g_notice_until = meow_ui_time_ms() + 6000; /* 6s 后由 shelf_on_tick 清掉,不常驻压列表 */
|
||||
}
|
||||
|
||||
/* 扫描 Books/ 并写入 g_books/g_nbooks(同时更新提示条)。首次进入书架与顶栏「刷新」才调。 */
|
||||
static void scan_books(void)
|
||||
/* 扫描/索引载入共用的报告 → 提示条文案(M29 语义不变)。
|
||||
* 索引里存了 truncated/depth_skipped,载入时如实重建,避免「用索引打开」后提示失真。 */
|
||||
static void notice_from_report(const shelf_scan_report *rep)
|
||||
{
|
||||
const char *dir = READER_SD_ROOT "/Books";
|
||||
g_notice[0] = 0;
|
||||
g_notice2[0] = 0;
|
||||
g_notice_ms = 0;
|
||||
shelf_scan_report rep;
|
||||
g_nbooks = shelf_scan_books(dir, g_books, SHELF_MAX_BOOKS, shelf_ts_of, &rep);
|
||||
g_scanned = 1;
|
||||
if (rep.root_missing) {
|
||||
if (rep->root_missing) {
|
||||
char line[160];
|
||||
snprintf(line, sizeof line, "未找到 %s 目录,请放入书籍(TXT)", dir);
|
||||
notice_set(line, NULL);
|
||||
} else if (rep.truncated && rep.depth_skipped > 0) {
|
||||
} else if (rep->truncated && rep->depth_skipped > 0) {
|
||||
/* 长句拆两行(480px 单行会被切掉) */
|
||||
char l1[160], l2[160];
|
||||
snprintf(l1, sizeof l1, "书籍超过 %d 本,已只显示前 %d 本", SHELF_MAX_BOOKS, SHELF_MAX_BOOKS);
|
||||
snprintf(l2, sizeof l2, "另有 %d 个文件目录超过 %d 层未显示",
|
||||
rep.depth_skipped, SHELF_SCAN_MAX_DEPTH);
|
||||
rep->depth_skipped, SHELF_SCAN_MAX_DEPTH);
|
||||
notice_set(l1, l2);
|
||||
} else if (rep.truncated) {
|
||||
} else if (rep->truncated) {
|
||||
char line[160];
|
||||
snprintf(line, sizeof line, "书籍超过 %d 本,已只显示前 %d 本",
|
||||
SHELF_MAX_BOOKS, SHELF_MAX_BOOKS);
|
||||
notice_set(line, NULL);
|
||||
} else if (rep.depth_skipped > 0) {
|
||||
} else if (rep->depth_skipped > 0) {
|
||||
char line[160];
|
||||
snprintf(line, sizeof line, "有 %d 个文件因目录层数超过 %d 层未显示",
|
||||
rep.depth_skipped, SHELF_SCAN_MAX_DEPTH);
|
||||
rep->depth_skipped, SHELF_SCAN_MAX_DEPTH);
|
||||
notice_set(line, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/* 全量扫描 Books/ 并写回索引(首次无索引 / 刷新 / 索引校验失败时走这里)。
|
||||
* 索引写盘是纯附加:open 失败(卡满/写保护/无 MeowData)→ 传 NULL 闭包,扫描照常;
|
||||
* commit 失败只丢缓存。**索引问题绝不影响书架能否打开**。 */
|
||||
static void scan_books_full(void)
|
||||
{
|
||||
const char *dir = READER_SD_ROOT "/Books";
|
||||
shelf_scan_report rep;
|
||||
/* 扫描时把每个目录的 mtime 直接流进索引临时文件,不占额外内存、也不二次 readdir。 */
|
||||
shelf_idx_writer *w = shelf_index_writer_open(dir);
|
||||
g_nbooks = shelf_scan_books_ex(dir, g_books, SHELF_MAX_BOOKS, shelf_ts_of, &rep,
|
||||
w ? shelf_index_writer_dir : NULL, w);
|
||||
if (w) shelf_index_writer_commit(w, g_books, g_nbooks, &rep); /* 成功/失败都接管 w */
|
||||
if (rep.root_missing) shelf_index_remove(dir); /* 卡/目录没了:旧索引作废,别留陈旧缓存 */
|
||||
g_scanned = 1;
|
||||
notice_from_report(&rep);
|
||||
}
|
||||
|
||||
/* 首次进入书架:先试索引(命中=只 stat 目录 + 读进度,不 readdir);任何异常退回全扫。 */
|
||||
static void scan_books_first(void)
|
||||
{
|
||||
const char *dir = READER_SD_ROOT "/Books";
|
||||
shelf_scan_report rep;
|
||||
if (shelf_index_load(dir, g_books, SHELF_MAX_BOOKS, shelf_ts_of, &rep)) {
|
||||
g_nbooks = rep.nbooks;
|
||||
g_scanned = 1;
|
||||
notice_from_report(&rep);
|
||||
return;
|
||||
}
|
||||
scan_books_full();
|
||||
}
|
||||
|
||||
/* 排队行:folder 模式按目录分段,否则按格式(默认)。 */
|
||||
static void build_rows(void)
|
||||
{
|
||||
@@ -304,12 +339,13 @@ static void shelf_on_enter(meow_view *v, void *ctx)
|
||||
const rdconf *c = rdconf_get();
|
||||
g_layout = (c && c->shelf[0] && strcmp(c->shelf, "grid") == 0) ? 1 : 0;
|
||||
g_group_folder = (c && strcmp(c->shelf_group, "folder") == 0) ? 1 : 0;
|
||||
/* 书架缓存:本进程只在首次进入时扫一次 Books/(从阅读页返回不再重扫)。
|
||||
* 扫描在真机是 3.1s(exFAT 434 目录 opendir/readdir),是「回书架冻结」的唯一大头。
|
||||
* 代价:新放入的书不会自动出现,需点顶栏「刷新」(强制重扫)。
|
||||
/* 书架缓存:本进程只在首次进入时取一次 Books/(从阅读页返回不再重扫)。
|
||||
* M32:首次先试索引(命中=只 stat 目录,跳过 readdir);索引不可用才全量扫描。
|
||||
* 扫描/校验在真机是 3.1s / ~0.1s 的差别(exFAT 434 目录 opendir/readdir 是扫描的唯一大头)。
|
||||
* 代价:进程内不再自动发现新书,需点顶栏「刷新」;跨进程重启则由索引 mtime 校验兜底。
|
||||
* M30:刚读完那本例外——promote_just_read() 只读它一个进度文件并重排,低成本置顶。
|
||||
* 提示条(超上限截断 / 超深目录)语义不变:首次扫描时算好并保留在 g_notice*。 */
|
||||
if (!g_scanned) scan_books();
|
||||
* 提示条(超上限截断 / 超深目录)语义不变:索引里存了这两个值,载入时如实重建。 */
|
||||
if (!g_scanned) scan_books_first();
|
||||
else promote_just_read();
|
||||
build_rows();
|
||||
const meow_theme *th = meow_theme_get();
|
||||
@@ -437,10 +473,14 @@ int shelf_on_tap(int x, int y)
|
||||
} else if (bi == 1) { /* 分组切换:format ↔ folder */
|
||||
shelf_set_group(g_group_folder ? 0 : 1);
|
||||
return 1;
|
||||
} else if (bi == 2) { /* 刷新:强制重扫(缓存失效 + 重扫;新书/刚读完的书在此出现) */
|
||||
} else if (bi == 2) { /* 刷新:清索引 + 全量重扫(新书/改名/删书在此出现) */
|
||||
g_scanned = 0;
|
||||
font_cache_invalidate(); /* 字体路径记忆化也一并失效(新拷入的字库在此生效) */
|
||||
scan_books(); build_rows();
|
||||
/* 先删索引再全量扫描:即使旧索引 mtime 校验碰巧通过(如文件系统时间戳精度不足),
|
||||
* 刷新也一定重扫;扫描成功后写回新索引(写失败只丢缓存)。 */
|
||||
shelf_index_remove(READER_SD_ROOT "/Books");
|
||||
scan_books_full();
|
||||
build_rows();
|
||||
shelf_build_content();
|
||||
shelf_redraw();
|
||||
return 1;
|
||||
|
||||
Reference in new issue
Block a user