- Bump version to 3.5.0 - Performance: hot-path timing only when log_enabled/simulate; execute_parallel takes &[Arc<SwqosClient>]; shared HTTP client constants for SWQoS - Code quality: validate_protocol_params extracted for buy/sell; BYTES_PER_ACCOUNT, MAX_INSTRUCTIONS_WARN, HTTP timeout constants; prefetch/syscall bypass comments - Documentation: bilingual (EN + 中文) doc comments in execution, executor, perf, swqos; README/README_CN version and What's new in 3.5.0 - Add release_notes_v3.5.0.md Co-authored-by: Cursor <cursoragent@cursor.com>
249 lines
7.9 KiB
Rust
249 lines
7.9 KiB
Rust
//! Transaction serialization module.
|
|
|
|
use anyhow::Result;
|
|
use base64::Engine;
|
|
use base64::engine::general_purpose::STANDARD;
|
|
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,
|
|
};
|
|
|
|
/// Zero-allocation serializer using a buffer pool to avoid runtime allocation.
|
|
pub struct ZeroAllocSerializer {
|
|
buffer_pool: Arc<ArrayQueue<Vec<u8>>>,
|
|
buffer_size: usize,
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
// Serialize into buffer
|
|
let serialized = bincode::serialize(data)?;
|
|
buffer.clear();
|
|
buffer.extend_from_slice(&serialized);
|
|
|
|
Ok(buffer)
|
|
}
|
|
|
|
pub fn return_buffer(&self, buffer: Vec<u8>) {
|
|
// Return buffer to the pool
|
|
let _ = self.buffer_pool.push(buffer);
|
|
}
|
|
|
|
/// Get pool statistics.
|
|
pub fn get_pool_stats(&self) -> (usize, usize) {
|
|
let available = self.buffer_pool.len();
|
|
let capacity = self.buffer_pool.capacity();
|
|
(available, capacity)
|
|
}
|
|
}
|
|
|
|
/// Global serializer instance.
|
|
static SERIALIZER: Lazy<Arc<ZeroAllocSerializer>> = Lazy::new(|| {
|
|
Arc::new(ZeroAllocSerializer::new(
|
|
10_000, // Pool size
|
|
256 * 1024, // Buffer size: 256KB
|
|
))
|
|
});
|
|
|
|
/// Compile-time optimized event processor (zero runtime cost).
|
|
static COMPILE_TIME_PROCESSOR: CompileTimeOptimizedEventProcessor =
|
|
CompileTimeOptimizedEventProcessor::new();
|
|
|
|
/// Base64 encoder.
|
|
pub struct Base64Encoder;
|
|
|
|
impl Base64Encoder {
|
|
#[inline(always)]
|
|
pub fn encode(data: &[u8]) -> String {
|
|
// Use compile-time optimized hash for fast routing
|
|
let _route = if !data.is_empty() {
|
|
COMPILE_TIME_PROCESSOR.route_event_zero_cost(data[0])
|
|
} else {
|
|
0
|
|
};
|
|
|
|
// Use SIMD-accelerated Base64 encoding
|
|
SIMDSerializer::encode_base64_simd(data)
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn serialize_and_encode<T: serde::Serialize>(
|
|
value: &T,
|
|
event_type: &str,
|
|
) -> Result<String> {
|
|
let serialized = SERIALIZER.serialize_zero_alloc(value, event_type)?;
|
|
Ok(STANDARD.encode(&serialized))
|
|
}
|
|
}
|
|
|
|
/// Guard that returns the serialization buffer to the pool on drop.
|
|
pub struct PooledTxBufGuard(pub Vec<u8>);
|
|
|
|
impl std::ops::Deref for PooledTxBufGuard {
|
|
type Target = [u8];
|
|
fn deref(&self) -> &[u8] {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl Drop for PooledTxBufGuard {
|
|
fn drop(&mut self) {
|
|
if !self.0.is_empty() {
|
|
SERIALIZER.return_buffer(std::mem::take(&mut self.0));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Serialize transaction to bincode bytes using buffer pool. The returned guard returns the buffer
|
|
/// to the pool when dropped; use `&*guard` or `guard.as_ref()` for `&[u8]`.
|
|
pub fn serialize_transaction_bincode_sync(
|
|
transaction: &impl SerializableTransaction,
|
|
) -> Result<(PooledTxBufGuard, Signature)> {
|
|
let signature = transaction.get_signature();
|
|
let serialized_tx = SERIALIZER.serialize_zero_alloc(transaction, "transaction")?;
|
|
Ok((PooledTxBufGuard(serialized_tx), *signature))
|
|
}
|
|
|
|
/// Return a buffer to the pool (for manual use when not using `PooledTxBufGuard`).
|
|
pub fn return_serialization_buffer(buffer: Vec<u8>) {
|
|
SERIALIZER.return_buffer(buffer);
|
|
}
|
|
|
|
/// Sync serialize + encode using buffer pool; use in hot path to reduce allocs.
|
|
/// Base64 path uses SIMD-accelerated encoding.
|
|
pub fn serialize_transaction_sync(
|
|
transaction: &impl SerializableTransaction,
|
|
encoding: UiTransactionEncoding,
|
|
) -> Result<(String, Signature)> {
|
|
let signature = transaction.get_signature();
|
|
let serialized_tx = SERIALIZER.serialize_zero_alloc(transaction, "transaction")?;
|
|
let serialized = match encoding {
|
|
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
|
UiTransactionEncoding::Base64 => SIMDSerializer::encode_base64_simd(&serialized_tx),
|
|
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
|
};
|
|
SERIALIZER.return_buffer(serialized_tx);
|
|
Ok((serialized, *signature))
|
|
}
|
|
|
|
/// Serialize a transaction (async; no I/O, kept for API compatibility).
|
|
pub async fn serialize_transaction(
|
|
transaction: &impl SerializableTransaction,
|
|
encoding: UiTransactionEncoding,
|
|
) -> Result<(String, Signature)> {
|
|
let signature = transaction.get_signature();
|
|
|
|
// Use zero-allocation serialization
|
|
let serialized_tx = SERIALIZER.serialize_zero_alloc(transaction, "transaction")?;
|
|
|
|
let serialized = match encoding {
|
|
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
|
UiTransactionEncoding::Base64 => SIMDSerializer::encode_base64_simd(&serialized_tx),
|
|
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
|
};
|
|
|
|
// Return buffer to pool immediately
|
|
SERIALIZER.return_buffer(serialized_tx);
|
|
|
|
Ok((serialized, *signature))
|
|
}
|
|
|
|
/// Sync batch serialize + encode using buffer pool.
|
|
pub fn serialize_transactions_batch_sync(
|
|
transactions: &[impl SerializableTransaction],
|
|
encoding: UiTransactionEncoding,
|
|
) -> Result<Vec<String>> {
|
|
let mut results = Vec::with_capacity(transactions.len());
|
|
for tx in transactions {
|
|
let serialized_tx = SERIALIZER.serialize_zero_alloc(tx, "transaction")?;
|
|
let encoded = match encoding {
|
|
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
|
UiTransactionEncoding::Base64 => SIMDSerializer::encode_base64_simd(&serialized_tx),
|
|
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
|
};
|
|
SERIALIZER.return_buffer(serialized_tx);
|
|
results.push(encoded);
|
|
}
|
|
Ok(results)
|
|
}
|
|
|
|
/// Batch transaction serialization.
|
|
pub async fn serialize_transactions_batch(
|
|
transactions: &[impl SerializableTransaction],
|
|
encoding: UiTransactionEncoding,
|
|
) -> Result<Vec<String>> {
|
|
let mut results = Vec::with_capacity(transactions.len());
|
|
|
|
for tx in transactions {
|
|
let serialized_tx = SERIALIZER.serialize_zero_alloc(tx, "transaction")?;
|
|
|
|
let encoded = match encoding {
|
|
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
|
UiTransactionEncoding::Base64 => SIMDSerializer::encode_base64_simd(&serialized_tx),
|
|
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
|
};
|
|
|
|
SERIALIZER.return_buffer(serialized_tx);
|
|
results.push(encoded);
|
|
}
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
/// Get serializer statistics.
|
|
pub fn get_serializer_stats() -> (usize, usize) {
|
|
SERIALIZER.get_pool_stats()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_base64_encode() {
|
|
let data = b"Hello, World!";
|
|
let encoded = Base64Encoder::encode(data);
|
|
assert!(!encoded.is_empty());
|
|
|
|
// Verify it decodes correctly
|
|
let decoded = STANDARD.decode(&encoded).unwrap();
|
|
assert_eq!(&decoded[..data.len()], data);
|
|
}
|
|
|
|
#[test]
|
|
fn test_serializer_stats() {
|
|
let (available, capacity) = get_serializer_stats();
|
|
assert!(available <= capacity);
|
|
assert_eq!(capacity, 10_000);
|
|
}
|
|
}
|