|
|
aed8f1f808
|
test(webui): WebAPI 只读端点压测 + 实测 2160 请求零失败
打的是**生产实例** 127.0.0.1:8080。
## 为什么只压只读端点
压测绝不能改状态。18 个端点**逐个探测确认**是 GET + 只读:
小(/status…/proxy/services,104B–1.8KB)、中(/plugins…/knowledge,
4.4KB–53KB)、大(/kernel 157KB、/memory/graph 338KB、/knowledge/tree 425KB)。
**排除**:/chat(真调 LLM)、/chat/interrupt(中断在跑的任务)、
/settings/* 与 /plugins/*(改配置)、/login /logout(改会话)、
knowledge/memory 写接口、/device/*(控制真实设备)。
## ★ 两条判据(都不是"看有没有报错")
### 1. 必须先 `--probe`:webui 无认证时返回 200 + 登录页 HTML
含 `THEME_PLACEHOLDER`,**状态码是 200**。只看 `http_code` 会把登录页
当成健康响应 ⇒「全部 200」是假的。脚本因此额外校验响应体。
### 2. 端点表漏了 `/api/v1` 前缀 ⇒ 18 个端点全 404
第一版把端点存成路径后半段(`"/status"`),拼出
`http://127.0.0.1:8080/status`,而真实路径是 `/api/v1/status` ⇒
**全部 404**,同一时刻 curl `/api/v1/status` 却是 200。
⇒ **压测脚本必须先 probe 再压。** 不 probe 的话那一跑的结论会是
「webui 全挂」,完全错误。这条已写进手册。
## 实测(2026-09-28)
| scale | 请求 | 吞吐 | p50 | p95 | p99 | max | 成功 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | 360 | 779 req/s | 6.7ms | 26.8ms | 45.5ms | 51.6ms | 360/360 |
| 2 | 720 | 788 req/s | 12.2ms | 52.6ms | 163.0ms | 213.5ms | 720/720 |
| 3 | 1080 | 1038 req/s | 16.4ms | 60.4ms | 79.7ms | 123.0ms | 1080/1080 |
**2160 请求零失败**;压测期间服务端 `active`、0 个 5xx、0 个 webui 错误,
QQ/agent 链路未受影响,homed CPU 仅 1.2%。
### 重响应不是瓶颈(并发 1 → 16)
| 端点 | 大小 | 并发 1 | 并发 16 | 吞吐 |
| --- | --- | --- | --- | --- |
| /status | 0.2KB | 104 req/s | **1781 req/s** | 320KB/s |
| /kernel | 153KB | 126 req/s | 375 req/s | 19 → **57 MB/s** |
| /memory/graph | 330KB | 70 req/s | 410 req/s | 23 → **135 MB/s** |
| /knowledge/tree | 415KB | 79 req/s | 717 req/s | 33 → **297 MB/s** |
大 JSON 端点的 p50 **不随并发上升**(/knowledge/tree 12.3ms → 8.9ms,
排队更充分、效率更高)⇒ 瓶颈不在 JSON 序列化。
## ⚠ 一处未下结论的观察
scale=2 的 p99(163ms)反而**高于** scale=3(79.7ms)。看着反常,但当时
机器上另一个 agent 的 chromium 占 **766% CPU**(另有 cjpm/cjc 在编译)
⇒ 是**环境噪声**。
**未在可比条件下重测就不下结论** —— 不拿这一组当性能特征。要判定需先
固定负载条件再跑。这条也写进手册。
## 顺带
ruff 报的三条 blocker 都修了:`pct()` 空样本不再抛错(调用方直接拿去做
f-string 格式化)、写 JSON 失败明确报错而非静默丢报告、错误体读取用
`contextlib.suppress` 免得二次异常盖掉真正的状态码。
|
2026-09-28 09:22:16 +08:00 |
|
|
|
20e5d917c3
|
chore(stress): 加 waiter 远程更新脚本;修 cmp.py 的 docstring 格式
## deploy-waiter.sh:106 / 30 的 waiter 更新
现状(更新前):两台都是 8月27日构建的 /opt/waiter/waiter(11,388,177 字节),
以 root 跑 waiter-remote.service,配置指向 ws://192.168.2.60:9890/…
安全设计:
- 先备份旧二进制(`$BIN.bak-<时间戳>`),失败即回滚(脚本内自动)
- **只换二进制,不动 waiter.yaml**(配置由 deploy 后单独追加)
- **逐台更新并验证,不并行** —— 两台都连同一网关,同时重启会同时断链
- 106 走 admin+sudo、30 走 root
- 验证项:服务 active + 进程时长 + **配置 md5 未变**
用法:`check`(只读)/ `deploy <ip>`(需输 yes)/ `rollback <ip>`
## cmp.py:两处格式
docstring 的 `"""` 紧贴内容、以及函数段之间缺两个空行(PEP8)。纯格式,
无逻辑改动。
|
2026-09-27 19:42:06 +08:00 |
|
|
|
190e46908d
|
test(stress): 补更新前后全面对比脚本(cmp.py),修 blast 的计数错误
## cmp.py:更新前后对比的四个维度
★ 每轮都记录**处理数**(响应里有多少个工具结果标记)与**是否出现 error 帧**,
任一不符即记失败。理由:工具调用这一路的失败模式几乎都是**静默**的 ——
工具没跑、参数混拼、只处理了第一个 tool_call,都不报错只是结果不对。
"跑完没崩"完全不能说明它 work。
1. **批内工具调用**:!slowbatchN 在两版上各跑几轮,比中位耗时与处理数
2. **并发 vs 强制串行**(新版内部对照):!slowbatchN vs !serialbatchN
3. **调度器并发轰炸**:多连接并发排队,算通过率
4. **连续稳定性**:20 轮无错误率
## 修掉 mock 的 !serialbatch 缺失
之前只在 /tmp 的临时副本里加过,没进仓库,导致 cmp.py 测「强制串行」时
那个 marker 根本不存在 —— 测出来的"串行"其实是并发,**加速比是假的**
(0.34x / 0.31x,看起来并发比串行慢)。已加回并说明它的用途:跨版本做不了
并发/串行对照(旧版适配器缺 stream_index,工具一个都没真跑),只能在
同一套内核上做。
## 修掉 blast 的计数错误
第一版按 `conns * inputs` 起线程、每个线程又跑 `inputs` 轮 ⇒ 总输入数是
conns×inputs²,分子分母量纲不一致,算出过 **"128/32 = 400%"** 这种荒谬数字。
现在:恰好 conns 个 worker、每个跑 inputs 轮;且分母用**实际发出的**输入数
(含连接失败的),否则连接失败时通过率会虚高。
## 踩过的两个坑(都写进注释)
- 内核 `task.go:353` 有输入去重(`isDuplicateInput`,为 webui 断线重连重放
而设),相同文本被丢弃并回空响应 ⇒ 每轮输入必须带唯一后缀
- cli 的 auth 帧本身就是 `{"type":"response"}` ⇒ 必须先吃掉它再开始收集,
否则第一轮的"终止帧"是 auth,测出来耗时恒为 0
|
2026-09-27 18:56:25 +08:00 |
|
|
|
17bc9a5553
|
docs(stress): abtest 补「跨版本对比的陷阱」—— 老版本可能只是没干活
脚本原本只写「加速比 = 基线延迟 / 新版延迟」。实测发现跨版本对比时这个
数字**没有意义**:
N 老版本 中位/处理数 新版本 中位/处理数 算出的"加速"
2 0.273s / 0 个 0.480s / 2 个 0.57x
4 0.273s / 0 个 0.492s / 4 个 0.55x
8 0.273s / 0 个 0.512s / 8 个 0.53x
"老版本更快"是假的 —— 它只是没干活:适配器缺 stream_index 透传 ⇒ 多个
分片并到槽 0 ⇒ argsRaw 混拼 ⇒ 每个工具报"参数不是合法 JSON"而一个都没
真跑。
★ 跨版本能比的硬指标是**处理数**;耗时对比只在新版内部做(混一个非并发安全
工具触发整批降级)才有意义 —— 那种对照下 N=12 时加速 5.72×。
顺带记下:老版本的内核分桶逻辑其实完整(`idx := tc.StreamIndex` + `accs[idx]`,
来自 2026-08-26 的 ddef195),缺的只是适配器那一个字段;而生产在当天 15:46
就手工补上了,比该提交(16:10)早 32 分钟。
|
2026-09-27 18:30:09 +08:00 |
|
|
|
8fafd5eafe
|
test(stress): 补批内并发压测与 A/B 对比脚本,并给 mock 加多 tool_call 能力
## 缺口
现有 kernel-stress 只压**调度器**(多连接排队 + L4 中断 + 驻留子),mock 每轮
只发**一个** tool_call。而内核的并发判据是:
if f == nil || len(f.PendingTools) <= 1 { return false }
⇒ **一个 tool_call 永远不并发**。所以现有压测压的全是串行路径,批内并发
一条都没走过。
## mockllm.py:让 mock 能发多个 tool_call
- `!batchN` N 个全部 ParallelSafe 的只读工具 ⇒ 强制走 stepToolBatch
- `!mixedN` 夹一个 knowledge_create(未声明并发安全)⇒ 验证整批降级
- `!slowbatchN` N 个 sleep 型 cmd_run(单工具约 200ms,时长递增)
⇒ 工具慢才能让并发的收益从噪声里显出来;时长递增使**完成序与声明序相反**,
可据此判定"是否按声明序落消息"
- `!serialbatchN` 在 !slowbatch 基础上插入一个非并发安全工具 ⇒ 强制整批串行,
作为并发对照的另一半
- `!img` / `!ocr` 触发多模态工具路径(不加载模型,只验内核 IPC 接线与错误处理)
- `!err` / `!hang` 故障注入
### 三个协议细节(踩过才知道,都写在注释里)
1. **必须带 `index`** —— 内核按 `StreamIndex`(上游 JSON 的 index)分槽累积
arguments。缺 index 时所有分片落到槽 0,几个 tool_call 的参数被**混拼**,
症状是每个工具都报"参数不是合法 JSON"而工具一次没真跑过。
单 tool_call 时不设 index 也正常,所以老 mock 一直没暴露。
2. **必须按协议分片** —— 一个 chunk 一个 tool_call,各自带 index;
后续 chunk 只续 arguments。整数组塞进一个 chunk 会被内核按"续传"语义累积。
3. **每个工具都要发"带 name 的首片"** —— 我第一版只给第 0 个发首片、其余直接
发续传片,看起来省事,但内核 flush 时按"无 name 即丢弃"处理,
于是 idx=1/2/3 全被丢,只跑 1 个工具。
## batchstress.py:批内并发压测(带校验)
只看峰值是不够的 —— 校验:工具是否真跑(响应里应有结果标记)、
消息顺序是否稳定(并发执行但按索引落消息)、mixed 批是否整批降级。
## abtest.py:串行 vs 并发的定量对比
同一套内核上用 !slowbatch / !serialbatch 两组对照,交替执行抵消机器负载漂移,
取中位数(长尾会污染均值)。
★ 判据里加了"工具是否真执行"这一项:**只看耗时是不够的** —— 出现过
"内核 274ms 就回复、工具一个没跑"的情况,那种情况下并发与串行都是 0.2s,
加速比毫无意义。
## 实测(隔离 netns 实例,mock + 内核同网段,5 轮中位)
N 并发 串行 加速 上限
2 0.481s 0.685s 1.42x 2
4 0.491s 1.114s 2.27x 4
8 0.513s 2.037s 3.97x 8
12 0.531s 3.039s 5.72x 12
并发批耗时几乎不随 N 增长,串行批严格线性。
★ 另一个踩过的坑(abtest 脚本自己的):内核有输入去重
(task.go:353 `isDuplicateInput`,为 webui 断线重连重放而设),相同文本会被
丢弃并回空响应。第一版每轮发同一个 marker ⇒ 只有第 1 轮有效,后面全是
0 秒 0 工具。修法是每轮加 `time.time_ns()` 唯一后缀。
|
2026-09-27 18:29:39 +08:00 |
|
|
|
f0be8cbaa5
|
feat(remotedevice): 设备能力 outputch 化 —— 每设备一个 device/<id> 通道 + 设备指令类工具补授权闸
用户指出 remotedevice 的能力应当 outputch 化。查证后发现比"应当"更严重:
设备方向**根本没有出站实现**。
## 查到的三个缺口
1. `devicectlDevice` 一直声明 `OutputCapabilities() = CapStructured`(对外宣称可作输出目标),
但 `Execute` 的 switch 里**没有 "output" 分支** ⇒ `output_send__devicectl` 必然拿到
`unknown device tool output`,回模型"通过 [devicectl] 通道发送失败"。
2. 全插件没有 `RegisterOutputChannel`,也没有任何 EmitOutput/output_send 路径:
设备方向只有「工具(请求-响应)」与「设备→agent 注入」,**agent → 设备是断的**
(唯一的下行通道是个 HTTP 端点 `/api/v1/device/push`,不在 agent 的工具/通道模型里)。
3. 寻址是聚合的:所有设备共用一个名字 `devicectl`,没有 `device/<id>`;且设备 caps 只在
插件内部软检查(`SupportsTool`),**绕过**了内核的 `AllowedOutputs` 授权闸 ——
驻留子只要拿到 `device_ctl_cmdrun` 就能指挥**任意**设备。
## 按方向切分(不是一刀切)
**出站/消息类 → 每设备一个输出通道 `device/<id>`**(与入站同名):
- 上线注册、掉线注销(caps 由设备声明的 caps 映射:文本恒有;有屏→图/文件;
speaker→音频;可跑命令(cmd/cmdrun/cmdresult)或未声明已知能力→全能力,与
`deviceSupportsTool` 的旧设备兼容规则一致)。断连不注销会留下死通道骗模型。
- 于是自动获得:内核按 caps 在**发送前**拦(送图给纯文本音箱直接拒);
`output_list_channels` 能列出设备;`AllowedOutputs` 可按设备收窄给驻留子。
- 上下线钩子用 `Registry.SetPresenceHandler`(**同步回调**)而不是既有的 `ChangeChan`
(那是 select+default,缓冲满会丢事件;丢一次就留下死通道或漏注册)。
- `devicectl` 保留为聚合通道,并把它"声明了却不实现"的 output 补实:按
`meta.device_id`(或 meta 就是设备 id / args.device_id)路由;缺省时返回**可执行**的
报错(列出在线设备),而不是含糊失败。
**RPC 类保留为工具**(`devicedetect`/`screensee`/`computeruse`/`clipboard*`/
`device_ctl_status|cmdrun|cmdresult`):它们的返回值(图像/命令输出/状态)必须进模型
上下文,做成通道会丢掉这个语义。
**并给设备指令类工具补上同一道授权闸**(core/toolcall.go):`device_id` 指向的设备
必须是本 agent 被授权的 `device/<id>`。根 agent 默认完整授权 ⇒ 无行为变化;
驻留子收窄后,"拿到工具就能指挥任意设备"的缺口被堵上(新增 3 条 core 测试钉住)。
**设备端参考实现**(`internal/devicebridge/client/bridge.go` + waiter):新增 `op=push`
分发与 `OnPush` 回调(文本/结构化;二进制走既有 `cmd_speech_*` → `DataHandler`),
waiter 把它打到终端。
## 设计口径(用户当场纠偏,已写进代码注释与 harness README)
**主动转发只有 webui 与 cli 两个交互界面**(webui 订阅 EventAgentOutput 渲染气泡、
cli 用同步回程写回终端)。其它通道一律要求 agent **显式** `output_send__<通道>`。
我第一版给 remotedevice 加了 `EventAgentOutput` 订阅来自动回投设备——那是凭空造了
第三个转发者,违背"输出是 agent 的主动调用",已撤回(该测试一并删除)。
## 验收
- 单测:caps 映射词表;设备上线→注册通道(含同名 inputch)/掉线→注销;push 真落到
WS 设备;聚合通道 `devicectl` 的寻址(无 device_id 报可执行错误、按 meta 投递、
指定不存在设备报错);core 授权闸 3 例。
- 全量 `go test ./...` = 37 包 ok / 0 FAIL;`-race`(agent/plugins/plugin/devicebridge/sdk)干净。
- **真二进制端到端**(私有 netns + mock LLM + 真 WS 设备客户端 scripts/kernel-stress/devclient.py):
设备上线 → 通道表出现 `device/pydev-1`(caps=[text file image audio structured],由
`caps:["cmd"]` 映射)→ agent 经 `output_send__device/pydev-1` 主动发送 → 设备收到
`{"op":"push","payload":"内核推给你的消息","type":"text"}` → 设备掉线 → 通道从表中消失。
|
2026-09-13 12:23:28 +08:00 |
|
|
|
9f80289a89
|
test(kernel-stress): 把"真二进制压力测试"harness 固化进仓库(私有 netns + mock LLM + 多连接驱动)
本轮压测是从 /var/tmp 里现写脚本跑的,能复现才能长期用 —— 固化到这。
为什么要有它(与 go test 的分工):单测都在进程内、参数显式给,抓不到集成面问题;
这一轮它抓到了三个只有真二进制才暴露的问题(DataDir 漏接线、插件通道未登记为
inputch、create 后子不开工),以及两处生成器缺陷(旧 SDK 导致生成工程编译失败;
模板工程从不示范通道登记)。
- `launch.sh`:在**私有 netns** 里同时起 mock LLM 与内核实例。
必须隔离的原因:生产实例占着 *:8080/*:9890/*:9876 且插件设了 SO_REUSEADDR,
同机再起一个实例会在 127.0.0.1 上与之并存绑定(首次实测抢到 9890 约 1 分钟);
netns 里只有 lo,结构上不可能碰到生产端口。unix socket 是文件系统对象,
所以驱动脚本在 netns 外照样能连。
- `mockllm.py`:OpenAI 兼容 mock(可控延迟 + SSE 分块 + 按 `!resident`/`!notify`
标记回工具调用),让无外网也能跑、且**流式段可被中断**(压抢占路径的前提)。
注意必须实现 `do_HEAD`:内核探活用 HEAD,501 会被判不可达 → degraded → rollback 循环。
- `stress.py`:多并发连接轰炸(单连接是串行的,造不出队列压力)+ 中断线程 + 峰值采样。
- `kcli.py`:CLI socket 客户端(`/auth` → `/kernel` / 广播)。
- README:前置配置(含探活端点、rollback 关掉)、用法、指标读法、已知坑。
实测结果(详见 6e60351 提交信息):密集 274 任务 executed=274/rejected=0;
稀疏中断下 suspended=resumed=preempted=27;驻留子全链路 + 优雅退出不留孤儿。
|
2026-09-13 11:38:31 +08:00 |
|