mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
按架构约束推进两步:
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 分段
308 lines
7.9 KiB
Go
308 lines
7.9 KiB
Go
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 2(A 的回收不能误伤 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)
|
||
}
|
||
}
|
||
}
|