docs(plan): 第 12 节 —— 子进程化迁移剩余工作与目标效果

迁移主体已完成上生产(内核 v1.0.0,17 插件全部子进程化),
但有若干项未做完或未达成。写进 plan.md 而非只留在对话里,
避免下次接手时靠猜。

八个小节按「阻塞程度」排:

**12.1 合并到 main + 发布分支** —— 卡在四个决策点,非技术阻塞:
merge 方式(--no-ff vs squash)、是否删 feature 分支、release 构建是否
再替换生产二进制、SDK 仓是否同步。附完整执行序列。

记了一个易错点:v1.0.0 tag 当前打在 feature 分支中间点 d524a68,
按规范应在 release 分支上,需删除重打。

**12.2 验收清单两项未达成** —— 这两项在迁移计划里已如实标 ⚠️:
- SetToolBlocks 仍未实现。C ABI 时代也是空实现故不算回归,
  但 §3.8 明确承诺过「二进制写入 arena + Slice 描述符回传」,没兑现。
- 内存 88MB 远超「基线 +29MB」。根因是每插件静态链接整个 Go runtime,
  15 个不同二进制无共同物理页(PSS/RSS 99.9% vs 基线 44%)。
  基线用 2.68MB 最小插件复制 17 份,绝对数字本就不可比。

**12.3 事件环零真实负载检验** —— 机制完成、压测通过(2.29ms 与实验 4
一致),但 grep 确认无任何插件使用 Events().Subscribe。压测是我构造的
负载,生产上这条路径从未被真实插件走过。只写 ✅ 会掩盖这一点。

**12.4 三套 ABI 只收敛两套** —— C ABI 删了、Windows DLL 收敛了,
Lua 仍走独立解释器路径。§9.2 那句「三套收敛为单一 RPC」本轮兑现 2/3。
不阻塞是因为 Lua 不经 C ABI,不属于要消除的 6 类缺陷。

**12.5 Windows 无真机验证** —— 只做了交叉编译 + 单元测试。
已知语义差异(Event 是二元信号非计数器)推理上不影响正确性,
但没在真机确认过。§9.2 声称 Windows「从受害者变受益方」缺实证。

**12.6 stage 往返省两次 IPC** —— 132µs 里编解码只占 3.7µs,
其余是 3 次进程往返(invoke + 插件侧反向 lock/unlock)。合并后预期
降到 ~30µs。风险是锁持有时机改变,插件若在 handler 里再请求锁会死锁。

**12.7 遗留项** —— homed 主 heap 2.36GB(与插件无关)、鸿蒙端未提交改动。

**12.8 已达成目标留档** —— 6 类缺陷逐条对账,每条附证据
(测试名或生产日志),便于日后确认哪些是真解决了。
This commit is contained in:
JianFeeeee
2026-09-03 08:50:48 +08:00
parent da560e6fbf
commit ad1b05e6ef

179
plan.md
View File

@ -910,3 +910,182 @@ homed RSS = 2.34 GB RssAnon = 2.35 GB(真实驻留)
context 累积导致的内存增长。
- [ ] 单独排查 homed 主 heap 的 2.36GB 驻留来源
---
## 12. 子进程化迁移收尾:剩余工作与目标效果
> 状态锚点(2026-09-03):迁移主体已完成并上生产。内核 **v1.0.0**,
> 生产 17 个外部插件全部经子进程通道运行,15 个子进程稳定存活。
> 分支:主仓 `feature/plugin-proc-migration` @ `12259ed`(领先 main 26),
> SDK 仓 `feature/plugin-proc-migration` @ `5ed8d65`(领先 main 4)。
>
> 三项合入门禁**已全部通过**:`make test` 零失败、`go vet ./...` 无告警、
> `git diff main -- third_party/homeagent-sdk/sdk/` 为空(接口冻结不变量)。
>
> 详细执行记录见 `docs/zh/plugin-migration-plan.md`(Part 0~6 全部标记完成)。
### 12.1 待用户决策后执行:合并到 main + 发布分支
**当前卡在四个决策点**,不是技术阻塞:
| # | 决策点 | 备选 | 倾向 |
|---|---|---|---|
| 1 | merge 方式 | `--no-ff` 保留 25 commit / squash 压成一条 | `--no-ff`——commit message 记录了「为何共享同一块 memfd」「为何 procCore 不能嵌入」等踩坑过程 |
| 2 | 合回后是否删 feature 分支 | 删(规范要求)/ 留(8-9 周大特性) | 听用户 |
| 3 | release 构建是否再替换生产二进制 | 换(溯源干净)/ 不换(避免停服) | 听用户 |
| 4 | SDK 仓是否同步 main + release | 同步 / 只合 main / 暂不处理 | 同步——规范说「两仓版本对齐是第一优先级」 |
**目标效果**:
- `main` 含全部迁移工作且**永远可部署**(规范 §二.1)。
- 存在 `release/v1.0.0` 分支,`v1.0.0` tag **打在 release 分支上**而非 feature。
⚠️ 当前 tag 指向 `670efcd`(feature 分支中间点),需删除重打。
- 两仓版本对齐:主仓 `internal/meta.Version` = SDK 仓 `meta.Version` = `1.0.0`,
且 vendored SDK 与 SDK 仓 release tag 内容一致。
- 现网部署产物可追溯到 release tag 构建(规范 §四)。
**执行序列**(决策落定后):
```bash
# 主仓
git checkout main && git merge --no-ff feature/plugin-proc-migration
git checkout -b release/v1.0.0 main
git tag -d v1.0.0 && git tag -a v1.0.0 # 重打在 release 上
make build VERSION=1.0.0 # 发布产物
# SDK 仓(同上流程)
cd third_party/homeagent-sdk
git checkout main && git merge --no-ff feature/plugin-proc-migration
git checkout -b release/v1.0.0 main && git tag -a v1.0.0
```
---
### 12.2 验收清单里两项**未达成**的目标
这两项在 `docs/zh/plugin-migration-plan.md` 的最终验收清单里如实标了 ⚠️,
不是遗漏而是明确的未兑现承诺。
#### 12.2.1 `SetToolBlocks` 仍是未实现(承诺未兑现)
- **现状**:`io.setToolBlocks` 已在 `proc/protocol.go` 定义、已划入 `CapCore`
能力组,但 `corehandler.go` 的 handler 仍返回未实现。
- **为何不算回归**:C ABI 时代它也是空实现(§1.4 / `case` 无对应逻辑),
能力从「给不了」变成「暂未接」,没变差。
- **但 §3.8 承诺过**:迁移评估明确写「`SetToolBlocks` → 二进制写入 arena,
返回 `Slice` 描述符 ✅」。这条没做到。
- **目标效果**:插件调用 `SetToolBlocks(blocks)` 后,多模态内容块经共享段
arena 传给内核,内核把它并入工具返回值;`Slice` 描述符回传避免拷贝。
- **当前无用户**:17 个外部插件均未调用,故不阻塞发布。
- [ ] 实现 `io.setToolBlocks` 的内核侧 handler(arena 写入 + Slice 回传)
- [ ] 补一个真实使用它的 example 插件,否则无法验证
#### 12.2.2 内存开销超出计划目标(结构性问题)
- **计划目标**:迁移后常驻 ≤ 基线 +29MB(实验 5 量级)。
- **实测**:15 个插件进程 `RSS=88.0MB` / `PSS=87.9MB`,均摊 5.87MB。
- **根因**:每插件静态链接整个 Go runtime。15 个**不同**二进制之间无共同
物理页可映射,`PSS/RSS = 99.9%`(基线是 44%——那次用同一个 2.68MB 最小
插件复制 17 份,页可共享)。
- **绝对数字不可比**:基线插件 2.68MB,真实插件 3.1~14.8MB(browser 最大)。
结构性指标(均摊线程 5.5 vs 4.9)同量级。
- **实际开销高于 §4.3 乐观估计**,这是「每插件独立二进制」的固有代价。
- **目标效果(若要压)**:共享一个 launcher 二进制 + 各插件只提供业务模块,
让 15 个进程映射同一份 runtime 物理页,把 PSS 压回 RSS 的一半以下。
代价是插件不再是自包含可执行文件,分发与版本管理都变复杂。
- [ ] 决定是否值得为此改变分发模型(当前倾向:不改,88MB 可接受)
---
### 12.3 事件环:机制完成但**零真实负载检验**
- **已完成**:内核侧 `proc/evtring.go`(写端 + 消费端 + 事件类型位编码)、
`internal/plugin/evtring.go`(Bus ↔ EvtRing 适配)、模板侧 `evtConsumerLoop`、
三平台通知机制(Linux eventfd / macOS pipe / Windows Event)。
- **压测通过**:5000 次 Publish + 20µs 慢消费者 = 2.29ms(与实验 4 一致);
订阅者 1→8 耗时不变;环溢出仍 O(1)。
- **但**:`grep` 确认**无任何外部插件使用 `Events().Subscribe`**。
压测是我构造的负载,生产上这条路径从未被真实插件走过。
- **目标效果**:至少一个真实插件订阅内核事件并正确处理,
验证「独立游标 + 溢出跳过 + dropped 计数」在真实时序下的行为。
- [ ] 写一个订阅 `stage`/`tool_call` 事件的 example 插件做真实验证
- [ ] 观察长时间运行下 `dropped` 计数是否异常增长
---
### 12.4 三套 ABI 只收敛了两套:Lua 仍独立
- **已收敛**:C ABI(删除)+ Windows DLL(改走同一 RPC)。
- **未收敛**:`internal/plugin/lua_plugin.go` / `dynamic_lua.go` 仍走
gopher-lua 解释器的独立路径。
- **为何不阻塞本轮**:Lua 经解释器不经 C ABI,不属于本轮要消除的 6 类缺陷
(热重载失效、崩溃隔离缺失、stage lost update、cgo 超时泄漏、
output_send 假成功、能力断层)。§9.2 的「三套 ABI 收敛为单一 RPC」
这句话本轮只兑现了 2/3。
- **目标效果**:Lua 插件也走 `proc` 通道(launcher 进程内嵌解释器),
内核侧只有一套加载逻辑与一套权限检查。
- **收益**:Lua 插件获得崩溃隔离与共享内存 stage 全字段可见;
内核侧删掉 `lua_plugin.go` 的平行实现。
- [ ] 评估 Lua 走 proc 通道的代价(解释器进程启动开销 vs 隔离收益)
---
### 12.5 Windows 只做了交叉编译,无真机验证
- **已完成**:`shmalloc_windows.go`(`CreateFileMappingW` + `MapViewOfFile`)、
`evtfd_windows.go`(`CreateEventW` + `SetEvent`)、`shmpass_windows.go`
(名字经环境变量传递)、插件侧 `proc_shm_windows.go.tmpl`
(`syscall.NewLazyDLL` 绑定 `OpenFileMappingW`/`OpenEventW`)。
- **验证程度**:仅 `GOOS=windows GOARCH=amd64 go build` 通过 + 单元测试。
**无 Windows 测试机,从未真机跑过**。
- **已知的语义差异**(代码注释里记了,但未实测):
Windows Event 是二元信号而非计数器,多次 `SetEvent` 只唤醒一次。
推理上不影响正确性(消费者按 `readSeq` 追 `writeSeq` 批量 drain),
但没在真机确认过。
- **目标效果**:Windows 真机上完成一次完整的插件加载 → 工具调用 →
stage 改写 → 事件消费闭环,确认 16 字段全可见且写回生效
(这是 §9.2 声称 Windows「从受害者变受益方」的实证)。
- [ ] 找一台 Windows 机器跑端到端验证
- [ ] 特别验证命名对象的撞名防护(名字带 PID + 递增序号)
---
### 12.6 性能优化候选:stage 往返省两次 IPC
- **实测**:完整 stage 往返 132µs,其中共享段编解码只占 3.7µs(2.8%)。
- **成本构成**:一次 stage 要走 **3 次进程间往返**——`stage.invoke`
加上插件侧反向的 `stage.lock` / `stage.unlock`。
- **相对 LLM 往返 2-8 秒可忽略**,故非紧急。
- **目标效果**:把 lock/unlock 合入 `stage.invoke` 的请求/应答
(内核在下发 invoke 前就代插件持锁,应答时释放),
stage 往返从 3 次 IPC 降到 1 次,预期 132µs → ~30µs。
- **风险**:改变锁的持有时机。当前是插件主动请求,
改后内核代持——插件若在 handler 里再次请求锁会死锁,需要额外防护。
- [ ] 评估锁语义变化的影响面(哪些插件依赖显式 lock 时机)
---
### 12.7 无关本次迁移的遗留项
- [ ] 单独排查 homed 主 heap 的 2.36GB 驻留来源(见 §11.9,与插件无关)
- [ ] `cmd/ohos/.../SettingsPage.ets` 有 80 行未提交的鸿蒙端改动
(非本次迁移内容,一直未碰)
---
### 12.8 本次迁移**已达成**的目标(对照 §11.0 起因)
留档备查——6 类 C ABI 前提缺陷的消除状态:
| 缺陷 | 原状 | 现状 | 证据 |
|---|---|---|---|
| 热重载失效(11.6) | `DF_1_NODELETE` 让 `dlclose` 成 no-op | ✅ 换 `plugin.bin` 即生效 | 生产实测 `unloaded (config kept)` → 重载 |
| 崩溃隔离缺失 | 插件 panic 带崩 homed | ✅ 子进程独立崩溃 | `TestRealPlugin_CrashDoesNotKillKernel` |
| stage lost update(11.3) | 副本模型互相覆盖 35.8~36.8% | ✅ 0% | `TestPlugin_FiveProcessesConcurrentAppendNoLostUpdate` |
| cgo 超时泄漏(11.2) | 现网泄漏 26 次 | ✅ 整套新架构零 cgo | `Process.Kill()` 真取消 |
| output_send 假成功(11.1) | 永远返回成功 | ✅ 真实结果 | 生产实测 `map[status:sent]` |
| 能力断层(11.5 + §3.8) | Windows 只见 3 字段、无写回 | ✅ 18 字段全可见可写回 | 生产实测 sanitizer 跨进程改写 13590 字节 |
额外收益:权限梯度从「C ABI 表达能力的意外产物」变成**显式三道闸**
(类型层 `procCore` 命名字段 + manifest 能力声明 + RPC 边界明确拒绝)。