bench: add hft loop benchmark

This commit is contained in:
floor-licker
2026-06-22 21:32:59 -04:00
parent fd8a2ebdb3
commit 105ddf238d
3 changed files with 362 additions and 0 deletions
+4
View File
@@ -108,6 +108,10 @@ harness = false
name = "ws_hot_path"
harness = false
[[bench]]
name = "hft_loop"
harness = false
[profile.release]
# Optimizations for HFT performance
opt-level = 3
+4
View File
@@ -73,8 +73,12 @@ Real-world Polymarket API latency broken down by request phase:
| **Spread/Mid Calculations** | 26.6 ns | best bid/ask + spread + mid over sorted-vector book sides |
| **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.24 µs / 1.56 µs / 5.92 µs | 1 / 16 / 64 levels-per-side, strict fixed-point tape parser with generation-marked snapshot retention (see `benches/ws_hot_path.rs`) |
| **Local HFT Loop** | ~39.0 µs | WS apply + best-ask decision + prepared EIP-712 signing + submit-body serialization + L2 auth headers, no live network I/O (see `benches/hft_loop.rs`) |
Run the WS hot-path benchmark locally with `cargo bench --bench ws_hot_path`.
Run the local HFT-loop benchmark with `cargo bench --bench hft_loop --features official-client-benchmark`.
**Local HFT-loop comparison:** In the current local offline run, `polyfill-rs` measured ~39.0 µs for a full synthetic loop with 16 book levels per side: WS `book` apply, best-ask decision, prepared order signing, submit-body serialization, and L2 auth header construction. The comparable order-only path without WS parsing or L2 auth measured ~35.4 µs for `polyfill-rs` and ~35.4 µs for `rs-clob-client-v2`. The `rs-clob-client-v2` row uses its warmed public builder/sign/serialize API; it does not include L2 auth header construction because that helper is not public in the SDK.
**Parsing paths:** `polyfill-rs` keeps two parsing layers on purpose. The allocation-sensitive WS `book` path uses `WsBookUpdateProcessor` in `src/ws_hot_path.rs`, which walks a reusable `simd-json` tape and applies fixed-point book levels directly. The generic stream parser in `src/decode.rs` is an ergonomic compatibility path: it parses through `serde_json::Value` so it can tolerate batches, unknown event types, and mixed message shapes. Likewise, several generic numeric/decimal deserializers in `src/decode.rs` accept string-or-number API fields through `serde_json::Value`; they are not the zero-allocation hot path.
+354
View File
@@ -0,0 +1,354 @@
//! End-to-end local HFT-loop benchmarks.
//!
//! These benches intentionally avoid live network I/O. They measure the local loop a trading
//! process actually controls: receive/apply a book update, make a simple best-ask decision, build
//! and sign an order, serialize the exact submit body, and build L2 auth headers.
//!
//! Run the polyfill benches with:
//! `cargo bench --bench hft_loop`
//!
//! Run the optional rs-clob-client-v2 comparison with:
//! `cargo bench --bench hft_loop --features official-client-benchmark`
use alloy_signer_local::PrivateKeySigner;
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
use polyfill_rs::{
auth::{create_l2_headers_with_body_bytes, PreparedApiCredentials},
orders::{OrderBuilder, BYTES32_ZERO},
types::{ApiCredentials, OrderType, PostOrder, PostOrderOptions},
OrderBookManager, Side, WsBookUpdateProcessor,
};
use rust_decimal::Decimal;
use std::sync::atomic::{AtomicU64, Ordering};
const ASSET_ID: &str = "12345678901234567890";
const MARKET: &str = "0xabc";
const CHAIN_ID: u64 = 137;
const PRIVATE_KEY: &str = "0x1234567890123456789012345678901234567890123456789012345678901234";
const START_TIMESTAMP: u64 = 1_000_000_000_000_000;
fn test_signer() -> PrivateKeySigner {
PRIVATE_KEY.parse().expect("valid benchmark private key")
}
fn api_credentials() -> ApiCredentials {
ApiCredentials {
api_key: "benchmark-api-key".to_string(),
secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(),
passphrase: "benchmark-passphrase".to_string(),
}
}
fn price_decimal_from_ticks(ticks: u32) -> Decimal {
Decimal::new(ticks as i64, 4)
}
#[derive(Clone, Copy)]
struct TimestampRange {
start: usize,
end: usize,
}
impl TimestampRange {
fn find(bytes: &[u8]) -> Self {
let needle = b"\"timestamp\":";
let Some(pos) = bytes.windows(needle.len()).position(|w| w == needle) else {
panic!("timestamp field not found in WS template JSON");
};
let start = pos + needle.len();
let mut end = start;
while end < bytes.len() && bytes[end].is_ascii_digit() {
end += 1;
}
Self { start, end }
}
fn write_fixed_width(&self, bytes: &mut [u8], mut value: u64) {
for idx in (self.start..self.end).rev() {
bytes[idx] = b'0' + (value % 10) as u8;
value /= 10;
}
}
}
fn price_string_from_ticks(ticks: u32) -> String {
let whole = ticks / 10_000;
let frac = ticks % 10_000;
format!("{whole}.{frac:04}")
}
fn build_ws_book_template(levels_per_side: usize) -> Vec<u8> {
let mut json = String::new();
json.push_str("{\"event_type\":\"book\",\"asset_id\":\"");
json.push_str(ASSET_ID);
json.push_str("\",\"market\":\"");
json.push_str(MARKET);
json.push_str("\",\"timestamp\":");
json.push_str(&START_TIMESTAMP.to_string());
json.push_str(",\"bids\":[");
for i in 0..levels_per_side {
if i != 0 {
json.push(',');
}
let bid_price = price_string_from_ticks(7_500 - i as u32);
json.push_str("{\"price\":\"");
json.push_str(&bid_price);
json.push_str("\",\"size\":\"100.0000\"}");
}
json.push_str("],\"asks\":[");
for i in 0..levels_per_side {
if i != 0 {
json.push(',');
}
let ask_price = price_string_from_ticks(7_501 + i as u32);
json.push_str("{\"price\":\"");
json.push_str(&ask_price);
json.push_str("\",\"size\":\"100.0000\"}");
}
json.push_str("]}");
json.into_bytes()
}
fn build_polyfill_order_context() -> (
PrivateKeySigner,
polyfill_rs::orders::PreparedOrderPath,
ApiCredentials,
PreparedApiCredentials,
PostOrderOptions,
) {
let signer = test_signer();
let builder = OrderBuilder::new(signer.clone(), None, None);
let prepared_order = builder
.prepare_order_path(
CHAIN_ID,
ASSET_ID.to_string(),
Decimal::new(1, 4),
false,
Some(BYTES32_ZERO),
Some(BYTES32_ZERO),
)
.unwrap();
let api_creds = api_credentials();
let prepared_api_creds = PreparedApiCredentials::try_new(api_creds.clone()).unwrap();
let post_options = PostOrderOptions {
order_type: OrderType::GTD,
post_only: false,
defer_exec: false,
};
(
signer,
prepared_order,
api_creds,
prepared_api_creds,
post_options,
)
}
fn benchmark_polyfill_hft_loop(c: &mut Criterion) {
let (signer, prepared_order, api_creds, prepared_api_creds, post_options) =
build_polyfill_order_context();
let books = OrderBookManager::new(64);
books.get_or_create_book(ASSET_ID).unwrap();
books
.with_book_mut(ASSET_ID, |book| {
book.set_tick_size_ticks(1);
Ok(())
})
.unwrap();
let template = build_ws_book_template(16);
let timestamp_range = TimestampRange::find(&template);
let timestamp = AtomicU64::new(START_TIMESTAMP + 1);
let mut processor = WsBookUpdateProcessor::new(template.len());
c.bench_function("polyfill_hft_ws_apply_decide_sign_serialize_auth", |b| {
b.iter_batched_ref(
|| {
let mut message = template.clone();
let next_timestamp = timestamp.fetch_add(1, Ordering::Relaxed);
timestamp_range.write_fixed_width(&mut message, next_timestamp);
message
},
|message| {
let stats = processor.process_bytes(black_box(message), &books).unwrap();
debug_assert_eq!(stats.book_messages, 1);
let selected_price = books
.with_book_mut(ASSET_ID, |book| {
let best_ask = book.best_ask_fast().expect("seeded ask side");
Ok(price_decimal_from_ticks(best_ask.price))
})
.unwrap();
let signed_order = prepared_order
.create_limit_order(
black_box(Side::BUY),
black_box(selected_price),
black_box(Decimal::new(10_025, 2)),
black_box(Some(1_900_000_000)),
)
.unwrap();
let body = PostOrder::new(
black_box(signed_order),
black_box(api_creds.api_key.clone()),
black_box(post_options),
);
let body_bytes = serde_json::to_vec(black_box(&body)).unwrap();
let headers = create_l2_headers_with_body_bytes(
&signer,
&prepared_api_creds,
"POST",
"/order",
Some(&body_bytes),
)
.unwrap();
black_box((stats, body_bytes, headers))
},
BatchSize::SmallInput,
)
});
}
fn benchmark_polyfill_order_decision_to_payload(c: &mut Criterion) {
let (signer, prepared_order, api_creds, prepared_api_creds, post_options) =
build_polyfill_order_context();
let selected_price = Decimal::new(7_501, 4);
c.bench_function("polyfill_decide_sign_serialize_prepared", |b| {
b.iter(|| {
let signed_order = prepared_order
.create_limit_order(
black_box(Side::BUY),
black_box(selected_price),
black_box(Decimal::new(10_025, 2)),
black_box(Some(1_900_000_000)),
)
.unwrap();
let body = PostOrder::new(
black_box(signed_order),
black_box(api_creds.api_key.clone()),
black_box(post_options),
);
let body_bytes = serde_json::to_vec(black_box(&body)).unwrap();
black_box(body_bytes)
})
});
c.bench_function("polyfill_decide_sign_serialize_auth_prepared", |b| {
b.iter(|| {
let signed_order = prepared_order
.create_limit_order(
black_box(Side::BUY),
black_box(selected_price),
black_box(Decimal::new(10_025, 2)),
black_box(Some(1_900_000_000)),
)
.unwrap();
let body = PostOrder::new(
black_box(signed_order),
black_box(api_creds.api_key.clone()),
black_box(post_options),
);
let body_bytes = serde_json::to_vec(black_box(&body)).unwrap();
let headers = create_l2_headers_with_body_bytes(
&signer,
&prepared_api_creds,
"POST",
"/order",
Some(&body_bytes),
)
.unwrap();
black_box((body_bytes, headers))
})
});
}
#[cfg(feature = "official-client-benchmark")]
fn benchmark_official_order_decision_to_payload(c: &mut Criterion) {
use polymarket_client_sdk_v2::{
auth::{Credentials, LocalSigner, Signer, Uuid},
clob::{
types::{OrderType as OfficialOrderType, Side as OfficialSide, TickSize},
Client as OfficialClient, Config as OfficialConfig,
},
types::{dec, Decimal as OfficialDecimal, U256},
POLYGON,
};
use std::str::FromStr as _;
let mut server = mockito::Server::new();
let _version_mock = server
.mock("GET", "/version")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"version":2}"#)
.create();
let runtime = tokio::runtime::Runtime::new().unwrap();
let signer = LocalSigner::from_str(PRIVATE_KEY)
.unwrap()
.with_chain_id(Some(POLYGON));
let token_id = U256::from_str(ASSET_ID).unwrap();
let client = runtime.block_on(async {
let client = OfficialClient::new(&server.url(), OfficialConfig::default()).unwrap();
client.set_tick_size(token_id, TickSize::TenThousandth);
client.set_neg_risk(token_id, false);
client.version().await.unwrap();
client
.authentication_builder(&signer)
.credentials(Credentials::new(
Uuid::nil(),
"dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(),
"benchmark-passphrase".to_string(),
))
.authenticate()
.await
.unwrap()
});
c.bench_function("rs_clob_client_v2_decide_sign_serialize_warmed", |b| {
b.iter(|| {
let body_bytes = runtime.block_on(async {
let order = client
.limit_order()
.token_id(black_box(token_id))
.side(black_box(OfficialSide::Buy))
.price(black_box(dec!(0.7501)))
.size(black_box(OfficialDecimal::new(10_025, 2)))
.expiration(chrono::DateTime::from_timestamp(1_900_000_000, 0).unwrap())
.order_type(OfficialOrderType::GTD)
.build()
.await
.unwrap();
let signed = client.sign(&signer, order).await.unwrap();
serde_json::to_vec(black_box(&signed)).unwrap()
});
black_box(body_bytes)
})
});
}
#[cfg(not(feature = "official-client-benchmark"))]
criterion_group!(
benches,
benchmark_polyfill_hft_loop,
benchmark_polyfill_order_decision_to_payload
);
#[cfg(feature = "official-client-benchmark")]
criterion_group!(
benches,
benchmark_polyfill_hft_loop,
benchmark_polyfill_order_decision_to_payload,
benchmark_official_order_decision_to_payload
);
criterion_main!(benches);