diff --git a/Cargo.toml b/Cargo.toml index d762436b..7cfb312a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,9 @@ categories = ["caching", "database-implementations"] [profile.release] overflow-checks = true +lto = "thin" +codegen-units = 1 +strip = "symbols" [workspace.dependencies] # async runtime diff --git a/crates/ember-persistence/src/aof.rs b/crates/ember-persistence/src/aof.rs index d25cb9fd..6b11914f 100644 --- a/crates/ember-persistence/src/aof.rs +++ b/crates/ember-persistence/src/aof.rs @@ -190,9 +190,106 @@ pub enum AofRecord { } impl AofRecord { + /// Estimates the serialized size of this record in bytes. + /// + /// Used as a capacity hint for `to_bytes()` to avoid intermediate + /// reallocations. The estimate includes the tag byte plus all + /// length-prefixed fields, erring slightly high to avoid growing. + fn estimated_size(&self) -> usize { + // overhead per length-prefixed field: 4 bytes for the u32 length + const LEN_PREFIX: usize = 4; + + match self { + AofRecord::Set { + key, + value, + expire_ms: _, + } => 1 + LEN_PREFIX + key.len() + LEN_PREFIX + value.len() + 8, + AofRecord::Del { key } + | AofRecord::LPop { key } + | AofRecord::RPop { key } + | AofRecord::Persist { key } + | AofRecord::Incr { key } + | AofRecord::Decr { key } => 1 + LEN_PREFIX + key.len(), + AofRecord::Expire { key, .. } | AofRecord::Pexpire { key, .. } => { + 1 + LEN_PREFIX + key.len() + 8 + } + AofRecord::LPush { key, values } | AofRecord::RPush { key, values } => { + let values_size: usize = values.iter().map(|v| LEN_PREFIX + v.len()).sum(); + 1 + LEN_PREFIX + key.len() + 4 + values_size + } + AofRecord::ZAdd { key, members } => { + let members_size: usize = + members.iter().map(|(_, m)| 8 + LEN_PREFIX + m.len()).sum(); + 1 + LEN_PREFIX + key.len() + 4 + members_size + } + AofRecord::ZRem { key, members } + | AofRecord::SAdd { key, members } + | AofRecord::SRem { key, members } => { + let members_size: usize = members.iter().map(|m| LEN_PREFIX + m.len()).sum(); + 1 + LEN_PREFIX + key.len() + 4 + members_size + } + AofRecord::HSet { key, fields } => { + let fields_size: usize = fields + .iter() + .map(|(f, v)| LEN_PREFIX + f.len() + LEN_PREFIX + v.len()) + .sum(); + 1 + LEN_PREFIX + key.len() + 4 + fields_size + } + AofRecord::HDel { key, fields } => { + let fields_size: usize = fields.iter().map(|f| LEN_PREFIX + f.len()).sum(); + 1 + LEN_PREFIX + key.len() + 4 + fields_size + } + AofRecord::HIncrBy { key, field, .. } => { + 1 + LEN_PREFIX + key.len() + LEN_PREFIX + field.len() + 8 + } + AofRecord::IncrBy { key, .. } | AofRecord::DecrBy { key, .. } => { + 1 + LEN_PREFIX + key.len() + 8 + } + AofRecord::Append { key, value } => { + 1 + LEN_PREFIX + key.len() + LEN_PREFIX + value.len() + } + AofRecord::Rename { key, newkey } => { + 1 + LEN_PREFIX + key.len() + LEN_PREFIX + newkey.len() + } + #[cfg(feature = "vector")] + AofRecord::VAdd { + key, + element, + vector, + .. + } => { + 1 + LEN_PREFIX + key.len() + LEN_PREFIX + element.len() + 4 + vector.len() * 4 + 10 + } + #[cfg(feature = "vector")] + AofRecord::VRem { key, element } => { + 1 + LEN_PREFIX + key.len() + LEN_PREFIX + element.len() + } + #[cfg(feature = "protobuf")] + AofRecord::ProtoSet { + key, + type_name, + data, + .. + } => { + 1 + LEN_PREFIX + + key.len() + + LEN_PREFIX + + type_name.len() + + LEN_PREFIX + + data.len() + + 8 + } + #[cfg(feature = "protobuf")] + AofRecord::ProtoRegister { name, descriptor } => { + 1 + LEN_PREFIX + name.len() + LEN_PREFIX + descriptor.len() + } + } + } + /// Serializes this record into a byte vector (tag + payload, no CRC). fn to_bytes(&self) -> Result, FormatError> { - let mut buf = Vec::new(); + let mut buf = Vec::with_capacity(self.estimated_size()); match self { AofRecord::Set { key, diff --git a/crates/ember-persistence/src/snapshot.rs b/crates/ember-persistence/src/snapshot.rs index 30b88ecf..f606656e 100644 --- a/crates/ember-persistence/src/snapshot.rs +++ b/crates/ember-persistence/src/snapshot.rs @@ -179,6 +179,52 @@ pub struct SnapEntry { pub expire_ms: i64, } +impl SnapEntry { + /// Estimates the serialized byte size for buffer pre-allocation. + fn estimated_size(&self) -> usize { + const LEN_PREFIX: usize = 4; + + let key_size = LEN_PREFIX + self.key.len(); + let value_size = match &self.value { + SnapValue::String(data) => 1 + LEN_PREFIX + data.len(), + SnapValue::List(deque) => { + let items: usize = deque.iter().map(|v| LEN_PREFIX + v.len()).sum(); + 1 + 4 + items + } + SnapValue::SortedSet(members) => { + let items: usize = members.iter().map(|(_, m)| 8 + LEN_PREFIX + m.len()).sum(); + 1 + 4 + items + } + SnapValue::Hash(map) => { + let items: usize = map + .iter() + .map(|(f, v)| LEN_PREFIX + f.len() + LEN_PREFIX + v.len()) + .sum(); + 1 + 4 + items + } + SnapValue::Set(set) => { + let items: usize = set.iter().map(|m| LEN_PREFIX + m.len()).sum(); + 1 + 4 + items + } + #[cfg(feature = "vector")] + SnapValue::Vector { dim, elements, .. } => { + let items: usize = elements + .iter() + .map(|(name, _)| LEN_PREFIX + name.len() + (*dim as usize) * 4) + .sum(); + // tag + metric + quant + connectivity + expansion + dim + count + items + 1 + 2 + 4 + 4 + 4 + 4 + items + } + #[cfg(feature = "protobuf")] + SnapValue::Proto { type_name, data } => { + 1 + LEN_PREFIX + type_name.len() + LEN_PREFIX + data.len() + } + }; + // key + value + expire_ms (i64 = 8 bytes) + key_size + value_size + 8 + } +} + /// Writes a complete snapshot to disk. /// /// Entries are written to a temporary file first, then atomically @@ -271,7 +317,7 @@ impl SnapshotWriter { /// When encrypted, each entry is written as `[nonce: 12B][len: 4B][ciphertext]`. /// The footer CRC covers the encrypted bytes (nonce + len + ciphertext). pub fn write_entry(&mut self, entry: &SnapEntry) -> Result<(), FormatError> { - let mut buf = Vec::new(); + let mut buf = Vec::with_capacity(entry.estimated_size()); format::write_bytes(&mut buf, entry.key.as_bytes())?; match &entry.value { SnapValue::String(data) => { diff --git a/crates/ember-protocol/src/lib.rs b/crates/ember-protocol/src/lib.rs index 01fbb2de..bb33c194 100644 --- a/crates/ember-protocol/src/lib.rs +++ b/crates/ember-protocol/src/lib.rs @@ -29,4 +29,5 @@ pub mod types; pub use command::{Command, SetExpire, ZAddFlags}; pub use error::ProtocolError; pub use parse::parse_frame; +pub use types::wire; pub use types::Frame; diff --git a/crates/ember-protocol/src/serialize.rs b/crates/ember-protocol/src/serialize.rs index 1f68cc54..ad687d65 100644 --- a/crates/ember-protocol/src/serialize.rs +++ b/crates/ember-protocol/src/serialize.rs @@ -14,23 +14,35 @@ impl Frame { /// /// Writes the full RESP3 wire representation, including type prefix /// and trailing `\r\n` delimiters. + #[inline] pub fn serialize(&self, dst: &mut BytesMut) { + use crate::types::wire; + match self { - Frame::Simple(s) => { - dst.put_u8(b'+'); - dst.put_slice(s.as_bytes()); - dst.put_slice(b"\r\n"); - } + Frame::Simple(s) => match s.as_str() { + "OK" => dst.put_slice(wire::OK), + "PONG" => dst.put_slice(wire::PONG), + _ => { + dst.put_u8(b'+'); + dst.put_slice(s.as_bytes()); + dst.put_slice(b"\r\n"); + } + }, Frame::Error(msg) => { dst.put_u8(b'-'); dst.put_slice(msg.as_bytes()); dst.put_slice(b"\r\n"); } - Frame::Integer(n) => { - dst.put_u8(b':'); - write_i64(*n, dst); - dst.put_slice(b"\r\n"); - } + Frame::Integer(n) => match *n { + 0 => dst.put_slice(wire::ZERO), + 1 => dst.put_slice(wire::ONE), + -1 => dst.put_slice(wire::NEG_ONE), + _ => { + dst.put_u8(b':'); + write_i64(*n, dst); + dst.put_slice(b"\r\n"); + } + }, Frame::Bulk(data) => { dst.put_u8(b'$'); write_i64(data.len() as i64, dst); @@ -47,7 +59,7 @@ impl Frame { } } Frame::Null => { - dst.put_slice(b"_\r\n"); + dst.put_slice(wire::NULL); } Frame::Map(pairs) => { dst.put_u8(b'%'); diff --git a/crates/ember-protocol/src/types.rs b/crates/ember-protocol/src/types.rs index febecb8c..a6be1224 100644 --- a/crates/ember-protocol/src/types.rs +++ b/crates/ember-protocol/src/types.rs @@ -36,6 +36,24 @@ pub enum Frame { Map(Vec<(Frame, Frame)>), } +/// Pre-computed wire bytes for the most common responses. +/// Writing these directly to the output buffer avoids per-field +/// serialization overhead. +pub mod wire { + /// `+OK\r\n` + pub const OK: &[u8] = b"+OK\r\n"; + /// `+PONG\r\n` + pub const PONG: &[u8] = b"+PONG\r\n"; + /// `_\r\n` + pub const NULL: &[u8] = b"_\r\n"; + /// `:0\r\n` + pub const ZERO: &[u8] = b":0\r\n"; + /// `:1\r\n` + pub const ONE: &[u8] = b":1\r\n"; + /// `:-1\r\n` + pub const NEG_ONE: &[u8] = b":-1\r\n"; +} + impl Frame { /// Returns `true` if this frame is a null value. pub fn is_null(&self) -> bool {