chore(shm): 补齐 §13 验证项,删除统一区域遗留死代码

三件事,都是 plan.md §13 里未打勾的项:

1. §13.3「Bench: ToolInvoke 延迟对比」补测(bench_test.go)
   改造后原有的 BenchmarkToolInvoke 仍走 legacy 内联 Args,已不代表生产
   路径。拆成 inline / frame 两个子基准并各测两个尺寸(-count=3 取中位):

     payload      inline(RPC 报文)   frame(共享内存帧)   差
     16 B          26.4 µs/op           48.0 µs/op        +21.6 µs
     32 KiB       695.6 µs/op          391.7 µs/op       −303.9 µs

   取舍随尺寸翻转:小 payload 多付 ~22µs 帧固定开销,大 payload 省掉整份
   JSON 编解码与管道拷贝、快 44%。因为线上工具结果动辄几十 KB 且 22µs
   相对 LLM 往返 2-8 秒可忽略,所以统一走帧而不按大小分叉(§13.3 第 4 条)
   是对的。

2. 删除 §13.1 遗留死代码(evtring.go)
   allocEvtRing 自统一共享区域之后从未被调用——旧版它单独建 memfd + eventfd,
   现在只有一个 memfd,事件环只是区内的一个 segment(NewEvtRing 操作区内
   切片)。留着会让人误以为事件环还有独立段。

3. 修正误导性注释(plugin.go)
   仍写着旧 3-fd 布局「3=StageContext, 4=事件环, 5=通知」,与实现
   (procExtraFilesForShm 只返回 memfd+evtfd)和 shmpass_unix.go 的权威
   注释互相矛盾。

plan.md:§13.1/13.3/13.4/13.8 的验证项按实测打勾,并注明证据(提交号 /
测试名 / 实测数字),不留无依据的勾。
This commit is contained in:
JianFeeeee
2026-09-10 21:18:16 +08:00
parent e218d0f100
commit b71a88a471
4 changed files with 97 additions and 38 deletions

View File

@ -4,6 +4,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
@ -29,6 +30,18 @@ import (
// EvtRingWritePushConcurrent 83 ns/op ← 并发不恶化
// StageInvokeSharedMemory 132 µs/op ← 含 3 次进程间往返
// SegmentWriteAllReadInto 3.7 µs/op ← 占 stage 的 2.8%
//
// Payload 承载方式对比2026-09-10同机§13.3 取消按大小切内联后补测,
// -count=3 取中位):
//
// payload inlineRPC 报文) frame共享内存帧
// 16 B 26.4 µs/op 48.0 µs/op +21.6 µs
// 32 KiB 695.6 µs/op 391.7 µs/op 303.9 µs
//
// 取舍随 payload 大小翻转:小 payload 多付 ~22µs 的帧分配/读写固定开销,
// 大 payload 省掉整份 JSON 编解码与管道字节拷贝,快 44%。因为线上工具结果
// 动辄几十 KB且 22µs 相对 LLM 往返 2-8 秒可忽略,所以统一走帧而不是
// 按大小分叉——分叉还多一条只在小 payload 上才跑的分支要维护。
// buildBenchPlugin 编译 testdata 里的测试插件benchmark 版)。
func buildBenchPlugin(b *testing.B, srcName string) string {
@ -46,11 +59,33 @@ func buildBenchPlugin(b *testing.B, srcName string) string {
return bin
}
// BenchmarkToolInvoke 测量内核 → 插件的工具调用往返
// BenchmarkToolInvoke 对比工具调用 payload 的两种承载方式§13.3「延迟对比」)
//
// 链路Call 写 stdin → 插件读循环 → handler → 写 stdout →
// 内核 readLoop → pending channel 唤醒。对照实验 11 的 19.6µs。
// inline — legacy 内联:参数 JSON 直接放进 RPC 报文
// frame — 生产路径:内核 Alloc 帧payload 全在共享内存RPC 只传偏移描述符
//
// §13.3 取消了按大小切内联的分支,所以 frame 才是线上真实开销inline 仅留给
// 直连 RPC 的测试process/bench 不建 Host拿不到共享内存。两者共用同一条
// RPC 通道,差值就是「把 payload 挪进共享内存」的净成本:小 payload 会因为
// 多几次分配/拷贝而略慢,大 payload 则省掉整份 JSON 编解码与管道字节拷贝。
//
// 两个尺寸都测,是因为这一改动的取舍正好随 payload 大小翻转——只看单点
// 很容易得出相反结论。对照实验 11 的工具调用 RPC p50 = 19.6µs。
func BenchmarkToolInvoke(b *testing.B) {
for _, sz := range []struct {
name string
n int
}{
{"small", 16},
{"large", 32 << 10},
} {
b.Run("inline/"+sz.name, func(b *testing.B) { benchmarkToolInvokeInline(b, sz.n) })
b.Run("frame/"+sz.name, func(b *testing.B) { benchmarkToolInvokeFrame(b, sz.n) })
}
}
// benchmarkToolInvokeInline 是改造前的老路径:参数随 RPC 报文一起过管道。
func benchmarkToolInvokeInline(b *testing.B, payloadSize int) {
bin := buildBenchPlugin(b, "echoplugin.go")
p, err := Spawn("echo", bin, Options{Handler: noopHandler})
@ -59,7 +94,7 @@ func BenchmarkToolInvoke(b *testing.B) {
}
defer p.Kill()
args := map[string]interface{}{"text": "benchmark"}
args := map[string]interface{}{"text": strings.Repeat("a", payloadSize)}
b.ResetTimer()
for i := 0; i < b.N; i++ {
@ -72,6 +107,42 @@ func BenchmarkToolInvoke(b *testing.B) {
}
}
// benchmarkToolInvokeFrame 是 §13.3 之后的生产路径:内核 Alloc 帧 → 参数写帧
// 前段 → RPC 只传 {frame, args_len} → 插件从帧读参数、结果写回帧结果区 →
// 内核读回并归还整帧(调用帧模型,见 plan.md §13.3)。
func benchmarkToolInvokeFrame(b *testing.B, payloadSize int) {
bin := buildBenchPlugin(b, "stageplugin.go")
core := newFakeCore()
host, err := NewHost()
if err != nil {
b.Fatalf("NewHost: %v", err)
}
defer host.Close()
p := New("demo", bin, b.TempDir(), nil, host, nil)
if err := p.Start(core); err != nil {
b.Fatalf("Start: %v", err)
}
defer p.Close()
core.mu.Lock()
h, ok := core.tools["demo_big"]
core.mu.Unlock()
if !ok {
b.Fatal("插件应注册 demo_big 工具")
}
args := map[string]interface{}{"text": strings.Repeat("a", payloadSize)}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := h(args); err != nil {
b.Fatalf("第 %d 次调用失败: %v", i, err)
}
}
}
// BenchmarkStageLockArbitration 测量**内核侧锁仲裁本身**的成本。
//
// ⚠️ 不要拿这个数字对照实验 3 的 19.40µs——两者测的不是同一个东西

View File

@ -4,7 +4,6 @@ import (
"encoding/binary"
"encoding/json"
"fmt"
"os"
"sync"
"sync/atomic"
"time"
@ -124,26 +123,6 @@ func NewEvtRing(data []byte) (*EvtRing, error) {
}, nil
}
// allocEvtRing 创建事件环共享段memfd + mmap返回 (段句柄, mmap数据, eventfd fd)。
// 段句柄通过 ExtraFiles 传给子进程fd 4eventfdfd 5也通过 ExtraFiles 传。
func allocEvtRing() (*os.File, []byte, int, error) {
ringfd, ringData, err := allocShm(evtTotalSize)
if err != nil {
return nil, nil, -1, fmt.Errorf("创建事件环段: %w", err)
}
// 初始化头
binary.LittleEndian.PutUint32(ringData[evtOffMagic:], evtRingMagic)
binary.LittleEndian.PutUint32(ringData[evtOffVersion:], evtRingVersion)
binary.LittleEndian.PutUint32(ringData[evtOffCap:], evtRingCap)
efd, err := evtfdCreate()
if err != nil {
freeShm(ringfd, ringData)
return nil, nil, -1, fmt.Errorf("创建 eventfd: %w", err)
}
return ringfd, ringData, efd, nil
}
func (r *EvtRing) Init() {
binary.LittleEndian.PutUint32(r.data[evtOffMagic:], evtRingMagic)
binary.LittleEndian.PutUint32(r.data[evtOffVersion:], evtRingVersion)

View File

@ -134,8 +134,9 @@ func (p *Plugin) Start(core CoreSDK) error {
proc, err := Spawn(p.name, p.bin, Options{
Dir: p.dir,
// 共享段的传递机制按平台不同shmpass_*.go
// Unix 经 ExtraFiles 传继承 fd 3=StageContext, 4=事件环, 5=通知);
// Windows 无 fd 继承语义,改用命名内核对象,名字经环境变量传入。
// Unix 经 ExtraFiles 传继承 fd3=统一共享内存区域 SuperBlock+StageContext+EvtRing
// 4=事件通知 eventfdWindows 无 fd 继承语义,改用命名内核对象,名字经环境变量传入。
// 权威定义在 shmpass_unix.go 的 procExtraFilesForShm修改时三处必须同步。
Env: append(p.env, p.host.procEnvForShm()...),
ExtraFiles: p.host.procExtraFilesForShm(),
ShmSize: p.host.shmSize,

30
plan.md
View File

@ -1140,12 +1140,17 @@ SDK 仓 `v1.0.0` / `v1.1.0`。main 的版本路牌现为 `1.2.0`(尚无 tag
**验证**
- [ ] SuperBlock 写入读回一致
- [ ] StageContext 并发改写 0 lost update
- [ ] 事件环 post-and-forget 仍工作
- [ ] TestPlugin_ConcurrentWriterAndReaderNoLostUpdate 通过
- [ ] fd 数从 3 降到 2
- [ ] git commit -m "feat(shm): unified shared memory region"
- [x] SuperBlock 写入读回一致
- [x] StageContext 并发改写 0 lost update
- [x] 事件环 post-and-forget 仍工作
- [x] TestPlugin_ConcurrentWriterAndReaderNoLostUpdate 通过
- [x] fd 数从 3 降到 2`procExtraFilesForShm` 只返回 memfd + evtfd
- [x] git commit -m "feat(shm): unified shared memory region"ad016e4
**顺手清理**删除 §13.1 后遗留的死代码 `allocEvtRing`从未被调用
统一区域后只有操作区内切片的 `NewEvtRing` 仍在使用并修正
`plugin.go` 里仍写着旧 3-fd 布局3=StageContext, 4=事件环, 5=通知)
的过时注释
### 13.2 内核独占的共享内存分配器
@ -1209,7 +1214,7 @@ SDK 仓 `v1.0.0` / `v1.1.0`。main 的版本路牌现为 `1.2.0`(尚无 tag
- [x] `TestPlugin_ToolInvokeArgsResultViaArena`/ payload 都经帧往返结果内容一致且 arena 归零
- [x] `TestE2E_RealTemplatePluginFullLifecycle`真实 SDK 模板编译的插件跑通
- [x] `TestProcTemplate_ToolInvokeUsesSharedRef`模板必须处理 frame/args_len/result_ref防漂移
- [ ] Bench: ToolInvoke 延迟对比待补
- [x] Bench: ToolInvoke 延迟对比inline vs frame `bench_test.go`
### 13.4 Cleaner 迁移至 SharedRef
@ -1224,8 +1229,11 @@ SDK 仓 `v1.0.0` / `v1.1.0`。main 的版本路牌现为 `1.2.0`(尚无 tag
**验证**
- [ ] 三类 Cleaner 结果正确
- [ ] git commit -m "feat(shm): cleaner invoke via shared refs"
- [x] 三类 Cleaner 结果正确testdata/stageplugin.go 覆盖 tool/input/output 三类 scope
- [x] git commit -m "feat(shm): cleaner invoke via shared refs"5608abd
**顺带**:§13.4 的第六项工具调用帧同时把每批工具调用的性质写入
`lastBatchReplyOnly`供工具循环选择补位文案 `process.go`)。
### 13.5 InputChannel lane
@ -1283,8 +1291,8 @@ SDK 仓 `v1.0.0` / `v1.1.0`。main 的版本路牌现为 `1.2.0`(尚无 tag
**验证**
- [ ] qq_get_message prune 后上下文精简
- [ ] git commit -m "feat(ctx): context policy for tool results"
- [x] qq_get_message prune 后上下文精简QQ 插件已声明 `ContextPolicy: "prune"`
- [x] git commit -m "feat(ctx): context policy for tool results"772a494
### 13.9 llmsproxy 上下文溢出感知