use kevy_config::Config;
use kevy_resp::{Argv, encode_array_len, encode_bulk, encode_error, encode_integer, encode_null_bulk};
use kevy_store::{ENTRY_OVERHEAD, Store};
use super::{eviction_str, wrong_args};
pub(crate) fn cmd_memory(cfg: &Config, store: &Store, args: &Argv, out: &mut Vec<u8>) {
let Some(sub) = args.get(1) else {
return wrong_args(out, "memory");
};
let sub_upper = sub.to_ascii_uppercase();
match sub_upper.as_slice() {
b"USAGE" => cmd_memory_usage(store, args, out),
b"STATS" => cmd_memory_stats(cfg, store, out),
b"DOCTOR" => {
encode_bulk(out, b"Sam, I detected a few issues in this Kevy instance memory implants:\r\n\r\n * No issues detected. Memory looks fine.\r\n");
}
b"PURGE" => {
kevy_resp::encode_simple_string(out, "OK");
}
b"MALLOC-STATS" => {
encode_bulk(out, b"kevy uses the system allocator; no per-arena stats.\r\n");
}
_ => {
let shown = String::from_utf8_lossy(sub);
encode_error(out, &format!(
"ERR Unknown MEMORY subcommand or wrong number of arguments for '{}'",
shown.to_lowercase()
));
}
}
}
fn cmd_memory_usage(store: &Store, args: &Argv, out: &mut Vec<u8>) {
if args.len() < 3 {
return wrong_args(out, "memory|usage");
}
let key = &args[2];
match store.estimate_key_bytes(key) {
Some(b) => encode_integer(out, b as i64),
None => encode_null_bulk(out),
}
}
fn cmd_memory_stats(cfg: &Config, store: &Store, out: &mut Vec<u8>) {
let pairs: [(&[u8], StatValue<'_>); 8] = [
(b"peak.allocated", StatValue::Int(store.used_memory_peak() as i64)),
(b"total.allocated", StatValue::Int(store.used_memory() as i64)),
(
b"keys.count",
StatValue::Int(store.dbsize() as i64),
),
(
b"keys.bytes-per-key",
StatValue::Int(avg_bytes_per_key(store)),
),
(b"maxmemory", StatValue::Int(cfg.memory.maxmemory as i64)),
(
b"maxmemory.policy",
StatValue::Bulk(eviction_str(cfg.memory.maxmemory_policy).as_bytes()),
),
(
b"evicted.keys",
StatValue::Int(store.evictions_total() as i64),
),
(
b"entry.overhead",
StatValue::Int(ENTRY_OVERHEAD as i64),
),
];
encode_array_len(out, (pairs.len() * 2) as i64);
for (k, v) in &pairs {
encode_bulk(out, k);
match v {
StatValue::Int(n) => encode_integer(out, *n),
StatValue::Bulk(b) => encode_bulk(out, b),
}
}
}
enum StatValue<'a> {
Int(i64),
Bulk(&'a [u8]),
}
fn avg_bytes_per_key(store: &Store) -> i64 {
let n = store.dbsize();
if n == 0 {
return 0;
}
(store.used_memory() as i64) / (n as i64)
}
pub(crate) fn format_bytes_human(bytes: u64) -> String {
const UNITS: [(&str, u64); 4] = [
("G", 1024 * 1024 * 1024),
("M", 1024 * 1024),
("K", 1024),
("B", 1),
];
for (suffix, scale) in UNITS {
if bytes >= scale {
if suffix == "B" {
return format!("{bytes}B");
}
let scaled = bytes as f64 / scale as f64;
return format!("{scaled:.2}{suffix}");
}
}
format!("{bytes}B")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn human_format_matches_redis_style() {
assert_eq!(format_bytes_human(0), "0B");
assert_eq!(format_bytes_human(512), "512B");
assert_eq!(format_bytes_human(1024), "1.00K");
assert_eq!(format_bytes_human(1536), "1.50K");
assert_eq!(format_bytes_human(1024 * 1024), "1.00M");
assert_eq!(format_bytes_human(2 * 1024 * 1024 * 1024), "2.00G");
}
#[test]
fn memory_usage_returns_nil_for_absent_key() {
let store = Store::new();
let mut a = Argv::default();
a.push(b"MEMORY");
a.push(b"USAGE");
a.push(b"missing");
let mut out = Vec::new();
cmd_memory_usage(&store, &a, &mut out);
assert_eq!(out, b"$-1\r\n");
}
}