mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-15 23:22:59 +08:00
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,深拷贝回归会击穿), 以及正文共享的所有权断言。
81 lines
2.8 KiB
Go
81 lines
2.8 KiB
Go
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,
|
||
)
|
||
}
|