Files
acfunlive-remake/command_len_test.go
T
tamakyi a2b7185a7b 头像链接落盘 + 并发获取 + 单块上限按命令行实测值收紧
一、头像链接的反查结果落盘(avatar_cache/uid_urls.jsonl,7 天有效期)
   老录播的 jsonl 里没有头像链接,合并时要按 uid 向 AcFun 反查;但结果原来只存在内存里,
   进程一重启就得把整场 uid 重新查一遍。实测一个 uid 约 1 秒,几百人的场次要几分钟,
   日志还跟着打「正在获取头像:已处理 x/y 个用户」,看起来就像在重新下载头像
   (实测图片缓存本身是好的:同进程第二轮、重启进程后第三轮都是「新增 0、重写 0」)。
   现在查到就落盘(追加写,加载时顺手压掉重复/过期行),重启后直接读记录不再查。
   有效期 7 天是为了主播换头像后能自己更新过来。

二、头像获取改成 6 路并发
   查链接和下图都要走网络,串行做几百人要好几分钟。每个任务开头仍过闸门,
   暂停/停止的响应不受影响(任务走带缓冲的 channel,worker 提前退出也不会卡住投递端)。

三、日志分清楚「复用」和「真下载」
   原来的 looked++ 只看「这一条没有链接」,内存记忆命中时也会打印「已按 uid 向 AcFun 反查」,
   误导成在重下。现在汇总成一句:链接复用 X / 向 AcFun 查 Y,图片复用缓存 Z / 新下载 W。

四、单块条数上限从 200 收紧到 120(线上事故)
   线上一次 4 万条弹幕的合并在第 73 块失败:
   "fork/exec ...: The filename or extension is too long"(就是 200 条上限那一版)。
   真机实测:一块里每条弹幕来自不同用户时,每条占「-i 头像路径 + scale 行 + overlay 行」
   约 241 字符 —— 120 条 28864 字符、200 条 48288 字符,而 Windows 的 CreateProcess 限 32767。
   现在上限 120(给中文路径和其它参数留 3KB 余量),并加了一道自检:真超了就报
   「ffmpeg 命令过长(N 字符):这一块有 X 条弹幕、Y 个头像」,而不是烂在 exec 里。

   顺带试过 -filter_complex_script(滤镜写文件、命令行只留文件名),
   实测这个 ffmpeg 构建没有该选项(Unrecognized option),所以走收紧条数这条路。

验证:
- gofmt / go vet / go build / GOOS=linux go build 通过;go test 全绿(11.0s);
  go test -race 全绿(15.7s);staticcheck -checks=all 零告警
- 新增 avatar_cache_test.go(11 个)与 command_len_test.go(3 个)用例;反向验证扩到 17 项全过
- 隔离实例:e2e.py 36 项、smoke_extra.py 33 项全过
- 新增 smoke_manyuid.py(300 条密集弹幕 / 300 个不同用户 / 全带头像,就是线上事故的复现场景):
  合并成功、分 3 块(120/120/60 条)、日志无「命令过长」;
  第二次合并同一场「图片复用缓存 300 / 新下载 0」
2026-09-26 11:08:26 +08:00

77 lines
3.1 KiB
Go
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.
package main
import (
"fmt"
"path/filepath"
"strings"
"testing"
)
// 按最坏情况(每条弹幕一个不同用户、每个都带头像)渲染一块,返回这一块的命令行长度。
// 长度从 renderChunk 自己的自检错误里读出来;没触发自检就返回 (0, false)。
//
// 线上的那次事故就是这里没守住:单块 200 条时实测 48288 字符,远超 Windows 的 32767,
// 而 exec 只回一句 "The filename or extension is too long",看不出跟弹幕数量有关。
func chunkCommandLenProbe(t *testing.T, n int) (length int, over bool) {
t.Helper()
dir := t.TempDir()
avatars := make(map[int64]string, n)
records := make([]danmuRecord, 0, n)
for i := 0; i < n; i++ {
uid := int64(1000000 + i)
// 头像路径按真实缓存目录的形态造(uid_12位摘要.png),故意做得和线上一样长
avatars[uid] = filepath.Join(dir, avatarCacheDir,
fmt.Sprintf("%d_%012x.png", uid, i*0x9e3779b1))
records = append(records, danmuRecord{
Time: float64(i) * 0.05, UID: uid, Name: "观众甲",
Content: "这条弹幕稍微长一点测试宽度", Lane: i % 20, Width: 480,
})
}
ctx := &burnContext{
ffmpegFile: filepath.Join(dir, "没有这个-ffmpeg.exe"), // 自检在 exec 之前,跑不到它
outFile: filepath.Join(dir, "out.mp4"),
meta: danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 42},
avatars: avatars,
dir: dir,
}
err := ctx.renderChunk(danmuChunk{start: 0, end: 60, records: records}, "chunk.ass", ctx.outFile, false, nil)
if err == nil {
t.Fatal("用不存在的 ffmpeg 路径不该成功")
}
if !strings.Contains(err.Error(), "命令过长") {
return 0, false
}
if _, scanErr := fmt.Sscanf(err.Error(), "ffmpeg 命令过长(%d 字符", &length); scanErr != nil {
t.Fatalf("没能从错误里读出长度:%v / %s", scanErr, err)
}
return length, true
}
// 按硬上限切出来的块,在最坏情况下也不能顶破命令行上限
func TestChunkCommandLengthUnderLimit(t *testing.T) {
if length, over := chunkCommandLenProbe(t, maxDanmuPerChunkHard); over {
t.Fatalf("按硬上限 %d 条渲染,命令长达 %d 字符,已经超过安全值 %d",
maxDanmuPerChunkHard, length, maxCommandLineLen)
}
}
// 块大到一定程度必须被自检拦住,并报出「多少条弹幕、多少个头像」这种能定位的信息
func TestRenderChunkReportsTooLongCommand(t *testing.T) {
length, over := chunkCommandLenProbe(t, 300)
if !over {
t.Fatal("300 条的最坏情况应当被自检拦住,否则交给 exec 只会报一句看不出原因的错")
}
if length <= maxCommandLineLen {
t.Fatalf("自检报出的长度 %d 应当大于阈值 %d", length, maxCommandLineLen)
}
}
// 硬上限本身要留出足够的余量:按实测每条约 241 字符,上限条数不能逼近 32767
func TestChunkHardLimitLeavesHeadroom(t *testing.T) {
const perRecordWorstCase = 241 // 2026-09-26 真机实测(每条一个不同用户)
if est := maxDanmuPerChunkHard*perRecordWorstCase + 200; est > maxCommandLineLen {
t.Fatalf("硬上限 %d 条按最坏情况估算要 %d 字符,超过了安全值 %d",
maxDanmuPerChunkHard, est, maxCommandLineLen)
}
}