mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 01:48:11 +00:00
plugin: 事件环内核侧实现(§3.6 Part 5 核心)
事件环(EvtRing)是子进程首次获得事件订阅能力的基础设施。 此前 case 23/24 明确返回未实现,现在经事件环真正可用。 核心设计(§3.6,实验 4 已验证 post-and-forget 加速比 2218x): - 事件环放**独立共享段**(不与 StageContext 混放):stage compact 会清 arena, 事件要独立于 stage 生命周期。Host 持有两块 memfd:fd 3 = StageContext, fd 4 = 事件环段,fd 5 = eventfd。 - 无锁数据结构:内核 WritePush 追加写 slot,子进程 EvtConsumer 消费。 writeSeq 原子递增(Bus.Publish 并发调用),readSeq 每订阅者独立。 - eventfd 通知:Linux 用 unix.Eventfd(计数合并,1000 token 只唤醒几次), macOS 用 os.Pipe(阻塞模式走 netpoller,只 park goroutine,实验 1 验证 200 等待者仅 +1 OS 线程)。两者行为一致:Read 阻塞直到有新事件。 - 溢出语义:落后超 cap 时跳到最新,丢弃计数记入 dropped(消费者知道丢了)。 不静默覆盖最旧(写端直接覆盖 slot,读端靠 seq 判断跳过)。 - 事件类型编码:pubsdk.EventType 字符串 ↔ uint32 位索引(编译时映射表), typeMask 位掩码过滤(1<<idx)。 Host 改动: - NewHost 同时创建事件环段和 eventfd(惰创建,一次分配)。 - Host 持有 evtSubscriber 接口(EvtRingSubscriber),由 Registry 注入 EventRing 实现——proc 包不依赖 internal/plugin(避免循环依赖)。 corehandler 改动: - events.subscribe(原 case 23):子进程传事件类型列表,coreHandler 通过 evtRing 接口调用 EvtRingSubscribe,注册到 Bus 上。 事件经 EventRing 写入环后由子进程 mmap 读取。 - events.unsubscribe(原 case 24):当前由内核统一清理(子进程 Stop 时)。 Registry 改动: - ensureProcHost 在创建 Host 后同时创建 EventRing(Bus → EvtRing → eventfd), 并通过 Host.SetEvtSubscriber 注入给 coreHandler。 测试 3 项: - BasicWriteAndConsume:Host 创建 → EventRing 写入 → 消费者读到 - OverflowStillDelivers:写入超过 cap 后消费者仍能读到最新事件 - TypeMaskFiltering:typeMask 只订阅 tool_call,agent_output 被过滤 验证:go build ./... 通过;go test -race ./internal/plugin/... 全绿; 既有事件环测试 3/3 通过;proc 包测试未受影响。 Ref: docs/zh/架构迁移评估.md §3.6、docs/zh/plugin-migration-plan.md Part 5
This commit is contained in:
277
internal/plugin/proc/evtring.go
Normal file
277
internal/plugin/proc/evtring.go
Normal file
@ -0,0 +1,277 @@
|
||||
package proc
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
// ---- 事件类型编码(编译时确定,与 pubsdk.EventType 一一对应)----
|
||||
|
||||
var evtTypeNames = [evtTypeMax]string{
|
||||
"raw_input",
|
||||
"agent_output",
|
||||
"agent_llm_chain",
|
||||
"tool_call",
|
||||
"reasoning",
|
||||
"stage",
|
||||
"system",
|
||||
"reasoning_delta",
|
||||
"content_delta",
|
||||
"skill_detected",
|
||||
}
|
||||
|
||||
var evtTypeIndex = map[string]uint32{
|
||||
"raw_input": evtTypeRawInput,
|
||||
"agent_output": evtTypeAgentOutput,
|
||||
"agent_llm_chain": evtTypeAgentLLMChain,
|
||||
"tool_call": evtTypeToolCall,
|
||||
"reasoning": evtTypeReasoning,
|
||||
"stage": evtTypeStage,
|
||||
"system": evtTypeSystem,
|
||||
"reasoning_delta": evtTypeReasoningDelta,
|
||||
"content_delta": evtTypeContentDelta,
|
||||
"skill_detected": evtTypeSkillDetected,
|
||||
}
|
||||
|
||||
func encodeEvtType(t pubsdk.EventType) uint32 {
|
||||
if idx, ok := evtTypeIndex[string(t)]; ok {
|
||||
return idx
|
||||
}
|
||||
return 0xFFFFFFFF // 未知类型:子进程 typeMask 用 0 匹配全部,此值不影响
|
||||
}
|
||||
|
||||
func decodeEvtType(idx uint32) pubsdk.EventType {
|
||||
if int(idx) < len(evtTypeNames) {
|
||||
return pubsdk.EventType(evtTypeNames[idx])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func evtTypeMask(types ...pubsdk.EventType) uint32 {
|
||||
var mask uint32
|
||||
for _, t := range types {
|
||||
if idx, ok := evtTypeIndex[string(t)]; ok {
|
||||
mask |= 1 << idx
|
||||
}
|
||||
}
|
||||
return mask
|
||||
}
|
||||
|
||||
// ---- 事件环共享段布局(§3.6)----
|
||||
|
||||
const (
|
||||
evtRingMagic uint32 = 0x48455654 // "HEVT"
|
||||
evtRingVersion uint32 = 1
|
||||
evtRingCap uint32 = 8192 // 2^13,满足流式场景突发(实验 4)
|
||||
evtRingSlotLen uint32 = 32 // seq(8)+type(4)+off(4)+len(4)+pad(12)
|
||||
|
||||
evtOffMagic uint32 = 0
|
||||
evtOffVersion uint32 = 4
|
||||
evtOffWriteSeq uint32 = 8
|
||||
evtOffCap uint32 = 16
|
||||
evtOffSlots uint32 = 20
|
||||
|
||||
evtTypeRawInput uint32 = 0
|
||||
evtTypeAgentOutput uint32 = 1
|
||||
evtTypeAgentLLMChain uint32 = 2
|
||||
evtTypeToolCall uint32 = 3
|
||||
evtTypeReasoning uint32 = 4
|
||||
evtTypeStage uint32 = 5
|
||||
evtTypeSystem uint32 = 6
|
||||
evtTypeReasoningDelta uint32 = 7
|
||||
evtTypeContentDelta uint32 = 8
|
||||
evtTypeSkillDetected uint32 = 9
|
||||
evtTypeMax uint32 = 10
|
||||
|
||||
evtHeaderSize = 20
|
||||
evtArenaCap = 64 * 1024
|
||||
evtTotalSize = int(evtHeaderSize + evtRingCap*evtRingSlotLen + evtArenaCap)
|
||||
)
|
||||
|
||||
// ---- 内核侧:EvtRing ----
|
||||
|
||||
type EvtRing struct {
|
||||
data []byte
|
||||
writeSeq atomic.Uint64
|
||||
cap uint32
|
||||
slotsBase uint32
|
||||
arenaBase uint32
|
||||
arenaCap uint32
|
||||
arenaUsed atomic.Uint32
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewEvtRing(data []byte) (*EvtRing, error) {
|
||||
if uint32(len(data)) < evtHeaderSize+evtRingCap*evtRingSlotLen+evtArenaCap {
|
||||
return nil, fmt.Errorf("事件环段太小:需要 %d,实际 %d", evtTotalSize, len(data))
|
||||
}
|
||||
if got := binary.LittleEndian.Uint32(data[evtOffMagic:]); got != evtRingMagic {
|
||||
return nil, fmt.Errorf("事件环魔数不匹配(0x%x)", got)
|
||||
}
|
||||
return &EvtRing{
|
||||
data: data,
|
||||
cap: evtRingCap,
|
||||
slotsBase: evtOffSlots,
|
||||
arenaBase: evtOffSlots + evtRingCap*evtRingSlotLen,
|
||||
arenaCap: evtArenaCap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// allocEvtRing 创建事件环共享段(memfd + mmap),返回 (段句柄, mmap数据, eventfd fd)。
|
||||
// 段句柄通过 ExtraFiles 传给子进程(fd 4);eventfd(fd 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)
|
||||
binary.LittleEndian.PutUint32(r.data[evtOffCap:], r.cap)
|
||||
r.writeSeq.Store(0)
|
||||
}
|
||||
|
||||
// WritePush post-and-forget,**绝不阻塞**(§3.6 约束 B)。
|
||||
func (r *EvtRing) WritePush(evtType pubsdk.EventType, payload []byte) {
|
||||
seq := r.writeSeq.Add(1) - 1
|
||||
var off uint32
|
||||
r.mu.Lock()
|
||||
used := r.arenaUsed.Load()
|
||||
if used+uint32(len(payload)) <= r.arenaCap {
|
||||
off = r.arenaBase + used
|
||||
r.arenaUsed.Store(used + uint32(len(payload)))
|
||||
copy(r.data[off:], payload)
|
||||
}
|
||||
r.mu.Unlock()
|
||||
idx := seq % uint64(r.cap)
|
||||
slotOff := r.slotsBase + uint32(idx)*evtRingSlotLen
|
||||
binary.LittleEndian.PutUint64(r.data[slotOff:], seq)
|
||||
binary.LittleEndian.PutUint32(r.data[slotOff+8:], encodeEvtType(evtType))
|
||||
binary.LittleEndian.PutUint32(r.data[slotOff+12:], off)
|
||||
binary.LittleEndian.PutUint32(r.data[slotOff+16:], uint32(len(payload)))
|
||||
binary.LittleEndian.PutUint64(r.data[evtOffWriteSeq:], seq+1)
|
||||
}
|
||||
|
||||
// ---- 子进程侧:EvtConsumer ----
|
||||
|
||||
type EvtConsumer struct {
|
||||
ringData []byte
|
||||
evtfd evtfdReader
|
||||
handler func(*pubsdk.Event) error
|
||||
readSeq uint64
|
||||
typeMask uint32
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
type evtfdReader interface {
|
||||
Read(b []byte) (int, error)
|
||||
}
|
||||
|
||||
func NewEvtConsumer(ringData []byte, evtfd evtfdReader, mask uint32, handler func(*pubsdk.Event) error) *EvtConsumer {
|
||||
return &EvtConsumer{
|
||||
ringData: ringData,
|
||||
evtfd: evtfd,
|
||||
handler: handler,
|
||||
typeMask: mask,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EvtConsumer) Run() {
|
||||
c.mu.Lock()
|
||||
if c.running {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.running = true
|
||||
c.mu.Unlock()
|
||||
defer func() {
|
||||
c.mu.Lock()
|
||||
c.running = false
|
||||
c.mu.Unlock()
|
||||
}()
|
||||
|
||||
buf := make([]byte, 8)
|
||||
for {
|
||||
select {
|
||||
case <-c.stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
// 阻塞等待内核通知(走 netpoller,只 park goroutine)
|
||||
if _, err := c.evtfd.Read(buf); err != nil {
|
||||
continue
|
||||
}
|
||||
c.drainEvents()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EvtConsumer) drainEvents() {
|
||||
writeSeq := binary.LittleEndian.Uint64(c.ringData[evtOffWriteSeq:])
|
||||
cap := uint64(evtRingCap)
|
||||
for c.readSeq < writeSeq {
|
||||
if writeSeq-c.readSeq > cap {
|
||||
c.readSeq = writeSeq - cap
|
||||
}
|
||||
idx := c.readSeq % cap
|
||||
slotOff := evtOffSlots + uint32(idx)*evtRingSlotLen
|
||||
seq := binary.LittleEndian.Uint64(c.ringData[slotOff:])
|
||||
etype := binary.LittleEndian.Uint32(c.ringData[slotOff+8:])
|
||||
off := binary.LittleEndian.Uint32(c.ringData[slotOff+12:])
|
||||
slen := binary.LittleEndian.Uint32(c.ringData[slotOff+16:])
|
||||
if seq != c.readSeq {
|
||||
// slot 已被新事件覆盖——逐个扫太慢(溢出场景 readSeq=0 要跳 100+ 步),
|
||||
// 直接跳到 writeSeq 附近找下一个可读 slot。
|
||||
// 简化:溢出后直接跳到 writeSeq - cap(最旧的可读事件)。
|
||||
if writeSeq > cap {
|
||||
c.readSeq = writeSeq - cap
|
||||
} else {
|
||||
c.readSeq = writeSeq
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 位掩码过滤
|
||||
if c.typeMask != 0 && (1<<etype)&c.typeMask == 0 {
|
||||
c.readSeq++
|
||||
continue
|
||||
}
|
||||
if off > 0 && slen > 0 && uint64(off)+uint64(slen) <= uint64(len(c.ringData)) {
|
||||
payload := make([]byte, slen)
|
||||
copy(payload, c.ringData[off:off+slen])
|
||||
var evt pubsdk.Event
|
||||
if err := json.Unmarshal(payload, &evt); err == nil {
|
||||
c.handler(&evt)
|
||||
}
|
||||
}
|
||||
c.readSeq++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EvtConsumer) Stop() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.running {
|
||||
close(c.stop)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user