perf: 小优化组 + 恢复浏览器页签

1. MCP 空闲睡眠 50ms -> 10ms:#187 后单轮 drain 约 0.3ms,
   100 次/s 空闲迭代约 3% 单核,请求与远控输入拾取延迟
   从平均 25ms 降到 5ms。
2. stream 轮询 50ms -> 20ms。
3. stream payload 加 url/title;面板 stream 处理合并 url/
   title,5 秒 state 轮询只在未观看时跑(观看时 stream 已
   全量覆盖,轮询纯冗余;不观看时保留轮询,避免会话开始
   后页眉 stale)。
4. 恢复浏览器页签:#176(ccb5a86)改写 TaskDetailPanel
   页签时顺带删了 BrowserPanel 的挂载,导致线上控制台
   自 #176 起就没有浏览器页签(面板文件一直在,只是不
   被 import,构建产物因此不变)。本次把 import、页签
   与 Card 原样挂回;构建产物含“开始观看”标记可证。
console.html 由 npm run build:console 重打(342484 字节)。
console-redesign/ 为构建脚本审阅产物,不进仓库。
This commit is contained in:
host committed 2026-10-30 14:09:02 +08:00
1 parent 5e562d8413
commit cbf5e65a7a
7 files changed
+79 -40

No files matched your search

+8 -6
View File
@@ -34,7 +34,7 @@ Threading contract (unchanged from the socket version, measured
2026-10-30: Playwright sync calls from any thread but the
creating one raise "Cannot switch to a different thread"):
only the MCP main thread touches the browser. It calls
RemoteBridge.poll() on every idle main-loop iteration (~50ms),
RemoteBridge.poll() on every idle main-loop iteration (~10ms),
so control/input/frames flow even while the agent is idle, and
CDP frame acks keep flowing (no ack = CDP stops after the
first frame).
@@ -57,7 +57,7 @@ CONTROL_NAME = ".browser-remote-control.json"
STATE_STALE_SECONDS = 30
# Heartbeat interval for state writes. The server treats state
# older than STATE_STALE_SECONDS as dead, so 0.5s is plenty;
# writing every ~50ms iteration was pure gofer round-trips.
# writing every iteration was pure gofer round-trips.
STATE_HEARTBEAT_SECONDS = 0.5
@@ -190,7 +190,7 @@ class RemoteBridge:
"""One drain-loop iteration: control, inputs, frame, state.
Writes are change-gated. The old code rewrote both files
on every ~50ms iteration: ~20 atomic temp+replace
on every iteration: ~200 atomic temp+replace
round-trips per second through the runsc gofer mount for
identical bytes (pump returns the latest frame
persistently, and updated_at changes every call). Now the
@@ -269,12 +269,14 @@ def read_state(workdir):
def effective_holder(workdir, state):
"""Server-side holder with the control race closed.
The MCP rewrites the state file every ~50ms, so right after
a takeover the state file still shows the old holder. The
The MCP polls every ~10ms idle and rewrites the state file
at once when the holder changes (a key change bypasses the
heartbeat throttle), so right after a takeover the state
file still shows the old holder for at most ~10ms. The
control file is the newer truth when its mtime is at or
after the state's updated_at; otherwise the state file
(already refreshed by a poll) wins. Both agree once the
MCP has polled, so this only closes the ~50ms window.
MCP has polled, so this only closes the ~10ms window.
"""
try:
mtime = os.path.getmtime(os.path.join(workdir, CONTROL_NAME))