Perf/cdp network and lockfree (#214)
* perf(cdp): TCP_NODELAY + zero-buffer WS writes shave 130ms off newPage CDP traffic during browser.newPage() and Page.navigate is many small (~100-byte) request/response frames. Two pieces of buffering were adding ~10-15ms latency per WS frame on localhost, which on the ~10 round-trips Puppeteer makes during newPage compounded to a ~130ms penalty. 1. TCP_NODELAY on every accepted WS stream. With Nagle on, each small write either waits for an ACK from the previous one or hits the 40ms delayed-ACK timer, even on localhost. 2. WebSocketConfig.write_buffer_size = 0 on accept_async. tokio- tungstenite's default 128 KiB write BufWriter copies every frame into the buffer before flushing; setting it to 0 makes every small CDP reply hit the socket directly. Measured impact on localhost (puppeteer-core 24, lower is better): operation before after speedup browser.newPage() ~190ms ~50ms ~4x goto(data: URL) ~60ms ~22ms ~3x goto(repeat) ~60ms ~22ms ~3x Shared-page CDP scrape bench (9 scenarios): ~4000ms -> ~2900ms. 28/28 integration tests pass. * perf(cdp): bypass v8_lock for read-only / setup CDP methods Every CDP request unconditionally acquired the process-wide V8 lock, which serializes all CDP traffic behind any in-flight V8 work on a sibling page. During Puppeteer's browser.newPage() the ~8 setup calls (Page.enable, Runtime.enable, Page.getFrameTree, Page.addScriptToEvaluateOnNewDocument, Page.createIsolatedWorld, etc.) all queued behind a concurrent nav, turning a sub-millisecond setup into a multi-hundred-millisecond wait. is_v8_free_method now allowlists methods that demonstrably never touch a JsRuntime, either they don't touch any Page at all, or they use only the immutable get_session_page accessor and Rust-side field reads. get_session_page_mut (which can trigger suspend_js/resume_js on other pages) stays behind the lock, as does Target.createTarget (which calls page.navigate). Allowlisted: most of Target/Browser/Storage, plus Page.enable, Page.getFrameTree, Page.addScriptToEvaluateOnNewDocument, Page.createIsolatedWorld, Runtime.enable, Network.* cookie ops, Fetch.continue/fulfill/fail. Measured on the puppeteer-core 24 CDP scrape bench: browser.newPage() warm: ~50ms -> 13-32ms goto() repeat: ~22ms -> 14-48ms (variance) 28/28 integration tests pass. 18/18 realistic scrape scenarios pass.
This commit is contained in:
1 parent
282b4646d3
commit
89692f3cb7
2 files changed
+73
-2
No files matched your search
@@ -194,6 +194,16 @@ pub async fn start_with_full_serve_options(
|
||||
.set_nonblocking(true)
|
||||
.map_err(|e| error!("set_nonblocking on WS stream: {}", e))
|
||||
.ok();
|
||||
// Disable Nagle on the CDP socket. CDP exchanges many small
|
||||
// (~100-byte) request/response frames during browser.newPage()
|
||||
// and Page.navigate; with Nagle on, every small write either
|
||||
// waits for an ACK from the previous one or hits the 40ms
|
||||
// delayed-ACK timer. Measured impact: ~90ms shaved off
|
||||
// newPage on localhost, ~30ms shaved off goto.
|
||||
stream
|
||||
.set_nodelay(true)
|
||||
.map_err(|e| error!("set_nodelay on WS stream: {}", e))
|
||||
.ok();
|
||||
let tokio_stream = match TcpStream::from_std(stream) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
@@ -877,7 +887,16 @@ async fn handle_connection_ws(
|
||||
stream: TcpStream,
|
||||
msg_tx: mpsc::UnboundedSender<ServerMessage>,
|
||||
) -> anyhow::Result<()> {
|
||||
let ws_stream = tokio_tungstenite::accept_async(stream).await?;
|
||||
// tokio_tungstenite wraps the stream in a 128 KiB write BufWriter by
|
||||
// default. CDP traffic is many small (~100-byte) frames, and that buffer
|
||||
// adds extra latency per frame. write_buffer_size=0 makes every WS write
|
||||
// hit the socket directly. Combined with set_nodelay(true) above, gets
|
||||
// per-frame latency on localhost down toward ideal.
|
||||
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
|
||||
let mut cfg = WebSocketConfig::default();
|
||||
cfg.write_buffer_size = 0;
|
||||
cfg.max_write_buffer_size = 1 << 20;
|
||||
let ws_stream = tokio_tungstenite::accept_async_with_config(stream, Some(cfg)).await?;
|
||||
info!("WebSocket connected");
|
||||
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
|
||||
|
||||
|
||||
Reference in new issue
Block a user