Merge pull request #50 from floor-licker/fix/rest-simd-json-parsing

feat: use simd json for rest market parsing
This commit is contained in:
floor-licker
2026-06-22 18:00:12 -04:00
committed by GitHub
3 changed files with 33 additions and 30 deletions
+3 -3
View File
@@ -62,7 +62,7 @@ Real-world Polymarket API latency broken down by request phase:
- **32.5% more consistent** - **32.5% more consistent**
- **4.2x faster** than Official Python Client - **4.2x faster** than Official Python Client
**Benchmark Methodology:** The `rs-clob-client-v2` comparison separates cold start, warm connection, steady-state typed requests, network-only byte fetches, and CPU-only parsing. The latest local live-network run was on June 22, 2026 against `https://clob.polymarket.com/simplified-markets?next_cursor=MA==`. Steady-state rows use 40 paired iterations with alternating order and 100ms delay after 5 warmups; parse rows use 300 iterations from a cached 480KB payload. The network-only row compares byte fetches through each HTTP stack without typed deserialization. The CPU parse row compares polyfill typed parsing against the `rs-clob-client-v2` request-helper parse path; direct serde parsing of the SDK response type measured 0.5 / 0.6 / 0.6 ms. Run it with `cargo run --release --example official_client_side_by_side_benchmark --features official-client-benchmark`. See `examples/side_by_side_benchmark.rs` in commit `a63a170`: https://github.com/floor-licker/polyfill-rs/blob/a63a170/examples/side_by_side_benchmark.rs for the original legacy benchmark implementation. **Benchmark Methodology:** The `rs-clob-client-v2` comparison separates cold start, warm connection, steady-state typed requests, network-only byte fetches, and CPU-only parsing. The latest local live-network run was on June 22, 2026 against `https://clob.polymarket.com/simplified-markets?next_cursor=MA==`. Steady-state rows use 40 paired iterations with alternating order and 100ms delay after 5 warmups; parse rows use 300 iterations from a cached 480KB payload. The network-only row compares byte fetches through each HTTP stack without typed deserialization. The CPU parse row compares polyfill's SIMD-backed typed parser against the `rs-clob-client-v2` request-helper parse path; direct serde parsing of the SDK response type measured 0.5 / 0.6 / 0.6 ms. Run it with `cargo run --release --example official_client_side_by_side_benchmark --features official-client-benchmark`. See `examples/side_by_side_benchmark.rs` in commit `a63a170`: https://github.com/floor-licker/polyfill-rs/blob/a63a170/examples/side_by_side_benchmark.rs for the original legacy benchmark implementation.
**Computational Performance (pure CPU, no I/O)** **Computational Performance (pure CPU, no I/O)**
@@ -70,14 +70,14 @@ Real-world Polymarket API latency broken down by request phase:
|-----------|-------------|-------| |-----------|-------------|-------|
| **Order Book Updates (1000 ops)** | 159.6 µs ± 32 µs | 6,260 updates/sec, zero-allocation | | **Order Book Updates (1000 ops)** | 159.6 µs ± 32 µs | 6,260 updates/sec, zero-allocation |
| **Spread/Mid Calculations** | 70 ns ± 77 ns | 14.3M ops/sec, optimized BTreeMap | | **Spread/Mid Calculations** | 70 ns ± 77 ns | 14.3M ops/sec, optimized BTreeMap |
| **JSON Parsing (480KB)** | ~2.3 ms | SIMD-accelerated parsing (1.77x faster than serde_json) | | **JSON Parsing (480KB)** | ~0.5 ms | SIMD-backed parsing for large REST market responses and benchmarked polyfill typed parse path |
| **WS `book` hot path (decode + apply)** | ~0.23 µs / 1.73 µs / 6.74 µs | 1 / 16 / 64 levels-per-side, strict fixed-point tape parser with generation-marked snapshot retention (see `benches/ws_hot_path.rs`) | | **WS `book` hot path (decode + apply)** | ~0.23 µs / 1.73 µs / 6.74 µs | 1 / 16 / 64 levels-per-side, strict fixed-point tape parser with generation-marked snapshot retention (see `benches/ws_hot_path.rs`) |
Run the WS hot-path benchmark locally with `cargo bench --bench ws_hot_path`. Run the WS hot-path benchmark locally with `cargo bench --bench ws_hot_path`.
**Key Performance Optimizations:** **Key Performance Optimizations:**
The 21.4% performance improvement comes from SIMD-accelerated JSON parsing (1.77x faster than serde_json), HTTP/2 tuning with 512KB stream windows optimized for 469KB payloads, explicit Polymarket request headers, and opt-in connection prewarming/keep-alive support. The 21.4% performance improvement comes from HTTP/2 tuning with 512KB stream windows optimized for 469KB payloads, explicit Polymarket request headers, SIMD-backed parsing for large REST market responses, and opt-in connection prewarming/keep-alive support.
### Memory Architecture ### Memory Architecture
@@ -21,6 +21,8 @@ use polymarket_client_sdk_v2::clob::types::response::{
}; };
use polymarket_client_sdk_v2::clob::{Client as OfficialClient, Config as OfficialConfig}; use polymarket_client_sdk_v2::clob::{Client as OfficialClient, Config as OfficialConfig};
type BenchResult<T> = Result<T, Box<dyn std::error::Error>>;
const INITIAL_CURSOR: &str = "MA=="; const INITIAL_CURSOR: &str = "MA==";
const OFFICIAL_SDK_REV: &str = "8ba5008733c3c03e92041eef8b1cb8495dbed718"; const OFFICIAL_SDK_REV: &str = "8ba5008733c3c03e92041eef8b1cb8495dbed718";
@@ -323,21 +325,22 @@ fn official_style_http_client() -> Result<reqwest::Client, reqwest::Error> {
reqwest::Client::builder().default_headers(headers).build() reqwest::Client::builder().default_headers(headers).build()
} }
fn parse_polyfill_once(bytes: &[u8]) -> Result<Duration, serde_json::Error> { fn parse_polyfill_once(bytes: &[u8]) -> BenchResult<Duration> {
let start = Instant::now(); let start = Instant::now();
let page: polyfill_rs::types::SimplifiedMarketsResponse = serde_json::from_slice(bytes)?; let page: polyfill_rs::types::SimplifiedMarketsResponse =
polyfill_rs::decode::fast_parse::parse_json_fast_owned(bytes)?;
black_box(page); black_box(page);
Ok(start.elapsed()) Ok(start.elapsed())
} }
fn parse_official_direct_once(bytes: &[u8]) -> Result<Duration, serde_json::Error> { fn parse_official_direct_once(bytes: &[u8]) -> BenchResult<Duration> {
let start = Instant::now(); let start = Instant::now();
let page: OfficialPage<OfficialSimplifiedMarketResponse> = serde_json::from_slice(bytes)?; let page: OfficialPage<OfficialSimplifiedMarketResponse> = serde_json::from_slice(bytes)?;
black_box(page); black_box(page);
Ok(start.elapsed()) Ok(start.elapsed())
} }
fn parse_official_helper_once(bytes: &[u8]) -> Result<Duration, serde_json::Error> { fn parse_official_helper_once(bytes: &[u8]) -> BenchResult<Duration> {
let start = Instant::now(); let start = Instant::now();
let value: serde_json::Value = serde_json::from_slice(bytes)?; let value: serde_json::Value = serde_json::from_slice(bytes)?;
let page: Option<OfficialPage<OfficialSimplifiedMarketResponse>> = let page: Option<OfficialPage<OfficialSimplifiedMarketResponse>> =
@@ -350,9 +353,9 @@ fn run_parse_samples<F>(
bytes: &[u8], bytes: &[u8],
iterations: usize, iterations: usize,
mut parse_once: F, mut parse_once: F,
) -> Result<Vec<Duration>, serde_json::Error> ) -> BenchResult<Vec<Duration>>
where where
F: FnMut(&[u8]) -> Result<Duration, serde_json::Error>, F: FnMut(&[u8]) -> BenchResult<Duration>,
{ {
let mut times = Vec::with_capacity(iterations); let mut times = Vec::with_capacity(iterations);
for _ in 0..iterations { for _ in 0..iterations {
+21 -21
View File
@@ -17,9 +17,10 @@ use alloy_primitives::{Address, U256};
use alloy_signer_local::PrivateKeySigner; use alloy_signer_local::PrivateKeySigner;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, ACCEPT, CONTENT_TYPE, USER_AGENT}; use reqwest::header::{HeaderMap, HeaderName, HeaderValue, ACCEPT, CONTENT_TYPE, USER_AGENT};
use reqwest::Client; use reqwest::Client;
use reqwest::{Method, RequestBuilder}; use reqwest::{Method, RequestBuilder, Response};
use rust_decimal::prelude::FromPrimitive; use rust_decimal::prelude::FromPrimitive;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
@@ -117,6 +118,19 @@ struct ClientAuthConfig {
} }
impl ClobClient { impl ClobClient {
async fn parse_json_response<T>(response: Response) -> Result<T>
where
T: DeserializeOwned,
{
let mut bytes = response
.bytes()
.await
.map_err(|e| PolyfillError::network(format!("Failed to read response body: {e}"), e))?
.to_vec();
crate::decode::fast_parse::parse_json_fast(&mut bytes)
}
fn build_client( fn build_client(
host: &str, host: &str,
chain_id: u64, chain_id: u64,
@@ -368,8 +382,7 @@ impl ClobClient {
)); ));
} }
let order_book: OrderBookSummary = response.json().await?; Self::parse_json_response(response).await
Ok(order_book)
} }
/// Get midpoint for a token /// Get midpoint for a token
@@ -388,8 +401,7 @@ impl ClobClient {
)); ));
} }
let midpoint: MidpointResponse = response.json().await?; Self::parse_json_response(response).await
Ok(midpoint)
} }
/// Get spread for a token /// Get spread for a token
@@ -2407,10 +2419,7 @@ impl ClobClient {
.await .await
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
response Self::parse_json_response(response).await
.json::<crate::types::MarketsResponse>()
.await
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
} }
/// Get sampling simplified markets with pagination /// Get sampling simplified markets with pagination
@@ -2428,10 +2437,7 @@ impl ClobClient {
.await .await
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
response Self::parse_json_response(response).await
.json::<crate::types::SimplifiedMarketsResponse>()
.await
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
} }
/// Get markets with pagination /// Get markets with pagination
@@ -2449,10 +2455,7 @@ impl ClobClient {
.await .await
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
response Self::parse_json_response(response).await
.json::<crate::types::MarketsResponse>()
.await
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
} }
/// Get simplified markets with pagination /// Get simplified markets with pagination
@@ -2470,10 +2473,7 @@ impl ClobClient {
.await .await
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
response Self::parse_json_response(response).await
.json::<crate::types::SimplifiedMarketsResponse>()
.await
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
} }
/// Get single market by condition ID /// Get single market by condition ID