Skip to main content

kevy_resp/
argv_borrowed.rs

1//! Zero-copy argv that borrows directly from the reactor's read buffer.
2//!
3//! `ArgvBorrowed<'a>` records each parsed arg as a `(start, end)` range into a
4//! caller-provided input slice — typically `&conn.input[..]`. The local single-
5//! shard hot path can dispatch straight from these slices and skip the per-cmd
6//! memcpy that [`crate::Argv`] needs. Handoff junctures (cross-shard dispatch,
7//! the MULTI queue, AOF logging) call [`ArgvBorrowed::into_owned`] to materialise
8//! a normal `Argv` and preserve current owned semantics there.
9
10use crate::argv::Argv;
11use crate::inline_ranges::InlineRanges;
12
13/// A parsed command's argument vector that borrows its bytes from a contiguous
14/// input buffer.
15///
16/// Unlike [`Argv`], which packs all argument bytes into a fresh `Vec<u8>`,
17/// `ArgvBorrowed` stores only a `(start, end)` table over the original buffer.
18/// `get(i)` returns `&input[s..e]`, so no copy happens on the parse → dispatch
19/// path. Calls that need to outlive the buffer (cross-shard, MULTI queue, AOF)
20/// use [`into_owned`](Self::into_owned) to convert to `Argv`.
21///
22/// A5 (2026-06-20): the range table is a `(u32, u32) × 4` inline + heap-spill
23/// `InlineRanges`. Commands with ≤4 args (PING/GET/SET/INCR/MGET ≤4 keys —
24/// the vast majority of the -c1 hot mix) pay zero `malloc`/`free` for the
25/// ranges. H1 (`perf c2c`) confirmed libc cfree on the per-request `Vec`
26/// allocation showed up in cross-thread contention; the inline tier removes
27/// that source.
28#[derive(Clone, Debug)]
29pub struct ArgvBorrowed<'a> {
30    input: &'a [u8],
31    ranges: InlineRanges,
32}
33
34impl<'a> ArgvBorrowed<'a> {
35    /// An empty argv that will read arg bytes from `input`.
36    pub fn new(input: &'a [u8]) -> Self {
37        Self {
38            input,
39            ranges: InlineRanges::new(),
40        }
41    }
42
43    /// An empty argv, pre-sizing `ranges` for `argc` args.
44    pub fn with_capacity(input: &'a [u8], argc: usize) -> Self {
45        Self {
46            input,
47            ranges: InlineRanges::with_capacity(argc),
48        }
49    }
50
51    /// Record one argument as `input[start..end]`.
52    pub(crate) fn push_range(&mut self, start: usize, end: usize) {
53        debug_assert!(end <= self.input.len() && start <= end);
54        self.ranges.push((start as u32, end as u32));
55    }
56
57    /// Number of arguments.
58    pub fn len(&self) -> usize {
59        self.ranges.len()
60    }
61
62    /// Whether there are no arguments.
63    pub fn is_empty(&self) -> bool {
64        self.ranges.is_empty()
65    }
66
67    /// Argument `i` as a byte slice into the original input, or `None`.
68    pub fn get(&self, i: usize) -> Option<&[u8]> {
69        let (s, e) = self.ranges.get(i)?;
70        Some(&self.input[s as usize..e as usize])
71    }
72
73    /// The first argument (the command name), or `None` if empty.
74    pub fn first(&self) -> Option<&[u8]> {
75        self.get(0)
76    }
77
78    /// Iterate the arguments as byte slices into the original input.
79    pub fn iter(&self) -> impl Iterator<Item = &[u8]> {
80        (0..self.len()).map(move |i| self.get(i).expect("in range"))
81    }
82
83    /// Materialise an owned [`Argv`] — copies arg bytes into a fresh buffer.
84    /// Used at any handoff juncture (cross-shard dispatch, MULTI queue, AOF
85    /// logging) that needs to outlive the original input buffer.
86    pub fn into_owned(self) -> Argv {
87        let mut total: usize = 0;
88        for i in 0..self.ranges.len() {
89            let (s, e) = self.ranges.get(i).expect("in range");
90            total += (e - s) as usize;
91        }
92        let mut a = Argv::with_capacity(self.ranges.len(), total);
93        for i in 0..self.ranges.len() {
94            let (s, e) = self.ranges.get(i).expect("in range");
95            a.push(&self.input[s as usize..e as usize]);
96        }
97        a
98    }
99}
100
101impl core::ops::Index<usize> for ArgvBorrowed<'_> {
102    type Output = [u8];
103    fn index(&self, i: usize) -> &[u8] {
104        self.get(i).expect("argv-borrowed index out of bounds")
105    }
106}
107
108impl PartialEq<Vec<Vec<u8>>> for ArgvBorrowed<'_> {
109    fn eq(&self, other: &Vec<Vec<u8>>) -> bool {
110        self.len() == other.len() && self.iter().zip(other).all(|(a, b)| a == b.as_slice())
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn new_and_with_capacity_start_empty() {
120        let buf = b"hello";
121        let a = ArgvBorrowed::new(buf);
122        assert!(a.is_empty());
123        assert_eq!(a.len(), 0);
124        let b = ArgvBorrowed::with_capacity(buf, 8);
125        assert!(b.is_empty());
126    }
127
128    #[test]
129    fn push_range_and_get_round_trip() {
130        // input buffer carrying interleaved args + headers, like RESP2 multibulk
131        let buf: &[u8] = b"*2\r\n$3\r\nGET\r\n$5\r\nmykey\r\n";
132        let mut a = ArgvBorrowed::with_capacity(buf, 2);
133        // GET at offset 8..11, mykey at 17..22 (right after `\r\n$5\r\n`)
134        a.push_range(8, 11);
135        a.push_range(17, 22);
136        assert_eq!(a.len(), 2);
137        assert_eq!(a.first(), Some(b"GET" as &[u8]));
138        assert_eq!(a.get(0), Some(b"GET" as &[u8]));
139        assert_eq!(a.get(1), Some(b"mykey" as &[u8]));
140        assert_eq!(a.get(2), None);
141    }
142
143    #[test]
144    fn iter_yields_args_in_order() {
145        let buf: &[u8] = b"abcXYZdef";
146        let mut a = ArgvBorrowed::new(buf);
147        a.push_range(0, 3);
148        a.push_range(3, 6);
149        a.push_range(6, 9);
150        let collected: Vec<&[u8]> = a.iter().collect();
151        assert_eq!(collected, vec![b"abc" as &[u8], b"XYZ", b"def"]);
152    }
153
154    #[test]
155    fn first_empty_returns_none() {
156        let a = ArgvBorrowed::new(b"" as &[u8]);
157        assert_eq!(a.first(), None);
158        assert_eq!(a.get(0), None);
159    }
160
161    #[test]
162    fn index_returns_correct_slice() {
163        let buf: &[u8] = b"hithere";
164        let mut a = ArgvBorrowed::new(buf);
165        a.push_range(0, 2);
166        a.push_range(2, 7);
167        assert_eq!(&a[0], b"hi" as &[u8]);
168        assert_eq!(&a[1], b"there" as &[u8]);
169    }
170
171    #[test]
172    #[should_panic(expected = "argv-borrowed index out of bounds")]
173    fn index_out_of_bounds_panics() {
174        let a = ArgvBorrowed::new(b"" as &[u8]);
175        let _ = &a[0];
176    }
177
178    #[test]
179    fn eq_against_vec_of_vec() {
180        let buf: &[u8] = b"PINGhello";
181        let mut a = ArgvBorrowed::new(buf);
182        a.push_range(0, 4);
183        a.push_range(4, 9);
184        assert_eq!(a, vec![b"PING".to_vec(), b"hello".to_vec()]);
185        assert_ne!(a, vec![b"PING".to_vec()]);
186        assert_ne!(a, vec![b"PING".to_vec(), b"world".to_vec()]);
187    }
188
189    #[test]
190    fn into_owned_copies_args_into_argv() {
191        // Non-contiguous in the original buffer (interleaved with RESP markers).
192        let buf: &[u8] = b"*2\r\n$3\r\nSET\r\n$1\r\nk\r\n";
193        let mut a = ArgvBorrowed::with_capacity(buf, 2);
194        a.push_range(8, 11); // SET
195        a.push_range(17, 18); // k
196        let owned: Argv = a.into_owned();
197        assert_eq!(owned.len(), 2);
198        assert_eq!(owned.get(0), Some(b"SET" as &[u8]));
199        assert_eq!(owned.get(1), Some(b"k" as &[u8]));
200        // And the materialised Argv compares equal to the vec-of-vec form.
201        assert_eq!(owned, vec![b"SET".to_vec(), b"k".to_vec()]);
202    }
203
204    #[test]
205    fn into_owned_on_empty_argv_returns_empty_argv() {
206        let a = ArgvBorrowed::new(b"" as &[u8]);
207        let owned = a.into_owned();
208        assert!(owned.is_empty());
209        assert_eq!(owned.len(), 0);
210    }
211
212    #[test]
213    fn clone_shares_input_slice_independent_ranges() {
214        let buf: &[u8] = b"abcdef";
215        let mut a = ArgvBorrowed::new(buf);
216        a.push_range(0, 3); // abc
217        let b = a.clone();
218        assert_eq!(b.len(), 1);
219        assert_eq!(b.get(0), Some(b"abc" as &[u8]));
220        // Mutating original's ranges doesn't affect clone.
221        a.push_range(3, 6); // def
222        assert_eq!(a.len(), 2);
223        assert_eq!(b.len(), 1);
224    }
225}