Skip to content

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

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

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 existing 120×1s polling loop in ensure_injection with an event-driven approach:

Phase 1: Monitor stderr for Chrome/Electron's DevTools listening on ws:// signal (same pattern used by Playwright, 92k★). Signal arrives in 2-5s on a healthy system.

Phase 2: Exponential backoff TCP probe (100ms→10s) as fallback for packaged apps where stderr isn't available.

Phase 3: 1s-interval polling (30 attempts) as ultimate safety net — same as original behaviour.

Also eliminates nested retry: self.inject() internally called retry_injection() (20×500ms loop), creating a hidden 10s barrier inside every backoff step. Replaced with try_inject() which does a single TCP probe.

Performance Impact

Metric Before After
Startup wait ~55s (worst case) ~0-2s (typical)
Phase 2 worst case ~101s ~21s
Phase 3 worst case ~330s ~60s
Total worst case ~446s ~66s

Design

Pattern validated by Playwright's Electron launcher (electron.ts): pipe child process stderr, match DevTools listening on ws:// line. All Chromium-based apps (Chrome, Edge, Electron) print this line when launched with --remote-debugging-port.

wait_for_cdp_ready() uses impl AsyncRead + Unpin (not concrete ChildStderr) so unit tests can inject mock pipes.

Verification

  • 3 unit tests for wait_for_cdp_ready (signal detection, timeout, EOF)
  • 91 tests passing (1 pre-existing apply_relay_config_file_accepts_utf8_bom_config failure — also present on upstream/main)

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 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());
}
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