openai_ws_replay_allocation_test.go 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. package service
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "runtime"
  6. "strings"
  7. "testing"
  8. "github.com/stretchr/testify/require"
  9. )
  10. // buildReplayTurnPayload 构造第 turn 轮的全量重发 payload:客户端历史逐轮追加
  11. // itemBytes 大小的项(Codex store=false 模式的真实形态)。
  12. func buildReplayTurnPayload(turn, itemBytes int) []byte {
  13. var b strings.Builder
  14. _, _ = b.WriteString(`{"type":"response.create","model":"gpt-5.5","stream":true,"input":[`)
  15. filler := strings.Repeat("x", itemBytes)
  16. for i := 1; i <= turn; i++ {
  17. if i > 1 {
  18. _, _ = b.WriteString(",")
  19. }
  20. _, _ = fmt.Fprintf(&b, `{"type":"input_text","text":"turn-%d-%s"}`, i, filler)
  21. }
  22. _, _ = b.WriteString(`]}`)
  23. return []byte(b.String())
  24. }
  25. // TestOpenAIWSReplayStateBuildAllocationBounded 长会话分配回归:128 turn、每轮
  26. // ~10KiB 增量,全量历史逐轮重发。replay 状态构建(build + 历史保存)必须共享
  27. // 正文而非逐轮深拷贝,否则累计分配为 O(T²)(本场景 >160MB);共享实现的累计
  28. // 分配与客户端重发总量同阶(~85MB payload 本身之外仅头数组与解析开销)。
  29. func TestOpenAIWSReplayStateBuildAllocationBounded(t *testing.T) {
  30. const (
  31. turns = 128
  32. itemBytes = 10 * 1024
  33. )
  34. payloads := make([][]byte, 0, turns)
  35. for turn := 1; turn <= turns; turn++ {
  36. payloads = append(payloads, buildReplayTurnPayload(turn, itemBytes))
  37. }
  38. var history []json.RawMessage
  39. historyExists := false
  40. delta := []json.RawMessage{json.RawMessage(`{"type":"function_call","id":"item_1","call_id":"call_1","name":"exec","arguments":"{}"}`)}
  41. runtime.GC()
  42. var before runtime.MemStats
  43. runtime.ReadMemStats(&before)
  44. for turn := 1; turn <= turns; turn++ {
  45. items, exists, err := buildOpenAIWSReplayInputSequence(history, historyExists, payloads[turn-1], turn > 1)
  46. require.NoError(t, err)
  47. require.True(t, exists)
  48. require.Len(t, items, turn)
  49. // 保存历史 + collector 增量合并(与 ingress/bridge 保存点同构)。
  50. history = combineOpenAIWSReplayItems(items, delta)
  51. historyExists = true
  52. // 下一轮 payload 含全部用户项但不含 collector 项,触发 sanitize+merge 路径中
  53. // 最常见的 prefix 分支比较。
  54. history = history[:len(history)-1]
  55. }
  56. var after runtime.MemStats
  57. runtime.ReadMemStats(&after)
  58. allocated := after.TotalAlloc - before.TotalAlloc
  59. // 正文共享实现全程约 2.5MB(头数组与 gjson 解析开销)。任何逐轮 O(H) 的
  60. // 字节级复制(extract 拷贝、正文 clone、保存点深拷贝)都会叠加 ≥84MB
  61. // (sum(t×10KiB))的量级,直接击穿此上界。
  62. const maxAllocatedBytes = 32 * 1024 * 1024
  63. require.Lessf(
  64. t,
  65. allocated,
  66. uint64(maxAllocatedBytes),
  67. "replay 状态构建累计分配 %d bytes 超出上界,疑似回归为逐轮深拷贝",
  68. allocated,
  69. )
  70. }