Files
MeowOS-SDK/tuku/test/test_reader_perf.c
T
AskaEth 462cfd5bc0 图库: 阅读器翻页按显示尺寸解码(零重采样)+ 多页闪退防御 + 从第 N 张开始(M17)
用户反馈「横竖翻页都卡(40 页也卡)」「140 张图的文件夹一打开就闪退」
「要从中间某张开始看」三条,本次分别处理:

翻页卡顿(真因:每帧一次重采样)
- 解码目标从「屏长边」改成「当前 fit/zoom 的显示尺寸」:适宽 3:4 页原来解成
  540x720、显示 480x640,draw_page_at 于是每帧走 meow_ui_img_scaled —— 每页每帧
  多一遍全图最近邻重采样 + 一次 1.2MB 临时 malloc/free(>128KB 走 mmap)。
- 现在解码结果与显示框一致(±2px 容差)→ 翻页/滚动热路径只有原生 blit。
- fit/zoom 变化时按新几何重解(slot_ready 判据 + 同页替换),不再残留缩放;
  已到解码预算或已是原始分辨率的小图不会反复重解。
- 拖动/吸附/fling 帧只 invalidate 内容区(顶栏静态,不再每帧重绘标题文字);
  异步落位只在「该页真的在屏幕上」时才重绘,预取邻居不刷屏。
- 页原始尺寸按册缓存(8B/页),绘制几何不再每帧 fopen probe 文件。

多页闪退(宿主未能复现,按内存边界防御)
- 常驻页图改为「显示尺寸」并加上限:单页 ≤3MB、整窗 ≤9MB(超限淘汰最远页);
  常驻内存只与解码窗大小有关,不随页数增长。
- 解码中间峰值 >12MB 的页直接跳过并显示占位 + WARN(旧代码 PNG/BMP 放到 40MB,
  3000x3000 就是 36MB 瞬时分配;JPEG 完全没上限)。140 页夹具(含坏图/超大图)
  走真实 UI 链路打开、翻页、跳页、条漫全部通过。

新增 mr_reader_open_dir_at(dir, start_index)(文件管理器模式入口)
- 显式下标优先于续读记录;-1=续读(旧入口语义),其它负数钳到 0,≥页数钳到末页。
- mr_reader_open_dir 保留为 (dir, MR_READER_START_RESUME) 的包装;reader.h 导出。

测试:新增 test_reader_perf(翻页零重采样/fit 切换/驻留上界)与
test_reader_many_pages(140 页 + 坏图/超大图 + 起始下标/钳制/续读优先级 + 条漫跳页)。
2026-11-05 18:06:26 +08:00

223 lines
8.0 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:
* 解码尺寸(maxdim=屏长边) ≠ 显示尺寸(适宽/适高) → 每帧重采样 + 每帧 1.2MB 临时缓冲
* malloc/free。修法:解码期直接按显示尺寸落地,翻页只做原生 blit。
*
* 本测试用 600x800(3:4)合成 BMP 当页图,断言:
* P1 适宽:解码 480x640 == 显示框 → 绘页零重采样(scaled_draws 不增长);
* P2 连续翻页 10 次(预取窗口命中):每帧仍零重采样,且确实画了页(blit 增长);
* P3 切「适高」:重解到 540x720 后同样零重采样(fit 变化会按新几何重解,不残留缩放);
* P4 驻留内存有上界(<= 9MB:3~4 张显示尺寸页)。
*
* 编译: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 <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
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);
}
/* 合成 BMP24(stb 可解);填一点非零像素,避免全 0 触发任何「透明」特判。 */
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); /* 越过双击判定窗口,让单击生效 */
}
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, 600, 800, (unsigned)i) != 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 fit_x = w - 10 - 48 * 3 + 24 + 48 * 2; /* 顶栏第 3 个动作 = fit */
int bar_y = 28;
/* ---- P1 进入:适宽 480x640 == 解码尺寸 → 纯 blit ---- */
mr_reader_open_dir(book);
CHECK(mr_reader_ui_count() == 4, "book pages=%d", mr_reader_ui_count());
pump_ms(700);
draw_once();
int b0 = mr_reader_ui_blit_draws(), s0 = mr_reader_ui_scaled_draws();
draw_once();
CHECK(mr_reader_ui_blit_draws() > b0, "page drawn with native blit");
CHECK(mr_reader_ui_scaled_draws() == s0,
"fit-width: zero resample per frame (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();
/* RTL 默认:左 1/3=下一页、右 1/3=上一页;来回翻避免撞到首末页的册间衔接。 */
tap(i % 2 ? w - 40 : 40, 300);
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 切「适高」:按新几何重解后再测 ---- */
tap(fit_x, bar_y); /* 适宽 → 适高 */
pump_ms(600);
draw_once();
int s2 = mr_reader_ui_scaled_draws();
for (int i = 0; i < 5; i++) {
tap(i % 2 ? w - 40 : 40, 300);
pump_ms(350);
draw_once();
}
CHECK(mr_reader_ui_scaled_draws() == s2,
"fit-height: re-decoded page still zero resample (scaled %d -> %d)",
s2, mr_reader_ui_scaled_draws());
/* ---- P4 驻留内存上界 ---- */
long kb = mr_reader_ui_resident_kb();
printf(" info: resident=%ldKB blit=%d scaled=%d\n", kb,
mr_reader_ui_blit_draws(), mr_reader_ui_scaled_draws());
CHECK(kb <= 9 * 1024, "resident page images bounded (got %ldKB)", kb);
meow_view_page_pop();
meow_view_free(reader);
meow_view_free(lib);
meow_ui_shutdown();
rm_rf(book);
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;
}