mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 09:58:06 +00:00
refactor(shm): 变长块分配器 + 工具调用 funccall 调用帧(§13.2/§13.3)
按架构约束推进两步:
1) 分配器收回内核后,定长槽失去了唯一存在理由
(定长槽只是为了绕开"跨进程无法安全变长分配"),
于是 arena 换成**变长块分配器**:first-fit + 邻块合并,
块头 16B(size/state/owner/prevSize)。内核可以按需标定每块大小,
payload 不再受固定槽容量限制(旧上限 16KB)。
- Alloc/Put/Read/Free/ReclaimOwner 全在内核进程内,一把 sync.Mutex
- Free 走块链校验 offset 是已分配块的数据起点 + owner 匹配,
伪造引用不能改动分配器状态
- Read 允许块内偏移(调用帧的结果区就在帧块中间),但不许跨块边界
- arena 4MB,底层 memfd 惰性分配:未触碰的页不占物理内存
2) 工具调用 payload **始终**走共享内存,取消按大小切内联的分支。
按 funccall 模型,内核(caller)标定调用帧交给插件(callee):
[0, ArgsLen) 参数 JSON
[ArgsLen, Frame.Length) 结果区(内核预留的预算)
结果超出预算时插件才 `arena.alloc` 扩容块,引用上打
sharedRefFlagExpand 让内核单独归还。Cleaner 复用同一帧模型。
ToolInvokeParams.Args / ToolInvokeResult.Result 仅剩给直连 RPC 的
测试(process/bench 不建 Host);生产路径永远走帧。
测试:
- TestArena_*(分配/归属/回收/并发唯一/耗尽/超限/伪造 offset/
相邻合并/对齐/布局校验)
- TestPlugin_ToolInvokeArgsResultViaArena:大**小** payload 都经帧往返,
结果一致且 arena 归零("小 payload 同样走调用帧"是本轮行为变更)
- TestPlugin_ArenaAllocFreeAcrossProcess、TestE2E_* 保持通过
- 顺手修 process_test.go 一处预存 doc-comment 分段
This commit is contained in:
@ -500,3 +500,70 @@ func TestPlugin_ArenaAllocFreeAcrossProcess(t *testing.T) {
|
||||
t.Fatalf("插件归还后槽池应全空,实际 used=%d/%d", used, total)
|
||||
}
|
||||
}
|
||||
|
||||
// 工具调用的参数/结果走共享槽(§13.3)。
|
||||
//
|
||||
// 控制面仍是 RPC(请求 ID 关联、ctx 取消、崩溃唤醒都由它承载),
|
||||
// 只有 payload 走共享内存:
|
||||
// - 参数超过阈值时内核写入槽,把 ArgsRef 发给插件
|
||||
// - 结果放得下时插件写入内核预分配的响应槽,回 ResultRef
|
||||
// - 超限/池满时退回内联 JSON,不能影响功能
|
||||
//
|
||||
// demo_big 会在结果里报出它到底从哪里读到参数,因此本测试验证的是
|
||||
// “真的走了共享内存”,而不只是“返回值对”。
|
||||
func TestPlugin_ToolInvokeArgsResultViaArena(t *testing.T) {
|
||||
bin := buildTestPlugin(t, "stageplugin.go")
|
||||
core := newFakeCore()
|
||||
|
||||
host, err := NewHost()
|
||||
if err != nil {
|
||||
t.Fatalf("NewHost: %v", err)
|
||||
}
|
||||
defer host.Close()
|
||||
|
||||
p := New("demo", bin, t.TempDir(), nil, host, nil)
|
||||
if err := p.Start(core); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
defer p.Close()
|
||||
|
||||
core.mu.Lock()
|
||||
h, ok := core.tools["demo_big"]
|
||||
core.mu.Unlock()
|
||||
if !ok {
|
||||
t.Fatal("插件应注册 demo_big 工具")
|
||||
}
|
||||
|
||||
t.Run("大 payload 走共享槽", func(t *testing.T) {
|
||||
// 4 字节 × 3 × 1000 = 12000 字节,明显超过内联阈值且能放进槽
|
||||
big := strings.Repeat("共享内存", 1000)
|
||||
res, err := h(map[string]interface{}{"text": big})
|
||||
if err != nil {
|
||||
t.Fatalf("调用 demo_big: %v", err)
|
||||
}
|
||||
s, _ := res.(string)
|
||||
if !strings.HasPrefix(s, "shared:") {
|
||||
t.Fatalf("大参数应经共享槽传递,实际结果前缀不对(len=%d, head=%.40q)", len(s), s)
|
||||
}
|
||||
if want := "shared:" + strings.ToUpper(big); s != want {
|
||||
t.Fatalf("经共享槽往返的内容不一致:got len=%d want len=%d", len(s), len(want))
|
||||
}
|
||||
if used, total := host.Arena().Stats(); used != 0 {
|
||||
t.Fatalf("调用结束后槽池应全空,实际 used=%d/%d", used, total)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("小 payload 同样走调用帧", func(t *testing.T) {
|
||||
// 设计上不再有“小 payload 走内联”的按大小分支:内核总是标定调用帧。
|
||||
res, err := h(map[string]interface{}{"text": "abc"})
|
||||
if err != nil {
|
||||
t.Fatalf("调用 demo_big: %v", err)
|
||||
}
|
||||
if res != "shared:ABC" {
|
||||
t.Fatalf("小参数也应走内核标定的调用帧,实际 %v", res)
|
||||
}
|
||||
if used, total := host.Arena().Stats(); used != 0 {
|
||||
t.Fatalf("调用结束后应全部归还,实际 used=%d/%d", used, total)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user