From 69e311dbb48c371453f903846ce46b040f21f172 Mon Sep 17 00:00:00 2001 From: Kacy Fortner Date: Thu, 12 Feb 2026 11:50:37 -0500 Subject: [PATCH 1/5] perf: enable thin LTO, single codegen unit, and symbol stripping thin LTO gives cross-crate inlining with ~30% less compile time than fat LTO. codegen-units = 1 enables maximum LLVM optimization within each crate. strip = "symbols" reduces binary size ~40% with no runtime cost. intentionally skip panic = "abort" to preserve stack unwinding for graceful shutdown. --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) 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 From 8ce24be92888bbaf36a38437d9590a03fcc5dcd1 Mon Sep 17 00:00:00 2001 From: Kacy Fortner Date: Thu, 12 Feb 2026 11:52:41 -0500 Subject: [PATCH 2/5] perf: pre-computed wire bytes for common RESP3 responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adds a `wire` module with pre-serialized byte constants for OK, PONG, NULL, and small integers (0, 1, -1). Frame::serialize() now matches these common values and writes the constant bytes directly, avoiding per-field formatting overhead. these are the hottest responses in typical workloads — SET returns OK, EXISTS/DEL/EXPIRE return 0 or 1, GET misses return NULL. --- crates/ember-protocol/src/lib.rs | 1 + crates/ember-protocol/src/serialize.rs | 34 +++++++++++++++++--------- crates/ember-protocol/src/types.rs | 18 ++++++++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) 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 { From 441b605362a3b34cf398935755e5ff965870cc5e Mon Sep 17 00:00:00 2001 From: Kacy Fortner Date: Thu, 12 Feb 2026 11:53:21 -0500 Subject: [PATCH 3/5] perf: add capacity hints to AOF record serialization buffer AofRecord::to_bytes() now pre-computes the expected size via estimated_size() and allocates with Vec::with_capacity(). this eliminates 2-4 intermediate reallocations per persistence write, especially for SET records with large values. --- crates/ember-persistence/src/aof.rs | 90 ++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/crates/ember-persistence/src/aof.rs b/crates/ember-persistence/src/aof.rs index d25cb9fd..e46bc2f5 100644 --- a/crates/ember-persistence/src/aof.rs +++ b/crates/ember-persistence/src/aof.rs @@ -190,9 +190,97 @@ 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, From 0bdb510a95d9876164952d204773152599ea3b7e Mon Sep 17 00:00:00 2001 From: Kacy Fortner Date: Thu, 12 Feb 2026 11:53:54 -0500 Subject: [PATCH 4/5] perf: add capacity hints to snapshot entry serialization buffer SnapEntry::estimated_size() pre-computes the serialized size so write_entry() can allocate the buffer in a single shot. eliminates repeated growth + copy cycles for entries with large values or many collection elements. --- crates/ember-persistence/src/snapshot.rs | 50 +++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/crates/ember-persistence/src/snapshot.rs b/crates/ember-persistence/src/snapshot.rs index 30b88ecf..91063aa7 100644 --- a/crates/ember-persistence/src/snapshot.rs +++ b/crates/ember-persistence/src/snapshot.rs @@ -179,6 +179,54 @@ 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 +319,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) => { From 36c6366784615462995494ed9415bfc8cb904572 Mon Sep 17 00:00:00 2001 From: Kacy Fortner Date: Thu, 12 Feb 2026 12:21:00 -0500 Subject: [PATCH 5/5] style: fix formatting in persistence capacity hints --- crates/ember-persistence/src/aof.rs | 13 +++++++++++-- crates/ember-persistence/src/snapshot.rs | 4 +--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/ember-persistence/src/aof.rs b/crates/ember-persistence/src/aof.rs index e46bc2f5..6b11914f 100644 --- a/crates/ember-persistence/src/aof.rs +++ b/crates/ember-persistence/src/aof.rs @@ -219,7 +219,8 @@ impl AofRecord { 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(); + 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 } @@ -270,7 +271,15 @@ impl AofRecord { type_name, data, .. - } => 1 + LEN_PREFIX + key.len() + LEN_PREFIX + type_name.len() + LEN_PREFIX + data.len() + 8, + } => { + 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() diff --git a/crates/ember-persistence/src/snapshot.rs b/crates/ember-persistence/src/snapshot.rs index 91063aa7..f606656e 100644 --- a/crates/ember-persistence/src/snapshot.rs +++ b/crates/ember-persistence/src/snapshot.rs @@ -207,9 +207,7 @@ impl SnapEntry { 1 + 4 + items } #[cfg(feature = "vector")] - SnapValue::Vector { - dim, elements, .. - } => { + SnapValue::Vector { dim, elements, .. } => { let items: usize = elements .iter() .map(|(name, _)| LEN_PREFIX + name.len() + (*dim as usize) * 4)