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… 未变。
281 lines
11 KiB
C
281 lines
11 KiB
C
/* 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;
|
||
}
|