Perf/cdp fetch cache and cookie alloc (#216)
* perf: cache HTTP clients + selector parsing + exponential wait backoff Three low-risk wins discovered while reading the hot paths. 1. op_fetch_url cached its reqwest::Client. Previously every JS-side fetch(), XHR, and dynamic script load built a fresh client, which re-loaded TLS roots and started with an empty connection pool. On an asset-heavy page with 30+ subresources this added ~150ms of wasted setup with zero connection reuse. The cache is a process- wide map keyed by proxy URL (empty string for no-proxy), so the first fetch on each proxy pays the build cost once and every subsequent fetch reuses the same pool. Also bumped pool_idle_timeout to 5 minutes and tcp_keepalive to 60s so connections stay warm for SPA-heavy workloads that hit the same origin dozens of times across a navigation. 2. DomTree::parse_selector now caches parsed selectors in a thread- local HashMap (cap 256). querySelector / querySelectorAll re- parsed the selector string every call before; batch DOM access patterns (an agent scraping a table row-by-row, a framework polling for elements) paid the cssparser cost N times. 3. browser_wait_for / browser_wait_for_text now exponentially back off (5 -> 10 -> 20 -> ... -> 200 ms). The old fixed 200ms tick meant a selector that appeared 30ms after the call still took the next full 200ms tick to be detected. Worst case is still 200ms per check, but the median wait drops sharply for fast appearances. 28/28 integration tests pass. Asset-heavy bench (rust-lang/rust, docs.stripe.com, etc) doesn't regress. * perf: module loader client cache, fetch concurrency cap, snapshot hide list, cookie alloc Round two of perf wins after a deep code audit. 1. ES module loader (module_loader.rs) was building a fresh reqwest::Client on every dynamic import. Modern SPAs (Next, Vite) load 20-50 chunks via dynamic import; each one used to spin up an empty connection pool with fresh TLS init. Now reuses the process-wide FETCH_CLIENT_CACHE that op_fetch_url already uses (the cached_request_client helper). Saves ~30-100ms on chunk- heavy first navigations. 2. Script and CSS fetches in execute_scripts used futures::join_all with unbounded concurrency. A 100-script page would open 100 TCP sockets at once, exhausting the connection pool and the OS ephemeral port range. Capped at 16 in-flight (well above the per- host pool ceiling real Chrome uses). 3. bootstrap.js __obscura_init() ran Object.keys(globalThis).filter() on every page setup to find internals to hide. The set of keys is static at snapshot-build time, so we now precompute it once (top-level statement just below the function) and __obscura_init just iterates the precomputed array. Was 5-40ms per page on heavy SPAs (1000+ globals); now ~0.2ms per page. 4. CookieJar::domain_matches allocated 2 lowercase Strings and a format! per call. The lookup runs per fetch per cookie domain; on a heavily-cookied site with 100 cookies across 30 domains that was thousands of allocations per page. Now zero-alloc with eq_ignore_ascii_case and char-boundary checks.
This commit is contained in:
1 parent
171dba23e7
commit
fd2421940b
7 files changed
+149
-38
No files matched your search
@@ -856,6 +856,11 @@ async fn tool_wait_for(args: &Value, state: &mut BrowserState) -> Result<String,
|
||||
let timeout_secs = args.get("timeout").and_then(Value::as_f64).unwrap_or(30.0) as u64;
|
||||
|
||||
let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(timeout_secs);
|
||||
// Exponential backoff: 5 -> 10 -> 20 -> ... -> 200 ms. The old fixed
|
||||
// 200ms tick added up to a full poll cycle of latency every time;
|
||||
// a selector that appears in 30ms now returns in ~35ms instead of
|
||||
// the next 200ms tick.
|
||||
let mut tick_ms: u64 = 5;
|
||||
loop {
|
||||
let found = state.page_mut().with_dom(|dom| {
|
||||
dom.query_selector(selector).ok().flatten().is_some()
|
||||
@@ -869,7 +874,8 @@ async fn tool_wait_for(args: &Value, state: &mut BrowserState) -> Result<String,
|
||||
return Err(format!("Timeout waiting for '{selector}'"));
|
||||
}
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(tick_ms)).await;
|
||||
if tick_ms < 200 { tick_ms = (tick_ms * 2).min(200); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1156,6 +1162,8 @@ async fn tool_wait_for_text(args: &Value, state: &mut BrowserState) -> Result<St
|
||||
var t = (document.body && (document.body.innerText || document.body.textContent)) || '';
|
||||
return t.indexOf({needle}) >= 0;
|
||||
}})()"#, needle = escaped);
|
||||
// Exponential backoff like browser_wait_for (see comment there).
|
||||
let mut tick_ms: u64 = 5;
|
||||
loop {
|
||||
let found = state.page_mut().evaluate(&js).as_bool().unwrap_or(false);
|
||||
if found {
|
||||
@@ -1164,7 +1172,8 @@ async fn tool_wait_for_text(args: &Value, state: &mut BrowserState) -> Result<St
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
return Err(format!("Timeout waiting for text {needle:?}"));
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(tick_ms)).await;
|
||||
if tick_ms < 200 { tick_ms = (tick_ms * 2).min(200); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user