P1-1 内存账没算全:高档解码时低档还占着槽,但门控/预留只按新图算。 - 高档请求携带发起时的常驻快照(page_req.resident),解码前按 「常驻 + reader_estimate_peak()」算这次操作的总峰值,上限 MR_TIER_TOTAL_CAP (12MB,与 page_source 的绝对上限同值;常态 3MB 只约束 stb 回退路径, 后端页本来就超 3MB 且已被 M27 放行),超了跳过高档、保留低档(skip=coexist)。 - 峰值本身靠调度压回基线:分页模式焦点页还是低档时不预取邻居(预取来会被 换清晰腾掉),换清晰前 slots_reserve 腾掉非可见页,高档只与焦点低档共存。 B8(--wrap malloc,两实例并行):max_live_delta 3250KB→550KB, 绝对峰值 5279KB→3932KB(基线 3929KB)。 P1-2 换清晰挤掉预取:换清晰后不重跑窗口,邻居洞留到下次翻页(0/8 命中、4 次/页)。 - 新增每页 in-flight 去重(g_req_inflight):请求被 worker 取走后不再被窗口刷新重排, 这是重复解码/挤掉邻居的根因之一。 - 高档落位后按最近一次翻页方向补回邻居(sharpen_refill);反方向不补(3MB 常驻 上限下放不下,必然再被挤掉)。 - 真机几何探针(480×720,1280×2275 JPEG,8 次 700ms/页):0/8→8/8 命中, 4→1.5 次解码/页。B8 顺序 100 页:100/100 命中、149 次解码(1.49/页)。 P2-1:B8 改成 101 页册顺序前翻 100 次(每页都是不同的下一页),4MP/12MP 夹具 真正成为焦点页;断言最后一页=100、命中率 ≥90%、解码 ≤2/页+余量。 P2-2:B10 去掉恒真的 slot_kb 比较,改断言「总常驻不增加」+「高档操作增量 ≤3MB」+ 「两档共存绝对峰值有界」;补双击放大后换清晰不重置缩放/平移、条漫滚动位置不变。 P2-3:B10 整串生命周期解码次数 ≤12(实测 7);B8 解码次数上限。 夹具用硬链接(同文件系统),避免多页册真拷贝几百 MB。 顺带:MR_SHARPEN_MS 200→150(任务允许 150–300ms,给「换清晰→补邻居」留翻页余量); 新增宿主观察口 flip_hits/misses、zoom/pan/woff(只读)。
1044 lines
51 KiB
C
1044 lines
51 KiB
C
/* 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/M31:顺序连翻 100 页(101 页册,6 类循环,每页都是不同的下一页)不掉页、
|
||
* 不占位、存活增量 ≤3MB、绝对峰值有上界、预取命中 ≥90%、解码次数 ≤2/页+余量
|
||
* (M31 r2:修「每翻一页解 4 次」与高峰值,评审 P1-1/P1-2/P2-1/P2-3)
|
||
* B10 M31:两级清晰度生命周期(JPEG 大页):低档先落位(open 不阻塞)→ 停稳后高档落
|
||
* 同一槽(总常驻不增加、槽位不空、本次操作增量 ≤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;
|
||
}
|
||
|
||
/* 同一文件系统优先硬链接:多页册只是「同样的夹具当很多页」,101 页 × 12MP/BMP/PNG 真拷贝
|
||
* 要写几百 MB,跑一次测试 I/O 比解码还久(评审复跑成本)。读侧只按路径打开,硬链接完全等价。 */
|
||
static int link_or_copy(const char *src, const char *dst)
|
||
{
|
||
if (link(src, dst) == 0) return 0;
|
||
return copy_file(src, dst);
|
||
}
|
||
|
||
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);
|
||
|
||
/* 顺序连翻 100 页:6 类典型页循环(12MP / 4MP / baseline / 渐进 / PNG / BMP)。
|
||
* 每类都必须走后端且不占位——这是「修好了却用不上」的回归防线。
|
||
* M31 r2(评审 P2-1):101 页顺序遍历(不是 30 页来回跳)——每次翻页都落到**不同的
|
||
* 下一页**,4MP/12MP 夹具都会真正成为焦点页并被解码/换清晰。 */
|
||
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 = 101; /* 100 次前翻 → 第 101 页(不回卷、不触发册间衔接) */
|
||
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%03d_%s", mixed, i, g_mix[i % mix_n]);
|
||
if (link_or_copy(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);
|
||
/* M31:小页(480×640=0.3MP)不分档——低档只会多一次解码 + 落框,纯开销。 */
|
||
CHECK(mr_reader_ui_low_decodes() == 0,
|
||
"small pages stay single-tier (%d low decodes)", mr_reader_ui_low_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/M31:顺序连翻 100 页(101 页册,6 类典型页循环)——不掉页、不占位、
|
||
* 峰值不增长、预取命中、解码次数有界 ----
|
||
* M31 起这些 1.3–12MP 的页走两级清晰度(低档先上屏、停稳换高档)。评审 P1:
|
||
* - 顺序前翻(每页都落到不同的下一页),4MP/12MP 夹具真正成为焦点页;
|
||
* - 断言语义翻页命中率(目标页已在槽)与解码次数上限,防止「每翻一页解 4 次」;
|
||
* - 存活增量仍按单页解码峰值封顶(两档共存时换清晰前会腾掉非可见页,见 reader.c)。 */
|
||
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 low0 = mr_reader_ui_low_decodes(), high0 = mr_reader_ui_high_decodes();
|
||
int hits0 = mr_reader_ui_flip_hits(), miss0 = mr_reader_ui_flip_misses();
|
||
/* P2-3:只在窗口稳定后 reset 一次,之后不再 reset——这样淘汰旧图不会掩盖
|
||
* 「分配新图 + 解码」的真实峰值;指标 = 整个 100 翻窗口内相对稳态基线的最大存活增量。
|
||
* (旧实现每翻页 reset 一次,淘汰把基线拉低,报告里的 536KB 是伪值。) */
|
||
size_t base = peak_reset();
|
||
for (int i = 0; i < 100; i++) {
|
||
int before = mr_reader_ui_page();
|
||
tap(40, 300); /* RTL:左 1/3 = 下一页(顺序前翻) */
|
||
pump_ms(400); /* 700ms/页:给「换清晰→补邻居」留出余量 */
|
||
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("100 mixed flips: advanced=%d last_page=%d hits=%d miss=%d skipped=%d decodes=%d "
|
||
"(low=%d high=%d) steady=%zuKB resident=%ldKB max_live_delta=%zuKB abs_peak=%zuKB",
|
||
advanced, mr_reader_ui_page(), hits, miss, skipped_max, decodes,
|
||
mr_reader_ui_low_decodes() - low0, mr_reader_ui_high_decodes() - high0,
|
||
live_steady / 1024, mr_reader_ui_resident_kb(), transient / 1024, g_peak / 1024);
|
||
CHECK(advanced == 100 && mr_reader_ui_page() == 100,
|
||
"100 次顺序前翻逐页推进(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());
|
||
CHECK(mr_reader_ui_low_decodes() > low0 && mr_reader_ui_high_decodes() > high0,
|
||
"大页确实走了两级(low +%d, high +%d)",
|
||
mr_reader_ui_low_decodes() - low0, mr_reader_ui_high_decodes() - high0);
|
||
/* 顺序阅读的预取命中率:评审要求不低于基线 7/8。 */
|
||
CHECK(hits >= 90, "顺序翻页预取命中率 ≥90%%(hit=%d miss=%d)", hits, miss);
|
||
/* 解码次数上限:分档页 = 低档 + 高档(2 次/页),非 JPEG 单档(1 次/页)+ 开册/补邻居余量。
|
||
* 出现「每页 4 次」的放大(P1-2 的病灶)时这里会立刻失败。 */
|
||
CHECK(decodes <= 100 * 2 + 20, "100 次翻页解码次数 ≤2/页+余量 (decodes=%d)", decodes);
|
||
CHECK(mr_reader_ui_resident_kb() <= mr_reader_ui_resident_cap_kb(),
|
||
"常驻仍在上限内 (%ldKB ≤ %ldKB)", mr_reader_ui_resident_kb(),
|
||
mr_reader_ui_resident_cap_kb());
|
||
CHECK(transient <= 3 * 1024 * 1024,
|
||
"100 页连翻任何时刻存活增量 ≤3MB (%zuKB)", transient / 1024);
|
||
/* 两档共存的总账:常驻(≤3MB)+ 单次解码(≤3MB)——不许因为「基线里已含低档」
|
||
* 就看着没涨;绝对存活峰值同样要有上界。 */
|
||
CHECK(g_peak <= (size_t)(mr_reader_ui_resident_cap_kb() + 3 * 1024) * 1024,
|
||
"绝对存活峰值 ≤ 常驻上限+3MB (%zuKB)", g_peak / 1024);
|
||
meow_view_page_pop();
|
||
pump_ms(200);
|
||
}
|
||
|
||
/* ---- B10 M31:两级清晰度生命周期(2000×3000 JPEG,真机同类大页)----
|
||
* 1) 冻结停稳窗口 → 只解低档:焦点页 tier=low、常驻=显示框大小(仍是一次原生 blit),
|
||
* 主线程不等解码(open 立即返回);
|
||
* 2) 放开停稳窗口 → 高档落同一槽:tier=high、**总常驻不增加**(不是恒真的槽内字节比较)、
|
||
* 槽位全程不空、本次操作存活增量 ≤3MB、两档共存绝对峰值有上界;
|
||
* 3) 双击放大后按新几何先落低档,再换清晰 → 缩放/平移一字不差(P2-2);
|
||
* 4) 拖动中强塞的高档请求 → 被 M22 的 gen/窗口取消通道作废(skip=cancel),不会被解出来;
|
||
* 5) 整串生命周期解码次数有界(P2-3)。 */
|
||
if (have_jpeg) {
|
||
char tierbook[MR_PATH_MAX];
|
||
snprintf(tierbook, sizeof tierbook, "%s/tier", root);
|
||
int tier_ok = mkdir_p(tierbook) == 0;
|
||
for (int i = 0; tier_ok && i < 2; i++) {
|
||
char src[MR_PATH_MAX], dst[MR_PATH_MAX];
|
||
snprintf(src, sizeof src, "%s/base2000.jpg", jpgbook);
|
||
snprintf(dst, sizeof dst, "%s/t%d.jpg", tierbook, i + 1);
|
||
if (link_or_copy(src, dst) != 0) tier_ok = 0;
|
||
}
|
||
if (tier_ok) {
|
||
mr_reader_ui_test_set_mem(4096, 16000);
|
||
mr_reader_ui_test_set_sharpen_ms(60000); /* 冻结:先验证低档先落位 */
|
||
uint64_t t_open = (uint64_t)meow_ui_time_ms();
|
||
mr_reader_open_dir(tierbook);
|
||
uint64_t open_ms = (uint64_t)meow_ui_time_ms() - t_open;
|
||
CHECK(mr_reader_ui_count() == 2, "tier book pages=%d", mr_reader_ui_count());
|
||
CHECK(open_ms < 500, "open is non-blocking (%llu ms)",
|
||
(unsigned long long)open_ms);
|
||
pump_ms(800);
|
||
meow_ui_paint_full();
|
||
int page0 = mr_reader_ui_page();
|
||
int dec0 = mr_reader_ui_decode_count();
|
||
CHECK(mr_reader_ui_focus_tier() == MR_TIER_LOW,
|
||
"low tier lands first (tier=%d)", mr_reader_ui_focus_tier());
|
||
CHECK(mr_reader_ui_low_decodes() >= 1, "low decode issued (%d)",
|
||
mr_reader_ui_low_decodes());
|
||
CHECK(mr_reader_ui_high_decodes() == 0,
|
||
"no high decode before the settle window (%d)", mr_reader_ui_high_decodes());
|
||
long low_kb = mr_reader_ui_slot_kb(page0);
|
||
CHECK(low_kb > 0, "low tier image resident (%ldKB)", low_kb);
|
||
long res_low = mr_reader_ui_resident_kb();
|
||
int bl = mr_reader_ui_blit_draws(), sc = mr_reader_ui_scaled_draws();
|
||
meow_ui_paint_full();
|
||
CHECK(mr_reader_ui_blit_draws() > bl && mr_reader_ui_scaled_draws() == sc,
|
||
"low tier draws as native blit (blit +%d, scaled +%d)",
|
||
mr_reader_ui_blit_draws() - bl, mr_reader_ui_scaled_draws() - sc);
|
||
|
||
/* 放开升级:边等边采样,焦点槽任何时刻都不能空(不闪白)。 */
|
||
int high0 = mr_reader_ui_high_decodes();
|
||
long min_slot = low_kb;
|
||
size_t op_base = g_live;
|
||
peak_reset();
|
||
mr_reader_ui_test_set_sharpen_ms(0);
|
||
for (int i = 0; i < 100 && mr_reader_ui_focus_tier() != MR_TIER_HIGH; i++) {
|
||
pump_ms(10);
|
||
long k = mr_reader_ui_slot_kb(page0);
|
||
if (k > 0 && k < min_slot) min_slot = k;
|
||
}
|
||
size_t op_peak = peak_since(op_base); /* 高档这一次操作的存活增量 */
|
||
meow_ui_paint_full();
|
||
CHECK(mr_reader_ui_page() == page0, "tier swap keeps the page (%d -> %d)",
|
||
page0, mr_reader_ui_page());
|
||
CHECK(mr_reader_ui_focus_tier() == MR_TIER_HIGH,
|
||
"high tier replaces low in the same slot (tier=%d)",
|
||
mr_reader_ui_focus_tier());
|
||
CHECK(mr_reader_ui_high_decodes() > high0,
|
||
"high decode issued after settle (%d -> %d)", high0,
|
||
mr_reader_ui_high_decodes());
|
||
/* P2-2:旧的 `slot_kb` 相等是恒真(两档都落同一个 box)。改成真正会抓问题的
|
||
* 「总常驻不增加」:如果换清晰偷偷多留一张低档,这里立刻涨。 */
|
||
long res_high = mr_reader_ui_resident_kb();
|
||
CHECK(res_high > 0 && res_high <= res_low,
|
||
"换清晰不增加常驻(没有第二张低档常驻):%ldKB -> %ldKB",
|
||
res_low, res_high);
|
||
CHECK(op_peak <= 3 * 1024 * 1024,
|
||
"高档单次操作的存活增量 ≤3MB (%zuKB)", op_peak / 1024);
|
||
/* 两档共存总账:常驻 + 本次解码 的绝对峰值有上界(不是「基线含低档所以没涨」)。 */
|
||
CHECK(g_peak <= (size_t)(mr_reader_ui_resident_cap_kb() + 3 * 1024) * 1024,
|
||
"两档共存绝对峰值 ≤ 常驻上限+3MB (%zuKB)", g_peak / 1024);
|
||
CHECK(min_slot > 0, "no blank frame while swapping tiers (min slot %ldKB)",
|
||
min_slot);
|
||
CHECK(mr_reader_ui_scaled_draws() == sc,
|
||
"high tier still native blit (scaled %d -> %d)", sc,
|
||
mr_reader_ui_scaled_draws());
|
||
|
||
/* P2-2 之二:换清晰不得重置缩放/平移。先冻结窗口 → 双击放大(低档按新几何重解)
|
||
* → 记录缩放/平移 → 放开升级 → 高档落位后两者必须一字不差。 */
|
||
mr_reader_ui_test_set_sharpen_ms(60000); /* 先冻结,让放大后的低档先落位 */
|
||
reader_on_touch_down(240, 360);
|
||
reader_on_tap(240, 360);
|
||
pump_ms(60);
|
||
reader_on_touch_down(240, 360);
|
||
reader_on_tap(240, 360);
|
||
pump_ms(150);
|
||
pump_ms(600);
|
||
int zoom_before = mr_reader_ui_zoom_pct();
|
||
int panx_before = mr_reader_ui_pan_x();
|
||
int pany_before = mr_reader_ui_pan_y();
|
||
CHECK(zoom_before > 100, "double tap zoomed in (zoom=%d)", zoom_before);
|
||
CHECK(mr_reader_ui_focus_tier() == MR_TIER_LOW,
|
||
"zoomed page re-decodes to low tier first (tier=%d)",
|
||
mr_reader_ui_focus_tier());
|
||
mr_reader_ui_test_set_sharpen_ms(0);
|
||
for (int i = 0; i < 100 && mr_reader_ui_focus_tier() != MR_TIER_HIGH; i++)
|
||
pump_ms(10);
|
||
CHECK(mr_reader_ui_focus_tier() == MR_TIER_HIGH,
|
||
"zoomed page reaches the target tier (tier=%d)",
|
||
mr_reader_ui_focus_tier());
|
||
CHECK(mr_reader_ui_zoom_pct() == zoom_before &&
|
||
mr_reader_ui_pan_x() == panx_before &&
|
||
mr_reader_ui_pan_y() == pany_before,
|
||
"换清晰不重置缩放/平移(zoom %d→%d, pan %d,%d→%d,%d)",
|
||
zoom_before, mr_reader_ui_zoom_pct(), panx_before, pany_before,
|
||
mr_reader_ui_pan_x(), mr_reader_ui_pan_y());
|
||
|
||
/* 拖动中取消高档:拖动期间强塞一个「换清晰」请求,必须被取消通道作废。 */
|
||
int hd0 = mr_reader_ui_high_decodes(), hc0 = mr_reader_ui_high_canceled();
|
||
reader_on_touch_down(240, 400);
|
||
reader_on_touch_move(180, 400); /* 进入拖动 → g_sharpen_idx=-1 */
|
||
CHECK(mr_reader_ui_sharpen_idx() == -1, "drag clears sharpen eligibility");
|
||
mr_reader_ui_test_queue_high(page0);
|
||
pump_ms(200);
|
||
CHECK(mr_reader_ui_high_canceled() > hc0,
|
||
"high request canceled while dragging (%d -> %d)", hc0,
|
||
mr_reader_ui_high_canceled());
|
||
CHECK(mr_reader_ui_high_decodes() == hd0,
|
||
"canceled high request is never decoded (%d -> %d)", hd0,
|
||
mr_reader_ui_high_decodes());
|
||
reader_on_tap(240, 400); /* 松手:回弹,不翻页 */
|
||
pump_ms(300);
|
||
|
||
/* P2-2 之三:条漫滚动位置在换清晰时不得被重置(换的是视口中心页的档位)。 */
|
||
mr_reader_ui_test_set_sharpen_ms(60000); /* 先冻结:切模式时不要立刻换清晰 */
|
||
mr_reader_test_toggle_mode(); /* 翻页 → 条漫 */
|
||
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "tier book switched to webtoon");
|
||
pump_ms(300);
|
||
reader_on_touch_down(240, 600);
|
||
for (int k = 1; k <= 5; k++) {
|
||
reader_on_touch_move(240, 600 - k * 50);
|
||
pump_ms(10);
|
||
}
|
||
reader_on_tap(240, 300); /* 松手:fling */
|
||
pump_ms(2000); /* 等 fling 停稳 */
|
||
int woff0 = mr_reader_ui_woff();
|
||
int wpage0 = mr_reader_ui_page();
|
||
CHECK(woff0 > 0, "webtoon scrolled to a non-top offset (%d px)", woff0);
|
||
CHECK(mr_reader_ui_focus_tier() == MR_TIER_LOW,
|
||
"webtoon center page is low tier before settle (tier=%d)",
|
||
mr_reader_ui_focus_tier());
|
||
mr_reader_ui_test_set_sharpen_ms(0);
|
||
for (int i = 0; i < 200 && mr_reader_ui_focus_tier() != MR_TIER_HIGH; i++)
|
||
pump_ms(10);
|
||
CHECK(mr_reader_ui_focus_tier() == MR_TIER_HIGH,
|
||
"webtoon center page reaches the target tier (tier=%d)",
|
||
mr_reader_ui_focus_tier());
|
||
CHECK(mr_reader_ui_woff() == woff0 && mr_reader_ui_page() == wpage0,
|
||
"换清晰不重置条漫滚动位置(woff %d→%d, page %d→%d)",
|
||
woff0, mr_reader_ui_woff(), wpage0, mr_reader_ui_page());
|
||
mr_reader_ui_test_set_sharpen_ms(150); /* 恢复产品默认停稳窗口 */
|
||
/* P2-3:这一串(开册低+高、放大低+高、条漫低+高、一次被取消的高档)解码次数有界。 */
|
||
int decodes = mr_reader_ui_decode_count() - dec0;
|
||
CHECK(decodes <= 12, "两档生命周期解码次数有界 (decodes=%d)", decodes);
|
||
INFO("tier lifecycle: decodes=%d resident=%ldKB (低档/高档/放大/条漫 各一次换清晰)",
|
||
decodes, mr_reader_ui_resident_kb());
|
||
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;
|
||
}
|