/* 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 #include #include #include #include #include #include /* 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; /* 前缀字节数**实测**(zip 层 mz_read_prefix 的写出长度),不是按 4KB 硬算: * 否则「前缀总字节 < 旧 1/4」只要页 >16KB 就恒真,抓不到前缀读变多。 * 注意 deflate 条目的 inflate 窗口在 libz 内部(动态库,--wrap 看不到), * 所以下面 new_peak 是「本进程侧」的堆峰值,不是 zlib 窗口。 */ unsigned char probe[4096]; size_t probe_len = 0; int probe_rc = e0 ? mz_read_prefix((mz_archive *)zh.u.zip.arch, e0, probe, sizeof probe, &probe_len) : MZ_ERR_ARG; long long bytes_old = 500LL * (long long)(e0 ? e0->uncomp_size : 0); long long bytes_new = 500LL * (long long)probe_len; INFO("CBZ 500 页[%s]:旧=整页解压 %lldkB/页·总%lldkB 峰值%zuKB 耗时%llums;" "新=前缀 %lldB/页·总%lldkB 峰值%zuB 耗时%llums(zlib 窗口在库内、不计入峰值)", deflate ? "deflate" : "stored", e0 ? (long long)e0->uncomp_size / 1024 : 0LL, bytes_old / 1024, old_page_peak / 1024, (unsigned long long)ms_old, (long long)probe_len, 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(probe_rc == MZ_OK && probe_len == 4096, "[%s] mz_read_prefix 实测只读 4KB(rc=%d, %zuB/页)", deflate ? "deflate" : "stored", probe_rc, probe_len); CHECK(bytes_new * 4 < bytes_old, "[%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; }