From 63afac7aea67090dacf7276eb30b4839f5fb2ce7 Mon Sep 17 00:00:00 2001 From: Hookie Date: Sat, 28 Feb 2026 22:22:46 +0200 Subject: [PATCH] perf(swqos): reduce serializer cold-start overhead Replace full eager preallocation in the SWQOS serializer with a bounded\nprewarm and lazy growth strategy.\n\n- Prewarm only a small hot set of buffers instead of allocating all\n 10,000 upfront\n- Keep the same max pool capacity and buffer size to preserve the\n original buffer-pool design in steady state\n- Stop zero-filling each buffer during initialization; reserve capacity\n and grow data on demand\n- Return temporary buffers in Base64Encoder::serialize_and_encode to\n avoid pool depletion\n- Add tests for bounded prewarm and lazy allocation/return behavior\n\nWhy:\nThe previous Lazy initialization allocated and touched ~2.44 GiB\n(10,000 x 256 KiB) on first use, which can block the first buy submit\npath and produce a large cold-start latency spike.\n\nValidation:\n- cargo fmt --all\n- cargo check --lib\n- cargo check (SolBot workspace with path dependency) --- src/swqos/serialization.rs | 95 +++++++++++++++++++++++++------------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/src/swqos/serialization.rs b/src/swqos/serialization.rs index abfd416..796a6b4 100644 --- a/src/swqos/serialization.rs +++ b/src/swqos/serialization.rs @@ -1,18 +1,24 @@ //! Transaction serialization module. +use crate::perf::{ + compiler_optimization::CompileTimeOptimizedEventProcessor, simd::SIMDSerializer, +}; use anyhow::Result; -use base64::Engine; use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use crossbeam_queue::ArrayQueue; use once_cell::sync::Lazy; use solana_client::rpc_client::SerializableTransaction; use solana_sdk::signature::Signature; use solana_transaction_status::UiTransactionEncoding; use std::sync::Arc; -use crossbeam_queue::ArrayQueue; -use crate::perf::{ - simd::SIMDSerializer, - compiler_optimization::CompileTimeOptimizedEventProcessor, -}; + +/// Max number of reusable buffers kept in the queue. +const SERIALIZER_POOL_SIZE: usize = 10_000; +/// Per-buffer reserved capacity (bytes). +const SERIALIZER_BUFFER_SIZE: usize = 256 * 1024; +/// Cold-start prewarm count. Keep small to avoid first-submit spikes. +const SERIALIZER_PREWARM_BUFFERS: usize = 64; /// Zero-allocation serializer using a buffer pool to avoid runtime allocation. pub struct ZeroAllocSerializer { @@ -22,28 +28,30 @@ pub struct ZeroAllocSerializer { impl ZeroAllocSerializer { pub fn new(pool_size: usize, buffer_size: usize) -> Self { - let pool = ArrayQueue::new(pool_size); - - // Pre-allocate buffers - for _ in 0..pool_size { - let mut buffer = Vec::with_capacity(buffer_size); - buffer.resize(buffer_size, 0); - let _ = pool.push(buffer); - } - - Self { - buffer_pool: Arc::new(pool), - buffer_size, - } + Self::new_with_prewarm(pool_size, buffer_size, SERIALIZER_PREWARM_BUFFERS) } - pub fn serialize_zero_alloc(&self, data: &T, _label: &str) -> Result> { + fn new_with_prewarm(pool_size: usize, buffer_size: usize, prewarm_buffers: usize) -> Self { + let pool = ArrayQueue::new(pool_size); + let prewarm_count = prewarm_buffers.min(pool_size); + + // Prewarm only a small hot set to avoid large cold-start blocking. + // Remaining buffers are allocated lazily and returned to this pool. + for _ in 0..prewarm_count { + let _ = pool.push(Vec::with_capacity(buffer_size)); + } + + Self { buffer_pool: Arc::new(pool), buffer_size } + } + + pub fn serialize_zero_alloc( + &self, + data: &T, + _label: &str, + ) -> Result> { // Try to get a buffer from the pool - let mut buffer = self.buffer_pool.pop().unwrap_or_else(|| { - let mut buf = Vec::with_capacity(self.buffer_size); - buf.resize(self.buffer_size, 0); - buf - }); + let mut buffer = + self.buffer_pool.pop().unwrap_or_else(|| Vec::with_capacity(self.buffer_size)); // Serialize into buffer let serialized = bincode::serialize(data)?; @@ -67,12 +75,8 @@ impl ZeroAllocSerializer { } /// Global serializer instance. -static SERIALIZER: Lazy> = Lazy::new(|| { - Arc::new(ZeroAllocSerializer::new( - 10_000, // Pool size - 256 * 1024, // Buffer size: 256KB - )) -}); +static SERIALIZER: Lazy> = + Lazy::new(|| Arc::new(ZeroAllocSerializer::new(SERIALIZER_POOL_SIZE, SERIALIZER_BUFFER_SIZE))); /// Compile-time optimized event processor (zero runtime cost). static COMPILE_TIME_PROCESSOR: CompileTimeOptimizedEventProcessor = @@ -101,7 +105,9 @@ impl Base64Encoder { event_type: &str, ) -> Result { let serialized = SERIALIZER.serialize_zero_alloc(value, event_type)?; - Ok(STANDARD.encode(&serialized)) + let encoded = STANDARD.encode(&serialized); + SERIALIZER.return_buffer(serialized); + Ok(encoded) } } @@ -243,6 +249,29 @@ mod tests { fn test_serializer_stats() { let (available, capacity) = get_serializer_stats(); assert!(available <= capacity); - assert_eq!(capacity, 10_000); + assert_eq!(capacity, SERIALIZER_POOL_SIZE); + } + + #[test] + fn test_serializer_prewarm_is_bounded() { + let serializer = ZeroAllocSerializer::new_with_prewarm(128, 1024, 8); + let (available, capacity) = serializer.get_pool_stats(); + assert_eq!(capacity, 128); + assert_eq!(available, 8); + } + + #[test] + fn test_serializer_lazy_alloc_and_return() { + let serializer = ZeroAllocSerializer::new_with_prewarm(8, 1024, 0); + let (available_before, capacity) = serializer.get_pool_stats(); + assert_eq!(capacity, 8); + assert_eq!(available_before, 0); + + let buf = serializer.serialize_zero_alloc(&"hello", "test").unwrap(); + assert!(buf.capacity() >= 1024); + serializer.return_buffer(buf); + + let (available_after, _) = serializer.get_pool_stats(); + assert_eq!(available_after, 1); } }