|
| 1 | +# kvcpu Agent 调试机制设计 |
| 2 | + |
| 3 | +> 草稿 · 2026-07-15 |
| 4 | +
|
| 5 | +--- |
| 6 | + |
| 7 | +## 0. 定位与边界 |
| 8 | + |
| 9 | +**`kvspace` 管空间,调试机制管时间。** |
| 10 | + |
| 11 | +| 能力 | 工具 | 本质 | |
| 12 | +|------|------|------| |
| 13 | +| 读/写/浏览 KV | `kvlang kvspace get/dump/tree` | 静态快照 | |
| 14 | +| 等待/通知 KV 变化 | `kvlang kvspace watch/notify` | 被动监听 | |
| 15 | +| **激活调试模式** | `kvlang kvspace set /vthread/<vtid>/.debug "step"` | **时序控制** | |
| 16 | +| **追踪单步执行** | `kvlang kvspace trace <vtid>` | **NDJSON 轨迹** | |
| 17 | +| **单步/恢复/终止** | `kvlang kvspace notify /vthread/<vtid>/.debug.resume <cmd>` | **时序控制** | |
| 18 | + |
| 19 | +检查变量仍然用 `kvspace dump /vthread/<vtid>/...`,不重复。 |
| 20 | + |
| 21 | +--- |
| 22 | + |
| 23 | +## 1. 架构原则 |
| 24 | + |
| 25 | +### 1.1 不需要特殊启动方式 |
| 26 | + |
| 27 | +所有通过 `kvlang run`(含 `kvlang serve`)执行的程序,其 kvcpu 实例均内置调试检查点, |
| 28 | +**无需** 使用特殊命令行参数启动,也无需重启程序即可激活调试。 |
| 29 | + |
| 30 | +> 即使程序已运行数分钟,agent 随时可以写 `.debug = "step"` 进入单步模式。 |
| 31 | +
|
| 32 | +### 1.2 调试状态存储在 vthread 自身命名空间 |
| 33 | + |
| 34 | +调试相关键全部位于 `/vthread/<vtid>/` 下,以 `.` 开头(引擎保留): |
| 35 | + |
| 36 | +``` |
| 37 | +/vthread/<vtid>/ |
| 38 | + .debug 调试控制键(agent 写,CPU 读) |
| 39 | + "" = 正常执行 |
| 40 | + "step" = 每条指令后暂停 |
| 41 | + .debug.pause 暂停事件键(CPU Notify,agent Watch 等待) |
| 42 | + 值:JSON {"pc":"...","func":"...","frame":"...","op":"..."} |
| 43 | + .debug.resume 恢复命令键(agent Notify,CPU Watch 等待) |
| 44 | + 值:"step"(步进)| "continue"(恢复全速)| "abort"(终止) |
| 45 | +``` |
| 46 | + |
| 47 | +**不引入任何全局命名空间**(无 `/dbg/` 等),与现有 `/vthread/` `/func/` `/src/` `/sys/` 完全融合。 |
| 48 | + |
| 49 | +### 1.3 性能策略 |
| 50 | + |
| 51 | +| 模式 | 检查频率 | 原因 | |
| 52 | +|------|----------|------| |
| 53 | +| 非单步(正常运行) | 仅在函数入口(`isFuncEntryPC`) | 每次函数调用 1 次 KV 读,函数体内零开销 | |
| 54 | +| 单步模式 | 每条指令 | 已在调试中,overhead 可接受 | |
| 55 | + |
| 56 | +`stepping` 是 `Execute` goroutine 的局部变量,无需加锁(每个 vthread 对应一个 goroutine)。 |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +## 2. 实现(核心部分) |
| 61 | + |
| 62 | +### 2.1 keytree 键定义(`internal/keytree/vthread.go`) |
| 63 | + |
| 64 | +```go |
| 65 | +func VThreadDebug(vtid string) string { return "/vthread/" + vtid + "/.debug" } |
| 66 | +func VThreadDebugPause(vtid string) string { return "/vthread/" + vtid + "/.debug.pause" } |
| 67 | +func VThreadDebugResume(vtid string) string { return "/vthread/" + vtid + "/.debug.resume" } |
| 68 | +``` |
| 69 | + |
| 70 | +### 2.2 execute.go 内联检查点 |
| 71 | + |
| 72 | +```go |
| 73 | +stepping := false // 局部变量,无需加锁 |
| 74 | + |
| 75 | +for { |
| 76 | + // ... decode ... |
| 77 | + |
| 78 | + // 检查点:decode 之后、dispatch 之前(KV 状态一致的时间窗口) |
| 79 | + if stepping || isFuncEntryPC(pc) { |
| 80 | + v, _ := c.kv.Get(keytree.VThreadDebug(vtid)) |
| 81 | + switch mode := v.Str(); { |
| 82 | + case mode == "" && stepping: |
| 83 | + stepping = false // agent 清除标志 → 退出单步 |
| 84 | + case mode != "": |
| 85 | + if !stepping { stepping = true } |
| 86 | + debugNotifyPause(ctx, c.kv, vtid, pc, inst) |
| 87 | + switch cmd := debugWaitResume(c.kv, vtid); cmd { |
| 88 | + case "abort": |
| 89 | + vthread.SetError(ctx, c.kv, vtid, pc, "debug: aborted by agent") |
| 90 | + return fmt.Errorf("debug: aborted by agent") |
| 91 | + case "continue": |
| 92 | + stepping = false |
| 93 | + c.kv.Del(keytree.VThreadDebug(vtid)) // 恢复全速 |
| 94 | + // "step" → 保持单步 |
| 95 | + } |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + // ... dispatch ... |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +### 2.3 `kvlang kvspace trace <vtid>` |
| 104 | + |
| 105 | +监听 `.debug.pause` 事件,输出 NDJSON,自动 step: |
| 106 | + |
| 107 | +```go |
| 108 | +func kvTrace(kv kvspace.KVSpace, vtid string) { |
| 109 | + for { |
| 110 | + val, err := kv.Watch(pauseKey, 10*time.Second) |
| 111 | + if err != nil { |
| 112 | + // 超时:检查 vthread 是否已终止 |
| 113 | + if 已终止 || idle >= 3 { return } |
| 114 | + continue |
| 115 | + } |
| 116 | + fmt.Println(val.Str()) // 输出 NDJSON |
| 117 | + kv.Notify(resumeKey, kvspace.Str("step")) // 自动 step |
| 118 | + } |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +--- |
| 123 | + |
| 124 | +## 3. Agent 工作流 |
| 125 | + |
| 126 | +### A. 轨迹录制(全自动) |
| 127 | + |
| 128 | +```bash |
| 129 | +# 终端 1:启动程序 |
| 130 | +kvlang run tutorial/06-while/main.kv & |
| 131 | + |
| 132 | +# 获取 vtid(新建 vthread 后 seq 即为 vtid) |
| 133 | +VTID=$(kvlang kvspace get /vthread/seq) |
| 134 | + |
| 135 | +# 激活单步模式 |
| 136 | +kvlang kvspace set /vthread/$VTID/.debug "step" |
| 137 | + |
| 138 | +# 终端 2:开始追踪(输出每条指令的 NDJSON) |
| 139 | +kvlang kvspace trace $VTID > /tmp/trace.ndjson |
| 140 | +``` |
| 141 | + |
| 142 | +### B. 交互式单步检查 |
| 143 | + |
| 144 | +```bash |
| 145 | +# 激活单步 |
| 146 | +kvlang kvspace set /vthread/$VTID/.debug "step" |
| 147 | + |
| 148 | +# 等待第一个暂停事件 |
| 149 | +EVENT=$(kvlang kvspace watch /vthread/$VTID/.debug.pause) |
| 150 | +echo "$EVENT" | jq . |
| 151 | + |
| 152 | +# 检查当前帧的变量 |
| 153 | +FRAME=$(echo "$EVENT" | jq -r .frame) |
| 154 | +kvlang kvspace dump "$FRAME" |
| 155 | + |
| 156 | +# 步进 |
| 157 | +kvlang kvspace notify /vthread/$VTID/.debug.resume "step" |
| 158 | + |
| 159 | +# 恢复全速 |
| 160 | +kvlang kvspace notify /vthread/$VTID/.debug.resume "continue" |
| 161 | + |
| 162 | +# 终止 |
| 163 | +kvlang kvspace notify /vthread/$VTID/.debug.resume "abort" |
| 164 | +``` |
| 165 | + |
| 166 | +### C. 分析轨迹 |
| 167 | + |
| 168 | +```bash |
| 169 | +# 只看特定函数 |
| 170 | +jq 'select(.func=="first_div7")' /tmp/trace.ndjson |
| 171 | + |
| 172 | +# 统计各函数执行次数 |
| 173 | +jq -r '.func' /tmp/trace.ndjson | sort | uniq -c | sort -rn |
| 174 | + |
| 175 | +# 找 return 指令 |
| 176 | +jq 'select(.op=="return")' /tmp/trace.ndjson |
| 177 | +``` |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## 4. 暂停事件 NDJSON 格式 |
| 182 | + |
| 183 | +每次暂停输出一行: |
| 184 | + |
| 185 | +```json |
| 186 | +{"pc":"/vthread/42/[3,0]/_fn/[1,2]","func":"first_div7","frame":"/vthread/42/[3,0]","op":"ne"} |
| 187 | +{"pc":"/vthread/42/[3,0]/_fn/[1,3]","func":"first_div7","frame":"/vthread/42/[3,0]","op":"goto"} |
| 188 | +``` |
| 189 | + |
| 190 | +| 字段 | 含义 | |
| 191 | +|------|------| |
| 192 | +| `pc` | 当前指令绝对路径 | |
| 193 | +| `func` | 当前函数名(从 `frameRoot/.rootfunc` 读取) | |
| 194 | +| `frame` | 当前帧根路径(可用于 `kvspace dump` 检查局部变量) | |
| 195 | +| `op` | 即将执行的 opcode(尚未执行) | |
| 196 | + |
| 197 | +--- |
| 198 | + |
| 199 | +## 5. 与 GDB / dlv / monkeypatch 对比 |
| 200 | + |
| 201 | +| 特性 | GDB | dlv | monkeypatch | kvlang 调试机制 | |
| 202 | +|------|-----|-----|-------------|-----------------| |
| 203 | +| 目标用户 | 人类 REPL | 人类 REPL | 测试框架 | **Agent / 自动化** | |
| 204 | +| 激活方式 | 特殊启动 | 特殊启动 | 代码注入 | **写 KV 键(随时)** | |
| 205 | +| 状态检查 | `print x` | `print x` | `assert` | `kvspace dump` | |
| 206 | +| 单步 | `next/step` | `next/step` | — | `notify .debug.resume "step"` | |
| 207 | +| 轨迹 | — | `trace` | — | `kvspace trace <vtid>` | |
| 208 | +| 热替换 | 复杂 | 无 | `setattr` | `kvspace set /func/...` | |
| 209 | +| 已运行程序 | 需 attach | 需 attach | 不可用 | **直接写 KV(无需 attach)** | |
| 210 | + |
| 211 | +--- |
| 212 | + |
| 213 | +## 6. 高级特性(待实现 / 复杂) |
| 214 | + |
| 215 | +### A. 函数入口断点(break:<func>) |
| 216 | + |
| 217 | +`.debug` 写入 `"break:<funcname>"` 时,CPU 在 `isFuncEntryPC` 处比较函数名, |
| 218 | +仅命中指定函数时才暂停,其余函数继续全速执行。 |
| 219 | + |
| 220 | +### B. 源码位置映射(PC → file:line) |
| 221 | + |
| 222 | +需要 parser 在 AST 节点上记录 `Pos`,lower 保留,layoutcode 写入 |
| 223 | +`/src/<pkg>/<func>/<pc>` = `"file.kv:18"`。 |
| 224 | + |
| 225 | +### C. `next`(跳过函数调用) |
| 226 | + |
| 227 | +执行到当前帧深度不增加为止: |
| 228 | +watch `.debug.pause` 直到 `strings.Count(pc, "/_fn/") == startDepth`。 |
| 229 | + |
| 230 | +### D. Patch 模式(函数热替换) |
| 231 | + |
| 232 | +```bash |
| 233 | +# 直接通过 kvspace 修改已加载函数的指令序列 |
| 234 | +kvlang kvspace set /func/main/classify/[0,0] '"PATCHED"' |
| 235 | +kvlang kvspace notify /vthread/$VTID/.debug.resume "continue" |
| 236 | +``` |
0 commit comments