Files
MeowOS-For-R3P2/patch_v2/reader_app/test/shelf_index_bench.c
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

255 lines
10 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.
/* test/shelf_index_bench.c — M32 索引方案可行性计时(宿主,纯 C + clock_gettime)。
*
* 用法:
* ./shelf_index_bench_host [bench|indexonly] [root]
* bench —— 建虚拟书库(默认 452 目录 / 688 本,贴合真机 434/595 量级)并计时
* indexonly —— 只载入已有索引一次(供 `strace -e getdents64` 断言「载入不 readdir」)
*
* 为什么宿主数字不能直接当真机用:宿主是 ext4/f2fs 热缓存(每次 stat ~1µs),真机 TF 卡是
* exFAT(M28 实测 1079 次 stat = 160–170ms,约 150µs/次)。本工具用来量**调用次数与代码路径**
* 的相对成本;绝对毫秒按 M28 的每次 stat/条目成本外推(见报文)。
*
* 计时口径:
* - stat_all :先 opendir 走一遍把目录列表收好(不计时),再单独循环 stat(计时的只有 stat);
* - full_scan :shelf_scan_books_ex(生产全量扫描形态,ts_fn=NULL);
* - scan+save :全量扫描 + 索引落盘(生产「无索引首次进入」路径);
* - index_load :shelf_index_load 命中(生产「有索引首次进入」路径,ts_fn=NULL 以隔离进度读);
* - index_read :单次 open+fstat+read 整个索引文件(校验之前的纯 IO 成本)。
*/
#include "shelf_model.h"
#include "shelf_index.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
static char g_cfgdir[500];
const char *meow_config_dir(void) { return g_cfgdir; }
static double now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
}
static void mkdirs(const char *path)
{
char t[600];
snprintf(t, sizeof t, "%s", path);
for (char *p = t + 1; *p; p++) {
if (*p == '/') { *p = 0; mkdir(t, 0755); *p = '/'; }
}
mkdir(t, 0755);
}
static void put_file(const char *path, long size)
{
FILE *f = fopen(path, "wb");
if (!f) return;
fseek(f, size > 0 ? size - 1 : 0, SEEK_SET);
fputc('x', f);
fclose(f);
}
/* 虚拟书库:精确复刻真机量级 —— 434 个目录 / 595 本书 / 51 个 .rar(不支持格式)。
* 根 1 + 一级 10 + 「400+本高质量完本合集」下 40 分类 + 382 作品目录 + 轻小说/子目录 1 = 434;
* 书 = 382 作品 × 1 + 其它 9 个一级目录 × 20 + 根 3 + 子目录 30 = 595。 */
static int make_library(const char *root)
{
char p[600];
DIR *d = opendir(root);
if (d) { closedir(d); return 0; } /* 已存在:复用 */
mkdirs(root);
const char *l1[] = { "400+本高质量完本合集", "轻小说", "玄幻", "都市", "科幻",
"历史", "武侠", "游戏", "悬疑", "同人科幻" };
for (size_t i = 0; i < sizeof l1 / sizeof l1[0]; i++) {
snprintf(p, sizeof p, "%s/%s", root, l1[i]); mkdirs(p);
}
int works = 0, rar = 0;
/* 40 个分类目录,382 个作品目录(前 22 个分类各 10 个,其余各 9 个):
* 1 根 + 10 一级 + 40 分类 + 382 作品 + 1 子目录 = 434 个目录。 */
for (int c = 1; c <= 40; c++) {
snprintf(p, sizeof p, "%s/400+本高质量完本合集/%02d、分类%d", root, c, c); mkdirs(p);
int nw = (c <= 22) ? 10 : 9;
for (int w = 1; w <= nw; w++, works++) {
char wd[600];
snprintf(wd, sizeof wd, "%s/400+本高质量完本合集/%02d、分类%d/%02d、作品%d",
root, c, c, w, w);
mkdirs(wd);
snprintf(p, sizeof p, "%s/第1卷.txt", wd);
put_file(p, 1024 + (long)((works * 7919) % 200000));
if ((works % 7) == 0 && rar < 51) { snprintf(p, sizeof p, "%s/合集.rar", wd); put_file(p, 1000); rar++; }
}
}
for (size_t i = 1; i < sizeof l1 / sizeof l1[0]; i++) {
for (int b = 1; b <= 20; b++) {
snprintf(p, sizeof p, "%s/%s/书%d.txt", root, l1[i], b);
put_file(p, 4096);
}
}
for (int b = 1; b <= 3; b++) {
snprintf(p, sizeof p, "%s/根下%d.txt", root, b);
put_file(p, 2048);
}
snprintf(p, sizeof p, "%s/轻小说/子目录", root); mkdirs(p);
for (int b = 1; b <= 30; b++) {
snprintf(p, sizeof p, "%s/轻小说/子目录/深%d.txt", root, b);
put_file(p, 4096);
}
/* 补齐 51 个 rar(若作品目录里不够,放到一级目录) */
while (rar < 51) {
snprintf(p, sizeof p, "%s/玄幻/压缩%d.rar", root, rar);
put_file(p, 100);
rar++;
}
return 1;
}
/* 收集目录列表(不计时;用于单独量 stat 成本)。 */
static int collect_dirs(const char *dir, char paths[][512], int max, int n, int depth)
{
if (depth > 8 || n >= max) return n;
DIR *d = opendir(dir);
if (!d) return n;
struct dirent *e;
while ((e = readdir(d))) {
if (e->d_name[0] == '.') continue;
char path[512];
if (snprintf(path, sizeof path, "%s/%s", dir, e->d_name) >= (int)sizeof path) continue;
struct stat st;
if (stat(path, &st) != 0) continue;
if (S_ISDIR(st.st_mode) && n < max) {
snprintf(paths[n], 512, "%s", path);
n = collect_dirs(path, paths, max, n + 1, depth + 1);
}
}
closedir(d);
return n;
}
static int count_files(const char *dir, int depth)
{
if (depth > 8) return 0;
DIR *d = opendir(dir);
if (!d) return 0;
int n = 0;
struct dirent *e;
while ((e = readdir(d))) {
if (e->d_name[0] == '.') continue;
char path[512];
snprintf(path, sizeof path, "%s/%s", dir, e->d_name);
struct stat st;
if (stat(path, &st) != 0) continue;
if (S_ISDIR(st.st_mode)) n += count_files(path, depth + 1);
else n++;
}
closedir(d);
return n;
}
int main(int argc, char **argv)
{
const char *mode = (argc > 1) ? argv[1] : "bench";
const char *tmp = getenv("TMPDIR");
if (!tmp || !tmp[0]) tmp = "/tmp";
char base[500], root[520];
if (argc > 2) snprintf(base, sizeof base, "%s", argv[2]);
else snprintf(base, sizeof base, "%s/m32bench", tmp);
snprintf(root, sizeof root, "%s/Books", base);
snprintf(g_cfgdir, sizeof g_cfgdir, "%s/data", base);
mkdirs(g_cfgdir);
static shelf_book books[SHELF_MAX_BOOKS];
shelf_scan_report rep;
if (strcmp(mode, "indexonly") == 0) {
int hit = shelf_index_load(root, books, SHELF_MAX_BOOKS, NULL, &rep);
printf("indexonly hit=%d nbooks=%d\n", hit, rep.nbooks);
return hit ? 0 : 1;
}
int created = make_library(root);
static char dirs[4096][512];
snprintf(dirs[0], 512, "%s", root);
int nd = 1 + collect_dirs(root, dirs, 4096, 0, 0);
int nfiles = count_files(root, 0);
printf("library: %s (%s) dirs=%d files=%d\n", root, created ? "created" : "reused", nd, nfiles);
/* ① 目录逐个 stat(校验索引的唯一 IO) */
struct stat st;
int reps = 20;
double t0 = now_ms();
for (int r = 0; r < reps; r++)
for (int i = 0; i < nd; i++) stat(dirs[i], &st);
double t1 = now_ms();
printf("stat_all_dirs: %d x%d = %.2f ms total, %.3f ms/pass, %.2f us/stat\n",
nd, reps, t1 - t0, (t1 - t0) / reps, (t1 - t0) * 1000.0 / (reps * nd));
/* ② 全量扫描(无 ts) */
double best_scan = 1e9, best_save = 1e9;
int n = 0;
for (int r = 0; r < 3; r++) {
double a = now_ms();
n = shelf_scan_books_ex(root, books, SHELF_MAX_BOOKS, NULL, &rep, NULL, NULL);
double b = now_ms();
if (b - a < best_scan) best_scan = b - a;
}
printf("full_scan: books=%d truncated=%d depth_skipped=%d best=%.2f ms\n",
n, rep.truncated, rep.depth_skipped, best_scan);
int scan_trunc = rep.truncated, scan_skip = rep.depth_skipped;
/* ③ 全量扫描 + 写索引 */
for (int r = 0; r < 3; r++) {
double a = now_ms();
shelf_idx_writer *w = shelf_index_writer_open(root);
int n2 = shelf_scan_books_ex(root, books, SHELF_MAX_BOOKS, NULL, &rep,
w ? shelf_index_writer_dir : NULL, w);
int ok = w ? shelf_index_writer_commit(w, books, n2, &rep) : 0;
double b = now_ms();
if (b - a < best_save) best_save = b - a;
if (r == 0) printf("scan+save: scan_books=%d index_commit=%d\n", n2, ok);
}
printf("scan+save: best=%.2f ms\n", best_save);
/* ④ 索引命中(不含 ts 读取) */
double best_load = 1e9;
int hit = 0, ln = 0;
for (int r = 0; r < 5; r++) {
double a = now_ms();
hit = shelf_index_load(root, books, SHELF_MAX_BOOKS, NULL, &rep);
double b = now_ms();
ln = rep.nbooks;
if (b - a < best_load) best_load = b - a;
}
printf("index_load: hit=%d nbooks=%d best=%.2f ms\n", hit, ln, best_load);
/* ⑤ 索引文件大小 + 单次顺序读成本 */
char ip[600];
if (shelf_index_path(root, ip, sizeof ip) == 0 && stat(ip, &st) == 0) {
char *buf = (char *)malloc((size_t)st.st_size);
double a = now_ms();
for (int r = 0; r < 20 && buf; r++) {
FILE *f = fopen(ip, "rb");
if (f) { if (fread(buf, 1, (size_t)st.st_size, f) != (size_t)st.st_size) { /* 短读 */ } fclose(f); }
}
double b = now_ms();
printf("index_file: %lld bytes, open+read %.3f ms/pass\n",
(long long)st.st_size, buf ? (b - a) / 20 : 0.0);
free(buf);
}
/* ⑥ 生产路径估算:索引命中 + 595 次 rdprog_load(M28 实测 40–50ms,未在此重复测) */
printf("NOTE 生产路径 = index_load + rdprog_load×nbooks(M28: 40–50ms)+ build_rows(<20ms)\n");
/* ⑦ 模拟真机书库的正确性断言:595 本全可见、无截断、无超深跳过,索引与扫描同数。 */
int pass = (n == 595 && ln == 595 && hit == 1 && scan_trunc == 0 && scan_skip == 0);
printf("CHECK library_sim scan_books=%d load_books=%d hit=%d truncated=%d depth_skipped=%d -> %s\n",
n, ln, hit, scan_trunc, scan_skip, pass ? "PASS" : "FAIL");
return pass ? 0 : 1;
}