Files
HomeAgent/internal/plugin/proc/ipc_floor_test.go
JianFeeeee a006237105 test(proc): 建立进程间通信的成本地板(判定「优化 IPC」的空间)
目标:回答「工具调用往返 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 全量测试绿。
2026-09-26 13:06:10 +08:00

82 lines
2.8 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.

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)
}
}