Files
MeowOS-SDK/tuku/test/test_reader_membudget.c
T
AskaEth 40751c09bd Tuku reader: 真实解码峰值模型 + 3MB 硬阈值 + 解码取消 + 3 槽(M22 止损版)
真机(R3 Pro II 480×720 / MemTotal 56872kB):空闲 RSS 6044kB → 翻页 11400kB →
峰值 15600kB(MemFree 仅 1404kB)→ SIGKILL。M20 审计坐实根因是「单页解码瞬时峰值」,
且旧模型把 stb 当缩解库,对 JPEG 低估 3.5–8 倍(实测 baseline 2000×3000 +10.1MB、
渐进 +23.5MB、1200×1600 PNG +12.9MB)。本次按实测重定预算:

- 真实峰值模型 decode_peak_bytes():JPEG 分量面(+渐进 short 系数面)、PNG 源缓冲 +
  RGBA 转换副本、BMP 直写目标,再叠精修/落框两段一次性重采样;上限常态 3MB
  (MR_DECODE_PEAK_CAP),只有 MemFree 真富余时才放宽到 min(12MB, MemFree-4MB)。
- 超预算时的降级:JPEG 逐步加粗 stbi 缩解档位(meow_img_load_file_step),
  PNG/BMP 无解码期缩放 → 占位;结果精确重采样回显示框(meow_img_resample),
  命中页仍是单次 blit(M17 热路径纪律不破)。
- 占位可重试(g_page_skip 与永久失败 g_page_fail 分离):焦点页冷却 1.5s 自动重试、
  中间区点按立即重试、离开再回来即重试;坏图仍是永久占位。
- 解码前内存门控:MemAvailable < 6MB 不解码(读不到就放行,不猜)。
- 解码请求取消:worker 出队后、mr_book_open 前二次判 gen/窗口;拖动/fling 期间只解落点页,
  预取邻居仅当 MemAvailable ≥ 12MB。
- 先释放再解码:queue_page 按落地字节 slots_reserve() 先淘汰不可见旧图;可见页不淘汰。
- 槽位 4→3、常驻上限 9MB→3MB、结果队列 4→2。
- 可观测性:`reader decode page=N src=WxH peak=XkB memfree=YkB memavail=ZkB ok|skip=… step=S ms=M`
  与进出阅读器 rss/hwm/memfree/memavail。

page_source: 头部探测新增 codec/progressive/channels/planes(mr_probe_info),
只带最小 SOF 的合成头仍能取尺寸(旧契约不退化)。

宿主证据:新增 test_reader_membudget(--wrap malloc 计真实峰值):
PNG 600×960 model 4001kB vs 实测 3943kB;JPEG 1000×1500 step2 model 3749/实测 3711;
连翻 100 页 peak_transient 0kB(先释放再解码让缓冲复用)、驻留 ≤3MB、解码 100 次(取消生效)、
3MB 阈值下 1.5MP JPEG 可解、4MP JPEG 需 MemFree 放行(否则占位)、渐进式模型更高于 baseline。

run_tests.sh 全绿(含 zip 106 / cbz 325)、run_webtoon_tests.sh 全绿、SDK 与 tuku 交叉构建零警告。
2026-11-05 21:28:50 +08:00

542 lines
22 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_membudget.c — 阅读器解码内存预算回归(M22;需要 SDK 源码树)。
*
* 真机事故(R3 Pro II 480×720 / MemTotal 56872kB):空闲 RSS 6044kB → 翻页 11400kB
* → 峰值 15600kB(当时 MemFree 仅 1404kB)→ SIGKILL。M20 审计把账算清:4 槽 RGBA 常驻
* 5356kB、单页解码尖峰 3.4–35MB(旧模型低估 3.5–8 倍)。本测试用 `--wrap malloc` 量
* **真实分配峰值**(不是 VmHWM 估计),把「模型 / 阈值 / 驻留」钉在实测上。
*
* 断言:
* B1 峰值模型 ≤ 实测(模型必须是不低估的上界;同时 ≤ 实测×2,避免离谱保守)
* B2 单页解码峰值(预算内的页)≤ 3MB 硬阈值
* B3 连翻 100 页:峰值不增长、无永久失败页、解码次数有界(取消生效)
* B4 常驻页图 ≤ 常驻上限(3MB)
* B5 内存门控:MemAvailable 不足 → 不解码直接占位(可重试,不是永久失败)
* B6 预算跳过可重试:MemFree 充足时同一页能解出来(占位不是永久 failed)
* B7 快翻取消:拖动期间只解落点页,不产生中间页解码
*
* 夹具:BMP/PNG 由本文件用 C 写(不依赖 Pillow);JPEG 由 python3+Pillow 生成,
* 缺 PIL 时跳过 JPEG 相关断言(打印 SKIP)。全部夹具体积极小(结构化图案)。
*
* 编译:test/run_tests.sh(与 test_reader_perf 同口径 + --wrap malloc 家族)。
*/
#define _GNU_SOURCE
#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>
/* ------------------------------------------------------------------ */
/* 分配打点:--wrap malloc/free 家族(与 sdk/test_meow_img_budget.c 同法)*/
/* ------------------------------------------------------------------ */
#define AHDR 32
#define AMAGIC 0x6D6B4D32324D454DU /* 只统计本包装器的分配;libc 内部(strdup 等)原样透传 */
static size_t g_live = 0;
static size_t g_peak = 0;
extern void *__real_malloc(size_t);
extern void *__real_calloc(size_t, size_t);
extern void *__real_realloc(void *, size_t);
extern void __real_free(void *);
static void acc_alloc(size_t n)
{
g_live += n;
if (g_live > g_peak) g_peak = g_live;
}
void *__wrap_malloc(size_t n)
{
size_t *h = (size_t *)__real_malloc(n + AHDR);
if (!h) return NULL;
h[0] = n;
h[1] = (size_t)AMAGIC;
acc_alloc(n);
return (char *)h + AHDR;
}
void *__wrap_calloc(size_t a, size_t b)
{
size_t n = a * b;
void *p = __wrap_malloc(n);
if (p) memset(p, 0, n);
return p;
}
void __wrap_free(void *p)
{
size_t *h;
if (!p) return;
h = (size_t *)((char *)p - AHDR);
if (h[1] != (size_t)AMAGIC) { __real_free(p); return; } /* 非本包装器的分配 */
h[1] = 0;
g_live -= h[0];
__real_free(h);
}
void *__wrap_realloc(void *p, size_t n)
{
size_t *h = p ? (size_t *)((char *)p - AHDR) : NULL;
if (p && (!h || h[1] != (size_t)AMAGIC)) return __real_realloc(p, n); /* 外来指针 */
size_t old = h ? h[0] : 0;
void *q = __wrap_malloc(n);
if (!q) return NULL;
if (p) {
memcpy(q, p, old < n ? old : n);
__wrap_free(p);
}
return q;
}
static size_t peak_reset(void) { g_peak = g_live; return g_live; }
static size_t peak_since(size_t base) { return g_peak - base; }
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)
#define INFO(...) do { printf(" info: "); 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 put32be(unsigned char *p, unsigned v)
{
p[0] = (unsigned char)(v >> 24); p[1] = (unsigned char)(v >> 16);
p[2] = (unsigned char)(v >> 8); p[3] = (unsigned char)v;
}
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 unsigned crc32_buf(const unsigned char *p, size_t n)
{
unsigned c = 0xFFFFFFFFu;
for (size_t i = 0; i < n; i++) {
c ^= p[i];
for (int k = 0; k < 8; k++) c = (c >> 1) ^ (0xEDB88320u & (unsigned)(-(int)(c & 1)));
}
return c ^ 0xFFFFFFFFu;
}
static int write_png_rgb(const char *path, int w, int h, unsigned seed)
{
/* zlib 流用 stored 块(不依赖 zlib),只为造一个真 PNG 让 stb 走「源缓冲 + RGBA 转换」路径。 */
size_t rowlen = (size_t)w * 3 + 1;
size_t rawlen = rowlen * (size_t)h;
unsigned char *raw = (unsigned char *)malloc(rawlen);
if (!raw) return -1;
for (int y = 0; y < h; y++) {
unsigned char *r = raw + rowlen * (size_t)y;
r[0] = 0; /* filter none */
for (int x = 0; x < w; x++) {
r[1 + x * 3 + 0] = (unsigned char)((x + seed) & 0xFF);
r[1 + x * 3 + 1] = (unsigned char)(y & 0xFF);
r[1 + x * 3 + 2] = (unsigned char)((x ^ y) & 0xFF);
}
}
size_t nblk = (rawlen + 65534) / 65535;
size_t zlen = 2 + rawlen + nblk * 5 + 4;
unsigned char *z = (unsigned char *)malloc(zlen);
if (!z) { free(raw); return -1; }
size_t zi = 0;
z[zi++] = 0x78; z[zi++] = 0x01;
for (size_t off = 0; off < rawlen; off += 65535) {
size_t n = rawlen - off > 65535 ? 65535 : rawlen - off;
z[zi++] = (off + n == rawlen) ? 1 : 0; /* BFINAL */
z[zi++] = (unsigned char)(n & 0xFF);
z[zi++] = (unsigned char)(n >> 8);
z[zi++] = (unsigned char)(~n & 0xFF);
z[zi++] = (unsigned char)((~n >> 8) & 0xFF);
memcpy(z + zi, raw + off, n);
zi += n;
}
/* adler32(大端,zlib 流尾) */
{
unsigned a = 1, b = 0;
for (size_t i = 0; i < rawlen; i++) { a = (a + raw[i]) % 65521; b = (b + a) % 65521; }
put32be(z + zi, (b << 16) | a);
zi += 4;
}
free(raw);
FILE *f = fopen(path, "wb");
if (!f) { free(z); return -1; }
static const unsigned char sig[8] = { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A };
int ok = fwrite(sig, 1, 8, f) == 8;
unsigned char ihdr[13] = { 0 };
put32be(ihdr + 0, (unsigned)w);
put32be(ihdr + 4, (unsigned)h);
ihdr[8] = 8; /* bit depth */
ihdr[9] = 2; /* color type RGB */
struct { const char *tag; const unsigned char *data; unsigned len; } chunks[2] = {
{ "IHDR", ihdr, 13 }, { "IDAT", z, (unsigned)zi },
};
for (int k = 0; ok && k < 2; k++) {
unsigned char lenb[4], crcb[4];
put32be(lenb, chunks[k].len);
if (fwrite(lenb, 1, 4, f) != 4) ok = 0;
if (fwrite(chunks[k].tag, 1, 4, f) != 4) ok = 0;
if (fwrite(chunks[k].data, 1, chunks[k].len, f) != chunks[k].len) ok = 0;
unsigned crc = crc32_buf((const unsigned char *)chunks[k].tag, 4);
/* 逐段续算:简化起见重算整块 */
{
unsigned char *tmp = (unsigned char *)malloc(4 + chunks[k].len);
if (tmp) {
memcpy(tmp, chunks[k].tag, 4);
memcpy(tmp + 4, chunks[k].data, chunks[k].len);
crc = crc32_buf(tmp, 4 + chunks[k].len);
free(tmp);
}
}
put32be(crcb, crc);
if (fwrite(crcb, 1, 4, f) != 4) ok = 0;
}
if (ok) {
unsigned char iend[12] = { 0, 0, 0, 0, 'I', 'E', 'N', 'D', 0xAE, 0x42, 0x60, 0x82 };
if (fwrite(iend, 1, 12, f) != 12) ok = 0;
}
free(z);
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);
}
/* 生成 JPEG 夹具(Pillow 不可用则返回 0,相关断言 SKIP)。脚本本体在
* test/make_mem_samples.py(与本文件同目录;run_tests.sh 已 cd 到 tuku/)。 */
static int gen_jpeg_fixtures(const char *dir)
{
char cmd[512];
snprintf(cmd, sizeof cmd, "python3 test/make_mem_samples.py '%s' >/dev/null 2>&1", dir);
if (system(cmd) != 0) return 0;
struct stat st;
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/page.jpg", dir);
return stat(p, &st) == 0 && st.st_size > 0;
}
int main(void)
{
const char *base = getenv("TMPDIR");
if (!base || !base[0]) base = ".";
char tmpl[512];
snprintf(tmpl, sizeof tmpl, "%s/mr_membudget_XXXXXX", base);
char *data = mkdtemp(tmpl);
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
setenv("MEOW_DATA_DIR", data, 1);
char root[MR_PATH_MAX];
snprintf(root, sizeof root, "./mr_membudget_%ld", (long)getpid());
char book[MR_PATH_MAX], big[MR_PATH_MAX], pngbook[MR_PATH_MAX], jpgbook[MR_PATH_MAX];
snprintf(book, sizeof book, "%s/small", root);
snprintf(big, sizeof big, "%s/big", root);
snprintf(pngbook, sizeof pngbook, "%s/png", root);
snprintf(jpgbook, sizeof jpgbook, "%s/jpg", root);
if (mkdir_p(book) != 0 || mkdir_p(big) != 0 || mkdir_p(pngbook) != 0 ||
mkdir_p(jpgbook) != 0) {
printf("SKIP: mkdir failed\n");
return 0;
}
/* 480x640 BMP ×8:单页解码峰值 ≈ 480*640*4*2 ≈ 2.5MB,能进 3MB 预算(可连翻)。 */
for (int i = 0; i < 8; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/p%02d.bmp", book, i);
if (write_bmp24(p, 480, 640, (unsigned)i) != 0) { printf("SKIP: bmp write\n"); return 0; }
}
/* 1200x1600 BMP:模型峰值 ≈ 7.7MB + 目标 ≈ 9MB —— 3MB 常态预算下必须占位,
* MemFree 富余(放宽到 12MB)时必须能解出来。 */
{
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/huge.bmp", big);
if (write_bmp24(p, 1200, 1600, 3) != 0) { printf("SKIP: bmp write\n"); return 0; }
}
/* 600x960 PNG(RGB):走「源缓冲 + RGBA 转换」路径(stb 不能解码期缩解)。 */
for (int i = 0; i < 3; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/q%d.png", pngbook, i);
if (write_png_rgb(p, 600, 960, (unsigned)i) != 0) { printf("SKIP: png write\n"); return 0; }
}
int have_jpeg = gen_jpeg_fixtures(jpgbook);
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);
/* 宿主内存读数固定:3MB 常态预算生效(不被开发机的巨大 MemFree 放宽)。 */
mr_reader_ui_test_set_mem(4096, 16000);
/* ---- B1/B2 单页解码峰值 vs 模型(PNG:stb 不能解码期缩解 → 整幅落地)---- */
{
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/q0.png", pngbook);
long long model = mr_reader_ui_decode_peak_kb(p, 1, 480, 768);
size_t b0 = g_live;
peak_reset();
/* 走产品解码入口(含落框重采样),与阅读器 worker 完全同路径 */
meow_img *im = meow_img_load_file_step(p, 768, 1);
if (im && (meow_img_w(im) != 480 || meow_img_h(im) != 768)) {
meow_img *fit = meow_img_resample(im, 480, 768);
if (fit) { meow_img_free(im); im = fit; }
}
size_t measured_kb = peak_since(b0) / 1024 + 1;
int got = im ? 1 : 0;
if (im) meow_img_free(im);
INFO("png 600x960: model=%lldkB measured=%zukB decoded=%d", model, measured_kb, got);
CHECK(got, "png fixture decodes (writer valid)");
CHECK(model >= (long long)measured_kb, "model is an upper bound (%lldkB < %zukB)",
model, measured_kb);
CHECK(model <= (long long)measured_kb * 2, "model not wildly pessimistic (%lldkB vs %zukB)",
model, measured_kb);
}
/* ---- B5 内存门控:MemAvailable 不足 → 不解码、占位(可重试)---- */
{
mr_reader_ui_test_set_mem(20000, 1000); /* MemAvailable 1MB < 6MB 下限 */
mr_reader_open_dir(book);
CHECK(mr_reader_ui_count() == 8, "book pages=%d", mr_reader_ui_count());
int d0 = mr_reader_ui_decode_count();
pump_ms(600);
CHECK(mr_reader_ui_decode_count() - d0 <= 1,
"low mem: no decode attempt (%d new decodes)", mr_reader_ui_decode_count() - d0);
CHECK(mr_reader_ui_skipped_pages() > 0, "low mem: page skipped (skipped=%d)",
mr_reader_ui_skipped_pages());
CHECK(mr_reader_ui_failed_pages() == 0, "low mem: not a permanent failure (failed=%d)",
mr_reader_ui_failed_pages());
/* 内存恢复 + 点按显式重试 → 同一页能解出来(可重试语义) */
mr_reader_ui_test_set_mem(20000, 20000);
int y = meow_ui_height() / 2;
tap(meow_ui_width() / 2, y);
pump_ms(500);
CHECK(mr_reader_ui_skipped_pages() == 0, "retry after mem recovers (skipped=%d)",
mr_reader_ui_skipped_pages());
CHECK(mr_reader_ui_resident_kb() > 0, "retry landed a page image (resident=%ldkB)",
mr_reader_ui_resident_kb());
meow_view_page_pop();
pump_ms(200);
}
/* ---- B3/B4 连翻 100 页:真实分配峰值不增长、常驻有界、取消有界 ---- */
{
mr_reader_ui_test_set_mem(4096, 16000);
mr_reader_open_dir(book);
pump_ms(800);
int w = meow_ui_width();
int d0 = mr_reader_ui_decode_count();
long long resident_cap = mr_reader_ui_resident_cap_kb();
long long cap = mr_reader_ui_peak_cap_kb();
/* 基线 = 首屏稳定后的存活字节;之后任何分配都不该再超过 基线+3MB(= 单页解码峰值)。 */
size_t live_steady = g_live;
size_t peak_steady = g_peak;
int skipped_max = 0, advanced = 0;
for (int i = 0; i < 100; i++) {
int before = mr_reader_ui_page();
tap(i % 2 == 0 ? 40 : w - 40, 300); /* RTL:左 1/3=下一页 */
pump_ms(120); /* 覆盖滑页 + 紧随其后的预取解码 */
if (mr_reader_ui_page() != before) advanced++;
if (mr_reader_ui_skipped_pages() > skipped_max) skipped_max = mr_reader_ui_skipped_pages();
meow_ui_paint_full();
long kb = mr_reader_ui_resident_kb();
if (kb > resident_cap) {
g_fail++;
printf(" FAIL flip %d: resident %ldkB > cap %lldkB\n", i, kb, resident_cap);
break;
}
}
size_t transient = g_peak > peak_steady ? g_peak - peak_steady : 0;
size_t live_end = g_live;
int decodes = mr_reader_ui_decode_count() - d0;
INFO("100 flips: steady_live=%zuKB peak_transient=%zuKB end_live=%zuKB cap=%lldKB decodes=%d advanced=%d",
live_steady / 1024, transient / 1024, live_end / 1024, cap, decodes, advanced);
CHECK(advanced == 100, "every flip advanced the page (%d/100)", advanced);
CHECK(transient <= (size_t)cap * 1024 + 512 * 1024,
"no allocation above steady+3MB over 100 flips (%zuKB)", transient / 1024);
CHECK(live_end <= live_steady + 512 * 1024,
"live bytes do not drift up over 100 flips (%zuKB -> %zuKB)",
live_steady / 1024, live_end / 1024);
CHECK(skipped_max == 0, "no page skipped while flipping small pages (max=%d)", skipped_max);
CHECK(mr_reader_ui_failed_pages() == 0, "no permanently failed pages (%d)",
mr_reader_ui_failed_pages());
/* 取消/预取纪律:100 次翻页的解码次数必须有界(M20 实测曾 1.75× 浪费)。 */
CHECK(decodes <= 100 * 2, "decode count bounded over 100 flips (%d)", decodes);
meow_view_page_pop();
pump_ms(200);
}
/* ---- B6 预算跳过可重试:大页在低预算下占位,MemFree 充足后能解出来 ---- */
{
mr_reader_ui_test_set_mem(4096, 16000); /* MemFree 4MB → 3MB 常态预算 */
mr_reader_open_dir(big);
pump_ms(900);
CHECK(mr_reader_ui_skipped_pages() == 1, "over-budget page skipped (skipped=%d)",
mr_reader_ui_skipped_pages());
CHECK(mr_reader_ui_failed_pages() == 0, "over-budget page is not a permanent failure (failed=%d)",
mr_reader_ui_failed_pages());
/* MemFree 富余 → 放宽档(峰值上限 = MemFree-4MB,最高 12MB);大页仍应能解出。 */
mr_reader_ui_test_set_mem(48000, 48000);
tap(meow_ui_width() / 2, meow_ui_height() / 2);
pump_ms(1200);
INFO("relaxed budget=%ldkB skipped=%d resident=%ldkB", mr_reader_ui_peak_budget_kb(),
mr_reader_ui_skipped_pages(), mr_reader_ui_resident_kb());
CHECK(mr_reader_ui_skipped_pages() == 0,
"over-budget page decodable once MemFree is plenty (skipped=%d)",
mr_reader_ui_skipped_pages());
meow_view_page_pop();
pump_ms(200);
}
/* ---- B7 JPEG:模型不低估 + 3MB 阈值下「哪些 JPEG 会变占位」---- */
if (have_jpeg) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/page.jpg", jpgbook);
long long model = mr_reader_ui_decode_peak_kb(p, 2, 480, 720);
size_t b0 = g_live;
peak_reset();
meow_img *im = meow_img_load_file_step(p, 720, 2);
if (im && (meow_img_w(im) != 480 || meow_img_h(im) != 720)) {
meow_img *fit = meow_img_resample(im, 480, 720);
if (fit) { meow_img_free(im); im = fit; }
}
size_t measured = peak_since(b0) / 1024 + 1;
int got = im ? 1 : 0;
if (im) meow_img_free(im);
INFO("jpeg 1000x1500 step=2: model=%lldkB measured=%zukB decoded=%d", model, measured, got);
CHECK(got, "jpeg fixture decodes");
CHECK(model >= (long long)measured, "jpeg model upper bound (%lldkB < %zukB)", model, measured);
CHECK(model <= (long long)measured * 2, "jpeg model not pessimistic (%lldkB vs %zukB)",
model, measured);
/* 1.5MP(真机崩溃页档位)在 3MB 预算内必须可解:某档 step ≤ cap */
long long cap = mr_reader_ui_peak_cap_kb();
long long best = mr_reader_ui_decode_peak_kb(p, 8, 480, 720);
INFO("jpeg 1000x1500 step=8 model=%lldkB cap=%lldkB", best, cap);
CHECK(best <= cap, "1.5MP jpeg fits the 3MB cap at some step (%lldkB > %lldkB)", best, cap);
/* 4MP(手机照片档)在 3MB 预算下不可解 → 必须占位(诚实代价,见报告) */
snprintf(p, sizeof p, "%s/big4.jpg", jpgbook);
long long big = mr_reader_ui_decode_peak_kb(p, 8, 480, 480);
INFO("jpeg 2000x2000 step=8 model=%lldkB (cap=%lldkB -> %s)",
big, cap, big <= cap ? "decodable" : "placeholder");
CHECK(big > cap, "4MP jpeg exceeds the 3MB cap even at step=8 (as documented)");
/* 渐进式:识别 + 峰值模型包含 short 系数面(同样尺寸必须比 baseline 高) */
snprintf(p, sizeof p, "%s/prog.jpg", jpgbook);
long long prog = mr_reader_ui_decode_peak_kb(p, 2, 480, 720);
INFO("jpeg 1000x1500 progressive step=2 model=%lldkB", prog);
CHECK(prog > model, "progressive jpeg modeled higher than baseline (%lldkB vs %lldkB)",
prog, model);
} else {
printf(" SKIP jpeg assertions: Pillow unavailable (python3 -c 'import PIL')\n");
}
meow_view_page_pop();
meow_view_free(reader);
meow_view_free(lib);
meow_ui_shutdown();
rm_rf(root);
rm_rf(data);
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
g_fail ? "see failures above" : "reader memory budget checks passed");
return g_fail ? 1 : 0;
}