Files
HomeAgent/internal/plugin/proc/arena_test.go
JianFeeeee b096e8ba2c 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 分段
2026-09-10 18:54:36 +08:00

308 lines
7.9 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
import (
"bytes"
"sync"
"testing"
)
// newTestArena 构造一个独立块分配器(不经 Host保持单测快速。
func newTestArena(t *testing.T, size uint32) *arenaRegion {
t.Helper()
const base = 64 // 8 字节对齐
region := make([]byte, base+size)
a, err := initArena(region, base, size)
if err != nil {
t.Fatalf("initArena: %v", err)
}
return a
}
func TestArena_AllocFreeRoundTrip(t *testing.T) {
a := newTestArena(t, 8*1024)
if used, total := a.Stats(); used != 0 || total != 8*1024 {
t.Fatalf("初始 Stats: got (%d,%d), want (0,%d)", used, total, 8*1024)
}
ref, err := a.Put(OwnerHost, []byte("hello"), 0)
if err != nil {
t.Fatalf("Put: %v", err)
}
if used, _ := a.Stats(); used == 0 {
t.Fatal("Put 后 used 应大于 0")
}
got, err := a.Read(ref, 0)
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(got) != "hello" {
t.Fatalf("Read=%q, want hello", got)
}
if err := a.Free(OwnerHost, ref); err != nil {
t.Fatalf("Free: %v", err)
}
if used, _ := a.Stats(); used != 0 {
t.Fatalf("Free 后 used=%d, want 0", used)
}
// 归还后再读必须失败(块已回收)
if _, err := a.Read(ref, 0); err == nil {
t.Fatal("已释放块的引用应读取失败")
}
}
func TestArena_ConcurrentAllocUnique(t *testing.T) {
const workers = 64
a := newTestArena(t, 256*1024)
refs := make(chan SharedRef, workers)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
ref, err := a.Alloc(OwnerHost, 1024, 0)
if err != nil {
t.Errorf("Alloc: %v", err)
return
}
refs <- ref
}()
}
wg.Wait()
close(refs)
seen := make(map[uint32]bool, workers)
for ref := range refs {
if seen[ref.Offset] {
t.Fatalf("并发分配拿到重复 offset=%d", ref.Offset)
}
seen[ref.Offset] = true
}
if len(seen) != workers {
t.Fatalf("唯一块数=%d, want %d", len(seen), workers)
}
}
func TestArena_ExhaustionReturnsError(t *testing.T) {
a := newTestArena(t, 4*1024)
// 第一次分配吃掉几乎整块
if _, err := a.Alloc(OwnerHost, 3*1024, 0); err != nil {
t.Fatalf("首次 Alloc: %v", err)
}
// 再申请一大块必然失败
if _, err := a.Alloc(OwnerHost, 3*1024, 0); err == nil {
t.Fatal("空间不足时 Alloc 应返回错误")
}
}
func TestArena_RejectsOversizePayload(t *testing.T) {
a := newTestArena(t, 4*1024)
if _, err := a.Alloc(OwnerHost, a.MaxPayload()+1, 0); err == nil {
t.Fatal("超出 arena 容量的请求应被拒绝")
}
}
func TestArena_FreeEnforcesOwnership(t *testing.T) {
a := newTestArena(t, 8*1024)
const ownerA, ownerB = 7, 9
refA, err := a.Alloc(ownerA, 128, 0)
if err != nil {
t.Fatal(err)
}
// 另一个插件不能释放 A 的块
if err := a.Free(ownerB, refA); err == nil {
t.Fatal("跨 owner 归还应被拒绝")
}
if used, _ := a.Stats(); used == 0 {
t.Fatal("拒绝归还后块应仍然占用")
}
// 正确的 owner 可以归还
if err := a.Free(ownerA, refA); err != nil {
t.Fatalf("同 owner 归还: %v", err)
}
}
func TestArena_FreeRejectsForgedOffset(t *testing.T) {
a := newTestArena(t, 8*1024)
ref, err := a.Alloc(OwnerHost, 256, 0)
if err != nil {
t.Fatal(err)
}
// 未对齐到块数据起点的 offset 必须被拒绝——否则会直接破坏块链。
forged := ref
forged.Offset += 8
if err := a.Free(OwnerHost, forged); err == nil {
t.Fatal("伪造 offset 应被拒绝")
}
// 合法引用仍能正常归还(分配器状态未被破坏)
if err := a.Free(OwnerHost, ref); err != nil {
t.Fatalf("合法引用不应受影响: %v", err)
}
if used, _ := a.Stats(); used != 0 {
t.Fatalf("归还后 used=%d, want 0", used)
}
}
func TestArena_ReclaimOwnerReleasesOnlyItsBlocks(t *testing.T) {
a := newTestArena(t, 64*1024)
const ownerA, ownerB = 7, 9
for i := 0; i < 3; i++ {
if _, err := a.Alloc(ownerA, 1024, 0); err != nil {
t.Fatal(err)
}
}
for i := 0; i < 2; i++ {
if _, err := a.Alloc(ownerB, 1024, 0); err != nil {
t.Fatal(err)
}
}
if n := a.ReclaimOwner(ownerA); n != 3 {
t.Fatalf("ReclaimOwner(A)=%d, want 3", n)
}
// B 的两块必须保留:仍能读回
if n := a.ReclaimOwner(ownerB); n != 2 {
t.Fatalf("ReclaimOwner(B)=%d, want 2A 的回收不能误伤 B", n)
}
if used, _ := a.Stats(); used != 0 {
t.Fatalf("全部回收后 used=%d, want 0", used)
}
}
func TestArena_ReclaimHostIsNoop(t *testing.T) {
a := newTestArena(t, 4*1024)
if _, err := a.Alloc(OwnerHost, 128, 0); err != nil {
t.Fatal(err)
}
if n := a.ReclaimOwner(OwnerHost); n != 0 {
t.Fatalf("内核块不应被 ReclaimOwner 回收,实际回收 %d", n)
}
if used, _ := a.Stats(); used == 0 {
t.Fatal("used 应仍大于 0")
}
}
func TestArena_ReadRejectsInvalidRefs(t *testing.T) {
a := newTestArena(t, 8*1024)
ref, err := a.Put(OwnerHost, []byte("payload"), 0)
if err != nil {
t.Fatal(err)
}
// generation 过期
stale := ref
stale.Generation = 99
if _, err := a.Read(stale, 0); err == nil {
t.Fatal("generation 不匹配应被拒绝")
}
// offset 不是块数据起点
badOffset := ref
badOffset.Offset += 8
if _, err := a.Read(badOffset, 0); err == nil {
t.Fatal("offset 不是块数据起点应被拒绝")
}
// Length 超出块容量
tooLong := ref
tooLong.Length = 1 << 20
if _, err := a.Read(tooLong, 0); err == nil {
t.Fatal("Length 超容量应被拒绝")
}
}
func TestArena_PutReadLargePayload(t *testing.T) {
a := newTestArena(t, 512*1024)
payload := bytes.Repeat([]byte("abcdefgh"), 8192) // 64KB
ref, err := a.Put(OwnerHost, payload, 3)
if err != nil {
t.Fatalf("Put: %v", err)
}
got, err := a.Read(ref, 3)
if err != nil {
t.Fatalf("Read: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("读回数据不一致len(got)=%d len(want)=%d", len(got), len(payload))
}
// 大 payload 不能被 16KB 定长槽时代的容量假设卡住
if len(got) <= 16*1024 {
t.Fatalf("本用例应验证超过旧 16KB 槽容量的 payload实际 %d", len(got))
}
}
func TestArena_FreeCoalescesAdjacentBlocks(t *testing.T) {
a := newTestArena(t, 64*1024)
refs := make([]SharedRef, 0, 8)
for i := 0; i < 8; i++ {
r, err := a.Alloc(OwnerHost, 4*1024, 0)
if err != nil {
t.Fatalf("第 %d 次 Alloc: %v", i, err)
}
refs = append(refs, r)
}
for _, r := range refs {
if err := a.Free(OwnerHost, r); err != nil {
t.Fatalf("Free: %v", err)
}
}
// 全部归还并合并后,应能再分配一个接近整块的大块。
big, err := a.Alloc(OwnerHost, 48*1024, 0)
if err != nil {
t.Fatalf("合并后应能分配大块: %v", err)
}
if err := a.Free(OwnerHost, big); err != nil {
t.Fatal(err)
}
if n := a.BlockCount(); n != 1 {
t.Fatalf("全部归还并合并后应只剩 1 块,实际 %d", n)
}
}
func TestArena_AttachRejectsCorruptLayout(t *testing.T) {
a := newTestArena(t, 4*1024)
putU32(a.region[a.base+arOffMagic:], 0xDEADBEEF)
if _, err := attachArena(a.region, a.base, a.cap); err == nil {
t.Fatal("魔数错误应被拒绝")
}
putU32(a.region[a.base+arOffMagic:], arenaMagic)
putU32(a.region[a.base+arOffVersion:], 99)
if _, err := attachArena(a.region, a.base, a.cap); err == nil {
t.Fatal("版本不匹配应被拒绝")
}
putU32(a.region[a.base+arOffVersion:], arenaVersion)
putU32(a.region[a.base+arOffCapacity:], 1<<30)
if _, err := attachArena(a.region, a.base, a.cap); err == nil {
t.Fatal("capacity 不匹配应被拒绝")
}
}
func TestArena_InitRejectsTooSmall(t *testing.T) {
const base = 64
region := make([]byte, base+arenaMinSize()-8)
if _, err := initArena(region, base, arenaMinSize()-8); err == nil {
t.Fatal("过小的段应被拒绝")
}
}
func TestArena_DataOffsetsAligned(t *testing.T) {
a := newTestArena(t, 64*1024)
for i, size := range []uint32{1, 7, 8, 9, 100, 4096} {
ref, err := a.Alloc(OwnerHost, int(size), 0)
if err != nil {
t.Fatalf("第 %d 次 Alloc(size=%d): %v", i, size, err)
}
if ref.Offset%8 != 0 {
t.Fatalf("size=%d 的数据起点 %d 未 8 字节对齐", size, ref.Offset)
}
}
}