Tuku reader: 真实解码峰值模型 + 3MB 硬阈值 + 解码取消 + 3 槽(M22 止损版)
真机(R3 Pro II 480×720 / MemTotal 56872kB):空闲 RSS 6044kB → 翻页 11400kB → 峰值 15600kB(MemFree 仅 1404kB)→ SIGKILL。M20 审计坐实根因是「单页解码瞬时峰值」, 且旧模型把 stb 当缩解库,对 JPEG 低估 3.5–8 倍(实测 baseline 2000×3000 +10.1MB、 渐进 +23.5MB、1200×1600 PNG +12.9MB)。本次按实测重定预算: - 真实峰值模型 decode_peak_bytes():JPEG 分量面(+渐进 short 系数面)、PNG 源缓冲 + RGBA 转换副本、BMP 直写目标,再叠精修/落框两段一次性重采样;上限常态 3MB (MR_DECODE_PEAK_CAP),只有 MemFree 真富余时才放宽到 min(12MB, MemFree-4MB)。 - 超预算时的降级:JPEG 逐步加粗 stbi 缩解档位(meow_img_load_file_step), PNG/BMP 无解码期缩放 → 占位;结果精确重采样回显示框(meow_img_resample), 命中页仍是单次 blit(M17 热路径纪律不破)。 - 占位可重试(g_page_skip 与永久失败 g_page_fail 分离):焦点页冷却 1.5s 自动重试、 中间区点按立即重试、离开再回来即重试;坏图仍是永久占位。 - 解码前内存门控:MemAvailable < 6MB 不解码(读不到就放行,不猜)。 - 解码请求取消:worker 出队后、mr_book_open 前二次判 gen/窗口;拖动/fling 期间只解落点页, 预取邻居仅当 MemAvailable ≥ 12MB。 - 先释放再解码:queue_page 按落地字节 slots_reserve() 先淘汰不可见旧图;可见页不淘汰。 - 槽位 4→3、常驻上限 9MB→3MB、结果队列 4→2。 - 可观测性:`reader decode page=N src=WxH peak=XkB memfree=YkB memavail=ZkB ok|skip=… step=S ms=M` 与进出阅读器 rss/hwm/memfree/memavail。 page_source: 头部探测新增 codec/progressive/channels/planes(mr_probe_info), 只带最小 SOF 的合成头仍能取尺寸(旧契约不退化)。 宿主证据:新增 test_reader_membudget(--wrap malloc 计真实峰值): PNG 600×960 model 4001kB vs 实测 3943kB;JPEG 1000×1500 step2 model 3749/实测 3711; 连翻 100 页 peak_transient 0kB(先释放再解码让缓冲复用)、驻留 ≤3MB、解码 100 次(取消生效)、 3MB 阈值下 1.5MP JPEG 可解、4MP JPEG 需 MemFree 放行(否则占位)、渐进式模型更高于 baseline。 run_tests.sh 全绿(含 zip 106 / cbz 325)、run_webtoon_tests.sh 全绿、SDK 与 tuku 交叉构建零警告。
This commit is contained in:
1 parent
773ab9ff92
commit
40751c09bd
10 files changed
+1257
-166
No files matched your search
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""make_mem_samples.py — 为 test_reader_membudget.c 生成 JPEG 夹具(M22)。
|
||||
|
||||
用法:python3 make_mem_samples.py <输出目录>
|
||||
依赖:Pillow。没有 Pillow 时测试自动 SKIP JPEG 断言(BMP/PNG 夹具由 C 直接写)。
|
||||
|
||||
刻意用**结构化图案**而不是噪声:文件极小、解码路径与真实漫画页一致(白底 + 分镜框)。
|
||||
page.jpg 1000x1500 baseline(1.5MP,真机崩溃页所在档位)
|
||||
big4.jpg 2000x2000 baseline(4MP,手机照片档)
|
||||
prog.jpg 1000x1500 progressive(SOF2:验证渐进识别 + 峰值模型)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def pattern(w, h):
|
||||
im = Image.new("RGB", (w, h), (248, 247, 244))
|
||||
d = ImageDraw.Draw(im)
|
||||
for k in range(0, min(w, h) - 200, 70):
|
||||
d.rectangle([20 + k, 20 + k, 160 + k, 120 + k], outline=(10, 10, 12), width=3)
|
||||
return im
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
out = sys.argv[1]
|
||||
os.makedirs(out, exist_ok=True)
|
||||
pattern(1000, 1500).save(os.path.join(out, "page.jpg"), "JPEG", quality=60, subsampling=2)
|
||||
pattern(2000, 2000).save(os.path.join(out, "big4.jpg"), "JPEG", quality=60, subsampling=2)
|
||||
pattern(1000, 1500).save(os.path.join(out, "prog.jpg"), "JPEG", quality=60, subsampling=2,
|
||||
progressive=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -13,6 +13,9 @@
|
||||
# 交付环境只有库+头时自动 SKIP。
|
||||
# - test_open_book_host(可选):书库点击「封面图 + 含图子目录」的父目录 → 阅读器打开的
|
||||
# 完整 UI 链路回归(合成夹具,不读真实相册);同样需要 SDK 源码树。
|
||||
# - test_reader_membudget(可选,M22):--wrap malloc 计真实解码峰值 → 单页 ≤3MB 硬阈值、
|
||||
# 连翻 100 页峰值不增长、内存门控/预算跳过可重试、常驻 ≤3MB、取消机制有界。JPEG 夹具由
|
||||
# test/make_mem_samples.py 生成(无 Pillow 时该项 SKIP)。
|
||||
# reader.c 的方向持久化只 include 配置/日志两个模块头;宿主测试用 test/settings_reader_stub.c
|
||||
# 顶替 meow_config_* 与 meow_log(所以下面各条都带 -I"$SDK" 与该存根)。
|
||||
# logic/library.c 的探测缓存用 pthread 互斥锁 → 相关编译带 -pthread。
|
||||
@@ -143,6 +146,25 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -lz -o "$OUT"/test_reader_many_pages
|
||||
"$OUT"/test_reader_many_pages
|
||||
|
||||
# 解码内存预算(M22):--wrap malloc 计真实峰值 → 单页 ≤3MB、连翻 100 页峰值不增长、
|
||||
# 内存门控/预算跳过可重试、常驻 ≤3MB。真机 SIGKILL 事故的回归防线。
|
||||
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -DMR_READER_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \
|
||||
test/test_reader_membudget.c ui/reader/reader.c \
|
||||
logic/library.c logic/reader.c logic/reader/page_source.c \
|
||||
logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \
|
||||
logic/reader/nav.c \
|
||||
"$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -lz -o "$OUT"/test_reader_membudget \
|
||||
-Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free
|
||||
"$OUT"/test_reader_membudget
|
||||
else
|
||||
echo "SKIP test_settings_lock_host/test_browse_ui_host: SDK 源码树不可用(只发布 meow_ui.a 的环境)"
|
||||
fi
|
||||
|
||||
@@ -273,9 +273,11 @@ int main(void)
|
||||
CHECK(bad_ok, "flip across corrupt/huge pages keeps working (page=%d)", mr_reader_ui_page());
|
||||
CHECK(mr_reader_ui_resident_kb() <= 9 * 1024,
|
||||
"huge page skipped without allocating it (%ldKB)", mr_reader_ui_resident_kb());
|
||||
CHECK(mr_reader_ui_failed_pages() == 2,
|
||||
"garbage + oversized page both degraded to placeholder (failed=%d)",
|
||||
mr_reader_ui_failed_pages());
|
||||
/* M22:坏图 = 永久占位(failed);超预算大页 = 预算跳过(skipped,可重试,不是 failed)。 */
|
||||
CHECK(mr_reader_ui_failed_pages() == 1,
|
||||
"garbage page is a permanent placeholder (failed=%d)", mr_reader_ui_failed_pages());
|
||||
CHECK(mr_reader_ui_skipped_pages() == 1,
|
||||
"oversized page is a retryable budget skip (skipped=%d)", mr_reader_ui_skipped_pages());
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "still in reader after corrupt page");
|
||||
|
||||
/* ---- M5 条漫:140 页排版 + 跳页落位 ---- */
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
/* test_reader_membudget.c — 阅读器解码内存预算回归(M22;需要 SDK 源码树)。
|
||||
*
|
||||
* 真机事故(R3 Pro II 480×720 / MemTotal 56872kB):空闲 RSS 6044kB → 翻页 11400kB
|
||||
* → 峰值 15600kB(当时 MemFree 仅 1404kB)→ SIGKILL。M20 审计把账算清:4 槽 RGBA 常驻
|
||||
* 5356kB、单页解码尖峰 3.4–35MB(旧模型低估 3.5–8 倍)。本测试用 `--wrap malloc` 量
|
||||
* **真实分配峰值**(不是 VmHWM 估计),把「模型 / 阈值 / 驻留」钉在实测上。
|
||||
*
|
||||
* 断言:
|
||||
* B1 峰值模型 ≤ 实测(模型必须是不低估的上界;同时 ≤ 实测×2,避免离谱保守)
|
||||
* B2 单页解码峰值(预算内的页)≤ 3MB 硬阈值
|
||||
* B3 连翻 100 页:峰值不增长、无永久失败页、解码次数有界(取消生效)
|
||||
* B4 常驻页图 ≤ 常驻上限(3MB)
|
||||
* B5 内存门控:MemAvailable 不足 → 不解码直接占位(可重试,不是永久失败)
|
||||
* B6 预算跳过可重试:MemFree 充足时同一页能解出来(占位不是永久 failed)
|
||||
* B7 快翻取消:拖动期间只解落点页,不产生中间页解码
|
||||
*
|
||||
* 夹具:BMP/PNG 由本文件用 C 写(不依赖 Pillow);JPEG 由 python3+Pillow 生成,
|
||||
* 缺 PIL 时跳过 JPEG 相关断言(打印 SKIP)。全部夹具体积极小(结构化图案)。
|
||||
*
|
||||
* 编译:test/run_tests.sh(与 test_reader_perf 同口径 + --wrap malloc 家族)。
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include "tuku_internal.h"
|
||||
#include "ui/reader/reader.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "logic/reader/nav.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 分配打点:--wrap malloc/free 家族(与 sdk/test_meow_img_budget.c 同法)*/
|
||||
/* ------------------------------------------------------------------ */
|
||||
#define AHDR 32
|
||||
#define AMAGIC 0x6D6B4D32324D454DU /* 只统计本包装器的分配;libc 内部(strdup 等)原样透传 */
|
||||
|
||||
static size_t g_live = 0;
|
||||
static size_t g_peak = 0;
|
||||
|
||||
extern void *__real_malloc(size_t);
|
||||
extern void *__real_calloc(size_t, size_t);
|
||||
extern void *__real_realloc(void *, size_t);
|
||||
extern void __real_free(void *);
|
||||
|
||||
static void acc_alloc(size_t n)
|
||||
{
|
||||
g_live += n;
|
||||
if (g_live > g_peak) g_peak = g_live;
|
||||
}
|
||||
|
||||
void *__wrap_malloc(size_t n)
|
||||
{
|
||||
size_t *h = (size_t *)__real_malloc(n + AHDR);
|
||||
if (!h) return NULL;
|
||||
h[0] = n;
|
||||
h[1] = (size_t)AMAGIC;
|
||||
acc_alloc(n);
|
||||
return (char *)h + AHDR;
|
||||
}
|
||||
|
||||
void *__wrap_calloc(size_t a, size_t b)
|
||||
{
|
||||
size_t n = a * b;
|
||||
void *p = __wrap_malloc(n);
|
||||
if (p) memset(p, 0, n);
|
||||
return p;
|
||||
}
|
||||
|
||||
void __wrap_free(void *p)
|
||||
{
|
||||
size_t *h;
|
||||
if (!p) return;
|
||||
h = (size_t *)((char *)p - AHDR);
|
||||
if (h[1] != (size_t)AMAGIC) { __real_free(p); return; } /* 非本包装器的分配 */
|
||||
h[1] = 0;
|
||||
g_live -= h[0];
|
||||
__real_free(h);
|
||||
}
|
||||
|
||||
void *__wrap_realloc(void *p, size_t n)
|
||||
{
|
||||
size_t *h = p ? (size_t *)((char *)p - AHDR) : NULL;
|
||||
if (p && (!h || h[1] != (size_t)AMAGIC)) return __real_realloc(p, n); /* 外来指针 */
|
||||
size_t old = h ? h[0] : 0;
|
||||
void *q = __wrap_malloc(n);
|
||||
if (!q) return NULL;
|
||||
if (p) {
|
||||
memcpy(q, p, old < n ? old : n);
|
||||
__wrap_free(p);
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
static size_t peak_reset(void) { g_peak = g_live; return g_live; }
|
||||
static size_t peak_since(size_t base) { return g_peak - base; }
|
||||
|
||||
void mr_nav_report(void) {}
|
||||
|
||||
static int g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { printf(" PASS\n"); } \
|
||||
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
#define INFO(...) do { printf(" info: "); printf(__VA_ARGS__); printf("\n"); } while (0)
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 夹具写入 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void put16(unsigned char *p, unsigned v) { p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8); }
|
||||
static void put32be(unsigned char *p, unsigned v)
|
||||
{
|
||||
p[0] = (unsigned char)(v >> 24); p[1] = (unsigned char)(v >> 16);
|
||||
p[2] = (unsigned char)(v >> 8); p[3] = (unsigned char)v;
|
||||
}
|
||||
static void put32(unsigned char *p, unsigned v)
|
||||
{
|
||||
p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8);
|
||||
p[2] = (unsigned char)(v >> 16); p[3] = (unsigned char)(v >> 24);
|
||||
}
|
||||
|
||||
static int write_bmp24(const char *path, int w, int h, unsigned seed)
|
||||
{
|
||||
int stride = ((w * 3 + 3) / 4) * 4;
|
||||
int imgsz = stride * h;
|
||||
unsigned char fh[14] = { 0 }, ih[40] = { 0 };
|
||||
fh[0] = 'B'; fh[1] = 'M';
|
||||
put32(fh + 2, (unsigned)(54 + imgsz));
|
||||
put32(fh + 10, 54);
|
||||
put32(ih + 0, 40);
|
||||
put32(ih + 4, (unsigned)w);
|
||||
put32(ih + 8, (unsigned)h);
|
||||
put16(ih + 12, 1);
|
||||
put16(ih + 14, 24);
|
||||
put32(ih + 20, (unsigned)imgsz);
|
||||
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
int ok = fwrite(fh, 1, sizeof fh, f) == sizeof fh &&
|
||||
fwrite(ih, 1, sizeof ih, f) == sizeof ih;
|
||||
unsigned char *row = (unsigned char *)calloc(1, (size_t)stride);
|
||||
if (!row) ok = 0;
|
||||
for (int y = 0; ok && y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
row[x * 3 + 0] = (unsigned char)((x + seed) & 0xFF);
|
||||
row[x * 3 + 1] = (unsigned char)((y * 3 + seed) & 0xFF);
|
||||
row[x * 3 + 2] = (unsigned char)((x ^ y) & 0xFF);
|
||||
}
|
||||
if (fwrite(row, 1, (size_t)stride, f) != (size_t)stride) ok = 0;
|
||||
}
|
||||
free(row);
|
||||
if (fclose(f) != 0) ok = 0;
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
static unsigned crc32_buf(const unsigned char *p, size_t n)
|
||||
{
|
||||
unsigned c = 0xFFFFFFFFu;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
c ^= p[i];
|
||||
for (int k = 0; k < 8; k++) c = (c >> 1) ^ (0xEDB88320u & (unsigned)(-(int)(c & 1)));
|
||||
}
|
||||
return c ^ 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
static int write_png_rgb(const char *path, int w, int h, unsigned seed)
|
||||
{
|
||||
/* zlib 流用 stored 块(不依赖 zlib),只为造一个真 PNG 让 stb 走「源缓冲 + RGBA 转换」路径。 */
|
||||
size_t rowlen = (size_t)w * 3 + 1;
|
||||
size_t rawlen = rowlen * (size_t)h;
|
||||
unsigned char *raw = (unsigned char *)malloc(rawlen);
|
||||
if (!raw) return -1;
|
||||
for (int y = 0; y < h; y++) {
|
||||
unsigned char *r = raw + rowlen * (size_t)y;
|
||||
r[0] = 0; /* filter none */
|
||||
for (int x = 0; x < w; x++) {
|
||||
r[1 + x * 3 + 0] = (unsigned char)((x + seed) & 0xFF);
|
||||
r[1 + x * 3 + 1] = (unsigned char)(y & 0xFF);
|
||||
r[1 + x * 3 + 2] = (unsigned char)((x ^ y) & 0xFF);
|
||||
}
|
||||
}
|
||||
size_t nblk = (rawlen + 65534) / 65535;
|
||||
size_t zlen = 2 + rawlen + nblk * 5 + 4;
|
||||
unsigned char *z = (unsigned char *)malloc(zlen);
|
||||
if (!z) { free(raw); return -1; }
|
||||
size_t zi = 0;
|
||||
z[zi++] = 0x78; z[zi++] = 0x01;
|
||||
for (size_t off = 0; off < rawlen; off += 65535) {
|
||||
size_t n = rawlen - off > 65535 ? 65535 : rawlen - off;
|
||||
z[zi++] = (off + n == rawlen) ? 1 : 0; /* BFINAL */
|
||||
z[zi++] = (unsigned char)(n & 0xFF);
|
||||
z[zi++] = (unsigned char)(n >> 8);
|
||||
z[zi++] = (unsigned char)(~n & 0xFF);
|
||||
z[zi++] = (unsigned char)((~n >> 8) & 0xFF);
|
||||
memcpy(z + zi, raw + off, n);
|
||||
zi += n;
|
||||
}
|
||||
/* adler32(大端,zlib 流尾) */
|
||||
{
|
||||
unsigned a = 1, b = 0;
|
||||
for (size_t i = 0; i < rawlen; i++) { a = (a + raw[i]) % 65521; b = (b + a) % 65521; }
|
||||
put32be(z + zi, (b << 16) | a);
|
||||
zi += 4;
|
||||
}
|
||||
free(raw);
|
||||
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) { free(z); return -1; }
|
||||
static const unsigned char sig[8] = { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A };
|
||||
int ok = fwrite(sig, 1, 8, f) == 8;
|
||||
unsigned char ihdr[13] = { 0 };
|
||||
put32be(ihdr + 0, (unsigned)w);
|
||||
put32be(ihdr + 4, (unsigned)h);
|
||||
ihdr[8] = 8; /* bit depth */
|
||||
ihdr[9] = 2; /* color type RGB */
|
||||
struct { const char *tag; const unsigned char *data; unsigned len; } chunks[2] = {
|
||||
{ "IHDR", ihdr, 13 }, { "IDAT", z, (unsigned)zi },
|
||||
};
|
||||
for (int k = 0; ok && k < 2; k++) {
|
||||
unsigned char lenb[4], crcb[4];
|
||||
put32be(lenb, chunks[k].len);
|
||||
if (fwrite(lenb, 1, 4, f) != 4) ok = 0;
|
||||
if (fwrite(chunks[k].tag, 1, 4, f) != 4) ok = 0;
|
||||
if (fwrite(chunks[k].data, 1, chunks[k].len, f) != chunks[k].len) ok = 0;
|
||||
unsigned crc = crc32_buf((const unsigned char *)chunks[k].tag, 4);
|
||||
/* 逐段续算:简化起见重算整块 */
|
||||
{
|
||||
unsigned char *tmp = (unsigned char *)malloc(4 + chunks[k].len);
|
||||
if (tmp) {
|
||||
memcpy(tmp, chunks[k].tag, 4);
|
||||
memcpy(tmp + 4, chunks[k].data, chunks[k].len);
|
||||
crc = crc32_buf(tmp, 4 + chunks[k].len);
|
||||
free(tmp);
|
||||
}
|
||||
}
|
||||
put32be(crcb, crc);
|
||||
if (fwrite(crcb, 1, 4, f) != 4) ok = 0;
|
||||
}
|
||||
if (ok) {
|
||||
unsigned char iend[12] = { 0, 0, 0, 0, 'I', 'E', 'N', 'D', 0xAE, 0x42, 0x60, 0x82 };
|
||||
if (fwrite(iend, 1, 12, f) != 12) ok = 0;
|
||||
}
|
||||
free(z);
|
||||
if (fclose(f) != 0) ok = 0;
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_PATH_MAX];
|
||||
snprintf(tmp, sizeof tmp, "%s", path);
|
||||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int rm_rf(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -1;
|
||||
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||||
rm_rf(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static void pump_ms(int ms)
|
||||
{
|
||||
for (int i = 0; i < ms / 10; i++) {
|
||||
usleep(10 * 1000);
|
||||
meow_anim_tick();
|
||||
}
|
||||
}
|
||||
|
||||
static void tap(int x, int y)
|
||||
{
|
||||
reader_on_touch_down(x, y);
|
||||
reader_on_tap(x, y);
|
||||
pump_ms(300);
|
||||
}
|
||||
|
||||
/* 生成 JPEG 夹具(Pillow 不可用则返回 0,相关断言 SKIP)。脚本本体在
|
||||
* test/make_mem_samples.py(与本文件同目录;run_tests.sh 已 cd 到 tuku/)。 */
|
||||
static int gen_jpeg_fixtures(const char *dir)
|
||||
{
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof cmd, "python3 test/make_mem_samples.py '%s' >/dev/null 2>&1", dir);
|
||||
if (system(cmd) != 0) return 0;
|
||||
struct stat st;
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/page.jpg", dir);
|
||||
return stat(p, &st) == 0 && st.st_size > 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const char *base = getenv("TMPDIR");
|
||||
if (!base || !base[0]) base = ".";
|
||||
char tmpl[512];
|
||||
snprintf(tmpl, sizeof tmpl, "%s/mr_membudget_XXXXXX", base);
|
||||
char *data = mkdtemp(tmpl);
|
||||
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
|
||||
setenv("MEOW_DATA_DIR", data, 1);
|
||||
|
||||
char root[MR_PATH_MAX];
|
||||
snprintf(root, sizeof root, "./mr_membudget_%ld", (long)getpid());
|
||||
char book[MR_PATH_MAX], big[MR_PATH_MAX], pngbook[MR_PATH_MAX], jpgbook[MR_PATH_MAX];
|
||||
snprintf(book, sizeof book, "%s/small", root);
|
||||
snprintf(big, sizeof big, "%s/big", root);
|
||||
snprintf(pngbook, sizeof pngbook, "%s/png", root);
|
||||
snprintf(jpgbook, sizeof jpgbook, "%s/jpg", root);
|
||||
if (mkdir_p(book) != 0 || mkdir_p(big) != 0 || mkdir_p(pngbook) != 0 ||
|
||||
mkdir_p(jpgbook) != 0) {
|
||||
printf("SKIP: mkdir failed\n");
|
||||
return 0;
|
||||
}
|
||||
/* 480x640 BMP ×8:单页解码峰值 ≈ 480*640*4*2 ≈ 2.5MB,能进 3MB 预算(可连翻)。 */
|
||||
for (int i = 0; i < 8; i++) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/p%02d.bmp", book, i);
|
||||
if (write_bmp24(p, 480, 640, (unsigned)i) != 0) { printf("SKIP: bmp write\n"); return 0; }
|
||||
}
|
||||
/* 1200x1600 BMP:模型峰值 ≈ 7.7MB + 目标 ≈ 9MB —— 3MB 常态预算下必须占位,
|
||||
* MemFree 富余(放宽到 12MB)时必须能解出来。 */
|
||||
{
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/huge.bmp", big);
|
||||
if (write_bmp24(p, 1200, 1600, 3) != 0) { printf("SKIP: bmp write\n"); return 0; }
|
||||
}
|
||||
/* 600x960 PNG(RGB):走「源缓冲 + RGBA 转换」路径(stb 不能解码期缩解)。 */
|
||||
for (int i = 0; i < 3; i++) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/q%d.png", pngbook, i);
|
||||
if (write_png_rgb(p, 600, 960, (unsigned)i) != 0) { printf("SKIP: png write\n"); return 0; }
|
||||
}
|
||||
int have_jpeg = gen_jpeg_fixtures(jpgbook);
|
||||
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
|
||||
|
||||
static const meow_view_hooks lh = { 0 };
|
||||
meow_view *lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
|
||||
meow_view *reader = reader_view_create();
|
||||
meow_view_page_push(ACT_LIBRARY);
|
||||
|
||||
/* 宿主内存读数固定:3MB 常态预算生效(不被开发机的巨大 MemFree 放宽)。 */
|
||||
mr_reader_ui_test_set_mem(4096, 16000);
|
||||
|
||||
/* ---- B1/B2 单页解码峰值 vs 模型(PNG:stb 不能解码期缩解 → 整幅落地)---- */
|
||||
{
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/q0.png", pngbook);
|
||||
long long model = mr_reader_ui_decode_peak_kb(p, 1, 480, 768);
|
||||
size_t b0 = g_live;
|
||||
peak_reset();
|
||||
/* 走产品解码入口(含落框重采样),与阅读器 worker 完全同路径 */
|
||||
meow_img *im = meow_img_load_file_step(p, 768, 1);
|
||||
if (im && (meow_img_w(im) != 480 || meow_img_h(im) != 768)) {
|
||||
meow_img *fit = meow_img_resample(im, 480, 768);
|
||||
if (fit) { meow_img_free(im); im = fit; }
|
||||
}
|
||||
size_t measured_kb = peak_since(b0) / 1024 + 1;
|
||||
int got = im ? 1 : 0;
|
||||
if (im) meow_img_free(im);
|
||||
INFO("png 600x960: model=%lldkB measured=%zukB decoded=%d", model, measured_kb, got);
|
||||
CHECK(got, "png fixture decodes (writer valid)");
|
||||
CHECK(model >= (long long)measured_kb, "model is an upper bound (%lldkB < %zukB)",
|
||||
model, measured_kb);
|
||||
CHECK(model <= (long long)measured_kb * 2, "model not wildly pessimistic (%lldkB vs %zukB)",
|
||||
model, measured_kb);
|
||||
}
|
||||
|
||||
/* ---- B5 内存门控:MemAvailable 不足 → 不解码、占位(可重试)---- */
|
||||
{
|
||||
mr_reader_ui_test_set_mem(20000, 1000); /* MemAvailable 1MB < 6MB 下限 */
|
||||
mr_reader_open_dir(book);
|
||||
CHECK(mr_reader_ui_count() == 8, "book pages=%d", mr_reader_ui_count());
|
||||
int d0 = mr_reader_ui_decode_count();
|
||||
pump_ms(600);
|
||||
CHECK(mr_reader_ui_decode_count() - d0 <= 1,
|
||||
"low mem: no decode attempt (%d new decodes)", mr_reader_ui_decode_count() - d0);
|
||||
CHECK(mr_reader_ui_skipped_pages() > 0, "low mem: page skipped (skipped=%d)",
|
||||
mr_reader_ui_skipped_pages());
|
||||
CHECK(mr_reader_ui_failed_pages() == 0, "low mem: not a permanent failure (failed=%d)",
|
||||
mr_reader_ui_failed_pages());
|
||||
|
||||
/* 内存恢复 + 点按显式重试 → 同一页能解出来(可重试语义) */
|
||||
mr_reader_ui_test_set_mem(20000, 20000);
|
||||
int y = meow_ui_height() / 2;
|
||||
tap(meow_ui_width() / 2, y);
|
||||
pump_ms(500);
|
||||
CHECK(mr_reader_ui_skipped_pages() == 0, "retry after mem recovers (skipped=%d)",
|
||||
mr_reader_ui_skipped_pages());
|
||||
CHECK(mr_reader_ui_resident_kb() > 0, "retry landed a page image (resident=%ldkB)",
|
||||
mr_reader_ui_resident_kb());
|
||||
meow_view_page_pop();
|
||||
pump_ms(200);
|
||||
}
|
||||
|
||||
/* ---- B3/B4 连翻 100 页:真实分配峰值不增长、常驻有界、取消有界 ---- */
|
||||
{
|
||||
mr_reader_ui_test_set_mem(4096, 16000);
|
||||
mr_reader_open_dir(book);
|
||||
pump_ms(800);
|
||||
int w = meow_ui_width();
|
||||
int d0 = mr_reader_ui_decode_count();
|
||||
long long resident_cap = mr_reader_ui_resident_cap_kb();
|
||||
long long cap = mr_reader_ui_peak_cap_kb();
|
||||
/* 基线 = 首屏稳定后的存活字节;之后任何分配都不该再超过 基线+3MB(= 单页解码峰值)。 */
|
||||
size_t live_steady = g_live;
|
||||
size_t peak_steady = g_peak;
|
||||
int skipped_max = 0, advanced = 0;
|
||||
for (int i = 0; i < 100; i++) {
|
||||
int before = mr_reader_ui_page();
|
||||
tap(i % 2 == 0 ? 40 : w - 40, 300); /* RTL:左 1/3=下一页 */
|
||||
pump_ms(120); /* 覆盖滑页 + 紧随其后的预取解码 */
|
||||
if (mr_reader_ui_page() != before) advanced++;
|
||||
if (mr_reader_ui_skipped_pages() > skipped_max) skipped_max = mr_reader_ui_skipped_pages();
|
||||
meow_ui_paint_full();
|
||||
long kb = mr_reader_ui_resident_kb();
|
||||
if (kb > resident_cap) {
|
||||
g_fail++;
|
||||
printf(" FAIL flip %d: resident %ldkB > cap %lldkB\n", i, kb, resident_cap);
|
||||
break;
|
||||
}
|
||||
}
|
||||
size_t transient = g_peak > peak_steady ? g_peak - peak_steady : 0;
|
||||
size_t live_end = g_live;
|
||||
int decodes = mr_reader_ui_decode_count() - d0;
|
||||
INFO("100 flips: steady_live=%zuKB peak_transient=%zuKB end_live=%zuKB cap=%lldKB decodes=%d advanced=%d",
|
||||
live_steady / 1024, transient / 1024, live_end / 1024, cap, decodes, advanced);
|
||||
CHECK(advanced == 100, "every flip advanced the page (%d/100)", advanced);
|
||||
CHECK(transient <= (size_t)cap * 1024 + 512 * 1024,
|
||||
"no allocation above steady+3MB over 100 flips (%zuKB)", transient / 1024);
|
||||
CHECK(live_end <= live_steady + 512 * 1024,
|
||||
"live bytes do not drift up over 100 flips (%zuKB -> %zuKB)",
|
||||
live_steady / 1024, live_end / 1024);
|
||||
CHECK(skipped_max == 0, "no page skipped while flipping small pages (max=%d)", skipped_max);
|
||||
CHECK(mr_reader_ui_failed_pages() == 0, "no permanently failed pages (%d)",
|
||||
mr_reader_ui_failed_pages());
|
||||
/* 取消/预取纪律:100 次翻页的解码次数必须有界(M20 实测曾 1.75× 浪费)。 */
|
||||
CHECK(decodes <= 100 * 2, "decode count bounded over 100 flips (%d)", decodes);
|
||||
meow_view_page_pop();
|
||||
pump_ms(200);
|
||||
}
|
||||
|
||||
/* ---- B6 预算跳过可重试:大页在低预算下占位,MemFree 充足后能解出来 ---- */
|
||||
{
|
||||
mr_reader_ui_test_set_mem(4096, 16000); /* MemFree 4MB → 3MB 常态预算 */
|
||||
mr_reader_open_dir(big);
|
||||
pump_ms(900);
|
||||
CHECK(mr_reader_ui_skipped_pages() == 1, "over-budget page skipped (skipped=%d)",
|
||||
mr_reader_ui_skipped_pages());
|
||||
CHECK(mr_reader_ui_failed_pages() == 0, "over-budget page is not a permanent failure (failed=%d)",
|
||||
mr_reader_ui_failed_pages());
|
||||
/* MemFree 富余 → 放宽档(峰值上限 = MemFree-4MB,最高 12MB);大页仍应能解出。 */
|
||||
mr_reader_ui_test_set_mem(48000, 48000);
|
||||
tap(meow_ui_width() / 2, meow_ui_height() / 2);
|
||||
pump_ms(1200);
|
||||
INFO("relaxed budget=%ldkB skipped=%d resident=%ldkB", mr_reader_ui_peak_budget_kb(),
|
||||
mr_reader_ui_skipped_pages(), mr_reader_ui_resident_kb());
|
||||
CHECK(mr_reader_ui_skipped_pages() == 0,
|
||||
"over-budget page decodable once MemFree is plenty (skipped=%d)",
|
||||
mr_reader_ui_skipped_pages());
|
||||
meow_view_page_pop();
|
||||
pump_ms(200);
|
||||
}
|
||||
|
||||
/* ---- B7 JPEG:模型不低估 + 3MB 阈值下「哪些 JPEG 会变占位」---- */
|
||||
if (have_jpeg) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/page.jpg", jpgbook);
|
||||
long long model = mr_reader_ui_decode_peak_kb(p, 2, 480, 720);
|
||||
size_t b0 = g_live;
|
||||
peak_reset();
|
||||
meow_img *im = meow_img_load_file_step(p, 720, 2);
|
||||
if (im && (meow_img_w(im) != 480 || meow_img_h(im) != 720)) {
|
||||
meow_img *fit = meow_img_resample(im, 480, 720);
|
||||
if (fit) { meow_img_free(im); im = fit; }
|
||||
}
|
||||
size_t measured = peak_since(b0) / 1024 + 1;
|
||||
int got = im ? 1 : 0;
|
||||
if (im) meow_img_free(im);
|
||||
INFO("jpeg 1000x1500 step=2: model=%lldkB measured=%zukB decoded=%d", model, measured, got);
|
||||
CHECK(got, "jpeg fixture decodes");
|
||||
CHECK(model >= (long long)measured, "jpeg model upper bound (%lldkB < %zukB)", model, measured);
|
||||
CHECK(model <= (long long)measured * 2, "jpeg model not pessimistic (%lldkB vs %zukB)",
|
||||
model, measured);
|
||||
/* 1.5MP(真机崩溃页档位)在 3MB 预算内必须可解:某档 step ≤ cap */
|
||||
long long cap = mr_reader_ui_peak_cap_kb();
|
||||
long long best = mr_reader_ui_decode_peak_kb(p, 8, 480, 720);
|
||||
INFO("jpeg 1000x1500 step=8 model=%lldkB cap=%lldkB", best, cap);
|
||||
CHECK(best <= cap, "1.5MP jpeg fits the 3MB cap at some step (%lldkB > %lldkB)", best, cap);
|
||||
/* 4MP(手机照片档)在 3MB 预算下不可解 → 必须占位(诚实代价,见报告) */
|
||||
snprintf(p, sizeof p, "%s/big4.jpg", jpgbook);
|
||||
long long big = mr_reader_ui_decode_peak_kb(p, 8, 480, 480);
|
||||
INFO("jpeg 2000x2000 step=8 model=%lldkB (cap=%lldkB -> %s)",
|
||||
big, cap, big <= cap ? "decodable" : "placeholder");
|
||||
CHECK(big > cap, "4MP jpeg exceeds the 3MB cap even at step=8 (as documented)");
|
||||
/* 渐进式:识别 + 峰值模型包含 short 系数面(同样尺寸必须比 baseline 高) */
|
||||
snprintf(p, sizeof p, "%s/prog.jpg", jpgbook);
|
||||
long long prog = mr_reader_ui_decode_peak_kb(p, 2, 480, 720);
|
||||
INFO("jpeg 1000x1500 progressive step=2 model=%lldkB", prog);
|
||||
CHECK(prog > model, "progressive jpeg modeled higher than baseline (%lldkB vs %lldkB)",
|
||||
prog, model);
|
||||
} else {
|
||||
printf(" SKIP jpeg assertions: Pillow unavailable (python3 -c 'import PIL')\n");
|
||||
}
|
||||
|
||||
meow_view_page_pop();
|
||||
meow_view_free(reader);
|
||||
meow_view_free(lib);
|
||||
meow_ui_shutdown();
|
||||
|
||||
rm_rf(root);
|
||||
rm_rf(data);
|
||||
|
||||
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
|
||||
g_fail ? "see failures above" : "reader memory budget checks passed");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user