P1-1(会静默开错图):取消「书库从列表推导图片下标」。 - 新增 mr_reader_open_dir_at_name(dir, name):阅读器打开目录册后,在**自己刚枚举出的 全量目录册**(mr_dir_book,与翻页同一份枚举)里按 basename 定位起始页(先精确, 再大小写不敏感兜底;找不到退回续读/首页)。书库点击只传文件名。 - 删除 mr_library_image_index(不再有跨枚举的隐式契约)。 - 截断提示文案改为「仅显示前 2000 项(图片可在阅读器中翻到)」。 - 修复前(cb02722 探针,2100 张 4 位名夹具):rows=2000/truncated=1/ library_images=2000/reader_pages=2100/**mismatched=590**(首行 p1412 → 打开 p1411); 评审夹具为 1995/2000。修复后:2000 个可见图片行按名定位 **0 mismatch**, 2100 页全量可翻;26 次真实 UI 点开(首尾各 3 + 20 个确定性伪随机)逐张命中。 连带处理(评审 r1): 1. 删掉 reader_page_enter 的 c->view 死写(reopen 判定用 activity active/depth); 顺手删掉 library_page_create 里同款死写。 2. 阅读器册间衔接根锁定:新增 mr_path_within_root + mr_book_adjacent_rooted, book_switch 用浏览根(默认 MR_ROOT_DIR,测试可注入)钳制相邻册; 根外的兄弟册按「没有相邻册」处理。test_cont_adjacent A7 验证「明文 _ex 会越根、 rooted 拒绝、根内翻册不受影响」;test_mode_switch_host H6 验证条漫末页不越根。 3. test_reader_perf 不再依赖顶栏 tap 命中时序:模式切换改走测试钩子 mr_reader_test_toggle_mode()(与顶栏动作同一个 mode_set),负载下稳定。 4. page_cache 注释去掉「缩略图缓存/缩略图线程」过时描述(只改文字)。 回归:run_tests.sh(library 81 / nav 74 / lazy 57 / perf 25 / … / open_book 含 2100 用例) 与 run_webtoon_tests.sh(含 A7/H6)全 PASS;交叉构建零警告。
222 lines
8.6 KiB
C
222 lines
8.6 KiB
C
/* test_browse_perf.c — 单层扫描 / 行合成性能与有界性验收(纯 C,无 SDK)。
|
||
*
|
||
* 夹具:500 个顶层目录 × 8 层嵌套(4000 个子目录)+ 根目录 2000 个普通文件
|
||
* + 一个 big/ 目录(2000 张图片,验证 2000 项大目录)。
|
||
* 断言(对应 M18 任务③):
|
||
* P1 首帧扫描只做单层:opendir 次数 == 1,lstat 次数 == 本层条目数,耗时有界;
|
||
* P2 2000 项大目录:一次扫描出全部 2000 行,opendir=1、lstat=2000,耗时有界;
|
||
* P3 行合成(UI src 的数据面)纯内存、零 IO、耗时有界(2000 行 × 1000 遍);
|
||
* P4 进入深层/普通子目录同样只打开它自己(进入目录成本 = 该目录一层)。
|
||
*
|
||
* 编译(在 tuku/ 下):
|
||
* gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \
|
||
* test/test_browse_perf.c logic/library.c -pthread -o build/host/test_browse_perf
|
||
*/
|
||
#include "logic/library.h"
|
||
|
||
#include <dirent.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#include <time.h>
|
||
#include <unistd.h>
|
||
|
||
#define PERF_DIRS 500
|
||
#define PERF_DEPTH 8
|
||
#define PERF_FILES 2000
|
||
#define PERF_BIG 2000
|
||
|
||
static int g_pass, g_fail;
|
||
#define CHECK(cond, ...) do { \
|
||
if (cond) { g_pass++; } \
|
||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||
} while (0)
|
||
|
||
static int mkdir_p(const char *path)
|
||
{
|
||
char tmp[MR_PATH_MAX];
|
||
snprintf(tmp, sizeof tmp, "%s", path);
|
||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||
char ch = tmp[i];
|
||
tmp[i] = '\0';
|
||
if (mkdir(tmp, 0755) != 0) {
|
||
struct stat st;
|
||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||
}
|
||
tmp[i] = ch;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static int touch_file(const char *path)
|
||
{
|
||
FILE *f = fopen(path, "wb");
|
||
if (!f) return -1;
|
||
fputs("x", f);
|
||
return fclose(f);
|
||
}
|
||
|
||
static int rm_rf(const char *path)
|
||
{
|
||
struct stat st;
|
||
if (lstat(path, &st) != 0) return -1;
|
||
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||
DIR *d = opendir(path);
|
||
if (!d) return -1;
|
||
struct dirent *de;
|
||
while ((de = readdir(d)) != NULL) {
|
||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||
char full[MR_PATH_MAX];
|
||
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||
rm_rf(full);
|
||
}
|
||
closedir(d);
|
||
return rmdir(path);
|
||
}
|
||
|
||
static long long now_us(void)
|
||
{
|
||
struct timespec ts;
|
||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
return (long long)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000LL;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char tmpl[] = "./mr_perf_XXXXXX";
|
||
char *root = mkdtemp(tmpl);
|
||
CHECK(root != NULL, "mkdtemp");
|
||
if (!root) return 1;
|
||
|
||
char p[MR_PATH_MAX];
|
||
/* 根目录:2000 个普通文件(不成行)+ 500 个顶层目录(每个顶层目录内是 8 层嵌套 + 1 张图)。 */
|
||
for (int f = 0; f < PERF_FILES; f++) {
|
||
snprintf(p, sizeof p, "%s/f%04d.txt", root, f);
|
||
if (touch_file(p) != 0) { CHECK(0, "file %s", p); break; }
|
||
}
|
||
for (int d = 0; d < PERF_DIRS; d++) {
|
||
char deep[MR_PATH_MAX];
|
||
snprintf(deep, sizeof deep, "%s/d%04d", root, d);
|
||
if (mkdir_p(deep) != 0) { CHECK(0, "mkdir %s", deep); break; }
|
||
snprintf(p, sizeof p, "%s/001.png", deep);
|
||
if (touch_file(p) != 0) { CHECK(0, "touched top image"); break; }
|
||
for (int l = 1; l <= PERF_DEPTH; l++) {
|
||
char sub[MR_PATH_MAX];
|
||
snprintf(sub, sizeof sub, "%s/l%d", deep, l);
|
||
if (mkdir(sub, 0755) != 0) { CHECK(0, "mkdir %s", sub); break; }
|
||
snprintf(deep, sizeof deep, "%s", sub);
|
||
}
|
||
snprintf(p, sizeof p, "%s/001.png", deep);
|
||
if (touch_file(p) != 0) { CHECK(0, "touched deep image"); break; }
|
||
}
|
||
|
||
/* 2000 项大目录(图片)。 */
|
||
char big[MR_PATH_MAX];
|
||
snprintf(big, sizeof big, "%s/big", root);
|
||
CHECK(mkdir_p(big) == 0, "mkdir big");
|
||
for (int i = 0; i < PERF_BIG; i++) {
|
||
snprintf(p, sizeof p, "%s/img%04d.png", big, i);
|
||
if (touch_file(p) != 0) { CHECK(0, "big image %s", p); break; }
|
||
}
|
||
|
||
/* ---- P1:首帧扫描只做单层 ---- */
|
||
mr_library lib;
|
||
memset(&lib, 0, sizeof lib);
|
||
mr_library_test_io_reset();
|
||
long long t0 = now_us();
|
||
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||
long long scan_us = now_us() - t0;
|
||
mr_library_io_stats st;
|
||
mr_library_test_io_get(&st);
|
||
|
||
CHECK(n == PERF_DIRS + 1, "scan lists %d dirs (got %d)", PERF_DIRS + 1, n);
|
||
CHECK(lib.truncated == 0, "not truncated");
|
||
CHECK(st.opendirs == 1, "single-layer scan: opendir=%ld (want 1)", st.opendirs);
|
||
CHECK(st.lstats == PERF_FILES + PERF_DIRS + 1,
|
||
"lstat only current level: %ld (want %d)", st.lstats, PERF_FILES + PERF_DIRS + 1);
|
||
CHECK(scan_us < 1500000LL, "scan bounded: %lldus", scan_us);
|
||
if (lib.count == PERF_DIRS + 1) {
|
||
CHECK(strcmp(lib.items[0].name, "big") == 0, "first=%s", lib.items[0].name);
|
||
CHECK(strcmp(lib.items[lib.count - 1].name, "d0499") == 0,
|
||
"last=%s", lib.items[lib.count - 1].name);
|
||
}
|
||
printf(" root scan: %lldus, %d rows, opendir=%ld lstat=%ld\n",
|
||
scan_us, n, st.opendirs, st.lstats);
|
||
mr_library_free(&lib);
|
||
|
||
/* ---- P2:2000 项大目录(进入目录 = 单层扫描) ---- */
|
||
mr_library biglib;
|
||
memset(&biglib, 0, sizeof biglib);
|
||
mr_library_test_io_reset();
|
||
long long b0 = now_us();
|
||
int bn = mr_library_scan(&biglib, big, MR_LIB_MAX_DEPTH);
|
||
long long big_us = now_us() - b0;
|
||
mr_library_test_io_get(&st);
|
||
CHECK(bn == PERF_BIG, "big dir rows=%d (want %d)", bn, PERF_BIG);
|
||
CHECK(biglib.truncated == 0, "big dir not truncated at cap");
|
||
CHECK(st.opendirs == 1, "big dir scan: opendir=%ld", st.opendirs);
|
||
CHECK(st.lstats == PERF_BIG, "big dir scan: lstat=%ld (want %d)", st.lstats, PERF_BIG);
|
||
CHECK(big_us < 1500000LL, "big dir scan bounded: %lldus", big_us);
|
||
if (biglib.count == PERF_BIG) {
|
||
CHECK(strcmp(biglib.items[0].name, "img0000.png") == 0, "first=%s", biglib.items[0].name);
|
||
CHECK(strcmp(biglib.items[PERF_BIG - 1].name, "img1999.png") == 0,
|
||
"last=%s", biglib.items[PERF_BIG - 1].name);
|
||
CHECK(biglib.items[PERF_BIG - 1].kind == MR_LIB_KIND_IMAGE, "last is image");
|
||
}
|
||
printf(" big dir scan: %lldus, %d rows, opendir=%ld lstat=%ld\n",
|
||
big_us, bn, st.opendirs, st.lstats);
|
||
|
||
/* ---- P3:行合成(UI src 数据面)纯内存、2000 行 × 1000 遍 ---- */
|
||
mr_library_test_io_reset();
|
||
long long r0 = now_us();
|
||
int rows = 0;
|
||
for (int r = 0; r < 1000; r++)
|
||
for (int i = 0; i < biglib.count; i++) {
|
||
mr_library_row row;
|
||
if (mr_library_row_at(&biglib, i, &row)) rows++;
|
||
}
|
||
long long row_us = now_us() - r0;
|
||
mr_library_test_io_get(&st);
|
||
CHECK(rows == 1000 * biglib.count, "rows synthesized (%d)", rows);
|
||
CHECK(st.opendirs == 0 && st.readdirs == 0 && st.lstats == 0,
|
||
"row synthesis does no IO (open=%ld rd=%ld stat=%ld)",
|
||
st.opendirs, st.readdirs, st.lstats);
|
||
CHECK(row_us < 1000000LL, "row synthesis bounded: %lldus", row_us);
|
||
printf(" rows: %lldus for %d rows, zero IO\n", row_us, rows);
|
||
mr_library_free(&biglib);
|
||
|
||
/* ---- P4:进入子目录只打开它自己(不递归子树) ---- */
|
||
char deep[MR_PATH_MAX];
|
||
snprintf(deep, sizeof deep, "%s/d0000", root);
|
||
for (int l = 1; l <= PERF_DEPTH; l++) {
|
||
char sub[MR_PATH_MAX];
|
||
snprintf(sub, sizeof sub, "%s/l%d", deep, l);
|
||
snprintf(deep, sizeof deep, "%s", sub);
|
||
}
|
||
mr_library_test_io_reset();
|
||
mr_library deep_lib;
|
||
memset(&deep_lib, 0, sizeof deep_lib);
|
||
CHECK(mr_library_scan(&deep_lib, deep, MR_LIB_MAX_DEPTH) == 1, "deep leaf has one image");
|
||
mr_library_test_io_get(&st);
|
||
CHECK(st.opendirs == 1, "deep scan opens once (got %ld)", st.opendirs);
|
||
CHECK(st.lstats == 1, "deep scan lstats only its image (got %ld)", st.lstats);
|
||
mr_library_free(&deep_lib);
|
||
|
||
/* 顶层目录:一个子目录 + 一张图 = 2 行,只打开它自己。 */
|
||
snprintf(deep, sizeof deep, "%s/d0000", root);
|
||
memset(&deep_lib, 0, sizeof deep_lib);
|
||
mr_library_test_io_reset();
|
||
CHECK(mr_library_scan(&deep_lib, deep, MR_LIB_MAX_DEPTH) == 2, "top dir rows");
|
||
mr_library_test_io_get(&st);
|
||
CHECK(st.opendirs == 1 && st.lstats == 2, "top dir scan: open=%ld stat=%ld",
|
||
st.opendirs, st.lstats);
|
||
mr_library_free(&deep_lib);
|
||
|
||
rm_rf(root);
|
||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||
return g_fail ? 1 : 0;
|
||
}
|