mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-07-27 20:47:46 +00:00
fix: avoid websocket snapshot hash allocation
This commit is contained in:
+19
-7
@@ -208,11 +208,11 @@ pub struct OrderBook {
|
||||
/// Snapshot generation used to retain book levels without rescanning input payloads.
|
||||
snapshot_generation: u64,
|
||||
|
||||
/// Last accepted full-book snapshot hash, when the feed provides one.
|
||||
/// Last accepted full-book snapshot hash fingerprint, when the feed provides one.
|
||||
///
|
||||
/// This is used as a tie-breaker for same-millisecond snapshots. If the timestamp is equal
|
||||
/// but the hash differs, the snapshot can still represent a newer book state.
|
||||
last_snapshot_hash: Option<String>,
|
||||
/// but the hash fingerprint differs, the snapshot can still represent a newer book state.
|
||||
last_snapshot_hash_fingerprint: Option<u64>,
|
||||
|
||||
/// Minimum tick size for this market in ticks (like 10 for $0.001 increments)
|
||||
/// Some markets only allow certain price increments
|
||||
@@ -255,7 +255,7 @@ impl OrderBook {
|
||||
bids: BookSide::new(BookSideKind::Bid, max_depth), // Empty to start
|
||||
asks: BookSide::new(BookSideKind::Ask, max_depth), // Empty to start
|
||||
snapshot_generation: 0,
|
||||
last_snapshot_hash: None,
|
||||
last_snapshot_hash_fingerprint: None,
|
||||
tick_size_ticks: None, // We'll set this later when we learn about the market
|
||||
max_depth,
|
||||
}
|
||||
@@ -620,7 +620,7 @@ impl OrderBook {
|
||||
}
|
||||
|
||||
self.last_snapshot_timestamp_ms = update.timestamp;
|
||||
self.last_snapshot_hash = update.hash.clone();
|
||||
self.last_snapshot_hash_fingerprint = update.hash.as_deref().map(snapshot_hash_fingerprint);
|
||||
self.timestamp = chrono::DateTime::<Utc>::from_timestamp_millis(update.timestamp as i64)
|
||||
.unwrap_or_else(Utc::now);
|
||||
self.begin_snapshot();
|
||||
@@ -730,7 +730,9 @@ impl OrderBook {
|
||||
return false;
|
||||
}
|
||||
|
||||
hash.is_some_and(|hash| self.last_snapshot_hash.as_deref() != Some(hash))
|
||||
hash.is_some_and(|hash| {
|
||||
self.last_snapshot_hash_fingerprint != Some(snapshot_hash_fingerprint(hash))
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -769,7 +771,7 @@ impl OrderBook {
|
||||
}
|
||||
|
||||
self.last_snapshot_timestamp_ms = timestamp;
|
||||
self.last_snapshot_hash = hash.map(str::to_owned);
|
||||
self.last_snapshot_hash_fingerprint = hash.map(snapshot_hash_fingerprint);
|
||||
self.timestamp = chrono::DateTime::<Utc>::from_timestamp_millis(timestamp as i64)
|
||||
.unwrap_or_else(Utc::now);
|
||||
self.begin_snapshot();
|
||||
@@ -1005,6 +1007,16 @@ struct BookShard {
|
||||
books: RwLock<HashMap<String, OrderBook>>,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn snapshot_hash_fingerprint(hash: &str) -> u64 {
|
||||
let mut fingerprint = 0xcbf2_9ce4_8422_2325u64;
|
||||
for &byte in hash.as_bytes() {
|
||||
fingerprint ^= byte as u64;
|
||||
fingerprint = fingerprint.wrapping_mul(0x0000_0100_0000_01b3);
|
||||
}
|
||||
fingerprint
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn shard_index(token_id: &str, shard_count: usize) -> usize {
|
||||
debug_assert!(shard_count > 0);
|
||||
|
||||
@@ -182,6 +182,50 @@ fn ws_book_message(
|
||||
json.into_bytes()
|
||||
}
|
||||
|
||||
fn ws_book_message_with_hash(
|
||||
asset_id: &str,
|
||||
timestamp: u64,
|
||||
hash: &str,
|
||||
bid_ticks: &[i64],
|
||||
ask_ticks: &[i64],
|
||||
) -> Vec<u8> {
|
||||
let mut json =
|
||||
String::with_capacity(190 + hash.len() + (bid_ticks.len() + ask_ticks.len()) * 40);
|
||||
write!(
|
||||
&mut json,
|
||||
"{{\"event_type\":\"book\",\"asset_id\":\"{asset_id}\",\"market\":\"0xabc\",\"timestamp\":{timestamp},\"hash\":\"{hash}\",\"bids\":["
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for (idx, price_ticks) in bid_ticks.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
json.push(',');
|
||||
}
|
||||
write!(
|
||||
&mut json,
|
||||
"{{\"price\":\"{}\",\"size\":\"100.0000\"}}",
|
||||
Decimal::new(*price_ticks, 4)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
json.push_str("],\"asks\":[");
|
||||
for (idx, price_ticks) in ask_ticks.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
json.push(',');
|
||||
}
|
||||
write!(
|
||||
&mut json,
|
||||
"{{\"price\":\"{}\",\"size\":\"100.0000\"}}",
|
||||
Decimal::new(*price_ticks, 4)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
json.push_str("]}");
|
||||
json.into_bytes()
|
||||
}
|
||||
|
||||
fn contiguous_ticks(start: i64, len: usize, step: i64) -> Vec<i64> {
|
||||
(0..len).map(|idx| start + (idx as i64 * step)).collect()
|
||||
}
|
||||
@@ -410,6 +454,30 @@ fn no_alloc_ws_book_update_processor_apply_existing_levels() {
|
||||
guard.assert_no_heap_traffic();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_alloc_ws_book_update_processor_same_timestamp_hash_change() {
|
||||
let asset_id = "test_asset_id";
|
||||
let manager = OrderBookManager::new(100);
|
||||
manager.get_or_create_book(asset_id).unwrap();
|
||||
seed_book_levels(&manager, asset_id, &[7500], &[7600]);
|
||||
|
||||
let mut processor = WsBookUpdateProcessor::new(4096);
|
||||
let mut warmup_msg = ws_book_message_with_hash(asset_id, 10, "hash_a", &[7500], &[7600]);
|
||||
processor
|
||||
.process_bytes(warmup_msg.as_mut_slice(), &manager)
|
||||
.unwrap();
|
||||
|
||||
let mut msg = ws_book_message_with_hash(asset_id, 10, "hash_b", &[7500], &[7600]);
|
||||
|
||||
let _ = heap_operation_count();
|
||||
|
||||
let guard = NoHeapTrafficGuard::new();
|
||||
processor
|
||||
.process_bytes(msg.as_mut_slice(), &manager)
|
||||
.unwrap();
|
||||
guard.assert_no_heap_traffic();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_alloc_ws_book_update_processor_one_new_level_with_reserved_capacity() {
|
||||
let asset_id = "test_asset_id";
|
||||
|
||||
Reference in New Issue
Block a user