用户反馈四件事一起改: - 「目录里有一张图就被当成最终目录」→ 取消「目录含图=一本书」全部链路 (探测/页数/封面/续读角标/懒算 worker/sr2 补位),列表 = 目录 + 图片 + .cbz/.zip(目录优先、组内自然序)。点目录=进入(空目录也进),点图片= mr_reader_open_dir_at(当前目录, 图片自然序下标),点档案册=打开档案册。 - 「文件夹浏览特别卡」→ 删除缩略图线程/磁盘 bmp 缓存/LRU/解码路径与行内 缩略图;首帧只做一次 opendir/readdir+lstat,副标题(类型·大小)扫描期 生成一次,行合成/滚动/点击零 IO 零格式化;去掉逐行 snap(自由滚动手感)。 2000 项目录扫描 ~2.8ms(opendir=1, lstat=2000),行合成 2.78ms/200 万次 (宿主实测;旧实现 2000 项 entry 含 512B cover,内存 2.7MB→1.7MB)。 - 「不要访问 SD 卡上级目录」→ 浏览根锁定 MR_ROOT_DIR:删除越根的顶栏 「上级」/library_go_parent,根隐藏顶栏返回,左缘返回到根=退出 App。 - 「没有 SD 卡不要报错」→ 根不可访问(未插卡/挂载失败/根是文件)时友好空态 「未检测到 SD 卡」+ 顶栏刷新/重进页面重试;子目录不可读才弹提示横幅。 集成陷阱:书库图片下标与阅读器目录册共享同一套过滤器(隐藏项/图片扩展名/ 普通文件/符号链接)与 mr_natural_cmp,test_browse_lazy 直接对比 mr_dir_book_init 顺序,test_open_book_host 断言「点 img10 就开 img10」。 reader 侧 M17 遗留:mr_reader_open_dir_at 在阅读页已激活时先 pop 再 push, 走完整 leave/enter(SDK 对同 view 重复 push 是 no-op 且会压重复 activity)。 回归:run_tests.sh / run_webtoon_tests.sh 全 PASS;交叉构建零警告。
224 lines
8.7 KiB
C
224 lines
8.7 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");
|
||
CHECK(mr_library_image_index(&biglib, PERF_BIG - 1) == PERF_BIG - 1,
|
||
"image index at end");
|
||
}
|
||
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;
|
||
}
|