/* 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 #include #include #include #include #include #include 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; }