Files
HomeAgent/internal/agent
JianFeeeee f8add88dbc perf(stagehost): 工具声明查询免去结构体拷贝(热路径 1000 并发下省 1000 次)
## 问题

StageHost.ToolDef 返回 &def —— 一次**结构体拷贝**:3 个 string + 2 个 map 头
+ 2 个 bool + Cleaner 函数指针。

而 toolParallelSafe 在**每批**并发判据里对每个工具各调一次:
batchRunnable 遍历 PendingTools → toolParallelSafe(tc.Name)。
1000 并发批次 = 1000 次结构体拷贝,全在判定阶段(执行之前)。

不是"逃逸漏洞"(Go 1.22+ 循环变量每轮独立,go.mod 是 1.25),纯粹是白拷贝。

## 修法

ToolDef 保留 —— 它要给需要完整声明的调用方(Cleaner、Parameters 校验),
返回副本也是**有意**的(ToolDef 里有 map 与函数指针,交出内部元素会把
可变引用漏出去)。

新增免拷贝查询,热路径专用:

    ConcurrencySafeOf(name) (safe, found bool)   // 只读 ParallelSafe && !Serial
    NoMemoryOf(name)       (v, found bool)
    HasTool(name)          bool

全部在持 RLock 下走同一个 findLocked。

`toolParallelSafe` 切到 ConcurrencySafeOf。语义完全等价 —— 两者都算
`ParallelSafe && !Serial`,只差一次拷贝。

## 判据(两个都防"优化悄悄改了语义")

- TestNoCopyQueriesMatchToolDef  7 种声明组合(plain / parallel / serial /
  both / nomem / all / serial_nomem)下,免拷贝查询与 ToolDef(...).字段
  **逐字段等价**;不存在的工具三态一致(false/false/true)。

  ★ 这类优化最危险的失败模式就是语义漂移:并发判据若读错字段,
    能并发的批次会**悄悄退化成串行** —— 没有任何报错,只表现为"变慢了"。
    所以判据必须逐个组合比对,而不是只测一个典型值。

- TestNoCopyQueriesConcurrent  32 goroutine × 50 工具并发查询,
  -race 无竞态且结果与串行一致。

回归:go build ./... 通过;go test ./internal/... 全绿;
go test -race ./internal/agent/core 通过。
2026-09-27 16:29:24 +08:00
..