# iOS Bot 模式收发延迟:夭夭与 OpenMausBot 模拟器对比 2026-09-13。结论:OpenMausBot 在有网络延迟时更快结束“发送中”,逐步输出更密集;本机低延迟下连续发送的恢复速度相近。首段文本并非始终 OpenMausBot 更早出现:连续发送时夭夭更早,夭夭首次发送的 CSRF 初始化会改变这一结果。 ## 版本与条件 固定测试开始时(09:27)的最新已提交源码,避开其他任务同时进行的工作区修改: | 项目 | 客户端 | 服务端 | 源码 | | --- | --- | --- | --- | | 夭夭 | iOS 1.3 (200) | 0.4.15 | iOS `3f500e9`;服务端 `d4c98d4` | | OpenMausBot | iOS 1.0.0 (9) | 0.1.75 | `a49d2f7d` | - 同一台专用 iPhone 17 Pro 模拟器,iOS 26.5,Xcode 26.6;两边按顺序运行。 - 两个 App 均从上述源码重新编译、安装并运行,Debug 配置启用 Swift `-O`,测试不采集代码覆盖率。 - 使用真实聊天页面、原有发送方法、HTTP/SSE 客户端、消息合并与展示逻辑。临时副本只加入测试连接入口、无障碍发送按钮标识和计时探针;未修改产品源码仓库。 - 两边均通过新启动的隔离真实服务端。夭夭使用现有 workspace-server 夹具及假 Hermes Gateway;OpenMausBot 使用 `launchVerificationServer` 及假 Claude CLI。OpenMausBot 手机直连隔离 Harness 的受鉴权接口,未经过桌面 Companion sidecar 转发。没有连接任何真实模型、现有用户会话或真实桌面服务。 - 假上游在接到 prompt 后等待 250 ms,然后每 20 ms 输出一个片段。短回复 40 个片段;长回复 200 段、正文 8,000 字符。两边正文相同,仅末尾用于区分试次的标记不同。 - 网络条件:本机 HTTP;另设相同代理,为每个 REST 请求增加 80 ms 上行和 80 ms 下行延迟,为 SSE 数据增加 80 ms 下行延迟。没有模拟限速、丢包、TLS、蜂窝网络或代理缓冲。 - 4 组短回复(2 App × 2 网络条件),每组 6 次发送。第一轮单独列出,后五轮取中位数;另有两条长回复。正式测试共 26 次发送。之前两次冒烟发送不计入统计。 ## 时间定义 时间从 App 的发送处理开始计算。`postAckMs` 是提交请求返回,`sendReadyMs` 是 `busy/sendingMessage` 恢复,`firstViewMs` 是 SwiftUI `onChange` 首次观察到本轮非空回复文本。 “界面文本更新”是原生页面观察到数据的时间,不是物理屏幕像素亮起的时间,也不是帧率。截图和 XCTest 的原生无障碍断言确认回复确实呈现在聊天页面;毫秒值来自 App 内探针,未用 XCTest 轮询耗时冒充精密计时。 ## 连续发送结果 每格是 5 次中位数,单位 ms。 | 网络 | 指标 | 夭夭 | OpenMausBot | | --- | --- | ---: | ---: | | 本机 | 提交确认 | 20.38 | 55.23 | | 本机 | 发送状态恢复 | 52.48 | 55.52 | | 本机 | 首段进入界面 | 274.24 | 373.38 | | 本机 | 文本更新间隔 | 101.60 | 21.45 | | 增加 160 ms 往返 | 提交确认 | 173.65 | 209.38 | | 增加 160 ms 往返 | 发送状态恢复 | 675.32 | 209.68 | | 增加 160 ms 往返 | 首段进入界面 | 436.57 | 541.22 | | 增加 160 ms 往返 | 文本更新间隔 | 101.57 | 22.24 | 160 ms 条件下,夭夭连续发送恢复范围为 671.25–679.58 ms,OpenMausBot 为 204.28–211.30 ms。两边差距约 466 ms;OpenMausBot 的“发送中”持续时间约为夭夭的三分之一。 从服务端输出的首个非空文本事件到 SwiftUI 观察到它,本机中位数分别为 9.15 / 11.95 ms;加入 80 ms 下行延迟后为 88.95 / 93.22 ms。本轮没有测出夭夭手机侧存在额外的大幅首段接收延迟。 短回复期间,夭夭通常呈现 10 次文本变化,OpenMausBot 约 35–41 次。该差异与服务端 100 ms 合并刷新、以及 OpenMausBot 转发原始 delta 的实现吻合。 ## 首次发送 每组首次发送只有一个样本,不用于估计分布。 | 网络 | 指标 | 夭夭 | OpenMausBot | | --- | --- | ---: | ---: | | 本机 | 发送恢复 | 76.48 | 58.53 | | 增加 160 ms 往返 | 发送恢复 | 844.89 | 214.75 | | 增加 160 ms 往返 | 首段进入界面 | 611.76 | 542.45 | 请求记录显示夭夭首次提交前多一次 `/api/app/bootstrap`,用于初始化 cookie 会话的 CSRF 状态。随后提交确认、发送后刷新照常执行。这解释了为什么首次发送时首段顺序会反转。它不是每条消息都有的 160 ms 固定等待。 ## 实际请求链与判断 夭夭连续发送:POST messages 返回后,依次等待 capabilities → agents/conversations(并发)→ conversation detail;直到这些刷新结束才清除 busy。另有独立 read 请求。OpenMausBot 的发送操作在 POST 返回后就清除 sendingMessage,后续依靠 SSE 和独立 read 请求同步。 在 160 ms 条件下,夭夭已经约 174 ms 提交成功,却到约 675 ms 才恢复发送状态。它并没有一直未发送,也没有因此阻止 SSE 在约 437 ms 把首段文字送入界面。 优先值得优化的是发送确认后解除 busy、将无关刷新移出发送等待链,以及流式的合并频率和完整消息重复传输。此次证据不足以把瓶颈归因于手机原生渲染性能,也不能推出真实模型谁生成得更快。 ## 长回复检查 两边各一条长回复,200 段正文,末尾长度分别为 8,034 / 8,039 字符(差别全部来自试次标记)。原生 UI 断言找到最终标记,页面观察到的最终文本长度与生成内容相符,截图显示第 200 段。 - 夭夭:42 次文本界面更新,中位间隔 103.17 ms。 - OpenMausBot:150 次文本界面更新,中位间隔 27.90 ms;上游原始文本事件 201 个,部分事件被原生界面合并。 - 两边都存在较大的单次更新间隔;本轮没有测量帧率,不能将这些数字表述为 FPS 或“绝对无卡顿”。 - 仅累计文本更新事件,夭夭 495,647 字节,OpenMausBot 91,985 字节。这不是连接总流量:没有将 OpenMausBot 最终完整消息及其他事件纳入该子集,所以不能据此给出总带宽倍数。 ## 验证与局限 `xcodebuild test` 最终退出码 0,`TEST SUCCEEDED`,1 个编排测试包含 26 次真实原生 UI 发送和最终标记断言,0 failures。26 次发送的提交、恢复、首段时间均有记录,所有最终文本长度与生成内容相符。 测试运行器自身在少数配置切换处触发了同步 URL 加载诊断:请求的是隔离控制器 `/scenario`,发生在测量开始前,所在进程是 XCTest runner;它不是产品 App 的网络实现。测试结果仍为通过。计时探针也会产生少量开销,因此不解释几毫秒差异。 此次没有测试真机、Android、真实模型、群聊调度、真实账号登录/配对、后台恢复或附件上传。两套假模型适配器不同,首段总时间包含各自服务端与驱动开销;不能把“夭夭本轮首段更早”外推为真实模型更快。登录钩子只用于连接隔离服务,原有发送与接收方法保持原样。 ## 证据 - [逐轮数据](measurements.json)、[中位数与范围](summary.json)、[版本](versions.json) - [网络时间线](network.jsonl)、目录中的 App 原始 `latency-series` 时间线 - [临时源码差异](instrumentation.patch)、[分析脚本](analyze.py) - [夭夭短回复](series-yaoyao-160-short-6.png)、[OpenMausBot 短回复](series-openmausbot-160-short-6.png) - [夭夭长回复](series-yaoyao-160-long-1.png)、[OpenMausBot 长回复](series-openmausbot-160-long-1.png) 本机完整结果包:`/tmp/yaoyao-mobile-ab-20260913/evidence/comparison.xcresult`。构建日志、XCTest 日志和测试后段屏幕录像保留在同目录。该临时目录可能被系统清理;本目录中的 JSON、截图和补丁是随 Git 保留的证据。