Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/stream_sim.c
T

454 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* bench/stream_sim.c — 边下边播全链离线验证(REPORT §21;NCM_STREAM_PLAY §8 阶段 0/1/2 矩阵)。
*
* 链路:假网络线程(可调速率/抖动/断流/断续网)→ 真实 ncm_cache 写者(write(2) 直写 .tmp)
* → 真实 stream_src(pread + condvar + fread 语义 read_cb)→ 真实 decoder
* (mp3=open_cb SEEK_TO_BYTE|DO_NOT_SCAN / flac=drflac_open 回调)→ 真实 audio_out(SINK_NULL)
* + PCM 捕获,与「同文件整首离线解码」逐字节比对(金标准;断流场景比到截断点)。
*
* 矩阵:速率 {10MB/s, 400kbps, 160kbps} × {正常播完, seek 已下载区, seek 未下载区等待,
* 40% 断流, 断续网};附加 flac 边下边播用例(dr_flac 回调路径)。
* 断言:跑完无死锁(看门狗)、PCM 逐字节一致(seek 用例除外,断流比前缀)、首开声延迟、
* underrun 统计(慢网/断续网 >0)、commit 后缓存文件与源逐字节一致。
*
* 编译(宿主,ncm_app 目录下):
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
* -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac \
* bench/stream_sim.c app/play/stream_src.c app/play/decoder.c app/play/audio_out.c \
* core/ncm_cache.c -c # 逐个 -c 后链接,或整体:
* gcc -O2 -Wall -Wextra ...同上... -o stream_sim ... app/play/minimp3_impl.o dr_flac_impl.o -lpthread -lm
* (minimp3_impl/dr_flac_impl 为 vendor 豁免 TU,单独 -O2 无 -Wall 编译)
* mipsel:换 $CC_MIPS -static,产物用 qemu-mipsel 跑。
* 运行:./stream_sim [mp3 路径] (0 网络;PASS/FAIL 退出码)
*/
#include "../app/ncm_app_internal.h" /* audio_out 声明 + 全局态(g_st/g_play/g_app_quit) */
#include "../app/play/decoder.h"
#include "../app/play/stream_src.h"
#include "../core/ncm_cache.h"
#include "../core/ncm_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <time.h>
#include <sys/stat.h>
/* ---- 全局态(audio_out.c / ncm_app_internal.h 需要) ---- */
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER };
volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 1;
/* ---- 桩:SDK/核心库其他 TU 符号(本 sim 不链) ---- */
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
uint64_t meow_ui_time_ms(void)
{
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
}
/* audio_out.c 的 AUDIOD 后端符号(本 sim 恒 NCM_AUDIO_SINK=null,不会调到;仅链接用) */
int meow_audio_open(int rate, int ch, int fmt) { (void)rate; (void)ch; (void)fmt; return -1; }
int meow_audio_write(const void *pcm, int frames) { (void)pcm; return frames; }
int meow_audio_pause(int sid, int paused) { (void)sid; (void)paused; return 0; }
int meow_audio_close(int sid) { (void)sid; return 0; }
int meow_audio_query(meow_audio_state *out) { (void)out; return -1; }
void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx) { (void)cb; (void)ctx; }
/* ncm_cache.c 依赖(同 cache_evict_sim 口径内联) */
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
{
size_t n = 0;
if (sz) { while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; } dst[n] = 0; }
while (src[n]) n++;
return n;
}
int ncm_store_mkdir_p(const char *path)
{
if (!path || !*path) return -1;
char tmp[512];
size_t len = strlen(path);
if (len >= sizeof tmp) return -1;
memcpy(tmp, path, len + 1);
if (tmp[len - 1] == '/') tmp[len - 1] = '\0';
for (char *p = tmp + 1; *p; p++) {
if (*p == '/') { *p = '\0'; mkdir(tmp, 0755); *p = '/'; }
}
return mkdir(tmp, 0755) == 0 || errno == EEXIST ? 0 : -1;
}
int ncm_store_read_cfg_int(const char *dir, const char *key, int dflt)
{ (void)dir; (void)key; return dflt; }
/* ---- 小工具 ---- */
static double now_s(void)
{
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + ts.tv_nsec / 1e9;
}
typedef struct { unsigned char *b; size_t n, cap; } pcm_buf_t;
static void pcm_put(pcm_buf_t *p, const void *data, size_t len)
{
if (p->n + len > p->cap) {
size_t nc = p->cap ? p->cap * 2 : (1 << 20);
while (nc < p->n + len) nc *= 2;
p->b = (unsigned char *)realloc(p->b, nc);
if (!p->b) { fprintf(stderr, "oom\n"); exit(2); }
p->cap = nc;
}
memcpy(p->b + p->n, data, len);
p->n += len;
}
static int g_fail = 0;
#define CHECK(cond, fmt, ...) do { \
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
} while (0)
/* ---- 假网络线程:按速率/抖动/断流/断续网 把源文件写进真实 ncm_cache 写者 ---- */
typedef struct {
const unsigned char *data;
size_t size;
uint64_t rate_Bps; /* 字节/秒 */
int jitter_ms; /* 每块延迟 ±jitter */
double cut_frac; /* >0: 写到该比例即断流(finish 带错误) */
int flaky; /* 断续网:每 ~6 块额外停 100-400ms */
stream_src_t *src;
ncm_cache_writer_t *w;
int rc; /* NCM_OK / 负 */
} net_t;
static void *net_thread(void *arg)
{
net_t *nt = (net_t *)arg;
const size_t CH = 4096;
size_t off = 0;
size_t cut_at = (nt->cut_frac > 0) ? (size_t)(nt->size * nt->cut_frac) : (size_t)-1;
unsigned rng = 12345;
int chunk_no = 0;
while (off < nt->size && off < cut_at) {
size_t n = nt->size - off;
if (n > CH) n = CH;
if (off + n > cut_at) n = cut_at - off;
/* 节奏:块/速率 + 抖动 */
double base_ms = nt->rate_Bps ? (double)n * 1000.0 / (double)nt->rate_Bps : 0.0;
rng = rng * 1103515245u + 12345u;
double jit = nt->jitter_ms ? ((double)((rng >> 16) % 200) / 100.0 - 1.0) * nt->jitter_ms : 0.0;
long sleep_ms = (long)(base_ms + jit);
if (nt->flaky && chunk_no % 6 == 5) {
rng = rng * 1103515245u + 12345u;
sleep_ms += 100 + (long)((rng >> 16) % 300); /* 断续网:额外停滞 */
}
if (sleep_ms > 0) usleep((useconds_t)sleep_ms * 1000);
if (ncm_cache_write(nt->w, nt->data + off, n) != NCM_OK) { nt->rc = NCM_ERR_IO; goto out; }
stream_src_note_written(nt->src, n);
off += n;
chunk_no++;
}
nt->rc = (off >= nt->size) ? NCM_OK : NCM_ERR_NET; /* cut=断流 */
out:
stream_src_finish(nt->src, nt->rc == NCM_OK ? 0 : nt->rc);
return NULL;
}
/* ---- 整文件离线解码(金标准 PCM) ---- */
static int decode_offline(const char *path, pcm_buf_t *pcm, int *rate, int *ch, int *bits,
int64_t *frames)
{
ncm_decoder_t *dec = NULL;
if (ncm_dec_open(&dec, path, rate, ch, bits, NULL) != 0) return -1;
static unsigned char buf[4096 * 2 * 4];
int64_t fr, total = 0;
while ((fr = ncm_dec_read(dec, buf, 4096)) > 0) {
pcm_put(pcm, buf, (size_t)fr * (size_t)(*ch) * (size_t)(*bits) / 8);
total += fr;
}
ncm_dec_close(dec);
if (frames) *frames = total;
return 0;
}
/* ---- 单条用例 ---- */
enum { SC_NORMAL = 0, SC_SEEK_DL, SC_SEEK_WAIT, SC_CUT, SC_FLAKY };
typedef struct {
pcm_buf_t pcm;
int64_t frames; /* 实际解码输出帧 */
int64_t frames_at_seek; /* seek 时已解码帧(非 seek 用例=0) */
int64_t seek_tgt_frames; /* seek 目标帧(非 seek 用例=0) */
int rate, ch, bits;
double first_sound_s; /* 网线程启动 → 首块 PCM */
int underruns;
uint64_t wait_ms;
double wall_s;
int open_ok;
} case_result_t;
static int run_case(const char *tag, const unsigned char *data, size_t size,
const char *ext, uint64_t rate_Bps, int jitter_ms, double cut_frac, int flaky,
int scenario, int64_t golden_total_frames, case_result_t *res)
{
memset(res, 0, sizeof *res);
char dir[] = "/tmp/ncm_stream_sim_XXXXXX";
if (!mkdtemp(dir)) { printf(" FAIL mkdtemp\n"); return -1; }
char cdir[600];
snprintf(cdir, sizeof cdir, "%s/cache", dir);
ncm_cache_t cache;
ncm_cache_init(&cache, cdir);
char key[32];
static int case_no = 0;
snprintf(key, sizeof key, "sim%d", case_no++);
ncm_cache_writer_t *w = NULL;
if (ncm_cache_open(&cache, key, ext, &w) != NCM_OK) { printf(" FAIL cache open\n"); return -1; }
stream_src_t *src = NULL;
if (stream_src_open_stream(&src, ncm_cache_writer_tmp_path(w)) != 0) {
printf(" FAIL stream open\n"); return -1;
}
net_t nt = { data, size, rate_Bps, jitter_ms, cut_frac, flaky, src, w, NCM_OK };
pthread_t nth;
double t0 = now_s();
if (pthread_create(&nth, NULL, net_thread, &nt) != 0) { printf(" FAIL pthread\n"); return -1; }
/* 预缓冲 ≥128KB(同 player_manager 门槛) */
double wd_t0 = now_s();
int wr;
while ((wr = stream_src_wait(src, 128 * 1024, 100)) == 0) {
if (now_s() - wd_t0 > 120) { printf(" FAIL prebuffer watchdog (deadlock?)\n"); g_fail = 1; _exit(1); }
}
if (stream_src_failed(src) && stream_src_written(src) < 128 * 1024) {
/* 首开声前失败(written < 预缓冲门槛)。已下过门槛的断流
*(如 cut40@10MB/s:finish 与预缓冲满足的竞态)照常放行,播到截断点(F5) */
printf(" FAIL download failed before prebuffer\n");
pthread_join(nth, NULL);
ncm_cache_abort(w); stream_src_close(src);
return -1;
}
ncm_decoder_t *dec = NULL;
int rate = 0, ch = 0, bits = 0;
if (ncm_dec_open_src(&dec, src, &rate, &ch, &bits, NULL) != 0) {
printf(" FAIL dec open\n");
pthread_join(nth, NULL);
ncm_cache_abort(w); stream_src_close(src);
return -1;
}
res->open_ok = 1;
res->rate = rate; res->ch = ch; res->bits = bits;
if (audio_out_open(rate, ch, bits) != 0) { printf(" FAIL audio open\n"); g_fail = 1; _exit(1); }
int64_t dur_ms = (golden_total_frames > 0 && rate > 0) ? golden_total_frames * 1000 / rate : 0;
static unsigned char pcm[4096 * 2 * 4];
int64_t decoded = 0; /* 实际解码输出帧(seek 断言口径) */
int seek_done = 0;
double first_sound = -1;
for (;;) {
if (now_s() - t0 > 150) { printf(" FAIL case watchdog (deadlock?)\n"); g_fail = 1; _exit(1); }
int64_t fr = ncm_dec_read(dec, pcm, 4096);
if (fr == 0) break;
if (first_sound < 0) first_sound = now_s() - t0;
if (audio_out_write(pcm, (int)fr) != (int)fr) { printf(" FAIL audio write\n"); g_fail = 1; _exit(1); }
pcm_put(&res->pcm, pcm, (size_t)fr * (size_t)ch * (size_t)bits / 8);
decoded += fr;
/* 实时步调(模拟 audio_out 背压节拍器,慢网才会真 underrun) */
if (rate > 0) usleep((useconds_t)(fr * 1000000 / rate));
/* seek 用例:播 ~1s 后 seek */
if (!seek_done && dur_ms > 0 && rate > 0 && decoded * 1000 / rate >= 1000) {
seek_done = 1;
int64_t tgt = 0;
if (scenario == SC_SEEK_DL) tgt = dur_ms / 4; /* 25%:预缓冲内,必已下载 */
else if (scenario == SC_SEEK_WAIT) tgt = dur_ms * 9 / 10; /* 90%:慢网未下载区,等待 */
if (tgt > 0) {
res->frames_at_seek = decoded;
res->seek_tgt_frames = tgt * rate / 1000;
ncm_dec_seek_ms(dec, tgt, (int64_t)size, dur_ms);
}
}
}
res->first_sound_s = first_sound;
res->frames = decoded;
stream_src_stats(src, &res->underruns, &res->wait_ms);
res->wall_s = now_s() - t0;
audio_out_close();
ncm_dec_close(dec);
pthread_join(nth, NULL);
stream_src_abort(src); /* 已完成则无副作用 */
/* commit/abort + 完整下载的落盘校验(缓存文件与源逐字节一致) */
if (nt.rc == NCM_OK) {
if (ncm_cache_commit(w) != 0) { printf(" FAIL commit\n"); g_fail = 1; }
else {
char fp[700];
if (ncm_cache_path(&cache, key, ext, fp, sizeof fp) == NCM_OK) {
FILE *f = fopen(fp, "rb");
if (!f) { printf(" FAIL committed file missing\n"); g_fail = 1; }
else {
unsigned char *got = (unsigned char *)malloc(size);
size_t rn = fread(got, 1, size, f);
int extra = fgetc(f);
fclose(f);
if (rn != size || extra != EOF || memcmp(got, data, size) != 0) {
printf(" FAIL committed bytes mismatch (rn=%zu size=%zu)\n", rn, size);
g_fail = 1;
}
free(got);
}
}
}
} else {
ncm_cache_abort(w);
}
stream_src_close(src);
printf(" [%s] wall=%.2fs first_sound=%.2fs frames=%lld underruns=%d wait=%llums\n",
tag, res->wall_s, res->first_sound_s, (long long)res->frames,
res->underruns, (unsigned long long)res->wait_ms);
return 0;
}
int main(int argc, char **argv)
{
(void)argc;
const char *mp3 = (argc > 1) ? argv[1] : "vendor/minimp3/vectors/performance/MIPSTest.mp3";
setenv("NCM_AUDIO_SINK", "null", 1);
/* 源文件入内存 */
FILE *f = fopen(mp3, "rb");
if (!f) { fprintf(stderr, "open %s failed\n", mp3); return 1; }
fseek(f, 0, SEEK_END); long fsz = ftell(f); fseek(f, 0, SEEK_SET);
unsigned char *data = (unsigned char *)malloc((size_t)fsz);
if (!data || fread(data, 1, (size_t)fsz, f) != (size_t)fsz) { fprintf(stderr, "read failed\n"); return 1; }
fclose(f);
size_t size = (size_t)fsz;
printf("src %s: %zu bytes\n", mp3, size);
/* 金标准:整首离线解码(同样走 cb 路径——ncm_dec_open 内部也是静态 stream_src) */
pcm_buf_t golden = {0};
int grate, gch, gbits;
int64_t gframes;
if (decode_offline(mp3, &golden, &grate, &gch, &gbits, &gframes) != 0) {
fprintf(stderr, "golden decode failed\n"); return 1;
}
printf("golden: %dHz x%d %dbit frames=%lld pcm=%zu bytes\n",
grate, gch, gbits, (long long)gframes, golden.n);
/* 40% 前缀金标准(断流用例比对到截断点) */
char prefix[] = "/tmp/ncm_stream_prefix_XXXXXX";
int pfd = mkstemp(prefix);
if (pfd < 0) return 1;
size_t cut_sz = size * 2 / 5;
if (write(pfd, data, cut_sz) != (ssize_t)cut_sz) { fprintf(stderr, "prefix write\n"); return 1; }
close(pfd);
pcm_buf_t golden_cut = {0};
int t1, t2, t3;
if (decode_offline(prefix, &golden_cut, &t1, &t2, &t3, NULL) != 0) {
fprintf(stderr, "golden prefix decode failed\n"); return 1;
}
unlink(prefix);
printf("golden-40%%: pcm=%zu bytes\n", golden_cut.n);
struct { const char *name; uint64_t Bps; } rates[] = {
{ "10MB/s", 10485760u },
{ "400kbps", 50000u },
{ "160kbps", 20000u },
};
case_result_t r;
int flaky_ur_total = 0; /* 断续网 underrun 跨速率聚合(见 flaky 用例注释) */
for (int ri = 0; ri < 3; ri++) {
const char *rn = rates[ri].name;
uint64_t Bps = rates[ri].Bps;
char tag[64];
snprintf(tag, sizeof tag, "normal@%s", rn);
printf("== CASE %s ==\n", tag);
if (run_case(tag, data, size, "mp3", Bps, 0, 0, 0, SC_NORMAL, gframes, &r) == 0) {
CHECK(r.pcm.n == golden.n && memcmp(r.pcm.b, golden.b, golden.n) == 0,
"PCM 与整首离线解码逐字节一致 (%zu/%zu)", r.pcm.n, golden.n);
if (Bps >= 1000000u)
CHECK(r.first_sound_s >= 0 && r.first_sound_s < 2.0,
"首开声 %.2fs < 2s", r.first_sound_s);
else if (Bps >= 50000u)
/* 物理下限:预缓冲 128KB / 50KB/s = 2.62s(F3 红线:MINIMP3_IO_SIZE 强制),
* <2s@400kbps 与该红线冲突——按可达阈值断言并在 REPORT §21 记录。 */
CHECK(r.first_sound_s >= 0 && r.first_sound_s < 4.0,
"首开声 %.2fs < 4s (128KB/50KBps=2.62s 下限)", r.first_sound_s);
if (Bps <= 20000u)
CHECK(r.underruns > 0, "慢网 underrun 统计 >0 (=%d)", r.underruns);
}
snprintf(tag, sizeof tag, "seek_dl@%s", rn);
printf("== CASE %s ==\n", tag);
if (run_case(tag, data, size, "mp3", Bps, 0, 0, 0, SC_SEEK_DL, gframes, &r) == 0) {
/* 期望 = seek 前已解码 + 目标之后剩余(字节近似 seek,±10% 容差) */
int64_t expect = r.frames_at_seek + (gframes - r.seek_tgt_frames);
double dev = expect > 0 ? (double)(r.frames - expect) / (double)expect : 1;
if (dev < 0) dev = -dev;
CHECK(dev < 0.10, "seek 已下载区后播完:decoded=%lld ≈ %lld (dev=%.1f%%)",
(long long)r.frames, (long long)expect, dev * 100);
}
snprintf(tag, sizeof tag, "seek_wait@%s", rn);
printf("== CASE %s ==\n", tag);
if (run_case(tag, data, size, "mp3", Bps, 0, 0, 0, SC_SEEK_WAIT, gframes, &r) == 0) {
int64_t expect = r.frames_at_seek + (gframes - r.seek_tgt_frames);
double dev = expect > 0 ? (double)(r.frames - expect) / (double)expect : 1;
if (dev < 0) dev = -dev;
CHECK(dev < 0.15, "seek 未下载区等待后播完:decoded=%lld ≈ %lld (dev=%.1f%%)",
(long long)r.frames, (long long)expect, dev * 100);
}
snprintf(tag, sizeof tag, "cut40@%s", rn);
printf("== CASE %s ==\n", tag);
if (run_case(tag, data, size, "mp3", Bps, 0, 0.4, 0, SC_CUT, gframes, &r) == 0) {
CHECK(r.pcm.n == golden_cut.n && memcmp(r.pcm.b, golden_cut.b, golden_cut.n) == 0,
"断流:PCM 与 40%% 前缀离线解码逐字节一致 (%zu/%zu)", r.pcm.n, golden_cut.n);
}
snprintf(tag, sizeof tag, "flaky@%s", rn);
printf("== CASE %s ==\n", tag);
if (run_case(tag, data, size, "mp3", Bps, 15, 0, 1, SC_FLAKY, gframes, &r) == 0) {
CHECK(r.pcm.n == golden.n && memcmp(r.pcm.b, golden.b, golden.n) == 0,
"断续网:PCM 逐字节一致 (%zu/%zu)", r.pcm.n, golden.n);
/* 单次断续网是否打空缓冲取决于解码速度(qemu 远慢于宿主→缓冲消耗慢→可能 0 次),
* 属时序非确定性——不做单用例硬断言,跨速率聚合见循环后。 */
flaky_ur_total += r.underruns;
}
}
CHECK(flaky_ur_total > 0, "断续网跨速率 underrun 聚合 >0 (=%d)", flaky_ur_total);
/* flac 边下边播兼容(dr_flac 回调路径接到 stream_src) */
const char *flac = "out/test-vectors/sweep_44k1_16.flac";
FILE *ff = fopen(flac, "rb");
if (!ff) {
printf("== CASE flac_stream == SKIP(%s 不存在;§19.9 可一键重造)\n", flac);
} else {
fseek(ff, 0, SEEK_END); long fz = ftell(ff); fseek(ff, 0, SEEK_SET);
unsigned char *fdata = (unsigned char *)malloc((size_t)fz);
if (!fdata || fread(fdata, 1, (size_t)fz, ff) != (size_t)fz) { fprintf(stderr, "flac read\n"); return 1; }
fclose(ff);
pcm_buf_t fg = {0};
int64_t fgframes;
if (decode_offline(flac, &fg, &t1, &t2, &t3, &fgframes) != 0) { fprintf(stderr, "flac golden\n"); return 1; }
printf("== CASE flac_stream@400kbps ==\n");
if (run_case("flac_stream@400kbps", fdata, (size_t)fz, "flac",
50000u, 10, 0, 1, SC_NORMAL, fgframes, &r) == 0) {
CHECK(r.rate == 44100 && r.bits == 16, "flac 参数 44.1k/16bit (=%d/%d)", r.rate, r.bits);
CHECK(r.pcm.n == fg.n && memcmp(r.pcm.b, fg.b, fg.n) == 0,
"flac 边下边播 PCM 逐字节一致 (%zu/%zu)", r.pcm.n, fg.n);
}
free(fdata);
free(fg.b);
}
free(golden.b); free(golden_cut.b); free(data);
printf(g_fail ? "== STREAM SIM: FAIL ==\n" : "== STREAM SIM: ALL PASS ==\n");
return g_fail;
}