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;未签名不推卡。
255 lines
10 KiB
C
255 lines
10 KiB
C
/* 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;
|
||
}
|