diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index 2a8596e..be24482 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -2724,3 +2724,85 @@ de4c5e1939c7571b90f75a80f48236c0 build_ncm_host.sh # SDK_SRC 补 meow_ §20.3 档位链语义本身的验证属主会话(297 队列项)。 - **bss +~95KB**:`s_pls[NCM_PLAYLIST_MAX]`(120×808 ≈ 95KB)staging,与既存 `s_build[16384]` (128KB) 同口径;设备 RAM 56MB 可承受(总 bss 1.36MB)。 + +## 32. 内存瘦身:真机 OOM 被杀修复 —— 封面降采样 + 瞬态缓冲缩减(2026-09-02,agent-308) + +> 范围:`patch_v2/ncm_app/` + `patch_v2/sdk/meow_ui_img.c/.h`(新增一个纯增量 downscale API)。 +> 风控:真机验证用 autoplay 播**已缓存**曲目(0 播放请求);HOME 歌单列表走盘缓存 +> (`playlists.bin`,§26),首启一次网络刷新(少量真实请求,任务允许)。 + +### 32.1 动机(真机 dmesg 实锤) + +2026-09-02 02:56 设备内核 OOM:`Out of memory: Kill process 1151 (ncm) score 460`, +ncm `total-vm 97MB / anon-rss 26.9MB`,设备 56MB 总内存(系统+服务占完可用 ~20MB)。 +launcher `app_launcher.c:1647` 已有 OOM 护栏(`MemAvailable<24MB` → 大字库降 common), +说明系统侧早已见过该类压力。本批把 ncm 的匿名 RSS 拆清算清,并把能砍的砍到常态 ≤16MB。 + +### 32.2 内存画像(拆到每块,宿主 mem_sim + 设备实测 + 源码审计) + +| 块 | 实测/审计 | 说明 | +|---|---|---| +| **封面 RGBA(大头)** | 1448×1448 → **8.28MB**(mem_sim 实测 RssAnon 增量 8484KB) | `meow_img_load_file` 用 stbi 按**原生分辨率**解码为 RGBA 并驻留(`g_cover_img`)。网易云全尺寸封面即此规模。 | +| 字库(fonts=common 档) | plane **2.44MB**(`default/regular/font.png` 1280×3996 depth4 灰度)+glyphs ~130KB | app.json `fonts:"common"`,launcher 注入 `MEOW_FONT_TIER=0`。plane 懒触达(绘制 glyph 才换页)。GBK 档约 17MB(launcher 注释),common 远小。 | +| bss 静态数组 | **1.36MB** | `song_ids[16384]`128KB + `songs[256]`152KB + `search_songs[256]`156KB + `playlists[120]` + `qr_mods` + … | +| back_buf(SDK) | **0.69MB** | 480×720×2(16bpp,mmap fb 之外仍 heap 分配一份 `g_back_buf`)。 | +| 线程栈 | musl 静态默认 **128KB/线程** × ~5-8 线程 ≈ 0.6-1MB | 非 glibc 8MB/thread;实测调用链栈深 ≤~30KB。 | +| mbedtls(每连接瞬态) | in/out 各 16KB(默认)+ CA bundle 解析 ~440KB | 每 `http_run` 加载 182KB CA + 解析 + 32KB ssl。播放+封面并发 ~2-3 连接。 | +| API 响应 rbuf(瞬态) | 旧每 `api_post` 固定 **1MB** | song_url/lyric/song_detail/cloudsearch 响应实际 ≤100KB,1MB 严重超额;并发几 × 1MB 是可砍峰值。 | + +**结论**:真正的大头是**封面全分辨率 RGBA(8.28MB)**,其次瞬态 1MB rbuf。字库(common 2.44MB) +与 bss(1.36MB)不可由 ncm 轻易再砍(字库为系统字体档,bss 已是 §17 瘦身后)。 + +### 32.3 瘦身(按收益排序) + +1. **封面降采样(-7.65MB)**:`patch_v2/sdk/meow_ui_img.c` 新增纯增量 `meow_img_downscale(img,maxdim)` + (最近邻等比缩放到最长边 ≤maxdim,复用既有 `scale_buf`);声明进 `include/meow_ui_image.h`。 + `cover.c` 新增 `cover_cap()`(`NCM_COVER_DECODE_MAX=480`)在缓存命中/下载解码后统一降采样, + 释放原大图驻留小图。播放页黑胶方图 + 歌词页模糊铺底仅需 ≤480px,全尺寸属浪费。 +2. **瞬态 API rbuf 缩减**:`ncm_api.c` 新增 `RESP_SMALL(64KB)/RESP_MED(256KB)`—— + song_url→64KB、lyric/song_detail/cloudsearch/user_playlist→256KB;仅 + `playlist/detail`、`track/all`(全量 trackIds ≤16384)保留 1MB。 +3. **线程栈显式**:`ncm_app_internal.h` 新增 `ncm_thread_create()`(`pthread_attr_setstacksize` 128KB), + 替换全部 14 处 `pthread_create`。musl 默认即 128KB(本设备 0 收益),显式声明防未来 + libc/平台默认变大(如宿主 glibc 8MB/thread),并满足「按实际用量调小」要求。 +4. **滑动窗口/meta 复核**:`window_sim` 重跑确认零回退——`ncm_song_t`=608B(旧 1324B)、 + 窗口=155904B(256 首)、trackIds 索引=131072B(16384 首);6000 首场景旧 1292KB→新 280KB(恒定)。 + +### 32.4 验证记录 + +| 项 | 方式 | 结果 | +|---|---|---| +| 封面收益 | **mem_sim**(新 bench,链 host SDK):init 基线 → 载全尺寸封面 → downscale 480 | **PASS**:全尺寸 RssAnon +8484KB → 降采样后 -7836KB(8.28MB→0.78MB),尺寸 480×480 | +| 宿主渲染回归 | build_ncm_host_mem.sh + NCM_PAGE=home/player MEOW_UI_DUMP | rc=0、两页渲染正常(SDK img 改动无回归) | +| 播放链路回归 | stream_sim 全矩阵 + resume / player_flow_sim P1-P12 | **ALL PASS** | +| 大歌单/搜索/缓存 | window_sim / search_sim / cache_evict_sim | **ALL PASS**(含 6000 首滑动窗口内存断言、搜索分页、LRU 防淘汰+7 天回收) | +| core 回归 | build_core.sh → test_ncm_core_host + qemu test_ncm_core_mipsel | **PASS**(host + mipsel+qemu) | +| mipsel 构建 | build_ncm.sh(自有代码 -Wall -Wextra) | **0 warning**(set -e 下任何告警即失败) | +| 真机播放 | autoplay 播已缓存曲(新 ncm 已上卡) | 音频开流/播放正常(log `play: start … audiod open ok`);常驻 ~6MB(HOME 不触发封面,封面收益由 mem_sim 证明) | + +**真机峰值说明(诚实)**:裸启 ncm(不经 launcher)在拖入会话后偶收 GTP 复位产生的幽灵左滑 → `exit:swipe` +退出,无法做长时驻留采样;封面在设备上的运行时 RSS 收益以宿主 mem_sim 的逐字节实测为准 +(封面解码路径 `meow_img_load_file` 宿主/设备同源,差异仅在字库背板)。 + +### 32.5 md5(本批产物) + +``` +44fa16feba8e597d7dbe9535983f2629 ncm # mipsel 静态 App(text 1015150 / bss 1362500;未部署留主会话,仅真机播放在跑确认) +34be096f196aa24b755629bd43a543f7 ncm_host # 宿主离屏二进制(重链) +a9842284ecf9529ac1c39a9119561a20 mem_sim # 宿主内存测量 harness +(REPORT §32;sdk/meow_ui_img.c + include/meow_ui_image.h 变更见 git) +``` + +### 32.6 诚实出入 / 遗留 + +- **未部署**:新 ncm 已 scp 到设备 `/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm` 做真机播放在跑确认, + 正式部署/固件打包留主会话统一。 +- **字库(common 2.44MB)本批未动**:属系统字体档位,由 launcher 按 `MemAvailable` 护栏决定; + ncm 无法(也不应)自行缩减。若需更低,只能走「GBK/Full」降档之外的字体裁剪(另立批)。 +- **mbedtls in/out(16KB/16KB)未缩**:改 `MBEDTLS_SSL_OUT_CONTENT_LEN` 需重编 mbedtls,收益 ~12KB/连接, + 低于风险,留主会话。 +- **封面 downscale 只作用于封面**:SDK `meow_img_downscale` 为纯增量 API,未改动其它 App 的图片路径 + (reader/hello/files 不引入)。若后续发现其它 App 大图驻留,可复用同一 API。 +- **真机长期峰值**:幽灵左滑导致无法持续采样,设备端「≤16MB」是「基线 ~6MB + 封面 0.78MB + 字库 + 2.44MB」的推演上限(约 8-9MB),低于 16MB 目标;正式部署后建议主会话做一次带 launcher 的 + 前台三场景采样复核。 diff --git a/patch_v2/ncm_app/app/logic/appstate.c b/patch_v2/ncm_app/app/logic/appstate.c index 0092a8b..92e4747 100644 --- a/patch_v2/ncm_app/app/logic/appstate.c +++ b/patch_v2/ncm_app/app/logic/appstate.c @@ -722,7 +722,7 @@ void autoplay_maybe(void) meow_log(MEOW_LOG_INFO, "autoplay: triggered, sel='%s'", buf); char *sel = strdup(buf); pthread_t t; - if (pthread_create(&t, NULL, autoplay_worker, sel) == 0) pthread_detach(t); + if (ncm_thread_create(&t, autoplay_worker, sel) == 0) pthread_detach(t); else free(sel); } diff --git a/patch_v2/ncm_app/app/logic/cover.c b/patch_v2/ncm_app/app/logic/cover.c index f86d4c8..2cc0d4a 100644 --- a/patch_v2/ncm_app/app/logic/cover.c +++ b/patch_v2/ncm_app/app/logic/cover.c @@ -117,6 +117,19 @@ static void save_cover_raw(const char *path, size_t len) #define COVER_RETRY_MAX 8 /* 连续失败放弃次数(UI 重进页/切歌会重新提交目标) */ +/* 内存瘦身:封面解码后按显示上限降采样驻留。网易云全尺寸封面(如 1448×1448)解码成 + * RGBA = 8MB,而应用显示只需 ≤480px(播放页黑胶方图+歌词页模糊铺底均 ≤ 屏幕 480 宽)—— + * 缩到 480×480 = 0.9MB,省 ~7MB anon(设备 RAM 仅 56MB,OOM 真机被杀,最高优先级)。 + * 若已 ≤上限(小缩略图)则原样保留。返回本函数处理后的图(可能替换/释放旧图)。 */ +#define NCM_COVER_DECODE_MAX 480 +static meow_img *cover_cap(meow_img *im) +{ + if (!im) return NULL; + meow_img *small = meow_img_downscale(im, NCM_COVER_DECODE_MAX); + if (small) { meow_img_free(im); return small; } /* 已缩放:释放原始大图,换小图驻留 */ + return im; /* 本就够小:保留原图 */ +} + static void *cover_worker(void *arg) { (void)arg; @@ -168,6 +181,8 @@ static void *cover_worker(void *arg) } } } + /* 内存瘦身:全尺寸封面解码后降采样到 ≤480px 驻留(省 ~7MB anon,OOM 修复)。 */ + if (img) img = cover_cap(img); if (!img) { /* 失败:指数退避重试,连续 COVER_RETRY_MAX 次放弃(不无声死等; * 目标保留期间 UI 每帧重请求由 cover_request 去重挡住)。 */ @@ -218,7 +233,7 @@ void cover_request(const char *url) pthread_mutex_unlock(&g_cm); if (!g_thr_on) { g_thr_on = 1; - if (pthread_create(&g_thr, NULL, cover_worker, NULL) == 0) + if (ncm_thread_create(&g_thr, cover_worker, NULL) == 0) pthread_detach(g_thr); else g_thr_on = 0; diff --git a/patch_v2/ncm_app/app/ncm_app_internal.h b/patch_v2/ncm_app/app/ncm_app_internal.h index e295608..527bbd8 100644 --- a/patch_v2/ncm_app/app/ncm_app_internal.h +++ b/patch_v2/ncm_app/app/ncm_app_internal.h @@ -18,6 +18,7 @@ #include #include #include +#include #include "meow_ui.h" #include "meow_ui_view.h" #include "ncm.h" /* 核心库伞头(ctx/api/login/cache/...) */ @@ -81,6 +82,21 @@ /* active_playlist 哨兵:搜索歌单详情(复用 playlist_open 链路,读 g_st.search_pl_open) */ #define NCM_PL_SEARCH (-2) +/* 线程创建:显式小栈(内存瘦身,OOM 修复)。本 App 为 musl 静态二进制,pthread 默认栈 + * 即 128KB;这里显式声明 128KB(与默认一致、零成本)并防未来 libc/平台默认变化(如 + * glibc 8MB/thread 的宿主构建)。实测网络/解码调用链栈深 ≤~30KB,128KB 充足。 + * 返回 pthread_create 结果。 */ +#define NCM_THREAD_STACK (128 * 1024) +static inline int ncm_thread_create(pthread_t *t, void *(*fn)(void *), void *arg) +{ + pthread_attr_t a; + pthread_attr_init(&a); + pthread_attr_setstacksize(&a, (size_t)NCM_THREAD_STACK); + int r = pthread_create(t, &a, fn, arg); + pthread_attr_destroy(&a); + return r; +} + /* ---- 播放/媒体会话状态(play 线程 ↔ UI/IPC 共享,mu 保护)---- */ enum { NCM_PC_STOPPED = 0, diff --git a/patch_v2/ncm_app/app/play/ipc_media.c b/patch_v2/ncm_app/app/play/ipc_media.c index 45668b9..d5b4156 100644 --- a/patch_v2/ncm_app/app/play/ipc_media.c +++ b/patch_v2/ncm_app/app/play/ipc_media.c @@ -205,7 +205,7 @@ int ipc_media_start(void) { if (g_thr_on) return 0; g_quit = 0; - if (pthread_create(&g_thr, NULL, ipc_thread, NULL) != 0) return -1; + if (ncm_thread_create(&g_thr, ipc_thread, NULL) != 0) return -1; pthread_detach(g_thr); g_thr_on = 1; return 0; diff --git a/patch_v2/ncm_app/app/play/player_manager.c b/patch_v2/ncm_app/app/play/player_manager.c index fbb34a0..ef21550 100644 --- a/patch_v2/ncm_app/app/play/player_manager.c +++ b/patch_v2/ncm_app/app/play/player_manager.c @@ -369,7 +369,7 @@ static int song_download_attempt(const ncm_song_t *s, int idx, int seq, const ch size_t cap = (ext[0] == 'f') ? (1024u << 20) : (64u << 20); ss->maxb = (u.size > 0 && (uint64_t)u.size < cap) ? (size_t)u.size : cap; ncm_strlcpy(ss->url, u.url, sizeof ss->url); - if (pthread_create(&ss->dl_thr, NULL, dl_thread, ss) != 0) { + if (ncm_thread_create(&ss->dl_thr, dl_thread, ss) != 0) { ncm_cache_abort(ss->w); ss->w = NULL; stream_src_close(ss->src); ss->src = NULL; return NCM_ERR_IO; @@ -793,7 +793,7 @@ static void lyric_ensure_worker(void) pthread_mutex_lock(&g_lyrmu); if (!g_lyr_thr_on) { g_lyr_thr_on = 1; - if (pthread_create(&g_lyr_thr, NULL, lyric_worker, NULL) == 0) pthread_detach(g_lyr_thr); + if (ncm_thread_create(&g_lyr_thr, lyric_worker, NULL) == 0) pthread_detach(g_lyr_thr); else g_lyr_thr_on = 0; } pthread_mutex_unlock(&g_lyrmu); @@ -825,7 +825,7 @@ int player_manager_start(void) g_cache_gb = g_st.cache_limit_gb; pthread_mutex_init(&g_play.mu, NULL); meow_audio_on_preempt(on_audio_preempt, NULL); /* 被抢占 → 暂停(SDK 读线程分发) */ - if (pthread_create(&g_thr, NULL, player_thread, NULL) != 0) return -1; + if (ncm_thread_create(&g_thr, player_thread, NULL) != 0) return -1; g_thr_on = 1; return 0; } diff --git a/patch_v2/ncm_app/app/ui/home.c b/patch_v2/ncm_app/app/ui/home.c index ff5b30b..e4f8d2d 100644 --- a/patch_v2/ncm_app/app/ui/home.c +++ b/patch_v2/ncm_app/app/ui/home.c @@ -233,7 +233,7 @@ void home_reload(void) g_home_loading = 1; } g_home_thr_on = 1; - if (pthread_create(&g_home_thr, NULL, home_worker, NULL) != 0) { + if (ncm_thread_create(&g_home_thr, home_worker, NULL) != 0) { g_home_thr_on = 0; g_home_loading = 0; g_load_err = NCM_ERR_IO; } home_apply(); diff --git a/patch_v2/ncm_app/app/ui/login.c b/patch_v2/ncm_app/app/ui/login.c index bc6e316..0b95d43 100644 --- a/patch_v2/ncm_app/app/ui/login.c +++ b/patch_v2/ncm_app/app/ui/login.c @@ -176,7 +176,7 @@ int login_start(void) g_polling = 1; g_done = 0; g_err = 0; - if (pthread_create(&g_thread, NULL, login_worker, NULL) != 0) { + if (ncm_thread_create(&g_thread, login_worker, NULL) != 0) { g_polling = 0; return -1; } diff --git a/patch_v2/ncm_app/app/ui/playlist.c b/patch_v2/ncm_app/app/ui/playlist.c index ae8cb66..420e75e 100644 --- a/patch_v2/ncm_app/app/ui/playlist.c +++ b/patch_v2/ncm_app/app/ui/playlist.c @@ -306,7 +306,7 @@ static void pl_maybe_ensure(void) g_ens_first = 0; g_plloading = 1; g_plthr_on = 1; - if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) { + if (ncm_thread_create(&g_plthr, playlist_worker, NULL) != 0) { g_plthr_on = 0; g_plloading = 0; ncm_snack("加载失败"); } } @@ -340,7 +340,7 @@ void playlist_refresh(void) g_pl_stale = 0; g_plloading = 1; g_plthr_on = 1; - if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) { + if (ncm_thread_create(&g_plthr, playlist_worker, NULL) != 0) { g_plthr_on = 0; g_plloading = 0; g_load_err = NCM_ERR_IO; } pl_apply(1); diff --git a/patch_v2/ncm_app/app/ui/search.c b/patch_v2/ncm_app/app/ui/search.c index 667162b..3bfe6e7 100644 --- a/patch_v2/ncm_app/app/ui/search.c +++ b/patch_v2/ncm_app/app/ui/search.c @@ -211,7 +211,7 @@ static void sr_maybe_load_more(void) g_req_tab = g_tab; g_req_offset = n; g_req_replace = 0; /* 单飞:无 worker 才发(search_worker 尾部解锁) */ g_thr_on = 1; g_loading = 1; g_more_err = 0; - if (pthread_create(&g_thr, NULL, search_worker, NULL) != 0) { + if (ncm_thread_create(&g_thr, search_worker, NULL) != 0) { g_thr_on = 0; g_loading = 0; g_more_err = 1; } } @@ -270,7 +270,7 @@ void search_submit(const char *kw) g_st.search_err = 0; g_loading = 1; g_more_err = 0; g_thr_on = 1; - if (pthread_create(&g_thr, NULL, search_worker, NULL) != 0) { + if (ncm_thread_create(&g_thr, search_worker, NULL) != 0) { g_thr_on = 0; g_loading = 0; g_st.search_err = NCM_ERR_IO; } sr_apply(); @@ -290,7 +290,7 @@ static void sr_set_tab(int t) g_req_tab = g_tab; g_req_offset = 0; g_req_replace = 1; g_loading = 1; g_more_err = 0; g_thr_on = 1; - if (pthread_create(&g_thr, NULL, search_worker, NULL) != 0) { + if (ncm_thread_create(&g_thr, search_worker, NULL) != 0) { g_thr_on = 0; g_loading = 0; } } @@ -354,7 +354,7 @@ int search_on_tap(int x, int y) g_req_tab = g_tab; g_req_offset = 0; g_req_replace = 1; g_loading = 1; g_thr_on = 1; - if (pthread_create(&g_thr, NULL, search_worker, NULL) != 0) { + if (ncm_thread_create(&g_thr, search_worker, NULL) != 0) { g_thr_on = 0; g_loading = 0; g_st.search_err = NCM_ERR_IO; } } diff --git a/patch_v2/ncm_app/bench/mem_sim.c b/patch_v2/ncm_app/bench/mem_sim.c new file mode 100644 index 0000000..4c0a243 --- /dev/null +++ b/patch_v2/ncm_app/bench/mem_sim.c @@ -0,0 +1,77 @@ +/* bench/mem_sim.c — 宿主内存测量 harness(OOM 瘦身验证)。 + * + * 目的:把 ncm 相关块的匿名 RSS(RssAnon)拆开,验证封面降采样(meow_img_downscale)收益。 + * 依赖:仅 host SDK 对象(meow_ui_init 装字库 + meow_img 解码),无需 App/core 对象。 + * 用法:MEM_SIM=1 ./ncm_host + * + * 场景(各阶段 RssAnon,KB): + * [0] meow_ui_init 后基线(含字库 plane + back_buf + 启动堆)。 + * [1] meow_img_load_file(全尺寸封面 1448x1448) —— 全尺寸 RGBA ≈ 8MB 驻留。 + * [2] meow_img_downscale(480) + free 原图 —— RGBA 降到 ≤480x480 ≈ 0.9MB。 + * 断言:RssAnon[1]-RssAnon[0] > 2MB(大封面确实驻留),RssAnon[2] < RssAnon[1]-4MB + * (降采样省 >4MB,可验证)。 + */ +#include "../app/ncm_app_internal.h" +#include +#include +#include +#include + +static long rss_anon_kb(void) +{ + FILE *f = fopen("/proc/self/status", "r"); + if (!f) return -1; + char line[256]; + long v = -1; + while (fgets(line, sizeof line, f)) + if (strncmp(line, "RssAnon:", 8) == 0) { v = atol(line + 8); break; } + fclose(f); + return v; +} + +static int mem_sim_run(void) +{ + meow_ui_opts o = { APP_W, APP_H }; + if (meow_ui_init(&o) != 0) { printf("mem: meow_ui_init FAILED\n"); return 1; } + + long r0 = rss_anon_kb(); + printf("mem[0] baseline (font plane + back_buf + startup): RssAnon=%ldKB\n", r0); + if (r0 < 0) { printf("mem: cannot read RssAnon\n"); return 1; } + + const char *cover = "/tmp/ncmfont/cover_current.jpg"; + if (access(cover, F_OK) != 0) { + printf("mem: warning: %s not found; skip cover scenario\n", cover); + } else { + meow_img *f0 = meow_img_load_file(cover); + if (!f0) { printf("mem: cover load FAILED\n"); return 1; } + long r1 = rss_anon_kb(); + printf("mem[1] cover full-res %dx%d loaded: RssAnon=%ldKB (delta=%ldKB)\n", + meow_img_w(f0), meow_img_h(f0), r1, r1 - r0); + + meow_img *small = meow_img_downscale(f0, 480); + if (small) meow_img_free(f0); + long r2 = rss_anon_kb(); + int w = small ? meow_img_w(small) : (f0 ? meow_img_w(f0) : 0); + int h = small ? meow_img_h(small) : (f0 ? meow_img_h(f0) : 0); + printf("mem[2] cover downscaled to %dx%d: RssAnon=%ldKB (delta_from_full=%ldKB)\n", + w, h, r2, r2 - r1); + + int ok = (r1 - r0 > 2048) && (r1 - r2 > 4096) && (small != NULL) && + (w <= 480 && h <= 480); + printf("mem: cover-downscale %s (full_delta=%ldKB, saved=%ldKB, size=%dx%d)\n", + ok ? "PASS" : "FAIL", r1 - r0, r1 - r2, w, h); + if (small) meow_img_free(small); + } + + meow_ui_shutdown(); + printf("mem: done\n"); + return 0; +} + +int main(void) +{ + if (getenv("MEM_SIM") && getenv("MEM_SIM")[0] == '1') return mem_sim_run(); + /* 未设 MEM_SIM:宿主页面测试走 test_ncm_app.c 的 driver(本文件被链时只占位)。 */ + printf("mem_sim: set MEM_SIM=1 to run memory scenarios\n"); + return 0; +} diff --git a/patch_v2/ncm_app/build_ncm_host_mem.sh b/patch_v2/ncm_app/build_ncm_host_mem.sh new file mode 100755 index 0000000..89e6463 --- /dev/null +++ b/patch_v2/ncm_app/build_ncm_host_mem.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# build_ncm_host_mem.sh — 宿主编译 ncm 的真实页面/逻辑(离屏 480x720)+ 内存测量 harness。 +# +# 与 build_ncm_host.sh 的差异: +# * 复用既有 out/host/sdk_*.o(SDL 层既有对象,避免 SDK meow_ui_gfx.c 当前工作区 +# 的注释语法缺陷导致全量 SDK 重编失败),仅重编 meow_ui_img.c(新增 meow_img_downscale)。 +# * 用途:验证封面降采样(内存瘦身)在宿主按预期工作;供 mem_sim 报告各场景 RSS。 +# 运行:MEM_SIM=1 ./ncm_host(内置内存自测)或 NCM_PAGE=home MEOW_UI_DUMP=/tmp/home.ppm ./ncm_host +cd "$(dirname "$0")" +ROOT="$HOME/Project/R3ProII" +OUT="$PWD/out" +mkdir -p "$OUT/host" +CC_HOST=${CC_HOST:-gcc} + +SDK_DIR="$ROOT/patch_v2/sdk" +MBED="$OUT/mbedtls/host" + +INC="-I. -I../sdk -I../meow_launcherd -Icore \ + -I$SDK_DIR -I$ROOT/patch_v2/meow_launcherd \ + -Ivendor/mbedtls/include -Ivendor/jsmn -Ivendor/qrcodegen/c -Ivendor/minimp3 -Ivendor/dr_flac" + +echo "==> recompile host meow_ui_img (adds meow_img_downscale)" +$CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" \ + "$SDK_DIR/meow_ui_img.c" -o "$OUT/host/sdk_meow_ui_img.o" || exit 1 + +echo "==> host app sources (exclude app.c: test provides main + g_st)" +APP_SRC="app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \ + app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \ + app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \ + app/logic/cover.c app/logic/account.c app/play/player_manager.c app/play/audio_out.c \ + app/play/ipc_media.c app/play/decoder.c app/play/stream_src.c app/play/minimp3_impl.c" +for f in $APP_SRC; do + b=$(basename "$f" .c) + $CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -DJSMN_HEADER $INC "$f" -o "$OUT/host/$b.o" || { echo "app $f FAILED"; exit 1; } +done +# dr_flac 实现 TU +$CC_HOST -O2 -c -Ivendor/dr_flac app/play/dr_flac_impl.c -o "$OUT/host/dr_flac_impl.o" || exit 1 +$CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -DJSMN_HEADER $INC app/test/test_ncm_app.c -o "$OUT/host/app_test.o" || exit 1 + +echo "==> link ncm_host" +$CC_HOST -O2 -o ncm_host \ + "$OUT/host"/*.o \ + out/core-host/*.o \ + -L"$MBED" -lmbedtls -lmbedx509 -lmbedcrypto \ + -lz -lpthread -lm +ls -la ncm_host +echo "run: NCM_PAGE=home MEOW_UI_DUMP=/tmp/home.ppm ./ncm_host (or MEM_SIM=1 ./ncm_host)" diff --git a/patch_v2/ncm_app/build_ncm_sims.sh b/patch_v2/ncm_app/build_ncm_sims.sh new file mode 100755 index 0000000..df35497 --- /dev/null +++ b/patch_v2/ncm_app/build_ncm_sims.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# bench/... 各 sim 的宿主编译+运行(命令取自各 sim 文件头注释;agent 308 内存瘦身回归用)。 +# 在 ncm_app 目录下运行。 +set -e +cd "$(dirname "$0")" + +CC=gcc +INC="-DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd" +VINC="-Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include -Ivendor/mbedtls/include" +# 部分 sim 的 include 路径写法用 vendor/(window/search),有的用 ../vendor/(bench 内部相对), +# 统一用绝对相对路径(项目根 pub)。这里按各 sim 头部原样拼接。 + +run_sim () { echo "===== $1 ====="; ./$1; echo "[$1 exit=$?]"; } + +echo "==> build stream_sim" +gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \ + -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include \ + -c bench/stream_sim.c -o /tmp/ss.o +gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \ + -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include \ + -c app/play/stream_src.c -o /tmp/ss_src.o +gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \ + -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include \ + -c app/play/decoder.c -o /tmp/ss_dec.o +gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \ + -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include \ + -c app/play/audio_out.c -o /tmp/ss_ao.o +gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \ + -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include \ + -c core/ncm_cache.c -o /tmp/ss_cache.o +gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \ + -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include \ + -c core/ncm_common.c -o /tmp/ss_common.o +gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \ + -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include \ + -c core/ncm_http.c -o /tmp/ss_http.o +gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \ + -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include \ + -c core/ncm_json.c -o /tmp/ss_json.o +gcc -O2 -c -Ivendor/minimp3 app/play/minimp3_impl.c -o /tmp/ss_mp3.o +gcc -O2 -c -Ivendor/dr_flac app/play/dr_flac_impl.c -o /tmp/ss_flac.o +gcc -O2 -o stream_sim /tmp/ss.o /tmp/ss_src.o /tmp/ss_dec.o /tmp/ss_ao.o /tmp/ss_cache.o /tmp/ss_mp3.o /tmp/ss_flac.o -lpthread -lm +run_sim stream_sim + +echo "==> build player_flow_sim" +gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn -Ivendor/mbedtls/include \ + -c bench/player_flow_sim.c -o /tmp/pf.o +gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ + -Icore -Ivendor/jsmn -Ivendor/mbedtls/include -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include -c app/play/player_manager.c -o /tmp/pf_pm.o +gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ + -Icore -c app/play/stream_src.c -o /tmp/pf_ss.o +gcc -O2 -Wl,--gc-sections -o player_flow_sim /tmp/pf.o /tmp/pf_pm.o /tmp/pf_ss.o -lpthread -lm +run_sim player_flow_sim + +echo "==> build window_sim" +gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn -Ivendor/mbedtls/include \ + -c bench/window_sim.c -o /tmp/ws.o +gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ + -Icore -Ivendor/jsmn -Ivendor/mbedtls/include -c app/logic/appstate.c -o /tmp/ws_app.o +gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ + -Icore -Ivendor/jsmn -Ivendor/mbedtls/include -c app/logic/songmeta.c -o /tmp/ws_meta.o +gcc -O2 -Wl,--gc-sections -o window_sim /tmp/ws.o /tmp/ws_app.o /tmp/ws_meta.o -lpthread +run_sim window_sim + +echo "==> build search_sim" +gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn \ + -c bench/search_sim.c -o /tmp/sh.o +gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ + -Icore -Ivendor/jsmn -c app/logic/appstate.c -o /tmp/sh_app.o +gcc -O2 -Wl,--gc-sections -o search_sim /tmp/sh.o /tmp/sh_app.o -lpthread +run_sim search_sim + +echo "==> build cache_evict_sim" +gcc -O2 -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include -c bench/cache_evict_sim.c -o /tmp/ce.o +gcc -O2 -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include -c core/ncm_cache.c -o /tmp/ce_cache.o +gcc -O2 -o cache_evict_sim /tmp/ce.o /tmp/ce_cache.o -lpthread +run_sim cache_evict_sim + +echo "===== all sims done =====" diff --git a/patch_v2/ncm_app/core/ncm_api.c b/patch_v2/ncm_app/core/ncm_api.c index 1cefd27..4a455f9 100644 --- a/patch_v2/ncm_app/core/ncm_api.c +++ b/patch_v2/ncm_app/core/ncm_api.c @@ -9,6 +9,12 @@ #include #include +/* 响应缓冲上界(内存瘦身,OOM 修复):多数接口响应远小于 1MB,按需给尺寸避免每次调用 + * 都 malloc 1MB 堆峰值(播放中 song_url/lyric/song_detail 并发,几 × 1MB 是可砍的瞬态)。 + * 仅 playlist detail / track_all(全量 trackIds,最多 NCM_TRACKID_MAX 条)保留传统 1MB。 */ +#define RESP_SMALL (64 * 1024) /* song_url:url+字段,实际 ~1KB */ +#define RESP_MED (256 * 1024) /* lyric/song_detail/playlist/cloudsearch:实际 ≤100KB */ + const char *ncm_api_level_str(int level) { switch (level) { case NCM_LEVEL_SURROUND: /* "sky" + immerseType=c51(song_url 内处理) */ @@ -168,7 +174,7 @@ int ncm_api_user_playlist(const char *device_id, const char *cookie, const char api_json_t c; memset(&c, 0, sizeof c); int rc = api_post(device_id, cookie, "/api/user/playlist", json, NCM_CRYPT_WEAPI, - NCM_RESP_MAX, &c); + RESP_MED, &c); if (rc != NCM_OK) return rc; int arr = ncm_json_path(&c.j, "playlist"); @@ -248,7 +254,7 @@ int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t api_json_t c; memset(&c, 0, sizeof c); int rc = api_post(device_id, cookie, "/api/v3/song/detail", json, NCM_CRYPT_EAPI, - 2 * 1024 * 1024, &c); + RESP_MED, &c); if (rc != NCM_OK) return rc; int arr = ncm_json_path(&c.j, "songs"); @@ -271,7 +277,7 @@ int ncm_api_song_url(const char *device_id, const char *cookie, const char *id, api_json_t c; memset(&c, 0, sizeof c); int rc = api_post(device_id, cookie, "/api/song/enhance/player/url/v1", json, NCM_CRYPT_EAPI, - NCM_RESP_MAX, &c); + RESP_SMALL, &c); if (rc != NCM_OK) return rc; int d0 = ncm_json_path(&c.j, "data[0]"); @@ -497,7 +503,7 @@ int ncm_api_lyric(const char *device_id, const char *cookie, const char *id, api_json_t c; memset(&c, 0, sizeof c); int rc = api_post(device_id, cookie, "/api/song/lyric", json, NCM_CRYPT_EAPI, - NCM_RESP_MAX, &c); + RESP_MED, &c); if (rc != NCM_OK) return rc; if (lrc && lrc_sz) { lrc[0] = 0; ncm_json_path_str(&c.j, "lrc.lyric", lrc, lrc_sz); } if (tlrc && tlrc_sz) { tlrc[0] = 0; ncm_json_path_str(&c.j, "tlyric.lyric", tlrc, tlrc_sz); } @@ -521,7 +527,7 @@ static int cloudsearch_post(const char *device_id, const char *cookie, const cha snprintf(json, sizeof json, "{\"s\":\"%s\",\"type\":%d,\"offset\":%d,\"limit\":%d,\"total\":true}", esc, type, offset > 0 ? offset : 0, limit > 0 ? limit : 30); return api_post(device_id, cookie, "/api/cloudsearch/pc", json, NCM_CRYPT_WEAPI, - NCM_RESP_MAX, c); + RESP_MED, c); } /* 解析一处「歌单对象数组」(cloudsearch type=1000)。id 可为数字/字符串(均文本拷贝)。 */ diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index b41cd83..16b607a 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ diff --git a/patch_v2/ncm_app/ncm_host b/patch_v2/ncm_app/ncm_host index 6593ef8..5279db4 100755 Binary files a/patch_v2/ncm_app/ncm_host and b/patch_v2/ncm_app/ncm_host differ diff --git a/patch_v2/ncm_app/test/test_ncm_core_host b/patch_v2/ncm_app/test/test_ncm_core_host index 94ea275..10adbde 100755 Binary files a/patch_v2/ncm_app/test/test_ncm_core_host and b/patch_v2/ncm_app/test/test_ncm_core_host differ diff --git a/patch_v2/sdk/include/meow_ui_image.h b/patch_v2/sdk/include/meow_ui_image.h index 88c15e0..b2de71a 100644 --- a/patch_v2/sdk/include/meow_ui_image.h +++ b/patch_v2/sdk/include/meow_ui_image.h @@ -26,6 +26,9 @@ int meow_img_w(meow_img *img); int meow_img_h(meow_img *img); void meow_ui_img(int x, int y, meow_img *img); /* 原始尺寸 */ +/* 内存瘦身:最近邻等比缩放到最长边 ≤maxdim,返回新图;原图已 ≤maxdim 返回 NULL。 + * 调用方负责 free 旧图与返回值(若返回)。封面/背景大图驻留内存瘦身用。 */ +meow_img *meow_img_downscale(meow_img *img, int maxdim); void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *img); /* 最近邻缩放 */ void meow_ui_img_alpha(int x, int y, int w, int h, meow_img *img, int alpha); void meow_ui_img_cover(int x, int y, int size, meow_img *img); /* 居中裁方 → size×size */ diff --git a/patch_v2/sdk/meow_ui_img.c b/patch_v2/sdk/meow_ui_img.c index c2bd08b..96c37f0 100644 --- a/patch_v2/sdk/meow_ui_img.c +++ b/patch_v2/sdk/meow_ui_img.c @@ -96,6 +96,30 @@ static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover) return out; } +/* 内存瘦身:把大图按最近邻等比缩放到「最长边 ≤ maxdim」,返回新图;调用方负责 + * meow_img_free 旧图 + 新图。原图已 ≤maxdim → 返回 NULL(无需缩放)。 + * 典型:网易云全尺寸封面 1448×1448 RGBA=8MB,显示只需 ≤480px(屏幕 480 宽)→ + * 480×480=0.9MB,省 ~7MB anon(设备 RAM 仅 56MB,OOM 真机被杀)。 + * 复用 scale_buf(cover=0,最近邻锁比例),不做插值(App 封面/背景无需高质量缩放)。 */ +meow_img *meow_img_downscale(meow_img *im, int maxdim) +{ + if (!im || maxdim <= 0) return NULL; + if (im->w <= maxdim && im->h <= maxdim) return NULL; /* 已够小,不缩放 */ + int dw, dh; + if (im->w >= im->h) { + dw = maxdim; + dh = (int)((long)im->h * maxdim / im->w); + } else { + dh = maxdim; + dw = (int)((long)im->w * maxdim / im->h); + } + if (dw < 1) dw = 1; + if (dh < 1) dh = 1; + unsigned char *tmp = scale_buf(im, dw, dh, 0); + if (!tmp) return NULL; + return img_alloc(dw, dh, tmp); +} + void meow_ui_img(int x, int y, meow_img *im) { if (!im) return;