Files
MeowOS-SDK/tuku/test/test_reader_membudget.c
T
AskaEth 11e09af7a4 图库: 修 M27 评审 P1 门控旁路 + 三个 P2(r1)
P1-1:判定为 backend 的页不再无条件放行 + 后端中途失败不再静默回退 stb。
- meow_img_load_file_step_ex 增加 allow_stb_fallback 参数:0 = 只试后端,
  失败返回 NULL(坏图占位),绝不回落 stb。阅读器按 dec.path 传入。
- 旧行为实测:截断 12MP JPEG 18,403kB、3000×3000 24bpp 头-only BMP 36,056kB、
  非法位深 PNG 16,409kB(stb 都"成功"出图)。修复后:
  截断 JPEG 275kB→NULL、头-only BMP 908kB→NULL、非法位深 PNG 门控占位(0 分配)。

P2-1:PNG 支持矩阵补 png_depth_ok 同式(color 2/4/6 仅 8/16;color 3 无 16;
color 0 允许 1/2/4/8/16),不再出现「矩阵说支持、后端拒绝」。
P2-2:后端命中页也保留两道安全阀:MemAvailable<6MB 占位、估算 >12MB 硬上限占位。
P2-3:B8 指标改为「窗口稳定后只 reset 一次」,量整个 30 翻窗口内相对稳态基线的
最大存活增量(淘汰不再掩盖峰值),并打印稳态/驻留/增量。
P2-4:page_source.h 的估算精度口径限定为「后端命中/典型页」。

回归:
- B7c 新增截断 12MP JPEG / 24bpp 头-only BMP / 非法位深 PNG,断言 backend-only
  NULL 且峰值 ≤3MB、或 stb 门控占位;B7 典型页与 B7b 回退矩阵保持全绿。
- test_reader_many_pages 恢复 24bpp 头-only 夹具(1bpp 保留):24bpp 判 failed、
  1bpp 判 skipped;断言 failed=2 / skipped=1。
- run_tests.sh / run_webtoon_tests.sh EXIT=0;sdk/test_meow_img_stream ALL PASS;
  交叉构建(SDK + tuku)零警告。
2026-11-06 11:46:16 +08:00

830 lines
37 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;M27 接真缩解后端;需要 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 M27:6 类典型页(2000×3000 baseline/渐进/restart、1600×2400 PNG/BMP、4MP、12MP)
* 全部走后端、不占位;估算 ≥ 实测且 ≤1.5×;输出尺寸与显示框一致
* B7b 回退路径:CMYK / Adam7 PNG / 1bpp BMP 走 stb,按旧门控占位
* B7c P1-1 旁路回归:截断 12MP JPEG / 3000×3000 24bpp 头-only BMP(判定 backend,
* 后端失败必须 NULL、峰值 ≤3MB)/ 非法位深 PNG(矩阵拒绝→stb 占位)
* B8 M27:连翻 30 页(6 类循环)不掉页、不占位、任何时刻存活增量 ≤3MB
* B9 M27:CMYK 回退页集成 —— 占位可重试且不崩
*
* 夹具:BMP/PNG 小夹具由本文件用 C 写;JPEG/PNG/BMP 大夹具由 python3+Pillow 生成
* (test/make_mem_samples.py),缺 PIL 时跳过相关断言(打印 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 "logic/reader/page_source.h"
#include "stb_image.h" /* stbi_load_scaled:两条路径输出尺寸一致性对照 */
#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;
}
/* 1bpp BMP(后端不支持 → 回退 stb):用于「回退路径按旧门控占位」的集成断言。 */
static int write_bmp1(const char *path, int w, int h)
{
int stride = ((w + 31) / 32) * 4;
int imgsz = stride * h;
unsigned char fh[14] = { 0 }, ih[40] = { 0 };
unsigned char pal[8] = { 0, 0, 0, 0, 255, 255, 255, 0 };
fh[0] = 'B'; fh[1] = 'M';
put32(fh + 2, (unsigned)(14 + 40 + 8 + imgsz));
put32(fh + 10, (unsigned)(14 + 40 + 8));
put32(ih + 0, 40);
put32(ih + 4, (unsigned)w);
put32(ih + 8, (unsigned)h);
put16(ih + 12, 1);
put16(ih + 14, 1);
put32(ih + 20, (unsigned)imgsz);
put32(ih + 32, 2);
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 &&
fwrite(pal, 1, sizeof pal, f) == sizeof pal;
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 < stride; x++)
row[x] = (unsigned char)((y / 16 + x) & 1 ? 0x00 : 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 copy_file(const char *src, const char *dst)
{
FILE *in = fopen(src, "rb");
if (!in) return -1;
FILE *out = fopen(dst, "wb");
if (!out) { fclose(in); return -1; }
unsigned char buf[16384];
size_t n;
int ok = 1;
while ((n = fread(buf, 1, sizeof buf, in)) > 0)
if (fwrite(buf, 1, n, out) != n) { ok = 0; break; }
fclose(in);
if (fclose(out) != 0) ok = 0;
return ok ? 0 : -1;
}
static int have_file(const char *path)
{
struct stat st;
return stat(path, &st) == 0 && st.st_size > 0;
}
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;
static const char *const need[] = {
"page.jpg", "big4.jpg", "prog.jpg",
"base2000.jpg", "prog2000.jpg", "restart2000.jpg",
"page1600.png", "page1600.bmp", "mp4.jpg", "mp12.jpg",
"cmyk.jpg", "adam7.png", "mono1.bmp",
"trunc2000.jpg", "hdr3000.bmp", "baddepth.png",
};
for (size_t i = 0; i < sizeof need / sizeof need[0]; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/%s", dir, need[i]);
if (!have_file(p)) return 0;
}
return 1;
}
/* 与 reader_load_page 完全同路径:_step_ex → 兜底降采样 → 落框重采样。
* allow_stb 与阅读器一致:判定 backend 时传 0(后端失败即 NULL,绝不回退 stb)。
* 返回真实分配峰值(字节),并回报实际路径与最终尺寸。 */
static size_t measure_page_decode(const char *path, const mr_img_info *in, int step,
int allow_stb, int box_w, int box_h,
int *used_backend, int *ow, int *oh)
{
int dim = box_w > box_h ? box_w : box_h;
size_t base = g_live;
peak_reset();
int used = 0;
meow_img *im = meow_img_load_file_step_ex(path, dim, step, allow_stb, &used);
if (im && (meow_img_w(im) > dim || meow_img_h(im) > dim)) {
meow_img *small = meow_img_downscale(im, dim);
if (small) { meow_img_free(im); im = small; }
}
if (im && in && step > 1 && (in->w >= box_w || in->h >= box_h) &&
(meow_img_w(im) != box_w || meow_img_h(im) != box_h)) {
meow_img *fit = meow_img_resample(im, box_w, box_h);
if (fit) { meow_img_free(im); im = fit; }
}
if (used_backend) *used_backend = used;
if (ow && oh && im) { *ow = meow_img_w(im); *oh = meow_img_h(im); }
size_t peak = peak_since(base);
if (im) meow_img_free(im);
return peak;
}
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];
char mixed[MR_PATH_MAX], fallback[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);
snprintf(mixed, sizeof mixed, "%s/mixed", root);
snprintf(fallback, sizeof fallback, "%s/fallback", root);
if (mkdir_p(book) != 0 || mkdir_p(big) != 0 || mkdir_p(pngbook) != 0 ||
mkdir_p(jpgbook) != 0 || mkdir_p(mixed) != 0 || mkdir_p(fallback) != 0) {
printf("SKIP: mkdir failed\n");
return 0;
}
/* 480x640 BMP ×8:单页解码峰值 ≈ 480*640*4*2 ≈ 2.5MB,能进 3MB 预算(可连翻)。
* 长边 640 ≤ 解码长边 720 → 后端判定「小图走旧入口」→ stb,正好覆盖回退门控。 */
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 1bpp BMP(后端拒绝 → stb):stb 峰值 ≈ 7.7MB + 落框 ≈ 9MB。
* 3MB 常态预算下必须占位;MemFree 富余(放宽到 12MB)时必须能解出来。 */
{
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/huge.bmp", big);
if (write_bmp1(p, 1200, 1600) != 0) { printf("SKIP: bmp write\n"); return 0; }
}
/* 600x960 PNG(RGB):长边 960 > 720 → 走后端;覆盖 PNG 逐行路径。 */
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);
/* 连翻 30 页:6 类典型页循环(12MP / 4MP / baseline / 渐进 / PNG / BMP)。
* 每类都必须走后端且不占位——这是「修好了却用不上」的回归防线。 */
static const char *const g_mix[] = {
"base2000.jpg", "prog2000.jpg", "page1600.png",
"page1600.bmp", "mp4.jpg", "mp12.jpg",
};
const int mix_n = (int)(sizeof g_mix / sizeof g_mix[0]);
int mixed_pages = 0;
if (have_jpeg) {
for (int i = 0; i < 30; i++) {
char src[MR_PATH_MAX], dst[MR_PATH_MAX];
snprintf(src, sizeof src, "%s/%s", jpgbook, g_mix[i % mix_n]);
snprintf(dst, sizeof dst, "%s/p%02d_%s", mixed, i, g_mix[i % mix_n]);
if (copy_file(src, dst) == 0) mixed_pages++;
}
}
/* 回退路径集成:CMYK(4 分量)必须占位且不崩。 */
int have_fallback = 0;
if (have_jpeg) {
char src[MR_PATH_MAX], dst[MR_PATH_MAX];
snprintf(src, sizeof src, "%s/cmyk.jpg", jpgbook);
snprintf(dst, sizeof dst, "%s/cmyk.jpg", fallback);
if (copy_file(src, dst) == 0) have_fallback = 1;
}
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 M27:6 类典型页 —— 全部走后端、不占位、模型 ≥ 实测且 ≤1.5× ---- */
if (have_jpeg) {
static const char *const typical[] = {
"base2000.jpg", "prog2000.jpg", "restart2000.jpg",
"page1600.png", "page1600.bmp", "mp4.jpg", "mp12.jpg",
};
printf(" [B7] 典型页(fit-width 480,框按源宽高比):\n");
printf(" %-16s %-8s %10s %10s %6s\n", "page", "path", "est(kB)", "measured", "ratio");
for (size_t i = 0; i < sizeof typical / sizeof typical[0]; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/%s", jpgbook, typical[i]);
mr_img_info in;
if (!mr_probe_info(p, &in)) { CHECK(0, "probe %s", typical[i]); continue; }
int box_w = 480;
int box_h = (int)((long)in.h * box_w / in.w);
int dim = box_w > box_h ? box_w : box_h;
int step = in.codec == MR_IMG_CODEC_JPEG
? meow_img_pick_scale(in.w > in.h ? in.w : in.h, dim) : 1;
mr_decode_decision dec;
reader_estimate_peak(&in, step, box_w, box_h, -1, -1, &dec);
int used = 0, ow = 0, oh = 0;
size_t measured = measure_page_decode(p, &in, dec.step, 1, box_w, box_h,
&used, &ow, &oh) + 1;
printf(" %-16s %-8s %10lld %10zu %6.2f\n", typical[i],
dec.path == MR_DECODE_PATH_BACKEND ? "backend" : "stb",
dec.peak_bytes / 1024, measured / 1024,
(double)dec.peak_bytes / (double)measured);
CHECK(dec.path == MR_DECODE_PATH_BACKEND, "%s 走后端", typical[i]);
CHECK(dec.skip == MR_DECODE_OK, "%s 不占位", typical[i]);
CHECK(used == 1, "%s 实际路径=backend", typical[i]);
CHECK(dec.peak_bytes <= 2560LL * 1024, "%s 估算 ≤2560kB (%lldkB)",
typical[i], dec.peak_bytes / 1024);
CHECK(dec.peak_bytes >= (long long)measured, "%s 模型 ≥ 实测 (%lldkB < %zukB)",
typical[i], dec.peak_bytes / 1024, measured / 1024);
CHECK(dec.peak_bytes <= (long long)measured * 3 / 2, "%s 模型 ≤1.5× 实测",
typical[i]);
CHECK(ow == box_w && oh == box_h, "%s 输出 %dx%d == 显示框 %dx%d",
typical[i], ow, oh, box_w, box_h);
}
/* 输出尺寸口径:后端直出 == 「stbi_load_scaled(step) + 落框精修」旧路径
* (典型页必须逐像素同尺寸;源尺寸不是档位整数倍时 M23 已记录 1px 舍入口径差)。 */
for (size_t i = 0; i < sizeof typical / sizeof typical[0]; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/%s", jpgbook, typical[i]);
mr_img_info in;
if (!mr_probe_info(p, &in)) continue;
int box_w = 480;
int box_h = (int)((long)in.h * box_w / in.w);
int dim = box_w > box_h ? box_w : box_h;
int step = in.codec == MR_IMG_CODEC_JPEG
? meow_img_pick_scale(in.w > in.h ? in.w : in.h, dim) : 1;
int used = 0, ow = 0, oh = 0;
measure_page_decode(p, &in, step, 1, box_w, box_h, &used, &ow, &oh);
/* 旧路径(与 meow_img_load_file_step 的 stb 分支同式):先 stbi 缩解,再精修。 */
int sw = 0, sh = 0, sc = 0, ew = 0, eh = 0;
unsigned char *raw = stbi_load_scaled(p, &sw, &sh, &sc, 4, step);
if (raw) {
if (sw <= dim && sh <= dim) { ew = sw; eh = sh; }
else if (sw >= sh) {
ew = dim;
eh = (int)((long)sh * dim / sw);
if (eh < 1) eh = 1;
} else {
eh = dim;
ew = (int)((long)sw * dim / sh);
if (ew < 1) ew = 1;
}
}
if (raw) stbi_image_free(raw);
CHECK(used == 1 && ow == ew && oh == eh,
"%s 两条路径输出尺寸一致(后端 %dx%d / stb %dx%d)",
typical[i], ow, oh, ew, eh);
}
/* 回退路径:后端拒绝的 3 类必须走 stb,且按旧门控占位(不崩、不误放行)。 */
static const char *const fallback[] = { "cmyk.jpg", "adam7.png", "mono1.bmp" };
printf(" [B7b] 回退路径(必须 stb + 占位):\n");
for (size_t i = 0; i < sizeof fallback / sizeof fallback[0]; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/%s", jpgbook, fallback[i]);
mr_img_info in;
if (!mr_probe_info(p, &in)) { CHECK(0, "probe %s", fallback[i]); continue; }
int box_w = 480;
int box_h = (int)((long)in.h * box_w / in.w);
int dim = box_w > box_h ? box_w : box_h;
int step = in.codec == MR_IMG_CODEC_JPEG
? meow_img_pick_scale(in.w > in.h ? in.w : in.h, dim) : 1;
mr_decode_decision dec;
reader_estimate_peak(&in, step, box_w, box_h, -1, -1, &dec);
int used = 1, ow = 0, oh = 0;
size_t measured = measure_page_decode(p, &in, dec.step, 1, box_w, box_h,
&used, &ow, &oh) + 1;
printf(" %-16s %-8s %10lld %10zu %6.2f\n", fallback[i],
dec.path == MR_DECODE_PATH_BACKEND ? "backend" : "stb",
dec.peak_bytes / 1024, measured / 1024,
(double)dec.peak_bytes / (double)measured);
CHECK(dec.path == MR_DECODE_PATH_STB, "%s 回退 stb", fallback[i]);
CHECK(dec.skip == MR_DECODE_SKIP_PEAK, "%s 按旧门控占位", fallback[i]);
CHECK(used == 0, "%s 实际路径=stb", fallback[i]);
CHECK(dec.peak_bytes >= (long long)measured, "%s stb 模型 ≥ 实测", fallback[i]);
CHECK(dec.peak_bytes <= (long long)measured * 3 / 2, "%s stb 模型 ≤1.5× 实测",
fallback[i]);
if (ow > 0 && oh > 0) {} /* 尺寸仅记录 */
}
/* ---- B7c(P1-1 回归):头部判支持、解码中途失败的坏文件 ----
* 旧实现「后端失败 → 静默回退 stb」实测放出 16–36MB;现在必须:
* - 判定 backend 的(截断 JPEG / 头-only 24bpp BMP)→ backend-only 解码返回 NULL,
* 峰值只含失败前的小分配(≤3MB),绝不 stb 整幅;
* - 判定 stb 的(非法位深 PNG,矩阵已镜像 png_depth_ok)→ skip=peak,不分配。 */
static const char *const badmeta[] = { "trunc2000.jpg", "hdr3000.bmp", "baddepth.png" };
printf(" [B7c] 坏文件(P1-1 旁路回归):\n");
for (size_t i = 0; i < sizeof badmeta / sizeof badmeta[0]; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/%s", jpgbook, badmeta[i]);
mr_img_info in;
if (!mr_probe_info(p, &in)) { CHECK(0, "probe %s", badmeta[i]); continue; }
int box_w = 480;
int box_h = (int)((long)in.h * box_w / in.w);
int dim = box_w > box_h ? box_w : box_h;
int step = in.codec == MR_IMG_CODEC_JPEG
? meow_img_pick_scale(in.w > in.h ? in.w : in.h, dim) : 1;
mr_decode_decision dec;
reader_estimate_peak(&in, step, box_w, box_h, -1, -1, &dec);
if (dec.path == MR_DECODE_PATH_BACKEND) {
int used = 1, ow = 0, oh = 0;
size_t measured = measure_page_decode(p, &in, dec.step, 0, box_w, box_h,
&used, &ow, &oh) + 1;
printf(" %-16s backend-only measured=%zuKB (backend 失败→NULL)\n",
badmeta[i], measured / 1024);
CHECK(used == 0, "%s 不走 stb(used=0)", badmeta[i]);
CHECK(ow == 0 && oh == 0, "%s 返回 NULL(坏图占位)", badmeta[i]);
CHECK(measured <= 3 * 1024 * 1024,
"%s backend-only 峰值 ≤3MB (%zuKB)", badmeta[i], measured / 1024);
} else {
printf(" %-16s stb skip=%d est=%lldkB(门控占位,不分配)\n",
badmeta[i], dec.skip, dec.peak_bytes / 1024);
CHECK(dec.skip != MR_DECODE_OK, "%s 门控必须占位 (skip=%d)",
badmeta[i], dec.skip);
CHECK(dec.peak_bytes > 3LL * 1024 * 1024,
"%s stb 模型判超预算 (%lldkB)", badmeta[i], dec.peak_bytes / 1024);
}
}
} else {
printf(" SKIP jpeg assertions: Pillow unavailable (python3 -c 'import PIL')\n");
}
/* ---- B8 M27:连翻 30 页(6 类典型页循环)——不掉页、不占位、峰值 ≤3MB ---- */
if (have_jpeg && mixed_pages == 30) {
mr_reader_ui_test_set_mem(4096, 16000);
mr_reader_open_dir(mixed);
pump_ms(1200);
CHECK(mr_reader_ui_count() == 30, "mixed book pages=%d", mr_reader_ui_count());
int w = meow_ui_width();
int advanced = 0, skipped_max = 0;
size_t live_steady = g_live;
int d0 = mr_reader_ui_decode_count();
/* P2-3:只在窗口稳定后 reset 一次,之后不再 reset——这样淘汰旧图不会掩盖
* 「分配新图 + 解码」的真实峰值;指标 = 整个 30 翻窗口内相对稳态基线的最大存活增量。
* (旧实现每翻页 reset 一次,淘汰把基线拉低,报告里的 536KB 是伪值。) */
size_t base = peak_reset();
for (int i = 0; i < 30; i++) {
int before = mr_reader_ui_page();
tap(i % 2 == 0 ? 40 : w - 40, 300);
pump_ms(220);
if (mr_reader_ui_page() != before) advanced++;
if (mr_reader_ui_skipped_pages() > skipped_max)
skipped_max = mr_reader_ui_skipped_pages();
}
size_t transient = g_peak > base ? g_peak - base : 0;
INFO("30 mixed flips: advanced=%d skipped=%d decodes=%d steady=%zuKB "
"resident=%ldKB max_live_delta=%zuKB",
advanced, skipped_max, mr_reader_ui_decode_count() - d0,
live_steady / 1024, mr_reader_ui_resident_kb(), transient / 1024);
CHECK(advanced == 30, "30 页全部翻到 (%d/30)", advanced);
CHECK(skipped_max == 0, "典型页连翻无占位 (max=%d)", skipped_max);
CHECK(mr_reader_ui_failed_pages() == 0, "无永久失败页 (%d)",
mr_reader_ui_failed_pages());
CHECK(transient <= 3 * 1024 * 1024,
"30 页连翻任何时刻存活增量 ≤3MB (%zuKB)", transient / 1024);
meow_view_page_pop();
pump_ms(200);
}
/* ---- B9 M27:回退路径集成 —— CMYK 页占位(可重试)且不崩 ---- */
if (have_fallback) {
mr_reader_ui_test_set_mem(4096, 16000);
mr_reader_open_dir(fallback);
pump_ms(900);
CHECK(mr_reader_ui_count() == 1, "fallback book pages=%d", mr_reader_ui_count());
CHECK(mr_reader_ui_skipped_pages() == 1, "CMYK 页按旧门控占位 (skipped=%d)",
mr_reader_ui_skipped_pages());
CHECK(mr_reader_ui_failed_pages() == 0, "CMYK 页不是永久失败 (%d)",
mr_reader_ui_failed_pages());
CHECK(mr_reader_ui_resident_kb() == 0, "CMYK 页未驻留 (resident=%ldkB)",
mr_reader_ui_resident_kb());
meow_view_page_pop();
pump_ms(200);
}
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;
}