From 73f9178e298fb3f208f7ba3c48d65fc896174f6f Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Mon, 22 Jun 2026 04:17:40 +0300 Subject: [PATCH 1/2] perf(openapi): cache serialized string --- src/dispatch.rs | 18 ++++++++++++++++-- src/lib.rs | 19 ++++++++++++------- src/state.rs | 4 ++-- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/dispatch.rs b/src/dispatch.rs index efc1d15..b47d29e 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -354,7 +354,14 @@ pub fn try_rsgi_sync_short_circuit( if (method == "GET" || method == "HEAD") && path == "/openapi.json" && snapshot.include_openapi { let _ = protocol_py.setattr(py, "__oxyroute_path_template__", "/openapi.json"); - let doc = state.read().openapi.lock().to_string(); + let doc: Arc = { + let state_guard = state.read(); + let mut oa = state_guard.openapi.lock(); + if oa.1.is_none() { + oa.1 = Some(Arc::new(oa.0.to_string())); + } + Arc::clone(oa.1.as_ref().unwrap()) + }; if is_head { response::send_head_simple_sync( py, @@ -484,7 +491,14 @@ pub async fn run_rsgi( let _ = Python::with_gil(|py| { protocol.setattr(py, "__oxyroute_path_template__", "/openapi.json") }); - let doc = state.read().openapi.lock().to_string(); + let doc: Arc = { + let state_guard = state.read(); + let mut oa = state_guard.openapi.lock(); + if oa.1.is_none() { + oa.1 = Some(Arc::new(oa.0.to_string())); + } + Arc::clone(oa.1.as_ref().unwrap()) + }; if is_head { return response::send_head_simple( &protocol, diff --git a/src/lib.rs b/src/lib.rs index 7fea4a7..06fbb56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -283,7 +283,8 @@ impl App { }; { let mut oa = st.openapi.lock(); - App::openapi_add_path(&mut oa, &method, &path, &op_id, request_schema); + App::openapi_add_path(&mut oa.0, &method, &path, &op_id, request_schema); + oa.1 = None; } { let mut m = state::map_method_router(&st, &method).ok_or_else(|| { @@ -349,12 +350,13 @@ impl App { fn set_openapi_title(&self, title: &str) -> PyResult<()> { let st = self.state.read(); let mut oa = st.openapi.lock(); - if let Some(info) = oa - .as_object_mut() - .and_then(|m| m.get_mut("info")) - .and_then(|i| i.as_object_mut()) + if let Some(info) = + oa.0.as_object_mut() + .and_then(|m| m.get_mut("info")) + .and_then(|i| i.as_object_mut()) { info.insert("title".to_string(), json!(title)); + oa.1 = None; } Ok(()) } @@ -481,8 +483,11 @@ impl App { fn openapi_json(&self) -> PyResult { let st = self.state.read(); - let oa = st.openapi.lock(); - Ok(oa.to_string()) + let mut oa = st.openapi.lock(); + if oa.1.is_none() { + oa.1 = Some(Arc::new(oa.0.to_string())); + } + Ok(oa.1.as_ref().unwrap().to_string()) } } diff --git a/src/state.rs b/src/state.rs index c2b9166..2081ed7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -93,7 +93,7 @@ pub struct AppState { pub delete: Mutex>, pub options: Mutex>, pub websocket: Mutex>, - pub openapi: Mutex, + pub openapi: Mutex<(serde_json::Value, Option>)>, /// When `Some`, route matching uses these tables without taking per-router mutexes /// (populated in [`App::freeze`](crate::App::freeze)). pub compiled: Option>, @@ -132,7 +132,7 @@ impl AppState { delete: Mutex::new(Router::new()), options: Mutex::new(Router::new()), websocket: Mutex::new(Router::new()), - openapi: Mutex::new(openapi), + openapi: Mutex::new((openapi, None)), compiled: None, frozen: false, include_openapi: true, From 2280e6b945019510c6549f5d46419cd46732d364 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Mon, 22 Jun 2026 04:29:58 +0300 Subject: [PATCH 2/2] fix(tests): replace HashMap with Vec in test mocks to fix compilation --- src/state.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/state.rs b/src/state.rs index 2081ed7..9964d67 100644 --- a/src/state.rs +++ b/src/state.rs @@ -213,6 +213,7 @@ pub fn match_ws_route_compiled( /// Lookup an HTTP route in a precomputed [`CompiledRouters`] (lock-free). /// /// Returns ``None`` for unsupported method, ``Some(None)`` for no match, ``Some(Some(...))`` on hit. +#[allow(clippy::type_complexity)] pub fn match_route_compiled( compiled: &CompiledRouters, method: &str, @@ -353,26 +354,27 @@ fn methods_matching_path(state: &AppState, path: &str) -> Vec { /// Returns route index and path params, or `None` if the method is unsupported; `Some(None)` if /// no match; `Some(Some)` on success. Uses [`CompiledRouters`] when set (lock-free). #[cfg(test)] +#[allow(clippy::type_complexity)] fn match_route( state: &AppState, method: &str, path: &str, -) -> Option)>> { +) -> Option)>> { if let Some(c) = &state.compiled { let g = router_for_compiled(c, method)?; return Some(g.at(path).ok().map(|m| { - let mut pmap = HashMap::new(); + let mut pmap = Vec::new(); for (k, v) in m.params.iter() { - pmap.insert(k.to_string(), v.to_string()); + pmap.push((k.to_string(), v.to_string())); } (*m.value, pmap) })); } let g = map_method_router(state, method)?; Some(g.at(path).ok().map(|m| { - let mut pmap = HashMap::new(); + let mut pmap = Vec::new(); for (k, v) in m.params.iter() { - pmap.insert(k.to_string(), v.to_string()); + pmap.push((k.to_string(), v.to_string())); } (*m.value, pmap) })) @@ -394,7 +396,7 @@ mod tests { assert_eq!(pre, post); let inner = pre.expect("match"); assert_eq!(inner.0, 7); - assert_eq!(inner.1.get("id").map(String::as_str), Some("5")); + assert_eq!(inner.1.iter().find(|(k, _)| k == "id").map(|(_, v)| v.as_str()), Some("5")); } #[test]