Skip to content

perf: /v1/responses 只释放了 chatReq,未释放原始 respReq(比 chatReq 还大一份) #31

Description

@xelr233

现象

/v1/responses 的 handleResponses 里,原始请求体 respReq 在 echoOpts 构造完之后就没有任何引用了,但它是用 let respReq 声明的,一直活到请求结束。

对比 handleChatCompletions —— 那边在 7488662 之后已经是 openaiReq = null,问题只出在新端点上。

const echoOpts = {
  instructions: respReq.instructions === undefined ? null : respReq.instructions,
  ...
  tools: respReq.tools || [],          // ← respReq 最后一次被读
};
const ccBody = buildCcRequest(chatReq);
const promptCacheKey = chatReq.prompt_cache_key;
chatReq = null;                        // 已经释放了转换后的树
                                       // ← 但没释放原始树

为什么这里值得单独一提

respReq 是原始 input items 形态,chatReq 是从它转换出来的。两者都在内存里时:

  • respReq.input 持有完整的 input item 数组(Codex 多轮对话时可达 MB 级)
  • chatReq.messages 又持有一份等价的、重新组织过的结构

也就是说这条路径比 chat 路径多一棵树,而 release 只做了一半。

关于收益(先把预期压下来)

我把这个改动单独量过,峰值 RSS 上没有可测收益:

run1 run2 run3
不释放 +265 MB +274 MB +314 MB
释放 respReq +281 MB +273 MB +282 MB

8MB body × 8 并发,慢上游把生命周期拉长到数秒,60ms 采样。

原因是峰值与驻留不是一回事 —— 放大发生在 parse + 重建 + JSON.stringify 期间,那时两棵树按定义都活着。释放只降低驻留,而 OOM 看的是峰值。

所以这条应该按 清理(少留一棵已经无用的树)而不是内存优化来评估。

建议

   const ccBody = buildCcRequest(chatReq);
   const promptCacheKey = chatReq.prompt_cache_key;
+  // respReq 是 Codex 送来的完整会话,比 chatReq 还大一份
   chatReq = null;
+  respReq = null;

我已经把它并进了 #23(那条 PR 现在覆盖三条路径:chat / messages / responses),如果你想单独处理也可以直接从那边摘。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions