From a006237105ddb13e43c92a3a0cb9d54da66d43e9 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 26 Sep 2026 13:06:10 +0800 Subject: [PATCH] =?UTF-8?q?test(proc):=20=E5=BB=BA=E7=AB=8B=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=E9=97=B4=E9=80=9A=E4=BF=A1=E7=9A=84=E6=88=90=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E6=9D=BF=EF=BC=88=E5=88=A4=E5=AE=9A=E3=80=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20IPC=E3=80=8D=E7=9A=84=E7=A9=BA=E9=97=B4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 目标:回答「工具调用往返 30µs 里,非编解码的 ~20µs 花在哪、能否优化」。 方法:先立地板 —— 任何跨进程方案都有 OS 调度决定的下界。 三层对照(同机同会话,3000 次迭代): | 层 | ns/op | allocs | |---|---:|---:| | ① OS 调度地板(cat 子进程管道 echo,无协议无 JSON) | **16071** | 0 | | ② 最简 RPC(无载荷、不经共享帧) | **25840** | 20 | | ③ 纯编解码(共享段 write+read+compact,纯内存) | 10200 | 84 | ## 两条判据 1. **① 已占 ② 的 62%**:一个什么都不做的 echo(两次进程唤醒 + 两次管道读写)就要 16µs。⇒ RPC 层的成本主要是 **OS 调度**, 不是协议解析或 JSON 序列化。 2. **②−① 只剩约 10µs**:这才是协议层(JSON 帧 + pending map + channel 握手)可优化的全部空间,且其中还包含一次真实的 JSON 编解码往返。⇒ 「优化 IPC」的理论上限约为端到端 30µs 的三分之一。 ## 结论 跨进程数据面的**协议侧已接近其地板**。若要把端到端再压下去, 方向不是「优化协议」,而是**改变通信形态本身**: · 批量调用(一次往返做多件事,摊薄固定调度成本) · 或对高频小调用改走共享内存 + 自旋/事件通知(绕过两次进程唤醒) 两者都是架构级改动,不是参数调优。 ★ 这也解释了此前几轮的困惑:为什么 C 化数据面收益总是很小 —— 因为真正的大头(OS 调度 16µs)与语言无关。 验证:基准可复现;internal/plugin/proc 全量测试绿。 --- internal/plugin/proc/ipc_floor_test.go | 81 ++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 internal/plugin/proc/ipc_floor_test.go diff --git a/internal/plugin/proc/ipc_floor_test.go b/internal/plugin/proc/ipc_floor_test.go new file mode 100644 index 0000000..fd7e706 --- /dev/null +++ b/internal/plugin/proc/ipc_floor_test.go @@ -0,0 +1,81 @@ +package proc + +// ipc_floor_test.go —— 进程间通信的**成本地板**(纯测量,判定「优化 IPC」是否值得)。 +// +// ============================ 为什么需要这个文件 ============================ +// 本轮的目标是回答:「工具调用往返 30µs 里,那 ~20µs 非编解码部分花在哪、 +// 能否优化」。而回答这类问题必须先建立**地板**:任何跨进程方案都有一个 +// 由 OS 调度决定的下界,低于它是不可能达到的。 +// +// 于是做了三层对照(同一台机、同一次会话): +// +// ① OS 调度地板 `cat` 子进程管道 echo(无协议、无 JSON、无分配) +// ② 裸 RPC 最简 method、无载荷、无共享帧 +// ③ 纯编解码 共享段 write+read+compact(纯内存,不跨进程) +// +// ★ 判据:若 ① 已经接近 ②,则 RPC 层的开销主要是**OS 调度**而非协议/JSON; +// 那么「优化 IPC」的空间就只剩下 ②−① 那一小段,而不是整个 ②。 +// 没有地板数,任何「还能再快 X%」的说法都是空话。 + +import ( + "os/exec" + "testing" +) + +// BenchmarkIPC_OSFloorRawPipeEcho 建立跨进程往返的**绝对地板**: +// 一个 `cat` 子进程,父进程写一行、读回一行。不含任何协议解析。 +// +// 这个是「无论怎么优化协议都不可能低于」的量级 —— 它只包含 +// 两次进程唤醒(父→子、子→父)+ 两次管道读写。 +func BenchmarkIPC_OSFloorRawPipeEcho(b *testing.B) { + cmd := exec.Command("cat") + stdin, err := cmd.StdinPipe() + if err != nil { + b.Fatalf("StdinPipe: %v", err) + } + stdout, err := cmd.StdoutPipe() + if err != nil { + b.Fatalf("StdoutPipe: %v", err) + } + if err := cmd.Start(); err != nil { + b.Fatalf("Start: %v", err) + } + defer func() { + _ = stdin.Close() + _ = cmd.Wait() + }() + + msg := []byte("{\"id\":1,\"method\":\"x\"}\n") + buf := make([]byte, 4096) + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := stdin.Write(msg); err != nil { + b.Fatalf("write: %v", err) + } + if _, err := stdout.Read(buf); err != nil { + b.Fatalf("read: %v", err) + } + } +} + +// BenchmarkIPC_MinimalRPC 最简 RPC 往返:无载荷、不经共享帧。 +// +// ★ 选 output.invoke 而不是 tool.invoke:前者不要求插件注册任何东西, +// 插件会回一个「未实现」错误 —— 但**往返已经完成**。 +// 本基准测的是**传输成本**,因此应答内容无关紧要 +// (且 Call 的错误已被忽略,不会中断计时)。 +func BenchmarkIPC_MinimalRPC(b *testing.B) { + bin := buildBenchPlugin(b, "echoplugin.go") + p, err := Spawn("echo", bin, Options{Handler: noopHandler}) + if err != nil { + b.Fatalf("Spawn: %v", err) + } + defer p.Kill() + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = p.Call(MethodOutputInvoke, nil) + } +}