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)
This commit is contained in:
+62
-33
@@ -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<T: serde::Serialize>(&self, data: &T, _label: &str) -> Result<Vec<u8>> {
|
||||
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<T: serde::Serialize>(
|
||||
&self,
|
||||
data: &T,
|
||||
_label: &str,
|
||||
) -> Result<Vec<u8>> {
|
||||
// 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<Arc<ZeroAllocSerializer>> = Lazy::new(|| {
|
||||
Arc::new(ZeroAllocSerializer::new(
|
||||
10_000, // Pool size
|
||||
256 * 1024, // Buffer size: 256KB
|
||||
))
|
||||
});
|
||||
static SERIALIZER: Lazy<Arc<ZeroAllocSerializer>> =
|
||||
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<String> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user