use kevy_config::Config;
use kevy_resp::{
ArgvView, 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<A: ArgvView + ?Sized>(
cfg: &Config,
totals: &crate::state::Totals,
store: &Store,
args: &A,
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, totals, 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<A: ArgvView + ?Sized>(store: &Store, args: &A, 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, totals: &crate::state::Totals, out: &mut Vec<u8>) {
let bytes_per_key = totals.used_memory.checked_div(totals.keys).unwrap_or(0) as i64;
let pairs: [(&[u8], StatValue<'_>); 8] = [
(b"peak.allocated", StatValue::Int(totals.used_memory_peak as i64)),
(b"total.allocated", StatValue::Int(totals.used_memory as i64)),
(b"keys.count", StatValue::Int(totals.keys as i64)),
(b"keys.bytes-per-key", StatValue::Int(bytes_per_key)),
(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(totals.evicted_keys 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]),
}
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::*;
use kevy_resp::Argv;
#[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");
}
fn argv(parts: &[&[u8]]) -> Argv {
let mut a = Argv::default();
for p in parts {
a.push(p);
}
a
}
#[test]
fn memory_usage_returns_integer_for_present_key() {
let mut store = Store::new();
store.set(b"k", b"v".to_vec(), None, false, false);
let a = argv(&[b"MEMORY", b"USAGE", b"k"]);
let mut out = Vec::new();
cmd_memory_usage(&store, &a, &mut out);
assert!(out.starts_with(b":"), "expected integer reply, got {out:?}");
assert!(out.ends_with(b"\r\n"));
}
#[test]
fn memory_usage_wrong_arity() {
let store = Store::new();
let a = argv(&[b"MEMORY", b"USAGE"]);
let mut out = Vec::new();
cmd_memory_usage(&store, &a, &mut out);
assert!(out.starts_with(b"-ERR wrong number of arguments"));
}
#[test]
fn memory_top_level_dispatches_each_subcommand() {
let cfg = Config::default();
let store = Store::new();
let run = |args: &[&[u8]], out: &mut Vec<u8>| {
cmd_memory(&cfg, &crate::state::Totals::default(), &store, &argv(args), out);
};
let mut out = Vec::new();
run(&[b"MEMORY", b"DOCTOR"], &mut out);
assert!(
out.starts_with(b"$") && out.windows(5).any(|w| w == b"issue"),
"DOCTOR should return a bulk diagnostic; got {out:?}"
);
out.clear();
run(&[b"MEMORY", b"PURGE"], &mut out);
assert_eq!(out, b"+OK\r\n");
out.clear();
run(&[b"MEMORY", b"MALLOC-STATS"], &mut out);
assert!(out.starts_with(b"$"));
out.clear();
run(&[b"MEMORY"], &mut out);
assert!(out.starts_with(b"-ERR wrong number of arguments"));
out.clear();
run(&[b"MEMORY", b"NOPE"], &mut out);
assert!(out.starts_with(b"-ERR Unknown MEMORY subcommand"));
}
#[test]
fn memory_stats_encodes_all_eight_fields() {
let cfg = Config::default();
let totals = crate::state::Totals {
used_memory: 4096,
used_memory_peak: 8192,
keys: 2,
..Default::default()
};
let mut out = Vec::new();
cmd_memory_stats(&cfg, &totals, &mut out);
assert!(out.starts_with(b"*16\r\n"));
for label in [
b"peak.allocated".as_slice(),
b"total.allocated".as_slice(),
b"keys.count".as_slice(),
b"keys.bytes-per-key".as_slice(),
b"maxmemory".as_slice(),
b"maxmemory.policy".as_slice(),
b"evicted.keys".as_slice(),
b"entry.overhead".as_slice(),
] {
assert!(
out.windows(label.len()).any(|w| w == label),
"missing label {:?} in stats output",
std::str::from_utf8(label).unwrap()
);
}
}
}