/* 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 #include #include #include #include #include #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); } 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 页也必须零重采样 ---- */ tap(mode_x, bar_y); 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 恒按屏宽排版,解码必须跟随 ---- */ tap(mode_x, bar_y); /* 回翻页 */ 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); tap(mode_x, bar_y); /* 适高档 → 条漫 */ 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:缩回后必须按新几何重解)---- */ tap(mode_x, bar_y); /* 回翻页 */ 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); tap(mode_x, bar_y); /* 条漫 */ 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; }