Files
HomeAgent/internal/agent/api/codec_chunkfast_golden_test.go
JianFeeeee 7748ec450e perf(api): SSE 分块 C 导航层 + 差分等价验收(实测更慢 ⇒ 默认关闭)
第三刀:把 ha_json_scan 接进 parseOpenAICompatibleStreamChunkFull。
**结论是否定的** —— 实测比原实现慢,故默认关闭并如实记录。这条提交的
价值在于「已钉死的正确性 + 已定位的根因 + 一条防静默回退的断言」。

## 设计:只做「结构导航」,序列化留在 Go

接线前实测出两条 wire 语义,它们让「整条解析全 C 化」不成立:
  §5.1 重复键是**字段级合并**,不是替换:
       {"choices":[{content:a}],"choices":[{reasoning:r}]} → 两个都保留。
       机制:json.Unmarshal 的 object() 收尾做 v.SetIndex(i, subv.v),
       而 subv 拿到的是**已存在元素的指针** ⇒ 第二次是叠加。
  §5.2 stringifyContent 的 default 分支 = json.Marshal(interface{}),
       即**重新序列化**:{"b":1,"a":2}→{"a":2,"b":1}(键排序)、
       1e2→100、<→\u003c、大 int 先舍入成 float64。
       逐值一致 = 复刻 Ryu 最短浮点 + map 键排序 + HTML 转义 + int 舍入。
两条都只在**取值**阶段需要,故 C 只回答「值在哪里」(零分配零解码),
类型检查靠「用相同的 Go 类型 unmarshal 相同形状的子树」保证,不靠 C 复刻规则。

## 实测:新路径比原实现慢(20000 次迭代)

| 场景 | 新路径 | 原实现 |
|---|---|---|
| content_ascii | 2016ns / 20allocs | 1325ns / 13allocs |
| toolcall      | 5854ns / 33allocs | 3270ns / 21allocs |
| usage         | 3170ns / 24allocs | 2832ns / 12allocs |

分配数**也变多**(20 vs 13),与「消除 GC 抖动」的初衷相反。

根因(逐项测量,非猜测):裸 cgo 调用 168ns;**每次带 out-param 的键查找
205ns + 2 allocs**(out-param 逃逸到堆);一次解析需要 5+ 次查找
⇒ 边界与分配成本约 1µs,恰好吃掉全部收益。Go 侧只需**一次** Unmarshal。
一句话:**用很多次廉价调用换一次昂贵调用,在这个尺寸上不划算。**

## 天花板实验:方向对,但当前实现没到

假设拿到 span 完全免费,只测设计中必须由 Go 做的部分:
  我的 Go 侧 505ns/7allocs  vs  原实现 1239ns/13allocs
⇒ 边界归零后仍有 2.4× 时间、46% 分配的空间。故问题在**逐字段往返**
这个交互方式,不在 C 本身。正确改造:一次调用返回全部字段 span +
结果写调用方栈结构体 + 仅在确需重新编码时回退。

## 正确性:6 万+ 差分用例全过

同一批输入跑两条路径逐字段比对(Content/Reasoning/Done/Finish/ToolCalls/
Usage + bool),5 组:协议形态(含全部回退触发条件)、真实负载、随机 JSON
30000 例、随机字节 30000 例、优化有效性。

★ 差分测试当场抓出 4 个真实缺陷(其中一个正是「优化压根没生效」):
 1. ha_sse_arr_first 里「重新 init 到 sc.s+sc.i」使 base 变了 ⇒ start 恒 0
    ⇒ 返回的是**数组本身**而非首元素。症状是**快速路径永远不生效**——
    而若只看「结果与 Go 一致」,这个 bug 会**完全隐形**(回退总是对的)。
    ⇒ 这就是必须单独断言「优化确实被走到」的原因。
 2. chunkAssemble 的 bool 被丢弃 ⇒ 空对象被判 true(原实现 false)
 3. 键匹配层级搞错:delta 是 **struct**(字段名 CI),不是 map。
    我一度「推理」成 CS 并以为差分测试会通过——错的。
    教教训:哪层是 struct、哪层是 map 要**回原实现读类型**,不能凭字段名推断。
 4. cgo 边界:out-param 逃逸到堆

另修:C 代码从 cgo 前言移进 csrc/ ——前言里的 C **逃出全部 C 门禁**
(告警/sanitizer/交叉/模糊测试),而它恰是本刀最易出错处。

## 防静默回退

TestChunkFast_BenchGate 断言 chunkFastEnabled 必须为 false。
后来者看到「快速路径写得全 + 差分测试全过」,很自然会以为它已生效并打开它
—— 而实测更慢。断言把这个事实钉住,改动即判红。

## 实测汇总
- C 契约 119 项断言、黄金对照 5 组、差分 6 万+ 例:全过
- ASan+UBSan PASS;gcc+clang 零告警;arm64 交叉 0 告警(3 个源文件)
- 全量 go test -count=1 ./... 38 包 ok / 0 FAIL
- libFuzzer 4948 万次零崩溃(上一刀)

教训(与第一刀同源):**「C 比 Go 快」不是前提,是待验证的假设。**
第一刀被 C.CString 的 82% 自找开销推翻一次,这一刀被逐字段往返推翻一次。
两次都是测量推翻直觉。
2026-09-26 10:32:20 +08:00

319 lines
13 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.

//go:build cgo
package api
// codec_chunkfast_golden_test.go —— C 快速路径 vs 原 Go 实现的**逐值差分对照**。
//
// ============================ 这是接线的唯一验收 ============================
// 快速路径是**纯优化**:它与 chunkParseGo 必须在所有输入上等价。
// 而这条等价性不能靠「读代码觉得对」——本刀前面已经有 7 个「读三遍都认为对」
// 的 C 缺陷。故这里用**差分测试**:同一批输入,两条路径,逐字段比对。
//
// 输入来源三类:
// ① 手工枚举的协议形态(含全部回退触发条件)
// ② 真实负载形状(content / toolcall / usage 块)
// ③ 随机 JSON(用 encoding/json 生成合法值再编码,覆盖嵌套与转义)
//
// 比对字段:整个 StreamChunk(Content / ReasoningContent / Done /
// FinishReason / ToolCalls / Usage)与 bool 返回值。
import (
"encoding/json"
"fmt"
"math/rand"
"reflect"
"strings"
"testing"
)
// diffChunk 逐字段比对两个结果,不同则报告首个差异点。
func diffChunk(t *testing.T, in string, fastCK StreamChunk, fastOK bool, goCK StreamChunk, goOK bool) {
t.Helper()
if fastOK != goOK {
t.Errorf("返回 bool 分歧 in=%q: fast=%v go=%v", in, fastOK, goOK)
return
}
if !fastOK {
return
}
if fastCK.Content != goCK.Content {
t.Errorf("Content 分歧 in=%q:\n fast=%q\n go =%q", in, fastCK.Content, goCK.Content)
}
if fastCK.ReasoningContent != goCK.ReasoningContent {
t.Errorf("ReasoningContent 分歧 in=%q:\n fast=%q\n go =%q",
in, fastCK.ReasoningContent, goCK.ReasoningContent)
}
if fastCK.Done != goCK.Done || fastCK.FinishReason != goCK.FinishReason {
t.Errorf("Done/FinishReason 分歧 in=%q: fast=(%v,%q) go=(%v,%q)",
in, fastCK.Done, fastCK.FinishReason, goCK.Done, goCK.FinishReason)
}
if !reflect.DeepEqual(fastCK.Usage, goCK.Usage) {
t.Errorf("Usage 分歧 in=%q:\n fast=%+v\n go =%+v", in, fastCK.Usage, goCK.Usage)
}
if len(fastCK.ToolCalls) != len(goCK.ToolCalls) {
t.Errorf("ToolCalls 数量分歧 in=%q: fast=%d go=%d",
in, len(fastCK.ToolCalls), len(goCK.ToolCalls))
} else {
for i := range fastCK.ToolCalls {
if !reflect.DeepEqual(fastCK.ToolCalls[i], goCK.ToolCalls[i]) {
t.Errorf("ToolCalls[%d] 分歧 in=%q:\n fast=%+v\n go =%+v",
i, in, fastCK.ToolCalls[i], goCK.ToolCalls[i])
}
}
}
}
func checkPair(t *testing.T, in string) {
t.Helper()
fastCK, fastOK, _ := chunkParseFast(in)
if !fastCK.Done && !fastOK {
// handled=false ⇒ 走 Go。这里要区分「回退」与「快速路径给出失败」:
}
// 真实入口(含回退)
gotCK, gotOK := parseOpenAICompatibleStreamChunkFull(in)
goCK, goOK := parseOpenAICompatibleStreamChunkFullGo(in)
diffChunk(t, in, gotCK, gotOK, goCK, goOK)
}
// ---------------------------------------------------------------------
// 1. 手工协议形态(覆盖所有回退触发条件)
// ---------------------------------------------------------------------
func TestChunkFast_ProtocolForms(t *testing.T) {
cases := []string{
// —— 真实负载三形态(应走快速路径)——
`{"id":"c1","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"content":"这是一段中文内容。"},"finish_reason":null}]}`,
`{"id":"c1","choices":[{"index":0,"delta":{"content":"hi"},"finish_reason":"stop"}]}`,
`{"id":"c1","choices":[{"index":0,"delta":{"reasoning_content":"thinking..."},"finish_reason":null}]}`,
`{"id":"c1","choices":[{"index":0,"delta":{},"finish_reason":null}]}`,
`{"id":"c1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"memory_recall","arguments":"{\"query\":\"x\"}"}}]},"finish_reason":null}]}`,
`{"id":"c1","choices":[],"usage":{"prompt_tokens":10,"completion_tokens":2,"total_tokens":12}}`,
`{"id":"c1","usage":{"prompt_tokens":10,"completion_tokens":2,"total_tokens":12}}`,
`{"usage":{"prompt":1,"completion":2,"total":3}}`,
`{"usage":{"prompt_tokens":1}}`,
`{"usage":{"prompt_cache_hit_tokens":5,"prompt_tokens":1,"total_tokens":2}}`,
`{"usage":{"prompt_tokens_details":{"cached_tokens":7},"prompt_tokens":1,"total_tokens":2}}`,
`{}`,
`{"choices":null}`,
`{"usage":null}`,
`{"choices":[{"delta":null}]}`,
`{"choices":[{"finish_reason":null}]}`,
`{"choices":[{"finish_reason":""}]}`,
`{"choices":[{"finish_reason":"length"}]}`,
// content 的各种界面
`{"choices":[{"delta":{"content":""}}]}`,
`{"choices":[{"delta":{"content":null}}]}`,
`{"choices":[{"delta":{"content":123}}]}`,
`{"choices":[{"delta":{"content":true}}]}`,
`{"choices":[{"delta":{"content":[{"type":"text","text":"a"},{"type":"text","text":"b"}]}}]}`,
`{"choices":[{"delta":{"content":[]}}]}`,
`{"choices":[{"delta":{"content":["x",{"text":"y"}]}}]}`,
`{"choices":[{"delta":{"content":[{"text":123},{"text":"ok"}]}}]}`,
`{"choices":[{"delta":{"content":"a\"b\\c\nd"}}]}`,
`{"choices":[{"delta":{"content":"你好😀"}}]}`,
`{"choices":[{"delta":{"content":"\u4f60\u597d"}}]}`,
// —— 必须回退 Go 的形态 ——
// §5.2:对象 content 需 json.Marshal 重新编码(键排序 + HTML 转义)
`{"choices":[{"delta":{"content":{"b":1,"a":2}}}]}`,
`{"choices":[{"delta":{"content":{"k":"<a>&b"}}}]}`,
`{"choices":[{"delta":{"content":{"nested":{"deep":[1,2]}}}}]}`,
`{"choices":[{"delta":{"content":1e2}}]}`,
`{"choices":[{"delta":{"content":1.0}}]}`,
`{"choices":[{"delta":{"content":0.1}}]}`,
`{"choices":[{"delta":{"content":123456789012345678}}]}`,
// §5.1:重复键
`{"choices":[{"delta":{"content":"a"}}],"choices":[{"delta":{"content":"b"}}]}`,
`{"choices":[{"delta":{"content":"a"}}],"choices":[{"delta":{"reasoning_content":"r"}}]}`,
`{"usage":{"prompt_tokens":1},"usage":{"completion_tokens":2}}`,
`{"choices":[{"delta":{"content":{"x":1},"content":"s"}}]}`,
// 尾部残留
`{"a":1}{"b":2}`,
`{"choices":[{"delta":{"content":"x"}}]} trailing`,
// 类型不符(应两侧都 false)
`{"choices":{}}`,
`{"usage":{"prompt_tokens":"1"}}`,
`{"usage":{"prompt_tokens":1.5}}`,
`{"usage":{"prompt_cache_hit_tokens":"x","prompt_tokens":1}}`,
`{"choices":[{"delta":{"reasoning_content":123}}]}`,
`{"choices":[{"delta":{"content":"x"},"finish_reason":42}]}`,
`{"choices":[{"delta":{"tool_calls":{}}}]}`,
`{"choices":[{"delta":{"tool_calls":[{"index":1.5,"function":{"name":"f"}}]}}]}`,
// 键大小写
`{"CHOICES":[{"DELTA":{"CONTENT":"ci"}}]}`,
`{"choices":[{"delta":{"content":[{"TEXT":"up"}]}}]}`,
`{"choices":[{"delta":{"content":[{"text":"low"}]}}]}`,
`{"CHOICES":[{"DELTA":{"CONTENT":"a"}}],"choices":[{"DELTA":{"CONTENT":"b"}}]}`,
// 畸形
``, `{`, `null`, `[]`, `"str"`, `123`, `{"a":}`, `{"a":1,}`,
`{'a':1}`, `{"a":1 `, `{"choices":[`, `{"choices":[{"delta":`,
}
for _, in := range cases {
checkPair(t, in)
}
}
// ---------------------------------------------------------------------
// 2. 真实负载形状(从实际网关抓的形态)
// ---------------------------------------------------------------------
func TestChunkFast_Realistic(t *testing.T) {
cases := []string{
`{"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1727000000,"model":"deepseek-v4.1-flash","choices":[{"index":0,"delta":{"content":"这是一段来自真实流式响应的中文内容,用于测量解析开销。"},"finish_reason":null}]}`,
`{"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1727000000,"model":"deepseek-v4.1-flash","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_9a","type":"function","function":{"name":"memory_recall","arguments":"{\"query\":\"用户偏好\",\"limit\":20}"}}]},"finish_reason":null}]}`,
`{"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1727000000,"model":"deepseek-v4.1-flash","choices":[{"index":0,"delta":{"content":""},"finish_reason":null}],"usage":{"prompt_tokens":3821,"completion_tokens":117,"total_tokens":3938,"prompt_cache_hit_tokens":3584,"prompt_cache_miss_tokens":237}}`,
// 流式续传:name 不重发但 function.arguments 继续
`{"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"a\":"}}]},"finish_reason":null}]}`,
`{"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"1}"}}]},"finish_reason":null}]}`,
// 扁平形态(顶层 name/arguments)
`{"choices":[{"delta":{"tool_calls":[{"index":0,"name":"f","arguments":{"a":1}}]},"finish_reason":null}]}`,
`{"choices":[{"delta":{"tool_calls":[{"index":0,"type":"function","function":{"name":"f","arguments":null}}]},"finish_reason":null}]}`,
`{"choices":[{"delta":{"tool_calls":[{"index":0,"type":"function","function":{"name":"f","arguments":123}}]},"finish_reason":null}]}`,
`{"choices":[{"delta":{"tool_calls":[{"index":0,"type":"function","function":{"name":"f","arguments":[1,2]}}]},"finish_reason":null}]}`,
`{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"c1"}]},"finish_reason":null}]}`,
`{"choices":[{"delta":{"tool_calls":[]},"finish_reason":null}]}`,
`{"choices":[{"delta":{"tool_calls":null},"finish_reason":null}]}`,
// reasoning 与 content 同时出现
`{"choices":[{"delta":{"reasoning_content":"r","content":"c"},"finish_reason":null}]}`,
// 多个 choices(只读 [0])
`{"choices":[{"delta":{"content":"first"},"finish_reason":"stop"},{"delta":{"content":"second"}}]}`,
// 未知字段(应忽略)
`{"choices":[{"delta":{"content":"x"},"unknown":{"deep":[1,2]}}],"zzz":1}`,
`{"choices":[{"delta":{"content":"x"},"logprobs":{"tokens":["a"]}}],"system_fingerprint":"fp_1"}`,
}
for _, in := range cases {
checkPair(t, in)
}
}
// ---------------------------------------------------------------------
// 3. 随机 JSON(合法值 → 编码 → 解析),差分
// ---------------------------------------------------------------------
func randJSONValue(rng *rand.Rand, depth int) interface{} {
if depth <= 0 {
switch rng.Intn(6) {
case 0:
return nil
case 1:
return rng.Intn(1000)
case 2:
return rng.Float64() * 100
case 3:
return rng.Intn(2) == 0
default:
return randomString(rng)
}
}
switch rng.Intn(8) {
case 0:
return map[string]interface{}{"a": randJSONValue(rng, depth-1)}
case 1:
return []interface{}{randJSONValue(rng, depth-1)}
case 2:
return map[string]interface{}{
"prompt_tokens": rng.Intn(9999),
"total_tokens": rng.Intn(9999),
"completion": rng.Intn(999),
"prompt_cache_hit_tokens": rng.Intn(10),
}
default:
return randJSONValue(rng, 0)
}
}
func randomString(rng *rand.Rand) string {
alphabet := []rune("abc中文😀\"\\\n\t<>äöü")
n := rng.Intn(12)
var sb strings.Builder
for i := 0; i < n; i++ {
sb.WriteRune(alphabet[rng.Intn(len(alphabet))])
}
return sb.String()
}
func TestChunkFast_RandomJSON(t *testing.T) {
rng := rand.New(rand.NewSource(20260926))
for iter := 0; iter < 30000; iter++ {
// 构造一个「像 SSE chunk」的随机对象
obj := map[string]interface{}{}
switch rng.Intn(4) {
case 0:
obj["choices"] = []interface{}{map[string]interface{}{
"index": rng.Intn(3),
"delta": map[string]interface{}{"content": randJSONValue(rng, 2)},
"finish_reason": []interface{}{nil, "", "stop", "length"}[rng.Intn(4)],
}}
case 1:
obj["choices"] = []interface{}{map[string]interface{}{
"delta": map[string]interface{}{
"reasoning_content": randomString(rng),
"content": randomString(rng),
},
}}
case 2:
obj["usage"] = map[string]interface{}{
"prompt_tokens": rng.Intn(1000),
"completion_tokens": rng.Intn(100),
"total_tokens": rng.Intn(1000),
}
default:
obj["choices"] = []interface{}{map[string]interface{}{
"delta": map[string]interface{}{
"tool_calls": []interface{}{map[string]interface{}{
"index": rng.Intn(3),
"id": randomString(rng),
"type": "function",
"function": map[string]interface{}{
"name": randomString(rng),
"arguments": randJSONValue(rng, 1),
},
}},
},
}}
}
b, err := json.Marshal(obj)
if err != nil {
continue
}
checkPair(t, string(b))
}
}
// ---------------------------------------------------------------------
// 4. 随机字节(畸形输入)——两侧都必须拒绝、且不得 panic
// ---------------------------------------------------------------------
func TestChunkFast_RandomBytes(t *testing.T) {
rng := rand.New(rand.NewSource(777))
alphabet := []byte(`{}[]",:0123456789tfnul \` + "\n\t\xff\x80")
for iter := 0; iter < 30000; iter++ {
n := rng.Intn(60)
b := make([]byte, n)
for i := range b {
b[i] = alphabet[rng.Intn(len(alphabet))]
}
checkPair(t, string(b))
}
}
// ---------------------------------------------------------------------
// 5. 快速路径**确实被用到**(否则「优化」是假的)
// ---------------------------------------------------------------------
func TestChunkFast_ActuallyHandlesRealistic(t *testing.T) {
realistic := []string{
`{"id":"c","choices":[{"index":0,"delta":{"content":"中文内容"},"finish_reason":null}]}`,
`{"id":"c","choices":[{"index":0,"delta":{"content":"x"},"finish_reason":"stop"}]}`,
`{"id":"c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"i","type":"function","function":{"name":"n","arguments":"{}"}}]}}]}`,
`{"id":"c","choices":[],"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3}}`,
}
for _, in := range realistic {
_, handled, decided := chunkParseFast(in)
if !handled || !decided {
t.Errorf("真实负载未走快速路径(优化失效): %s", in)
}
}
_ = fmt.Sprint()
}