Files
HomeAgent/internal/plugin/proc/evtring.go
JianFeeeee 5bbfcc02fb 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
2026-09-02 16:48:27 +08:00

278 lines
7.2 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 (
"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 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)
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)
}
}