rebase 到 picbrowser(含 M7 条漫/续读、M11 CBZ 档案册)后的语义拼接: - 保留单层扫描重写:一次 opendir/readdir+lstat 只列当前层(目录 + 注入 archive_pages 时的 .cbz/.zip 档案文件),自然升序、隐藏项/符号链接跳过、2000 条截断标注; 系统目录/空目录/无图目录一律列出;彻底删除递归「子树含图」探测 - 保留并复用 M11 档案册分支:add_dir_entry / add_archive_entry / entry_subtitle / entry_badge / archive_pages_at / sibling_is_book / mr_library_scan_ex 注入; 档案册行同样先成行、页数由后台 probe(注入 zip 页数回调)懒算 - 保留 M7:mr_library_dir_has_images / mr_book_adjacent(_ex) 语义不变;为 adjacency 恢复 ignored_dir_name(仅册间衔接跳过系统/隐藏目录,浏览本身不隐藏); mr_library_format_subtitle 在补位后拼「· 续读 Pn」角标 - mr_library_probe_path 统一探测目录/档案册:mtime(秒+纳秒)+size 失效的 256 项 LRU + 互斥锁;评审 P2:只缓存「探测前后 stat 完全一致」的结果,期间变化则不写缓存 (消除「不完整结果绑定新 mtime」的假新鲜;下次调用重新探测) - UI:列表用 src2 每帧取行;后台线程探测优先于缩略图;档案册封面解压档内首图(M11); 行语义=书页数/缩略图、空目录「空目录」、无图/坏档「无图片」;可上溯至文件系统 /、 不可读目录提示并停留;TopAppBar「TF卡」回根;启动时根目录不可读也建空列表; 顶栏回根命中加 g_tb_home_idx>=0 防御 - tuku_internal.h:library_go_up 注释校正(0=到达文件系统 /,app.c 据此退出) - 测试:test_library 改单层语义;新增 browse_nav / browse_lazy / browse_perf / browse_ui_host(含评审 P2 的「探测期间变化不写缓存」用例);test_cont_adjacent A6 改两段式;test_cbz T8/T10 改两段式(坏档/无图档案列出但非书); run_tests.sh 保留双方全部条目(browse 4 套 + run_zip_tests/run_cbz_tests) - 复验:run_tests.sh 全 PASS(含 M11 zip/cbz)、run_webtoon_tests.sh 全 PASS; worktree 内 sdk/build_sdk.sh + build_tuku.sh 零警告静态 MIPS32r2 ELF
200 lines
7.4 KiB
C
200 lines
7.4 KiB
C
/* test_browse_perf.c — 单层扫描性能/有界性验收(纯 C,无 SDK)。
|
|
*
|
|
* 夹具:500 个顶层目录 × 8 层嵌套(4000 个子目录)+ 根目录 2000 个文件。
|
|
* 断言(对应 M13 任务⑤):
|
|
* P1 首帧扫描只做单层:opendir 次数 == 1(旧实现会递归打开数千个目录);
|
|
* lstat 次数 == 本层条目数;耗时有界;
|
|
* P2 后台探测同样只读一层:每个目录一次 opendir,不进入其嵌套子树;
|
|
* P3 行合成(UI src/bind 的数据面)纯内存、零 IO、O(1);
|
|
* P4 深目录直接扫描也只打开它自己。
|
|
*
|
|
* 编译(在 tuku/ 下):
|
|
* gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \
|
|
* test/test_browse_perf.c logic/library.c logic/reader.c test/settings_reader_stub.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
|
|
|
|
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; }
|
|
}
|
|
|
|
/* ---- 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, "scan lists %d dirs (got %d)", PERF_DIRS, 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,
|
|
"lstat only current level: %ld (want %d)", st.lstats, PERF_FILES + PERF_DIRS);
|
|
CHECK(scan_us < 1500000LL, "scan bounded: %lldus", scan_us);
|
|
if (lib.count > 1) {
|
|
CHECK(strcmp(lib.items[0].name, "d0000") == 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(" scan: %lldus, %d dirs, opendir=%ld lstat=%ld\n",
|
|
scan_us, n, st.opendirs, st.lstats);
|
|
|
|
/* ---- P2:后台探测每目录只开一次,不进入嵌套子树 ---- */
|
|
mr_library_test_io_reset();
|
|
long long p0 = now_us();
|
|
int bad = 0;
|
|
for (int i = 0; i < lib.count; i++) {
|
|
mr_library_probe probe;
|
|
if (mr_library_probe_cached(lib.items[i].path, &probe) != 0 || probe.pages != 1)
|
|
bad++;
|
|
}
|
|
long long probe_us = now_us() - p0;
|
|
mr_library_test_io_get(&st);
|
|
CHECK(bad == 0, "all top dirs have 1 direct image (bad=%d)", bad);
|
|
CHECK(st.opendirs == PERF_DIRS,
|
|
"probe stays shallow: opendir=%ld (want %d, not %d)",
|
|
st.opendirs, PERF_DIRS, PERF_DIRS * (PERF_DEPTH + 1));
|
|
CHECK(st.lstats == PERF_DIRS,
|
|
"probe lstats only image candidates: %ld (want %d)", st.lstats, PERF_DIRS);
|
|
CHECK(probe_us < 5000000LL, "probe bounded: %lldus", probe_us);
|
|
printf(" probe: %lldus, opendir=%ld lstat=%ld\n", probe_us, st.opendirs, st.lstats);
|
|
|
|
/* ---- P3:行合成(bind/src 数据面)纯内存 O(1) ---- */
|
|
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 < lib.count; i++) {
|
|
mr_library_row row;
|
|
if (mr_library_row_at(&lib, i, &row)) rows++;
|
|
}
|
|
long long row_us = now_us() - r0;
|
|
mr_library_test_io_get(&st);
|
|
CHECK(rows == 1000 * lib.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);
|
|
|
|
/* ---- P4:直接扫描深层目录同样只打开它自己 ---- */
|
|
char deep[MR_PATH_MAX];
|
|
snprintf(deep, sizeof deep, "%s", lib.items[0].path);
|
|
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) == 0, "deep leaf has no subdir");
|
|
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);
|
|
|
|
mr_library_free(&lib);
|
|
rm_rf(root);
|
|
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
|
return g_fail ? 1 : 0;
|
|
}
|