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% 自找开销推翻一次,这一刀被逐字段往返推翻一次。
两次都是测量推翻直觉。
This commit is contained in:
JianFeeeee
2026-09-26 10:32:20 +08:00
parent 4d3962a845
commit 7748ec450e
10 changed files with 1703 additions and 77 deletions

View File

@ -0,0 +1,318 @@
//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()
}