Files
MeowOS-SDK/tuku/test/test_reader_perf.c
T
AskaEth 4befbfe14c 图库: 书库点图改按文件名定位(修 P1 截断错位)+ 册间衔接根锁定(M18 评审 r1)
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;交叉构建零警告。
2026-11-05 20:26:40 +08:00

409 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* test_reader_perf.c — 阅读页绘页热路径回归(需要 SDK 源码树,宿主跑真实 view/anim/img)。
*
* 用户反馈:「翻页很卡,横翻竖翻都卡」「40 页也卡」。真因是每帧一次 meow_ui_img_scaled:
* 解码尺寸 ≠ 排版要的显示尺寸 → 每帧重采样 + 每帧 1MB 级临时缓冲 malloc/free。
* 修法:解码期直接按**排版要的几何**落地(翻页=fit/zoom,条漫=strip 排版高度),翻页只 blit。
*
* 夹具刻意用 **600x960(长宽比 1.6)**:条漫常见比例,显示高度 768 > 屏长边 720——
* 早期夹具用 600x800(1.33)恰好绕过「钳到屏长边」的边界,给过过于乐观的信号(评审 P1-1)。
*
* 断言(每项都看 blit/scaled 计数增量):
* P1 适宽进入:blit > 0 / scaled = 0;
* P2 连续翻页(点击来回 10 次):scaled 不增长;
* P3 翻页模式**跟手拖动**:scaled 不增长且确实画了页;
* P4 条漫模式(切换后滚动):scaled 不增长、blit 增长;
* P5 「先点适高,再切条漫」:scaled 不增长(评审点名的第二入口);
* P6 双击放大再双击缩回:缩回后按新几何重解 → scaled 不增长(P2-1);
* P7 静止若干帧不产生新解码(无重解抖动/活锁,P2-2);
* P8 驻留页图有字节上界(<= 9MB)。
*
* 编译:test/run_tests.sh(与 test_open_book_host 同口径;只发布 meow_ui.a 时 SKIP)。
*/
#include "tuku_internal.h"
#include "ui/reader/reader.h"
#include "meow_ui_internal.h"
#include "logic/reader/nav.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define PAGE_W 600
#define PAGE_H 960 /* 长宽比 1.6:适宽显示 480x768(高 > 屏长边 720) */
void mr_nav_report(void) {}
static int g_fail;
#define CHECK(cond, ...) do { \
if (cond) { printf(" PASS\n"); } \
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
static void put16(unsigned char *p, unsigned v) { p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8); }
static void put32(unsigned char *p, unsigned v)
{
p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8);
p[2] = (unsigned char)(v >> 16); p[3] = (unsigned char)(v >> 24);
}
static int write_bmp24(const char *path, int w, int h, unsigned seed)
{
int stride = ((w * 3 + 3) / 4) * 4;
int imgsz = stride * h;
unsigned char fh[14] = { 0 }, ih[40] = { 0 };
fh[0] = 'B'; fh[1] = 'M';
put32(fh + 2, (unsigned)(54 + imgsz));
put32(fh + 10, 54);
put32(ih + 0, 40);
put32(ih + 4, (unsigned)w);
put32(ih + 8, (unsigned)h);
put16(ih + 12, 1);
put16(ih + 14, 24);
put32(ih + 20, (unsigned)imgsz);
FILE *f = fopen(path, "wb");
if (!f) return -1;
int ok = fwrite(fh, 1, sizeof fh, f) == sizeof fh &&
fwrite(ih, 1, sizeof ih, f) == sizeof ih;
unsigned char *row = (unsigned char *)calloc(1, (size_t)stride);
if (!row) ok = 0;
for (int y = 0; ok && y < h; y++) {
for (int x = 0; x < w; x++) {
row[x * 3 + 0] = (unsigned char)((x + seed) & 0xFF);
row[x * 3 + 1] = (unsigned char)((y * 3 + seed) & 0xFF);
row[x * 3 + 2] = (unsigned char)((x ^ y) & 0xFF);
}
if (fwrite(row, 1, (size_t)stride, f) != (size_t)stride) ok = 0;
}
free(row);
if (fclose(f) != 0) ok = 0;
return ok ? 0 : -1;
}
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 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 void pump_ms(int ms)
{
for (int i = 0; i < ms / 10; i++) {
usleep(10 * 1000);
meow_anim_tick();
}
}
/* 单击(越过双击判定窗口,确保单击动作生效)。 */
static void tap(int x, int y)
{
reader_on_touch_down(x, y);
reader_on_tap(x, y);
pump_ms(300);
}
/* 真双击:两次点击间隔 < MR_DOUBLE_MS(240ms)。 */
static void double_tap(int x, int y)
{
reader_on_touch_down(x, y);
reader_on_tap(x, y);
pump_ms(60);
reader_on_touch_down(x, y);
reader_on_tap(x, y);
pump_ms(120);
}
/* 直接切模式(绕过顶栏 tap 的命中时序):本文件只关心绘制/解码,不验证 chrome 命中;
* 顶栏动作本身的回归在 test_mode_switch_host 覆盖。M18 评审:负载下该用例曾偶发。 */
static void mode_toggle(void)
{
mr_reader_test_toggle_mode();
pump_ms(100);
}
static void draw_once(void) { meow_ui_paint_full(); }
int main(void)
{
const char *base = getenv("TMPDIR");
if (!base || !base[0]) base = ".";
char tmpl[512];
snprintf(tmpl, sizeof tmpl, "%s/mr_perf_XXXXXX", base);
char *data = mkdtemp(tmpl);
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
setenv("MEOW_DATA_DIR", data, 1);
char book[MR_PATH_MAX];
snprintf(book, sizeof book, "./mr_perf_books_%ld/v1", (long)getpid());
if (mkdir_p(book) != 0) { printf("SKIP: mkdir failed\n"); return 0; }
for (int i = 0; i < 4; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/p%d.bmp", book, i + 1);
if (write_bmp24(p, PAGE_W, PAGE_H, (unsigned)i) != 0) {
printf("SKIP: bmp write failed\n");
return 0;
}
}
char book2[MR_PATH_MAX];
snprintf(book2, sizeof book2, "./mr_perf_books_%ld/v2", (long)getpid());
if (mkdir_p(book2) != 0) { printf("SKIP: mkdir failed\n"); return 0; }
for (int i = 0; i < 3; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/q%d.bmp", book2, i + 1);
if (write_bmp24(p, 800, 1280, (unsigned)(i + 7)) != 0) {
printf("SKIP: bmp write failed\n");
return 0;
}
}
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
static const meow_view_hooks lh = { 0 };
meow_view *lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
meow_view *reader = reader_view_create();
meow_view_page_push(ACT_LIBRARY);
int w = meow_ui_width();
int mode_x = w - 10 - 48 * 3 + 24; /* 顶栏动作 0 = 模式 */
int fit_x = mode_x + 48 * 2; /* 动作 2 = fit */
int bar_y = 28;
/* ---- P1 进入(适宽):解码 480x768 == 显示框 → 纯 blit ---- */
mr_reader_open_dir(book);
CHECK(mr_reader_ui_count() == 4, "book pages=%d", mr_reader_ui_count());
pump_ms(800);
draw_once();
int b0 = mr_reader_ui_blit_draws(), s0 = mr_reader_ui_scaled_draws();
draw_once();
CHECK(mr_reader_ui_blit_draws() > b0, "paged: page drawn with native blit");
CHECK(mr_reader_ui_scaled_draws() == s0,
"paged fit-width: zero resample (scaled %d -> %d)",
s0, mr_reader_ui_scaled_draws());
/* ---- P2 连续翻页 ---- */
int bt = mr_reader_ui_blit_draws(), st = mr_reader_ui_scaled_draws();
int pages_ok = 1;
for (int i = 0; i < 10; i++) {
int before = mr_reader_ui_page();
tap(i % 2 == 0 ? 40 : w - 40, 300); /* RTL:左 1/3=下一页、右 1/3=上一页 */
pump_ms(350);
if (mr_reader_ui_page() == before) pages_ok = 0;
draw_once();
}
CHECK(pages_ok, "10 flips advanced the page each time");
CHECK(mr_reader_ui_blit_draws() - bt >= 10, "flips drew pages (%d blits)",
mr_reader_ui_blit_draws() - bt);
CHECK(mr_reader_ui_scaled_draws() == st,
"flip hot path: zero resample (scaled %d -> %d)", st, mr_reader_ui_scaled_draws());
/* ---- P3 翻页模式跟手拖动(评审实测的 scaled 路径)---- */
st = mr_reader_ui_scaled_draws();
bt = mr_reader_ui_blit_draws();
reader_on_touch_down(400, 300);
for (int i = 1; i <= 8; i++) {
reader_on_touch_move(400 - i * 25, 300);
draw_once();
}
reader_on_tap(200, 300); /* 松手(未过阈值 → 回弹) */
pump_ms(300);
draw_once();
CHECK(mr_reader_ui_blit_draws() > bt, "paged drag drew pages (%d blits)",
mr_reader_ui_blit_draws() - bt);
CHECK(mr_reader_ui_scaled_draws() == st,
"paged drag: zero resample (scaled %d -> %d)", st, mr_reader_ui_scaled_draws());
/* ---- P4 条漫模式:切换后滚动,长宽比 1.6 页也必须零重采样 ---- */
mode_toggle();
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "switched to webtoon");
pump_ms(800);
draw_once();
st = mr_reader_ui_scaled_draws();
bt = mr_reader_ui_blit_draws();
reader_on_touch_down(240, 600);
for (int i = 1; i <= 8; i++) {
reader_on_touch_move(240, 600 - i * 30);
draw_once();
}
reader_on_tap(240, 300);
pump_ms(400);
draw_once();
CHECK(mr_reader_ui_blit_draws() > bt, "webtoon scroll drew pages (%d blits)",
mr_reader_ui_blit_draws() - bt);
CHECK(mr_reader_ui_scaled_draws() == st,
"webtoon (1.6 page): zero resample (scaled %d -> %d)", st,
mr_reader_ui_scaled_draws());
/* ---- P5 「先适高、再切条漫」:strip 恒按屏宽排版,解码必须跟随 ---- */
mode_toggle(); /* 回翻页 */
CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "back to paged");
pump_ms(500);
tap(fit_x, bar_y); /* 适宽 → 适高 */
CHECK(mr_reader_ui_fit() == MR_FIT_HEIGHT, "fit-height selected (got %d)",
mr_reader_ui_fit());
pump_ms(700);
mode_toggle(); /* 适高档 → 条漫 */
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon after fit-height");
pump_ms(900);
draw_once();
st = mr_reader_ui_scaled_draws();
bt = mr_reader_ui_blit_draws();
for (int i = 0; i < 5; i++) {
reader_on_touch_down(240, 600);
for (int k = 1; k <= 4; k++) {
reader_on_touch_move(240, 600 - k * 30);
draw_once();
}
reader_on_tap(240, 300);
pump_ms(250);
draw_once();
}
CHECK(mr_reader_ui_blit_draws() > bt, "webtoon after fit-height drew pages (%d blits)",
mr_reader_ui_blit_draws() - bt);
CHECK(mr_reader_ui_scaled_draws() == st,
"fit-height -> webtoon: zero resample (scaled %d -> %d)", st,
mr_reader_ui_scaled_draws());
/* ---- P6 双击放大再缩回(评审 P2-1:缩回后必须按新几何重解)---- */
mode_toggle(); /* 回翻页 */
CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "paged for zoom test");
pump_ms(600);
draw_once();
double_tap(240, 360); /* 放大 */
pump_ms(700);
draw_once();
double_tap(240, 360); /* 缩回 */
pump_ms(800);
draw_once();
st = mr_reader_ui_scaled_draws();
for (int i = 0; i < 5; i++) draw_once();
CHECK(mr_reader_ui_scaled_draws() == st,
"zoom out: page re-decoded at fit geometry, zero resample (scaled %d -> %d)",
st, mr_reader_ui_scaled_draws());
/* ---- P7 静止不抖:无新解码(重解活锁回归,P2-2)---- */
pump_ms(300);
int d0 = mr_reader_ui_decode_count();
for (int i = 0; i < 10; i++) { draw_once(); pump_ms(40); }
CHECK(mr_reader_ui_decode_count() == d0,
"idle: no extra decodes (%d -> %d)", d0, mr_reader_ui_decode_count());
/* ---- P8 驻留内存上界 ---- */
long kb = mr_reader_ui_resident_kb();
printf(" info: 600x960 resident=%ldKB blit=%d scaled=%d decodes=%d\n", kb,
mr_reader_ui_blit_draws(), mr_reader_ui_scaled_draws(),
mr_reader_ui_decode_count());
CHECK(kb <= 9 * 1024, "resident page images bounded (got %ldKB)", kb);
/* ---- P9 800x1280(评审表格另一档):翻页 + 条漫都必须零重采样 ---- */
meow_view_page_pop();
mr_reader_open_dir(book2);
pump_ms(800);
draw_once();
st = mr_reader_ui_scaled_draws();
bt = mr_reader_ui_blit_draws();
reader_on_touch_down(400, 300);
for (int i = 1; i <= 6; i++) {
reader_on_touch_move(400 - i * 25, 300);
draw_once();
}
reader_on_tap(200, 300);
pump_ms(300);
draw_once();
CHECK(mr_reader_ui_blit_draws() > bt && mr_reader_ui_scaled_draws() == st,
"800x1280 paged drag: zero resample (blit +%d, scaled +%d)",
mr_reader_ui_blit_draws() - bt, mr_reader_ui_scaled_draws() - st);
mode_toggle(); /* 条漫 */
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "800x1280 webtoon");
pump_ms(800);
draw_once();
st = mr_reader_ui_scaled_draws();
bt = mr_reader_ui_blit_draws();
for (int i = 0; i < 5; i++) {
reader_on_touch_down(240, 600);
for (int k = 1; k <= 4; k++) {
reader_on_touch_move(240, 600 - k * 30);
draw_once();
}
reader_on_tap(240, 300);
pump_ms(250);
draw_once();
}
CHECK(mr_reader_ui_blit_draws() > bt,
"800x1280 webtoon drew pages (%d blits)", mr_reader_ui_blit_draws() - bt);
CHECK(mr_reader_ui_scaled_draws() == st,
"800x1280 webtoon: zero resample (scaled %d -> %d)", st,
mr_reader_ui_scaled_draws());
meow_view_page_pop();
meow_view_free(reader);
meow_view_free(lib);
meow_ui_shutdown();
/* ---- P10 大屏(1080x1920):fit 目标 >3MB 被字节上限钳住,不得反复重解(P2-2)---- */
meow_ui_opts o2 = { 1080, 1920 };
if (meow_ui_init(&o2) == 0) {
lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
reader = reader_view_create();
meow_view_page_push(ACT_LIBRARY);
mr_reader_open_dir(book);
pump_ms(1200);
meow_ui_paint_full();
int dbig = mr_reader_ui_decode_count();
for (int i = 0; i < 10; i++) { meow_ui_paint_full(); pump_ms(40); }
CHECK(mr_reader_ui_decode_count() == dbig,
"large screen: clamped decode is stable, no live-lock (%d -> %d)",
dbig, mr_reader_ui_decode_count());
printf(" info: large screen resident=%ldKB decodes=%d\n",
mr_reader_ui_resident_kb(), mr_reader_ui_decode_count());
meow_view_page_pop();
meow_view_free(reader);
meow_view_free(lib);
meow_ui_shutdown();
}
rm_rf(book);
rm_rf(book2);
char rootdir[MR_PATH_MAX];
snprintf(rootdir, sizeof rootdir, "./mr_perf_books_%ld", (long)getpid());
rm_rf(rootdir);
rm_rf(data);
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
g_fail ? "see failures above" : "reader flip hot-path checks passed");
return g_fail ? 1 : 0;
}