Skip to content

perf: event-driven CDP readiness detection (−55s startup)#1302

Closed
lennney wants to merge 1 commit into
BigPizzaV3:mainfrom
lennney:perf/cdp-event-driven
Closed

perf: event-driven CDP readiness detection (−55s startup)#1302
lennney wants to merge 1 commit into
BigPizzaV3:mainfrom
lennney:perf/cdp-event-driven

Conversation

@lennney

@lennney lennney commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the 120×1s polling loop in ensure_injection with event-driven CDP readiness detection.

Why

The current approach polls TCP port availability in 1s intervals × 120 attempts, which adds up to 55s worst-case startup delay even when the process is ready within 2-5s.

Design

Three-phase progressive detection (each phase is a safety net for the previous):

Phase Mechanism When Time
1 Monitor stderr for DevTools listening on ws:// Chromium-based apps (Chrome, Edge, Electron) 2-5s
2 Exponential backoff TCP probe (100ms→10s) Packaged apps, codex-release up to ~21s
3 1s polling (30 attempts) Ultimate safety net up to 30s

Also eliminates nested retry: self.inject() internally called retry_injection() (20×500ms), creating a hidden 10s delay inside every backoff step. Replaced with try_inject() which probes once.

Pattern Validation

Phase 1 follows Playwright's Electron launcher (source): pipe child stderr, match DevTools signal line. All Chromium-based apps print this when launched with --remote-debugging-port.

API Design

wait_for_cdp_ready() uses impl AsyncRead + Unpin instead of concrete ChildStderr — enables unit test injection with mock pipes.

Performance

Metric Before After
Typical startup ~55s ~0-2s
Phase 2 worst ~101s ~21s
Phase 3 worst ~330s ~60s
Total worst case ~446s ~66s

Known CI Issue

Windows CI fails due to a pre-existing upstream bug in watcher.rs (ChatGPT-Desktop missing from APP_PACKAGE_SPECS). Fix tracked in #1450. macOS and all non-Windows tests pass.

@lennney lennney force-pushed the perf/cdp-event-driven branch from 3543015 to 1558fff Compare July 2, 2026 15:07
@lennney lennney marked this pull request as ready for review July 2, 2026 15:07
Copilot AI review requested due to automatic review settings July 2, 2026 15:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

该 PR 旨在将 Codex 启动后 CDP(Chrome DevTools Protocol)就绪检测从“固定 1s×120 次轮询”改为“事件驱动 + 退化回退”的策略,以显著降低健康环境下的启动等待时间,并避免嵌套重试带来的额外延迟。

Changes:

  • DefaultLaunchHooks::launch_codex() 中改为 pipe stderr,并尝试通过解析 DevTools listening on ws://... 行来触发 CDP 就绪信号。
  • DefaultLaunchHooks::ensure_injection() 中实现三阶段注入尝试:stderr 信号 → 指数退避 TCP 探测 → 30×1s 兜底轮询。
  • 增加 wait_for_cdp_ready() 的单元测试,并为 tokio 增补 io-util feature。

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
crates/codex-plus-core/src/launcher.rs 引入 stderr 监听 + 三阶段 ensure_injection 流程以加速 CDP 就绪判定与注入重试。
crates/codex-plus-core/tests/launcher.rs wait_for_cdp_ready 增加异步单测覆盖。
crates/codex-plus-core/Cargo.toml 为 tokio 启用 io-util 特性以支持 BufReader/read_line/duplex 等 I/O 工具。

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +740 to +757
// Read stderr to detect when the CDP port is ready.
// Chrome/Electron prints "DevTools listening on ws://..." to stderr.
// This is the same pattern used by Playwright (92k★) for Electron apps.
if let Some(stderr) = child.stderr.take() {
let (tx, rx) = tokio::sync::oneshot::channel();
*self.cdp_ready.lock().await = Some(rx);
tokio::spawn(async move {
if let Err(error) = wait_for_cdp_ready(stderr).await {
let _ = crate::diagnostic_log::append_diagnostic_log(
"launcher.cdp_stderr_listener_error",
serde_json::json!({"message": error.to_string()}),
);
}
// Drop sender: if ensure_injection is still waiting, it gets
// Canceled and falls through to the TCP-backoff fallback.
drop(tx);
});
}
Comment on lines +920 to +926
let backoff_delays = [100, 200, 400, 800, 1600, 3200, 5000, 10000u64];
for delay_ms in &backoff_delays {
if try_inject(debug_port, helper_port).await.is_ok() {
return true;
}
tokio::time::sleep(std::time::Duration::from_millis(*delay_ms)).await;
}
Comment on lines +1495 to +1503
#[tokio::test]
async fn test_wait_for_cdp_ready_detects_magic_line() {
let (mut writer, reader) = tokio::io::duplex(1024);
writer.write_all(b"some garbage\n").await.unwrap();
writer.write_all(b"DevTools listening on ws://127.0.0.1:9222\n").await.unwrap();
drop(writer);
let result = wait_for_cdp_ready(reader).await;
assert!(result.is_ok());
}
Comment on lines +1513 to +1523
#[tokio::test]
async fn test_wait_for_cdp_ready_ignores_noise_before_magic() {
let (mut writer, reader) = tokio::io::duplex(1024);
writer.write_all(b"[INFO] Starting process...\n").await.unwrap();
writer.write_all(b"[INFO] Loading configuration...\n").await.unwrap();
writer.write_all(b"[WARN] Something deprecated\n").await.unwrap();
writer.write_all(b"DevTools listening on ws://127.0.0.1:9222\n").await.unwrap();
drop(writer);
let result = wait_for_cdp_ready(reader).await;
assert!(result.is_ok());
}
Event-driven stderr pipe (Playwright pattern) replaces 120×1s polling.
Uses three-phase fallback:
  Phase 1: oneshot signal from stderr 'DevTools listening on ws://'
  Phase 2: exponential backoff TCP probe (100ms→10s)
  Phase 3: 1s-interval polling as ultimate safety net (30 attempts)

Also replaces self.inject() → try_inject() in all three phases to
eliminate the hidden 20×500ms retry_injection() loop nested inside
every backoff step.

Before: Phase 2 worst-case ~101s, Phase 3 ~330s, total ~446s
After:  Phase 2 worst-case ~21s, Phase 3 ~60s, total ~66s
@lennney lennney force-pushed the perf/cdp-event-driven branch from 1558fff to 9b1eaa8 Compare July 11, 2026 15:31
@lennney

lennney commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Closing: Phase 1 fast path is dead code (tx never sent in the spawn block). Redesign needed.

@lennney lennney closed this Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants