mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-07-27 20:47:46 +00:00
bench: add hft loop benchmark
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user