Skip to content

Commit ac7452a

Browse files
Merge pull request #177 from QueryaHub/perf-buffer-pool-160
perf(memory): implement thread-local buffer pool for request bodies (#160)
2 parents b804b38 + 5345138 commit ac7452a

3 files changed

Lines changed: 100 additions & 10 deletions

File tree

src/buffer_pool.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//! Thread-local buffer pool for request bodies to prevent heap fragmentation (issue #160).
2+
3+
use std::cell::RefCell;
4+
use std::ops::{Deref, DerefMut};
5+
6+
const POOL_CAPACITY_LIMIT: usize = 128 * 1024;
7+
const INITIAL_BUFFER_CAPACITY: usize = 16 * 1024;
8+
const MAX_POOL_SIZE: usize = 64;
9+
10+
thread_local! {
11+
static BODY_POOL: RefCell<Vec<Vec<u8>>> = const { RefCell::new(Vec::new()) };
12+
}
13+
14+
/// RAII wrapper around a pooled `Vec<u8>` that automatically returns itself to the
15+
/// thread-local buffer pool on `Drop` if capacity <= 128 KB.
16+
pub struct PooledBuffer(Vec<u8>);
17+
18+
impl PooledBuffer {
19+
pub fn new() -> Self {
20+
let buf = BODY_POOL
21+
.with(|pool| pool.borrow_mut().pop())
22+
.unwrap_or_else(|| Vec::with_capacity(INITIAL_BUFFER_CAPACITY));
23+
Self(buf)
24+
}
25+
26+
pub fn take(&mut self) -> Vec<u8> {
27+
std::mem::take(&mut self.0)
28+
}
29+
}
30+
31+
impl Default for PooledBuffer {
32+
fn default() -> Self {
33+
Self::new()
34+
}
35+
}
36+
37+
impl Deref for PooledBuffer {
38+
type Target = Vec<u8>;
39+
40+
#[inline]
41+
fn deref(&self) -> &Self::Target {
42+
&self.0
43+
}
44+
}
45+
46+
impl DerefMut for PooledBuffer {
47+
#[inline]
48+
fn deref_mut(&mut self) -> &mut Self::Target {
49+
&mut self.0
50+
}
51+
}
52+
53+
impl Drop for PooledBuffer {
54+
fn drop(&mut self) {
55+
if self.0.capacity() >= INITIAL_BUFFER_CAPACITY && self.0.capacity() <= POOL_CAPACITY_LIMIT {
56+
self.0.clear();
57+
let buf = std::mem::take(&mut self.0);
58+
BODY_POOL.with(|pool| {
59+
let mut p = pool.borrow_mut();
60+
if p.len() < MAX_POOL_SIZE {
61+
p.push(buf);
62+
}
63+
});
64+
}
65+
}
66+
}
67+
68+
#[cfg(test)]
69+
mod tests {
70+
use super::*;
71+
72+
#[test]
73+
fn test_pooled_buffer_recycle() {
74+
{
75+
let mut buf = PooledBuffer::new();
76+
buf.extend_from_slice(b"hello world");
77+
assert_eq!(&*buf, b"hello world");
78+
}
79+
// Dropped -> returned to pool.
80+
let buf2 = PooledBuffer::new();
81+
assert!(buf2.is_empty());
82+
assert!(buf2.capacity() >= INITIAL_BUFFER_CAPACITY);
83+
}
84+
}

src/dispatch.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use pyo3::types::{PyBytes, PyDict, PyList, PyString, PyTuple};
1010
use pyo3::IntoPyObjectExt;
1111
use serde_json::Value as JsonValue;
1212

13+
use crate::buffer_pool::PooledBuffer;
1314
use crate::config;
1415
use crate::form::{self, ParsedFile};
1516
use crate::params::{build_request_context, header_get_lax, parse_query, value_for_path_param};
@@ -699,22 +700,26 @@ pub async fn run_rsgi(
699700
routes_arc[route_idx].extra.path_template.clone(),
700701
)
701702
});
702-
let mut body_bytes: Vec<u8> = if should_read_body {
703+
let mut body_bytes: PooledBuffer = if should_read_body {
703704
let read_fut = Python::with_gil(|py| {
704705
let p = protocol.bind(py);
705706
let aw: Bound<PyAny> = p.call0()?;
706707
pyo3_async_runtimes::tokio::into_future(aw)
707708
})?;
708709
let body_obj: PyObject = read_fut.await?;
709-
let body = Python::with_gil(|py| -> PyResult<Vec<u8>> {
710+
let mut body = PooledBuffer::new();
711+
Python::with_gil(|py| -> PyResult<()> {
710712
let b = body_obj.bind(py);
711-
if let Ok(x) = b.extract::<Vec<u8>>() {
712-
return Ok(x);
713-
}
714-
if let Ok(s) = b.str() {
715-
return Ok(s.to_string().into_bytes());
713+
if let Ok(py_bytes) = b.downcast::<pyo3::types::PyBytes>() {
714+
body.extend_from_slice(py_bytes.as_bytes());
715+
} else if let Ok(py_str) = b.downcast::<pyo3::types::PyString>() {
716+
if let Ok(s) = py_str.to_str() {
717+
body.extend_from_slice(s.as_bytes());
718+
}
719+
} else if let Ok(bytes_vec) = b.extract::<Vec<u8>>() {
720+
body.extend_from_slice(&bytes_vec);
716721
}
717-
Ok(Vec::new())
722+
Ok(())
718723
})?;
719724
let max = form::max_body_bytes();
720725
if (body.len() as u64) > max {
@@ -728,7 +733,7 @@ pub async fn run_rsgi(
728733
}
729734
body
730735
} else {
731-
Vec::new()
736+
PooledBuffer::new()
732737
};
733738
let (auth, cookie_raw): (Option<String>, Option<String>) = if require_jwt {
734739
Python::with_gil(|py| -> PyResult<(Option<String>, Option<String>)> {
@@ -886,7 +891,7 @@ pub async fn run_rsgi(
886891
.await
887892
}
888893
};
889-
let multipart_body = std::mem::take(&mut body_bytes);
894+
let multipart_body = body_bytes.take();
890895
let parsed = match form::parse_multipart(multipart_body, &boundary).await {
891896
Ok(p) => p,
892897
Err(e) => {

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use pyo3::prelude::*;
88
use pyo3::types::{PyDict, PyList, PyTuple};
99
use serde_json::json;
1010

11+
mod buffer_pool;
1112
mod config;
1213
mod db;
1314
mod dispatch;

0 commit comments

Comments
 (0)