Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ categories = ["caching", "database-implementations"]

[profile.release]
overflow-checks = true
lto = "thin"
codegen-units = 1
strip = "symbols"

[workspace.dependencies]
# async runtime
Expand Down
99 changes: 98 additions & 1 deletion crates/ember-persistence/src/aof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>, FormatError> {
let mut buf = Vec::new();
let mut buf = Vec::with_capacity(self.estimated_size());
match self {
AofRecord::Set {
key,
Expand Down
48 changes: 47 additions & 1 deletion crates/ember-persistence/src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down
1 change: 1 addition & 0 deletions crates/ember-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
34 changes: 23 additions & 11 deletions crates/ember-protocol/src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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'%');
Expand Down
18 changes: 18 additions & 0 deletions crates/ember-protocol/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down