Files
MeowOS-SDK/tuku/test/test_reader_membudget.c
T
AskaEth 7036020005 图库: 阅读器页槽改 RGB565 常驻,页图内存减半(M32)
- 页槽从 RGBA 改成「已落框的 RGB565 位图」(2B/px):真机 480×853 页 1599KB → 800KB,
  两页常驻 3.2MB → 1.6MB。解码仍在 worker 里产出 RGBA,当场转 565 并释放 RGBA。
- SDK 新增 565 原语(meow_ui_draw.h / meow_ui_gfx.c):meow_gfx_blit_rgb565 命中页每行
  一次 memcpy(单次拷贝、无每帧重采样),缩放兜底走 565 最近邻。量化与 back_buf 用同一个
  rgb565(),565 屏上落下的像素与旧 RGBA 路径逐像素一致(不引入色偏/新增色带)。
- 预算按 565 重算:单页像素上限沿用旧 3MB@RGBA 的同几何口径(=1.5MB@565),
  MR_RESIDENT_CAP = 3 槽 + 1 张在途 = 3.2MB;reader_estimate_peak 新增 convert_bytes
  (落槽暂存 2B/px)与 peak_bytes 一起参与门控,避免实测超预算。
- 结果队列(已解码、未落槽)字节计入总预算(pending_bytes),不再「偷吃」最多 2 页。
- CBZ meta() 改读条目 4KB 前缀(mz_read_prefix;deflate 解到 4KB 即停,输入缓冲走栈),
  500 页排版不再触发 500 次全量解压。
- 测试:B3/B8 的 565 常驻与在途预算断言、CBZ 500 页 stored/deflate 前后对比、
  P12 565 与 RGBA 路径的逐像素颜色对比(量化误差/RMSE/灰阶梯)。
2026-11-06 18:43:11 +08:00

1113 lines
52 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 "logic/zip/zip.h" /* M32:CBZ 排版读数的对照(mz_read_alloc = 旧路径) */
#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>
#include <zlib.h> /* M32:CBZ 夹具的 deflate 条目 */
/* ------------------------------------------------------------------ */
/* 分配打点:--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;
}
/* 最小 CBZ 写入器:把同一个源文件写成 count 个 pNNN.bmp 条目。
* deflate=0:stored(未压缩,用来量「整页解压」的分配峰值);deflate=1:method 8(真实
* CBZ 的形态,用来量前缀读省下的 inflate CPU/字节)。 */
static int write_zip_cbz(const char *zip_path, const char *src, int count, int deflate_it)
{
FILE *in = fopen(src, "rb");
if (!in) return -1;
if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; }
long sz = ftell(in);
if (sz <= 0 || fseek(in, 0, SEEK_SET) != 0) { fclose(in); return -1; }
unsigned char *data = (unsigned char *)malloc((size_t)sz);
if (!data || fread(data, 1, (size_t)sz, in) != (size_t)sz) {
free(data);
fclose(in);
return -1;
}
fclose(in);
unsigned crc = crc32_buf(data, (size_t)sz);
/* 可选 deflate(raw,-15 窗口,与 zip 一致)。 */
unsigned char *packed = NULL;
size_t packed_len = (size_t)sz;
if (deflate_it) {
z_stream zs;
memset(&zs, 0, sizeof zs);
if (deflateInit2(&zs, 1, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
free(data);
return -1;
}
size_t cap = (size_t)deflateBound(&zs, (uLong)sz);
packed = (unsigned char *)malloc(cap);
if (!packed) { deflateEnd(&zs); free(data); return -1; }
zs.next_in = data;
zs.avail_in = (uInt)sz;
zs.next_out = packed;
zs.avail_out = (uInt)cap;
int zr = deflate(&zs, Z_FINISH);
packed_len = cap - zs.avail_out;
deflateEnd(&zs);
if (zr != Z_STREAM_END) { free(packed); free(data); return -1; }
}
const unsigned char *body = deflate_it ? packed : data;
unsigned body_len = (unsigned)packed_len;
unsigned method = deflate_it ? 8u : 0u;
FILE *f = fopen(zip_path, "wb");
if (!f) { free(packed); free(data); return -1; }
unsigned *offs = (unsigned *)calloc((size_t)count, sizeof *offs);
if (!offs) { free(packed); free(data); fclose(f); return -1; }
int ok = 1;
unsigned off = 0;
for (int i = 0; i < count && ok; i++) {
char name[32];
int nl = snprintf(name, sizeof name, "p%03d.bmp", i);
unsigned char lh[30] = { 0 };
put32(lh, 0x04034b50u);
put16(lh + 4, 20); /* version needed */
put16(lh + 8, (unsigned short)method); /* stored / deflate */
put32(lh + 14, crc);
put32(lh + 18, body_len); /* comp size */
put32(lh + 22, (unsigned)sz); /* uncomp size */
put16(lh + 26, (unsigned short)nl);
offs[i] = off;
if (fwrite(lh, 1, sizeof lh, f) != sizeof lh) ok = 0;
if (ok && fwrite(name, 1, (size_t)nl, f) != (size_t)nl) ok = 0;
if (ok && fwrite(body, 1, body_len, f) != body_len) ok = 0;
off += (unsigned)sizeof lh + (unsigned)nl + body_len;
}
unsigned cd_off = off;
for (int i = 0; i < count && ok; i++) {
char name[32];
int nl = snprintf(name, sizeof name, "p%03d.bmp", i);
unsigned char cd[46] = { 0 };
put32(cd, 0x02014b50u);
put16(cd + 4, 20); /* version made by */
put16(cd + 6, 20); /* version needed */
put16(cd + 10, (unsigned short)method);
put32(cd + 16, crc);
put32(cd + 20, body_len);
put32(cd + 24, (unsigned)sz);
put16(cd + 28, (unsigned short)nl);
put32(cd + 42, offs[i]);
if (fwrite(cd, 1, sizeof cd, f) != sizeof cd) ok = 0;
if (ok && fwrite(name, 1, (size_t)nl, f) != (size_t)nl) ok = 0;
off += (unsigned)sizeof cd + (unsigned)nl;
}
unsigned char eocd[22] = { 0 };
put32(eocd, 0x06054b50u);
put16(eocd + 8, (unsigned short)count);
put16(eocd + 10, (unsigned short)count);
put32(eocd + 12, off - cd_off);
put32(eocd + 16, cd_off);
if (ok && fwrite(eocd, 1, sizeof eocd, f) != sizeof eocd) ok = 0;
free(offs);
free(packed);
free(data);
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;
}
/* M32:与阅读器 worker 完全同路径的「一次取页」——解码 → 落框 → 转 RGB565 → 释放 RGBA。
* 返回这段操作的真实分配峰值,用来校验「解码峰值 peak_bytes + 落槽暂存 convert_bytes」
* 这一对预算口径(转换期间 RGBA 与 565 同时存活,正是 convert_bytes 建模的那笔内存)。 */
static size_t measure_page_565(const char *path, const mr_img_info *in, int step,
int allow_stb, int box_w, int box_h, 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 (im) {
int w = meow_img_w(im), h = meow_img_h(im);
uint16_t *px = (uint16_t *)malloc((size_t)w * h * sizeof *px);
if (px) {
meow_gfx_rgba_to_rgb565(meow_img_pixels(im), px, w * h);
if (ow && oh) { *ow = w; *oh = h; }
free(px);
}
meow_img_free(im);
}
return peak_since(base);
}
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)。
* 每类都必须走后端且不占位——这是「修好了却用不上」的回归防线。
* M32:夹具 31 页,保证 30 次顺序前翻不触发册间衔接(末页再前翻会开相邻册)。 */
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]);
const int mixed_goal = 31;
int mixed_pages = 0;
if (have_jpeg) {
for (int i = 0; i < mixed_goal; 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);
long long conv = mr_reader_ui_decode_convert_kb(p, 1, 480, 768);
size_t measured_565 = measure_page_565(p, NULL, 1, 1, 480, 768, NULL, NULL) / 1024 + 1;
INFO("png 600x960: model=%lldkB measured=%zukB conv=%lldkB measured565=%zukB decoded=%d",
model, measured_kb, conv, measured_565, 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);
/* M32:565 落槽暂存 = 2×tw×th(480×768 → 720KB),且「解码+落槽」总量仍是上界。 */
CHECK(conv == 480 * 768 * 2 / 1024, "565 落槽暂存 = 2×w×h (%lldkB)", conv);
CHECK(model + conv >= (long long)measured_565,
"解码+落槽 模型 ≥ 实测 (%lld+%lld < %zukB)", model, conv, measured_565);
CHECK(model + conv <= (long long)measured_565 * 2,
"解码+落槽 模型不离谱保守 (%lld+%lld vs %zukB)", model, conv, measured_565);
}
/* ---- 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();
/* 预热两翻(M32):首次进页窗口只有「焦点 + 下一页」(2 页),稳定态是焦点 ±1(3 页);
* 565 下第 3 页只要 600KB,会正常驻留下来。不预热就会把「多缓存一张预取页」
* 误判成内存漂移。 */
tap(40, 300);
pump_ms(200);
tap(w - 40, 300);
pump_ms(200);
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();
long total_resident_max = 0;
/* M32 预算自洽(按 565 重算,M31 两档已回滚 → 单档):常驻上限 = 3 槽 + 1 张在途
* = 4 × 800KB(真机 480×853 页 @565);单页像素上限沿用 RGBA 3MB 的同几何口径
* (显示清晰度不变,只把每像素 4B 变 2B);单次取页仍受 page_source 的 3MB 常态
* 预算 / 12MB 硬上限约束(reader_estimate_peak 的 peak_bytes + convert_bytes)。 */
CHECK(resident_cap == 4 * 800, "常驻上限按 565 重算(%lldKB = 4×800KB)", resident_cap);
CHECK(mr_reader_ui_page_px_cap() == 786432,
"单页像素上限沿用 RGBA 3MB 的同几何口径(%ldpx)", mr_reader_ui_page_px_cap());
CHECK(cap == 3 * 1024, "单次解码常态预算 3MB(%lldKB)", cap);
CHECK(resident_cap + cap <= 12 * 1024,
"常驻 + 一次取页 ≤ 12MB 包络(%lld + %lld)", resident_cap, cap);
/* 基线 = 首屏稳定后的存活字节;之后任何分配都不该再超过 基线+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();
long pend = mr_reader_ui_pending_kb();
if (kb + pend > resident_cap) {
g_fail++;
printf(" FAIL flip %d: resident %ldkB + pending %ldkB > cap %lldkB\n",
i, kb, pend, resident_cap);
break;
}
if (kb > total_resident_max) total_resident_max = kb;
}
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 "
"resident_max=%ldKB decodes=%d advanced=%d",
live_steady / 1024, transient / 1024, live_end / 1024, resident_cap,
total_resident_max, 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);
/* M32:槽内页图是 565(2B/px)。480×640 页 = 600KB(RGBA 时代 1200KB)。 */
CHECK(mr_reader_ui_slot_kb(mr_reader_ui_page()) == 600,
"单页常驻 = w×h×2 = 600KB(实测 %ldKB)",
mr_reader_ui_slot_kb(mr_reader_ui_page()));
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);
}
/* ---- B12 M32:500 页 CBZ 排版只读条目**前缀**(4KB),不再整页解压 ----
* 改前:zip_ops_meta 走 mz_read_alloc(整条目解压)→ 500 页 = 500 次全量解压;
* 改后:mz_read_prefix(stored 只读前 4KB / deflate 解到 4KB 即停)。
* 两个夹具:stored 量「每页分配峰值」(旧 27KB/页 → 新 0);deflate 是真实 CBZ 形态,
* 量前缀读省下的 inflate 字节与耗时。 */
{
char zsrc[MR_PATH_MAX];
snprintf(zsrc, sizeof zsrc, "%s/zip_src.bmp", root);
int zok = write_bmp24(zsrc, 96, 96, 7) == 0; /* 96×96 24bpp = 27648B 像素数据 */
CHECK(zok, "CBZ 夹具源图写入");
for (int mode = 0; zok && mode < 2; mode++) {
int deflate = mode; /* 0=stored, 1=deflate */
char zcbz[MR_PATH_MAX];
snprintf(zcbz, sizeof zcbz, "%s/layout500_%s.cbz", root,
deflate ? "deflate" : "stored");
if (write_zip_cbz(zcbz, zsrc, 500, deflate) != 0) {
CHECK(0, "500 页 CBZ 夹具写入(%s)", deflate ? "deflate" : "stored");
continue;
}
mr_book_handle zh;
mr_book zb;
int zn = mr_book_open_auto(&zb, &zh, zcbz, 720, NULL, NULL, NULL);
CHECK(zn == 500, "CBZ 页数=%d(%s)", zn, deflate ? "deflate" : "stored");
if (zn != 500) { mr_book_close_auto(&zb, &zh); continue; }
/* 旧路径读数:整条目读(= 改前 zip_ops_meta 的做法),量每页分配峰值与总耗时。 */
size_t old_page_peak = 0;
uint64_t t_old = meow_ui_time_ms();
const mz_entry *e0 = mz_entry_at((mz_archive *)zh.u.zip.arch, 0);
for (int i = 0; i < 500 && e0; i++) {
void *buf = NULL;
size_t len = 0;
size_t b0 = g_live;
peak_reset();
if (mz_read_alloc((mz_archive *)zh.u.zip.arch, e0, &buf, &len) == MZ_OK) {
size_t pk = peak_since(b0);
if (pk > old_page_peak) old_page_peak = pk;
}
free(buf);
}
uint64_t ms_old = (uint64_t)meow_ui_time_ms() - t_old;
/* 新路径:与条漫排版同一条调用(mr_book_meta 逐页)。
* 第一页会一次性分配 meta 缓存(2×500×4B = 4000B),基线放在它之后,
* 这样量到的就是「每页前缀读」自己的分配。 */
int pw0 = 0, ph0 = 0;
int first_ok = mr_book_meta(&zb, 0, &pw0, &ph0);
uint64_t t_new = meow_ui_time_ms();
size_t base = peak_reset();
int meta_ok = first_ok, dim_ok = (pw0 == 96 && ph0 == 96);
for (int i = 1; i < 500; i++) {
int pw = 0, ph = 0;
if (!mr_book_meta(&zb, i, &pw, &ph)) meta_ok = 0;
else if (pw != 96 || ph != 96) dim_ok = 0;
}
size_t new_peak = peak_since(base);
uint64_t ms_new = (uint64_t)meow_ui_time_ms() - t_new;
long long bytes_old = 500LL * (long long)(e0 ? e0->uncomp_size : 0);
long long bytes_new = 500LL * 4096; /* 上限:每页最多读 4KB */
INFO("CBZ 500 页[%s]:旧=整页解压 %lldkB/页·总%lldkB 峰值%zuKB 耗时%llums;"
"新=前缀 %lldkB/页·总%lldkB 峰值%zuB 耗时%llums",
deflate ? "deflate" : "stored",
e0 ? (long long)e0->uncomp_size / 1024 : 0LL, bytes_old / 1024,
old_page_peak / 1024, (unsigned long long)ms_old,
4LL, bytes_new / 1024, new_peak, (unsigned long long)ms_new);
CHECK(meta_ok, "[%s] 500 页 meta 全部探测成功", deflate ? "deflate" : "stored");
CHECK(dim_ok, "[%s] 500 页尺寸都取到 96x96", deflate ? "deflate" : "stored");
CHECK(new_peak <= 4096,
"[%s] 排版期(首帧之后)每页堆峰值 ≤4KB(实测 %zuB)",
deflate ? "deflate" : "stored", new_peak);
CHECK(old_page_peak >= 16 * 1024,
"[%s] 对照:旧路径每页分配 ≥16KB(实测 %zuB)",
deflate ? "deflate" : "stored", old_page_peak);
CHECK(bytes_new < bytes_old / 4,
"[%s] 前缀读总字节 < 旧路径 1/4(%lldkB vs %lldkB)",
deflate ? "deflate" : "stored", bytes_new / 1024, bytes_old / 1024);
mr_book_close_auto(&zb, &zh);
}
}
/* ---- 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);
/* M32:565 落槽暂存(2B/px)与解码峰值分开建模,但同属一次取页;
* 两者之和必须是「解码 → 转 565」真实峰值的上界(且不保守得离谱)。 */
size_t measured565 = measure_page_565(p, &in, dec.step, 1, box_w, box_h,
NULL, NULL) + 1;
CHECK(dec.convert_bytes == (long long)dec.tw * dec.th * 2,
"%s 落槽暂存 = 2×tw×th (%lldB, %dx%d)", typical[i], dec.convert_bytes,
dec.tw, dec.th);
CHECK(dec.peak_bytes + dec.convert_bytes >= (long long)measured565,
"%s 解码+落槽 模型 ≥ 实测 (%lld+%lldkB < %zukB)", typical[i],
dec.peak_bytes / 1024, dec.convert_bytes / 1024, measured565 / 1024);
CHECK(dec.peak_bytes + dec.convert_bytes <= (long long)measured565 * 3 / 2,
"%s 解码+落槽 模型 ≤1.5× 实测 (%lldkB vs %zukB)", typical[i],
(dec.peak_bytes + dec.convert_bytes) / 1024, measured565 / 1024);
}
/* 输出尺寸口径:后端直出 == 「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/M32:顺序前翻 30 页(31 页册,6 类典型页循环)——不掉页、不占位、
* 峰值不增长、预取命中率、解码次数有界,且常驻按 565 口径有界 ---- */
if (have_jpeg && mixed_pages == mixed_goal) {
mr_reader_ui_test_set_mem(4096, 16000);
mr_reader_open_dir(mixed);
pump_ms(1200);
CHECK(mr_reader_ui_count() == mixed_goal, "mixed book pages=%d", mr_reader_ui_count());
int advanced = 0, skipped_max = 0;
size_t live_steady = g_live;
int d0 = mr_reader_ui_decode_count();
int hits0 = mr_reader_ui_flip_hits(), miss0 = mr_reader_ui_flip_misses();
/* 只在窗口稳定后 reset 一次,之后不再 reset——这样淘汰旧图不会掩盖
* 「分配新图 + 解码」的真实峰值;指标 = 整个窗口内相对稳态基线的最大存活增量。 */
size_t base = peak_reset();
for (int i = 0; i < 30; i++) {
int before = mr_reader_ui_page();
tap(40, 300); /* RTL:左 1/3 = 下一页(顺序前翻) */
pump_ms(260);
if (mr_reader_ui_page() == before + 1) 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;
int hits = mr_reader_ui_flip_hits() - hits0;
int miss = mr_reader_ui_flip_misses() - miss0;
int decodes = mr_reader_ui_decode_count() - d0;
INFO("30 sequential flips: advanced=%d last=%d hits=%d miss=%d skipped=%d decodes=%d "
"steady=%zuKB resident=%ldKB pending=%ldKB max_live_delta=%zuKB",
advanced, mr_reader_ui_page(), hits, miss, skipped_max, decodes,
live_steady / 1024, mr_reader_ui_resident_kb(), mr_reader_ui_pending_kb(),
transient / 1024);
CHECK(advanced == 30 && mr_reader_ui_page() == 30,
"30 次顺序前翻逐页推进(advanced=%d last=%d)", advanced, mr_reader_ui_page());
CHECK(skipped_max == 0, "典型页连翻无占位 (max=%d)", skipped_max);
CHECK(mr_reader_ui_failed_pages() == 0, "无永久失败页 (%d)",
mr_reader_ui_failed_pages());
/* 顺序阅读的预取命中率:目标页已在槽(预取生效)≥90%。 */
CHECK(hits >= 27, "顺序翻页预取命中率 ≥90%%(hit=%d miss=%d)", hits, miss);
/* 单档(M31 已回滚):每页最多 1 次解码 + 开册/补邻居余量。 */
CHECK(decodes <= 30 + 15, "30 次翻页解码次数 ≤1/页+余量 (decodes=%d)", decodes);
CHECK(mr_reader_ui_resident_kb() + mr_reader_ui_pending_kb() <=
mr_reader_ui_resident_cap_kb(),
"常驻 + 在途 ≤ 上限 (%ldKB + %ldKB ≤ %ldKB)",
mr_reader_ui_resident_kb(), mr_reader_ui_pending_kb(),
mr_reader_ui_resident_cap_kb());
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;
}