Files
MeowOS-SDK/tuku/test/test_reader_perf.c
T
AskaEth 62826e1d13 图库: 阅读器两级清晰度,先低档上屏、停稳再换清晰(M31)
真机 1280x2275 单页解码 392ms,首次看某页有一瞬加载感。改成:
- 大页(JPEG ≥1MP)先按显示长边一半的档位(真机页=step 4)解一版低档,
  精确落到显示框后立刻上屏(换图仍是单次原生 blit,不闪白、不重置缩放/滚动);
- 停稳 200ms 后才解目标档位(step 2),在**同一槽**原地替换低档,低档随即释放,
  常驻不翻倍;高档失败/预算跳过时保留低档,不把整页标成「内存不足」;
- 拖动/连翻/条漫滚动/跳页在途期间只解低档;高档请求复用 M22 的 gen/窗口
  stale 判据作废(只多一条 sharpen 谓词),出队前/后两条路径都计入取消;
- 日志尾部新增 tier=low|high(取消行同),真机可直接核对先糊后清。

分档只给 JPEG:step 是 JPEG 缩解概念,PNG/BMP 后端按目标逐行丢行本来就不慢,
且峰值模型对非 JPEG 按「输出=显示框」建模,多一条小图→落框路径会让模型低估
(不新增解码路径,也不改 sdk/ 与 page_source 的唯一模型)。

宿主回归:
- test_reader_membudget B10:JPEG 低档先落位(open 不阻塞)→ 高档落同槽
  (槽内字节不变、槽位全程不空)→ 拖动中强塞的高档请求被取消且不被解码;
- B8 连翻 100 页(6 类 1.3–12MP 循环)峰值增量 2714KB ≤3MB、常驻 ≤3MB,
  低档/高档都确实跑过;B3 断言小页保持单档(不付两次解码);
- test_reader_perf P11:非 JPEG 大页不分档,仍是一次原生 blit。
2026-11-06 13:10:01 +08:00

453 lines
18 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);
* P9 800×1280(评审表格另一档):翻页 + 条漫都必须零重采样;
* P10 大屏(1080×1920):fit 目标被字节上限钳住后不得反复重解;
* P11 分档策略(M31):非 JPEG 大页不分档(一次解到目标档、仍是原生 blit);
* 低档先落位→停稳换清晰→拖动取消的完整生命周期在 test_reader_membudget 的 B10
* 用真实 JPEG 夹具覆盖。
*
* 编译: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());
/* ---- P11 分档策略(M31):非 JPEG 大页不分档——一次解到目标档,仍是原生 blit ----
* 低档/高档的完整生命周期(低档先落位→停稳换清晰→拖动取消)在 test_reader_membudget
* 的 B10 用真实 JPEG 夹具覆盖;这里只钉住「分档只给 JPEG」这条策略,防止把 PNG/BMP
* 也拖进未校准的「小图→落框」峰值路径。 */
{
char book3[MR_PATH_MAX];
snprintf(book3, sizeof book3, "./mr_perf_books_%ld/v3", (long)getpid());
int ok3 = mkdir_p(book3) == 0;
for (int i = 0; ok3 && i < 2; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/t%d.bmp", book3, i + 1);
/* 1600×2400 = 3.84MP:量级与真机大页相同,但格式不是 JPEG。 */
if (write_bmp24(p, 1600, 2400, (unsigned)(i + 41)) != 0) ok3 = 0;
}
if (!ok3) {
printf(" SKIP: tier fixture write failed\n");
} else {
meow_view_page_pop(); /* 关掉 book2 */
uint64_t t_open = meow_ui_time_ms();
mr_reader_open_dir(book3);
uint64_t open_ms = meow_ui_time_ms() - t_open;
CHECK(mr_reader_ui_count() == 2, "tier book pages=%d", mr_reader_ui_count());
CHECK(open_ms < 500, "open returns without decoding on the UI thread (%llu ms)",
(unsigned long long)open_ms);
pump_ms(600);
draw_once();
CHECK(mr_reader_ui_focus_tier() == MR_TIER_HIGH,
"non-JPEG big page decodes straight to the target tier (tier=%d)",
mr_reader_ui_focus_tier());
CHECK(mr_reader_ui_low_decodes() == 0,
"no low tier for PNG/BMP (%d low decodes)", mr_reader_ui_low_decodes());
int bl = mr_reader_ui_blit_draws(), sc = mr_reader_ui_scaled_draws();
draw_once();
CHECK(mr_reader_ui_blit_draws() > bl && mr_reader_ui_scaled_draws() == sc,
"big BMP page draws as native blit (blit +%d, scaled +%d)",
mr_reader_ui_blit_draws() - bl, mr_reader_ui_scaled_draws() - sc);
}
}
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;
}