perf: retain snapshots by generation

This commit is contained in:
floor-licker
2026-06-22 13:46:29 -04:00
parent ebc2929b1e
commit ddbacf5fbb
3 changed files with 102 additions and 104 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ 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 |
| **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) |
| **WS `book` hot path (decode + apply)** | ~0.27 µs / 7.46 µs / 93.24 µs | 1 / 16 / 64 levels-per-side, strict 4dp fixed-point tape parser, no Decimal/rounding/clamping in the feed path (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`.
+100 -70
View File
@@ -9,6 +9,12 @@ use std::collections::BTreeMap; // BTreeMap keeps prices sorted automatically -
use std::sync::{Arc, RwLock}; // For thread-safe access across multiple tasks
use tracing::{debug, trace, warn}; // Logging for debugging and monitoring
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct StoredLevel {
qty: Qty,
generation: u64,
}
/// High-performance order book implementation
///
/// This is the core data structure that holds all the live buy/sell orders for a token.
@@ -40,20 +46,23 @@ pub struct OrderBook {
/// Key = price in ticks (like 6500 for $0.65), Value = size in fixed-point units
///
/// BEFORE (slow): bids: BTreeMap<Decimal, Decimal>,
/// AFTER (fast): bids: BTreeMap<Price, Qty>,
/// AFTER (fast): bids: BTreeMap<Price, StoredLevel>,
///
/// Why this is faster:
/// - Integer comparisons are ~10x faster than Decimal comparisons
/// - No memory allocation for each price level
/// - Better CPU cache utilization (smaller data structures)
bids: BTreeMap<Price, Qty>,
bids: BTreeMap<Price, StoredLevel>,
/// Ask side (price -> size, sorted ascending) - NOW USING FIXED-POINT!
/// BTreeMap keeps lowest asks first - people selling at cheapest prices
///
/// BEFORE (slow): asks: BTreeMap<Decimal, Decimal>,
/// AFTER (fast): asks: BTreeMap<Price, Qty>,
asks: BTreeMap<Price, Qty>,
/// AFTER (fast): asks: BTreeMap<Price, StoredLevel>,
asks: BTreeMap<Price, StoredLevel>,
/// Snapshot generation used to retain book levels without rescanning input payloads.
snapshot_generation: u64,
/// Minimum tick size for this market in ticks (like 10 for $0.001 increments)
/// Some markets only allow certain price increments
@@ -93,6 +102,7 @@ impl OrderBook {
timestamp: Utc::now(),
bids: BTreeMap::new(), // Empty to start - using Price/Qty types
asks: BTreeMap::new(), // Empty to start - using Price/Qty types
snapshot_generation: 0,
tick_size_ticks: None, // We'll set this later when we learn about the market
max_depth,
}
@@ -123,17 +133,14 @@ impl OrderBook {
// self.bids.iter().next_back().map(|(&price, &size)| BookLevel { price, size })
// AFTER (fast, ~5ns, no allocation for the lookup):
self.bids
.iter()
.next_back()
.map(|(&price_ticks, &size_units)| {
// Convert from internal fixed-point to external Decimal format
// This conversion only happens at the API boundary
BookLevel {
price: price_to_decimal(price_ticks),
size: qty_to_decimal(size_units),
}
})
self.bids.iter().next_back().map(|(&price_ticks, level)| {
// Convert from internal fixed-point to external Decimal format
// This conversion only happens at the API boundary
BookLevel {
price: price_to_decimal(price_ticks),
size: qty_to_decimal(level.qty),
}
})
}
/// Get the current best ask (lowest price someone is willing to sell at)
@@ -145,12 +152,12 @@ impl OrderBook {
// self.asks.iter().next().map(|(&price, &size)| BookLevel { price, size })
// AFTER (fast, ~5ns, no allocation for the lookup):
self.asks.iter().next().map(|(&price_ticks, &size_units)| {
self.asks.iter().next().map(|(&price_ticks, level)| {
// Convert from internal fixed-point to external Decimal format
// This conversion only happens at the API boundary
BookLevel {
price: price_to_decimal(price_ticks),
size: qty_to_decimal(size_units),
size: qty_to_decimal(level.qty),
}
})
}
@@ -161,7 +168,7 @@ impl OrderBook {
self.bids
.iter()
.next_back()
.map(|(&price, &size)| FastBookLevel::new(price, size))
.map(|(&price, level)| FastBookLevel::new(price, level.qty))
}
/// Get the current best ask in fast internal format
@@ -170,7 +177,7 @@ impl OrderBook {
self.asks
.iter()
.next()
.map(|(&price, &size)| FastBookLevel::new(price, size))
.map(|(&price, level)| FastBookLevel::new(price, level.qty))
}
/// Get the current spread (difference between best ask and best bid)
@@ -251,9 +258,9 @@ impl OrderBook {
.iter()
.rev() // Reverse because we want highest prices first
.take(depth) // Only take the top N levels
.map(|(&price_ticks, &size_units)| BookLevel {
.map(|(&price_ticks, level)| BookLevel {
price: price_to_decimal(price_ticks),
size: qty_to_decimal(size_units),
size: qty_to_decimal(level.qty),
})
.collect()
}
@@ -268,9 +275,9 @@ impl OrderBook {
self.asks
.iter() // Already in ascending order, so no need to reverse
.take(depth) // Only take the top N levels
.map(|(&price_ticks, &size_units)| BookLevel {
.map(|(&price_ticks, level)| BookLevel {
price: price_to_decimal(price_ticks),
size: qty_to_decimal(size_units),
size: qty_to_decimal(level.qty),
})
.collect()
}
@@ -283,7 +290,7 @@ impl OrderBook {
.iter()
.rev() // Reverse because we want highest prices first
.take(depth) // Only take the top N levels
.map(|(&price, &size)| FastBookLevel::new(price, size))
.map(|(&price, level)| FastBookLevel::new(price, level.qty))
.collect()
}
@@ -294,7 +301,7 @@ impl OrderBook {
self.asks
.iter() // Already in ascending order, so no need to reverse
.take(depth) // Only take the top N levels
.map(|(&price, &size)| FastBookLevel::new(price, size))
.map(|(&price, level)| FastBookLevel::new(price, level.qty))
.collect()
}
@@ -415,6 +422,7 @@ impl OrderBook {
self.sequence = timestamp;
self.timestamp = chrono::DateTime::<Utc>::from_timestamp_millis(timestamp as i64)
.unwrap_or_else(Utc::now);
self.begin_snapshot();
Ok(true)
}
@@ -435,22 +443,14 @@ impl OrderBook {
}
}
match side {
Side::BUY => self.apply_bid_delta_fast(price_ticks, size_units),
Side::SELL => self.apply_ask_delta_fast(price_ticks, size_units),
}
self.apply_snapshot_level(side, price_ticks, size_units);
Ok(())
}
/// Finish applying a WS `book` snapshot.
pub(crate) fn finish_ws_book_update(
&mut self,
mut has_bid: impl FnMut(Price) -> bool,
mut has_ask: impl FnMut(Price) -> bool,
) {
self.bids.retain(|price_ticks, _| has_bid(*price_ticks));
self.asks.retain(|price_ticks, _| has_ask(*price_ticks));
pub(crate) fn finish_ws_book_update(&mut self) {
self.finish_snapshot();
self.trim_depth();
}
@@ -479,6 +479,7 @@ impl OrderBook {
self.sequence = update.timestamp;
self.timestamp = chrono::DateTime::<Utc>::from_timestamp_millis(update.timestamp as i64)
.unwrap_or_else(Utc::now);
self.begin_snapshot();
// Apply bids (BUY) and asks (SELL) as level upserts.
for level in &update.bids {
@@ -493,11 +494,7 @@ impl OrderBook {
}
}
if size_units == 0 {
self.bids.remove(&price_ticks);
} else {
self.bids.insert(price_ticks, size_units);
}
self.apply_snapshot_level(Side::BUY, price_ticks, size_units);
}
for level in &update.asks {
@@ -512,17 +509,10 @@ impl OrderBook {
}
}
if size_units == 0 {
self.asks.remove(&price_ticks);
} else {
self.asks.insert(price_ticks, size_units);
}
self.apply_snapshot_level(Side::SELL, price_ticks, size_units);
}
self.bids
.retain(|price_ticks, _| book_update_has_level(&update.bids, *price_ticks));
self.asks
.retain(|price_ticks, _| book_update_has_level(&update.asks, *price_ticks));
self.finish_snapshot();
self.trim_depth();
Ok(())
}
@@ -568,7 +558,13 @@ impl OrderBook {
if size_units == 0 {
self.bids.remove(&price_ticks); // No more buyers at this price
} else {
self.bids.insert(price_ticks, size_units); // Update total size at this price
self.bids.insert(
price_ticks,
StoredLevel {
qty: size_units,
generation: self.snapshot_generation,
},
); // Update total size at this price
}
}
@@ -588,10 +584,49 @@ impl OrderBook {
if size_units == 0 {
self.asks.remove(&price_ticks); // No more sellers at this price
} else {
self.asks.insert(price_ticks, size_units); // Update total size at this price
self.asks.insert(
price_ticks,
StoredLevel {
qty: size_units,
generation: self.snapshot_generation,
},
); // Update total size at this price
}
}
#[inline]
fn begin_snapshot(&mut self) {
self.snapshot_generation = self.snapshot_generation.wrapping_add(1);
}
#[inline]
fn apply_snapshot_level(&mut self, side: Side, price_ticks: Price, size_units: Qty) {
let generation = self.snapshot_generation;
let map = match side {
Side::BUY => &mut self.bids,
Side::SELL => &mut self.asks,
};
if size_units == 0 {
map.remove(&price_ticks);
} else {
map.insert(
price_ticks,
StoredLevel {
qty: size_units,
generation,
},
);
}
}
#[inline]
fn finish_snapshot(&mut self) {
let generation = self.snapshot_generation;
self.bids.retain(|_, level| level.generation == generation);
self.asks.retain(|_, level| level.generation == generation);
}
/// Trim the book to maintain depth limits
/// We don't want to track every single price level - just the best ones
///
@@ -716,12 +751,20 @@ impl OrderBook {
match side {
Side::BUY => {
// How much we can buy at this price (look at asks)
let size_units = self.asks.get(&price_ticks).copied().unwrap_or_default();
let size_units = self
.asks
.get(&price_ticks)
.map(|level| level.qty)
.unwrap_or_default();
qty_to_decimal(size_units)
},
Side::SELL => {
// How much we can sell at this price (look at bids)
let size_units = self.bids.get(&price_ticks).copied().unwrap_or_default();
let size_units = self
.bids
.get(&price_ticks)
.map(|level| level.qty)
.unwrap_or_default();
qty_to_decimal(size_units)
},
}
@@ -755,7 +798,7 @@ impl OrderBook {
};
// Sum up the sizes, converting from fixed-point back to Decimal
let total_size_units: i64 = levels.into_iter().map(|(_, &size)| size).sum();
let total_size_units: i64 = levels.into_iter().map(|(_, level)| level.qty).sum();
qty_to_decimal(total_size_units)
}
@@ -769,19 +812,6 @@ impl OrderBook {
}
}
fn book_update_has_level(levels: &[OrderSummary], price_ticks: Price) -> bool {
levels.iter().any(|level| {
let Ok(level_price_ticks) = decimal_to_price(level.price) else {
return false;
};
let Ok(size_units) = decimal_to_qty(level.size) else {
return false;
};
size_units != 0 && level_price_ticks == price_ticks
})
}
/// Market impact calculation result
/// This tells you what would happen if you executed a large order
#[derive(Debug, Clone)]
@@ -978,8 +1008,8 @@ impl OrderBook {
let bid_count = self.bids.len();
let ask_count = self.asks.len();
// Sum up all bid/ask sizes, converting from fixed-point back to Decimal
let total_bid_size_units: i64 = self.bids.values().sum();
let total_ask_size_units: i64 = self.asks.values().sum();
let total_bid_size_units: i64 = self.bids.values().map(|level| level.qty).sum();
let total_ask_size_units: i64 = self.asks.values().map(|level| level.qty).sum();
let total_bid_size = qty_to_decimal(total_bid_size_units);
let total_ask_size = qty_to_decimal(total_ask_size_units);
+1 -33
View File
@@ -139,10 +139,7 @@ fn process_stream_object<'tape, 'input>(
applied += apply_levels(book, Side::SELL, asks)?;
}
book.finish_ws_book_update(
|price_ticks| ws_levels_contain_price(bids, price_ticks),
|price_ticks| ws_levels_contain_price(asks, price_ticks),
);
book.finish_ws_book_update();
Ok(applied)
})?;
@@ -188,35 +185,6 @@ fn apply_levels<'tape, 'input>(
Ok(applied)
}
fn ws_levels_contain_price<'tape, 'input>(
levels: Option<simd_json::tape::Array<'tape, 'input>>,
price_ticks: Price,
) -> bool {
let Some(levels) = levels else {
return false;
};
levels.iter().any(|level| {
let Some(obj) = level.as_object() else {
return false;
};
let Some(price_str) = obj.get("price").and_then(|v| v.into_string()) else {
return false;
};
let Some(size_str) = obj.get("size").and_then(|v| v.into_string()) else {
return false;
};
let Ok(level_price_ticks) = parse_price_ticks_4dp(price_str) else {
return false;
};
let Ok(size_units) = parse_qty_scaled_4dp(size_str) else {
return false;
};
size_units != 0 && level_price_ticks == price_ticks
})
}
#[inline]
fn parse_price_ticks_4dp(value: &str) -> Result<Price> {
let scaled = parse_scaled_4_u64(value)?;