//! Compare HTTP transport configurations against the Polymarket CLOB endpoint. //! //! Run with: //! `cargo run --release --example http_transport_matrix` use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONNECTION, CONTENT_TYPE, USER_AGENT}; use reqwest::{Client, ClientBuilder}; use std::time::{Duration, Instant}; const INITIAL_CURSOR: &str = "MA=="; #[derive(Clone, Copy)] struct Variant { name: &'static str, build: fn() -> Result, } #[derive(Clone, Copy)] struct Stats { mean_ms: f64, sd_ms: f64, p50_ms: f64, p95_ms: f64, p99_ms: f64, } fn official_headers() -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert(USER_AGENT, HeaderValue::from_static("rs_clob_client")); headers.insert(ACCEPT, HeaderValue::from_static("*/*")); headers.insert(CONNECTION, HeaderValue::from_static("keep-alive")); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); headers } fn polyfill_headers() -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert( USER_AGENT, HeaderValue::from_static(concat!("polyfill-rs/", env!("CARGO_PKG_VERSION"))), ); headers.insert(ACCEPT, HeaderValue::from_static("*/*")); headers.insert(CONNECTION, HeaderValue::from_static("keep-alive")); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); headers } fn polyfill_headers_no_connection() -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert( USER_AGENT, HeaderValue::from_static(concat!("polyfill-rs/", env!("CARGO_PKG_VERSION"))), ); headers.insert(ACCEPT, HeaderValue::from_static("*/*")); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); headers } fn polyfill_builder() -> ClientBuilder { Client::builder() .no_proxy() .http2_adaptive_window(true) .http2_initial_stream_window_size(512 * 1024) .tcp_nodelay(true) .pool_max_idle_per_host(10) .pool_idle_timeout(Duration::from_secs(90)) } fn light_polyfill_builder() -> ClientBuilder { Client::builder() .no_proxy() .tcp_nodelay(true) .pool_max_idle_per_host(10) .pool_idle_timeout(Duration::from_secs(90)) } fn build_polyfill_current() -> Result { polyfill_builder().build() } fn build_reqwest_default() -> Result { Client::builder().build() } fn build_official_headers_default() -> Result { Client::builder() .default_headers(official_headers()) .build() } fn build_polyfill_official_headers() -> Result { polyfill_builder() .default_headers(official_headers()) .build() } fn build_polyfill_headers() -> Result { polyfill_builder() .default_headers(polyfill_headers()) .build() } fn build_polyfill_headers_no_connection() -> Result { polyfill_builder() .default_headers(polyfill_headers_no_connection()) .build() } fn build_default_polyfill_headers() -> Result { Client::builder() .default_headers(polyfill_headers()) .build() } fn build_polyfill_light() -> Result { light_polyfill_builder().build() } fn build_http1_official_headers() -> Result { Client::builder() .http1_only() .default_headers(official_headers()) .build() } async fn fetch_once(client: &Client, url: &str) -> Result<(Duration, usize), reqwest::Error> { let start = Instant::now(); let bytes = client.get(url).send().await?.bytes().await?; Ok((start.elapsed(), bytes.len())) } fn percentile(sorted_ms: &[f64], percentile: f64) -> f64 { if sorted_ms.is_empty() { return 0.0; } let idx = ((sorted_ms.len() - 1) as f64 * percentile).round() as usize; sorted_ms[idx.min(sorted_ms.len() - 1)] } fn calc_stats(samples: &[Duration]) -> Stats { let values: Vec = samples .iter() .map(|duration| duration.as_micros() as f64 / 1000.0) .collect(); let mean_ms = values.iter().sum::() / values.len() as f64; let variance = values .iter() .map(|value| { let delta = value - mean_ms; delta * delta }) .sum::() / values.len() as f64; let mut sorted = values; sorted.sort_by(|a, b| a.total_cmp(b)); Stats { mean_ms, sd_ms: variance.sqrt(), p50_ms: percentile(&sorted, 0.50), p95_ms: percentile(&sorted, 0.95), p99_ms: percentile(&sorted, 0.99), } } fn env_usize(name: &str, default: usize) -> usize { std::env::var(name) .ok() .and_then(|value| value.parse().ok()) .unwrap_or(default) } fn env_u64(name: &str, default: u64) -> u64 { std::env::var(name) .ok() .and_then(|value| value.parse().ok()) .unwrap_or(default) } #[tokio::main] async fn main() -> Result<(), Box> { let host = std::env::var("POLYMARKET_BENCH_HOST") .unwrap_or_else(|_| "https://clob.polymarket.com".to_string()); let url = format!("{host}/simplified-markets?next_cursor={INITIAL_CURSOR}"); let iterations = env_usize("POLYMARKET_HTTP_MATRIX_ITERATIONS", 12); let warmups = env_usize("POLYMARKET_HTTP_MATRIX_WARMUPS", 2); let delay = Duration::from_millis(env_u64("POLYMARKET_HTTP_MATRIX_DELAY_MS", 100)); let mut variants = vec![ Variant { name: "polyfill-current", build: build_polyfill_current, }, Variant { name: "reqwest-default", build: build_reqwest_default, }, Variant { name: "official-headers-default", build: build_official_headers_default, }, Variant { name: "polyfill-current-official-headers", build: build_polyfill_official_headers, }, Variant { name: "polyfill-current-polyfill-headers", build: build_polyfill_headers, }, Variant { name: "polyfill-current-polyfill-headers-no-connection", build: build_polyfill_headers_no_connection, }, Variant { name: "reqwest-default-polyfill-headers", build: build_default_polyfill_headers, }, Variant { name: "polyfill-light-no-h2-window-tuning", build: build_polyfill_light, }, Variant { name: "http1-official-headers", build: build_http1_official_headers, }, ]; if let Ok(filter) = std::env::var("POLYMARKET_HTTP_MATRIX_FILTER") { let filters: Vec<_> = filter .split(',') .map(str::trim) .filter(|value| !value.is_empty()) .collect(); variants.retain(|variant| filters.iter().any(|filter| variant.name.contains(filter))); } println!("HTTP transport matrix"); println!("Endpoint: {url}"); println!("Iterations: {iterations}"); println!("Warmups: {warmups}"); println!("Delay: {} ms", delay.as_millis()); println!(); println!("Cold client construction + first byte fetch"); println!("------------------------------------------------------------"); for variant in &variants { let start = Instant::now(); let client = (variant.build)()?; let (fetch_elapsed, bytes) = fetch_once(&client, &url).await?; let total = start.elapsed(); println!( "{:<38} total {:>7.1} ms | fetch {:>7.1} ms | {bytes} bytes", variant.name, total.as_micros() as f64 / 1000.0, fetch_elapsed.as_micros() as f64 / 1000.0 ); tokio::time::sleep(delay).await; } println!(); let clients: Vec<_> = variants .into_iter() .map(|variant| Ok((variant.name, (variant.build)()?))) .collect::, reqwest::Error>>()?; for _ in 0..warmups { for (_, client) in &clients { let _ = fetch_once(client, &url).await; tokio::time::sleep(delay).await; } } let mut samples = vec![Vec::with_capacity(iterations); clients.len()]; for iteration in 0..iterations { for offset in 0..clients.len() { let idx = (iteration + offset) % clients.len(); let (_, client) = &clients[idx]; let (elapsed, bytes) = fetch_once(client, &url).await?; if bytes == 0 { eprintln!("empty response for {}", clients[idx].0); } samples[idx].push(elapsed); tokio::time::sleep(delay).await; } } println!("Warm steady-state byte fetch"); println!("------------------------------------------------------------"); for ((name, _), sample) in clients.iter().zip(samples.iter()) { let stats = calc_stats(sample); println!( "{name:<38} mean {:>7.1} +/- {:>5.1} ms | p50/p95/p99 {:>7.1} / {:>7.1} / {:>7.1} ms", stats.mean_ms, stats.sd_ms, stats.p50_ms, stats.p95_ms, stats.p99_ms ); } Ok(()) }