Files
MeowOS-For-R3P2/patch_v2/reader_app/test/shelf_index_bench.c
T
AskaEth 58f2d9bff4 perf(reader): M32 r2 书架索引改 level-1 校验 — 真机冷缓存首开 3.47s→0.13–0.16s(r1 全目录校验被真机证伪)
r1 用宿主 1.1µs/stat + M28 的**热** stat 150µs 外推「434 次≈65ms」,真机 drop_caches 后
实测是 3.15s(7.26ms/次),与 readdir 的 3.0s 同量级:全目录校验的索引命中冷启动仍要
3.06s,只比全扫快 0.2s。「3.0s→0.15s」在冷启动不成立。按任务 1 自己定的判据
(stat 接近 1s 就退回 level-1)改为只校验 Books 根 + 每个一级目录。

真机冷缓存实测(exFAT /usr/data/mnt/sd_0,434 目录/646 文件/595 本;sync + drop_caches;
探针走 /proc/uptime,UI 走 meow_ui_time_ms):
- 索引命中首开:**127 / 138 / 157ms**(复测 147ms),热 dentry 107–120ms;
- 全量扫描首开:**3428 / 3477ms**(复测 3711ms),热 dentry 不变(readdir 不吃 page cache);
- 探针冷分项:索引载入 20–30ms;载入+rdprog×595 70–120ms;4 个目录 stat 0–30ms;
- 从阅读页返回书架 29–31ms(M29 语义未变)。
→ 冷启动 22–26 倍提升,方案值得做;索引实测 **63,579B**(v2;r1 全目录 97,490B)。

实现(格式 v2,magic MEOWIDX2 / version 2):
- writer 回调过滤 rel 含 '/' 的目录:只落 root + 一级目录的 mtime;书/大小/格式记录仍全量;
  level-2+ 不落盘、载入不 stat。载入仍然 0 次 opendir/readdir(strace:0 getdents64,
  真实库 4 次 stat;宿主模拟库 11 次)。
- 校验记录里出现 level-2+ rel 视为坏档;sum(books_here) 改为「≤ nbooks」合理性上界。
- fail-open/原子写/CRC/安全加固/ts 不缓存全部保留(r1 已过评审的部分未动)。

失效矩阵(报文逐条写清):
自动(根 mtime):Books 根下直接增/删/改名书;一级目录新增/删除/改名。
自动(一级目录 mtime):一级目录里直接增/删/改名书、新建子目录。
必须刷新:level-2 及更深处的任何变化(只改深层目录 mtime)。刷新=删索引+全扫。
真机 exFAT mtime 实测 **1 秒**(tm_nsec=0;同秒连续增删 mtime 不变)。我复测 10 轮
「目录刚改过就扫描、同秒再改」→ 9/10 仍命中(陈旧),与评审 29/30 一致;sleep 跨秒后
全部正常失效。头注释已把「10ms」改为「1 秒」,并写明「不在同一秒刻度的变更靠索引失效,
同秒二次变更需刷新」。

bench 修复(P3-3):MEOW_BENCH_IDX=only —— **不预先 load/scan**,直接进书架量生产首开;
miss —— 先删索引再进书架。两者配合 drop_caches 即冷启动口径。旧行为(先 bench_index/
bench_scan 焐热)保留为默认模式。新增 test/shelf_index_probe_mipsel(write/scan/load/
loadts/statpaths/mtime/size)用于真机分项与 mtime 粒度验证,不进发行包。

测试:shelf_index_smoke 170 pass(新增:level-3/level-2 内变更「索引仍命中且先看到旧
列表 → 刷新后可见」、新建 level-2 目录自动失效、一级目录直接删书自动失效、v2 ndirs=
根+一级数);ASan+UBSan 干净;build_reader.sh 0 warning,core 6846 / seg 19 / chapter 71 /
shelf_group 63 / index 170 / font 14 / loaders PASS。真机 6 场景逐项验证:一级直接增/
新建子目录/一级改名/根下删书/一级直接删书 = 自动失效;level-3 深处新增 = 仍命中(需刷新)。

交付:app.json 0.1.5/code6 → **0.1.6/code7**;reader md5 5c848c99… 1,711,988B;
mpk md5 10d676f5… 814,574B;未签名不 push。设备验证后已清理 /tmp 与 m32probe,
真实 MeowData 的 shelf.idx 已删(app 自己的章节 .idx 未动),Books 434/646 不变,
已装 0.1.4 md5 4036b889… 未变。
2026-11-10 18:49:48 +08:00

281 lines
11 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 —— 建虚拟书库(434 目录 / 595 本 / 51 个 .rar,贴真机量级)并计时
* indexonly —— 只载入已有索引一次(供 `strace -e getdents64` 断言「载入不 readdir」)
*
* v2 校验范围 = Books 根 + 一级目录(真机冷缓存 4 次 stat ≈20ms;434 次=3.15s,已弃用)。
* 宿主数字只用于看相对成本/调用次数;真机绝对值见 M32 r2 报文(drop_caches 实测)。
*
* 为什么宿主数字不能直接当真机用:宿主是 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(v1 全目录校验的代价;已弃用,留作对照:真机冷缓存 3.15s) */
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));
/* ①b v2 实际校验成本:只 stat Books 根 + 一级目录 */
static char l1[64][512];
int nl1 = 0;
snprintf(l1[nl1++], 512, "%s", root);
DIR *ld = opendir(root);
if (ld) {
struct dirent *e;
while ((e = readdir(ld)) && nl1 < 64) {
if (e->d_name[0] == '.') continue;
char p[512];
snprintf(p, sizeof p, "%s/%s", root, e->d_name);
struct stat t;
if (stat(p, &t) == 0 && S_ISDIR(t.st_mode)) snprintf(l1[nl1++], 512, "%s", p);
}
closedir(ld);
}
t0 = now_ms();
for (int r = 0; r < reps; r++)
for (int i = 0; i < nl1; i++) stat(l1[i], &st);
t1 = now_ms();
printf("stat_level1: %d dirs (root + level-1) x%d = %.3f ms/pass, %.2f us/stat\n",
nl1, reps, (t1 - t0) / reps, (t1 - t0) * 1000.0 / (reps * nl1));
/* ② 全量扫描(无 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;
}