Files
sub2api/backend/internal/service/openai_ws_replay_allocation_test.go
T
feeeei ca9b4d73f2 fix(openai-ws): share immutable replay bodies to stop O(T²) copying
OpenAI Responses WS/HTTP-bridge 的 replay 历史此前每轮对全部正文做多次
深拷贝(构建、双份序列、保存点各一遍),长会话累计分配 O(T²),实测
128 turn x 10KiB 达 160MB+,是 #6382 内存持续增长直至 OOM 的主因。

改为共享不可变正文所有权:
- extract 经零拷贝 string 视图 + gjson Index 直接切 payload 子切片,
  并以 json.Valid 保持对断裂数组的既有错误语义
- 序列构建/sanitize/collector/保存点全部只传引用;跨持有者保存经
  combineOpenAIWSReplayItems 新建头数组,正文不复制
- bridge 每轮一次解析,正常 replay 与 account-failover 序列共享正文
- 前缀比较增加字节级快路径,避免逐轮全历史 decode+encode
- strict 状态可用时不再整包保留上一轮 payload

新增 128 turn 分配回归测试锚定上界(实测 2.5MB,深拷贝回归会击穿),
以及正文共享的所有权断言。
2026-08-31 01:01:20 +08:00

81 lines
2.8 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 service
import (
"encoding/json"
"fmt"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
// buildReplayTurnPayload 构造第 turn 轮的全量重发 payload:客户端历史逐轮追加
// itemBytes 大小的项(Codex store=false 模式的真实形态)。
func buildReplayTurnPayload(turn, itemBytes int) []byte {
var b strings.Builder
_, _ = b.WriteString(`{"type":"response.create","model":"gpt-5.5","stream":true,"input":[`)
filler := strings.Repeat("x", itemBytes)
for i := 1; i <= turn; i++ {
if i > 1 {
_, _ = b.WriteString(",")
}
_, _ = fmt.Fprintf(&b, `{"type":"input_text","text":"turn-%d-%s"}`, i, filler)
}
_, _ = b.WriteString(`]}`)
return []byte(b.String())
}
// TestOpenAIWSReplayStateBuildAllocationBounded 长会话分配回归:128 turn、每轮
// ~10KiB 增量,全量历史逐轮重发。replay 状态构建(build + 历史保存)必须共享
// 正文而非逐轮深拷贝,否则累计分配为 O(T²)(本场景 >160MB);共享实现的累计
// 分配与客户端重发总量同阶(~85MB payload 本身之外仅头数组与解析开销)。
func TestOpenAIWSReplayStateBuildAllocationBounded(t *testing.T) {
const (
turns = 128
itemBytes = 10 * 1024
)
payloads := make([][]byte, 0, turns)
for turn := 1; turn <= turns; turn++ {
payloads = append(payloads, buildReplayTurnPayload(turn, itemBytes))
}
var history []json.RawMessage
historyExists := false
delta := []json.RawMessage{json.RawMessage(`{"type":"function_call","id":"item_1","call_id":"call_1","name":"exec","arguments":"{}"}`)}
runtime.GC()
var before runtime.MemStats
runtime.ReadMemStats(&before)
for turn := 1; turn <= turns; turn++ {
items, exists, err := buildOpenAIWSReplayInputSequence(history, historyExists, payloads[turn-1], turn > 1)
require.NoError(t, err)
require.True(t, exists)
require.Len(t, items, turn)
// 保存历史 + collector 增量合并(与 ingress/bridge 保存点同构)。
history = combineOpenAIWSReplayItems(items, delta)
historyExists = true
// 下一轮 payload 含全部用户项但不含 collector 项,触发 sanitize+merge 路径中
// 最常见的 prefix 分支比较。
history = history[:len(history)-1]
}
var after runtime.MemStats
runtime.ReadMemStats(&after)
allocated := after.TotalAlloc - before.TotalAlloc
// 正文共享实现全程约 2.5MB(头数组与 gjson 解析开销)。任何逐轮 O(H) 的
// 字节级复制(extract 拷贝、正文 clone、保存点深拷贝)都会叠加 ≥84MB
// (sum(t×10KiB))的量级,直接击穿此上界。
const maxAllocatedBytes = 32 * 1024 * 1024
require.Lessf(
t,
allocated,
uint64(maxAllocatedBytes),
"replay 状态构建累计分配 %d bytes 超出上界,疑似回归为逐轮深拷贝",
allocated,
)
}