From 1aa09ab100c726a35cd19f5085b75904379f10d9 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 20 Jun 2026 10:03:38 +0300 Subject: [PATCH] perf(transcode): remove per-request route allocations on the hot path Two avoidable allocations were paid on every proxied request: - The handler closure captured the RouteEntry by value, so axum's per-request clone of the handler deep-cloned its String fields (http_path, grpc_path, response_body). Wrap the entry in Arc so the per-request clone is a single refcount bump. - grpc_path was re-parsed into a PathAndQuery on every request in both the unary and streaming handlers. Parse it once at route-build time and store the PathAndQuery (its clone is a cheap Bytes refcount bump); an invalid path now skips the route at startup instead of returning a per-request 500. No behavior change on the success path. SIMD/wildcopy are not applicable here: the byte-level work lives in serde_json / prost / hyper, and the proxy is I/O-bound on the upstream gRPC call. Closes #41 --- src/transcode/mod.rs | 59 ++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/src/transcode/mod.rs b/src/transcode/mod.rs index c668bcc..feb4b7f 100644 --- a/src/transcode/mod.rs +++ b/src/transcode/mod.rs @@ -49,8 +49,9 @@ struct RouteEntry { http_path: String, /// HTTP method (GET, POST, PUT, PATCH, DELETE). http_method: HttpMethod, - /// gRPC path (e.g., "/sid.v1.AuthService/OpaqueLoginStart"). - grpc_path: String, + /// gRPC path (e.g., "/sid.v1.AuthService/OpaqueLoginStart"), parsed once at + /// route-build time so each request clones a cheap `Bytes` refcount. + grpc_path: axum::http::uri::PathAndQuery, /// Method descriptor for input/output message resolution. method: MethodDescriptor, /// How the request body maps onto the gRPC request message. @@ -83,7 +84,7 @@ pub fn routes(pool: &DescriptorPool, aliases: &[AliasConfig]) let mut router: Router = Router::new(); for entry in &entries { - let entry_clone = entry.clone(); + let entry_clone = std::sync::Arc::new(entry.clone()); let handler = move |proxy_state: State, headers: HeaderMap, @@ -122,7 +123,7 @@ pub fn routes(pool: &DescriptorPool, aliases: &[AliasConfig]) continue; }; - let alias_entry = entry.clone(); + let alias_entry = std::sync::Arc::new(entry.clone()); let alias_handler = move |proxy_state: State, headers: HeaderMap, @@ -153,7 +154,7 @@ pub fn routes(pool: &DescriptorPool, aliases: &[AliasConfig]) // Server-streaming RPCs let streaming_entries = extract_streaming_routes(pool); for entry in &streaming_entries { - let entry_clone = entry.clone(); + let entry_clone = std::sync::Arc::new(entry.clone()); let axum_path = proto_path_to_axum(&entry.http_path); let handler = move |proxy_state: State, headers: HeaderMap| { @@ -176,7 +177,7 @@ pub fn routes(pool: &DescriptorPool, aliases: &[AliasConfig]) async fn streaming_handler( State(proxy_state): State, headers: HeaderMap, - entry: RouteEntry, + entry: std::sync::Arc, ) -> Response { let channel = proxy_state.grpc_channel(); @@ -191,20 +192,7 @@ async fn streaming_handler( let output_desc = entry.method.output(); let grpc_codec = codec::DynamicCodec::new(output_desc.clone()); - let grpc_path: axum::http::uri::PathAndQuery = match entry.grpc_path.parse() { - Ok(p) => p, - Err(e) => { - tracing::error!("Invalid gRPC path '{}': {e}", entry.grpc_path); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "error": "INTERNAL", - "message": "invalid gRPC path configuration", - })), - ) - .into_response(); - } - }; + let grpc_path = entry.grpc_path.clone(); let mut grpc_client = Grpc::new(channel); if let Err(e) = grpc_client.ready().await { @@ -264,7 +252,7 @@ async fn transcode_handler( Path(path_params): Path>, RawQuery(raw_query): RawQuery, body_bytes: axum::body::Bytes, - entry: RouteEntry, + entry: std::sync::Arc, ) -> Response { let channel = proxy_state.grpc_channel(); @@ -349,20 +337,7 @@ async fn transcode_handler( let output_desc = entry.method.output(); let grpc_codec = codec::DynamicCodec::new(output_desc.clone()); - let grpc_path: axum::http::uri::PathAndQuery = match entry.grpc_path.parse() { - Ok(p) => p, - Err(e) => { - tracing::error!("Invalid gRPC path '{}': {e}", entry.grpc_path); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "error": "INTERNAL", - "message": "invalid gRPC path configuration", - })), - ) - .into_response(); - } - }; + let grpc_path = entry.grpc_path.clone(); let mut grpc_client = Grpc::new(channel); if let Err(e) = grpc_client.ready().await { @@ -437,6 +412,13 @@ fn extract_routes(pool: &DescriptorPool) -> Vec { } let grpc_path = format!("/{}/{}", service.full_name(), method.name()); + let grpc_path: axum::http::uri::PathAndQuery = match grpc_path.parse() { + Ok(p) => p, + Err(e) => { + tracing::error!("skipping route with invalid gRPC path '{grpc_path}': {e}"); + continue; + } + }; for binding in extract_http_bindings(&method, &http_ext) { entries.push(RouteEntry { @@ -470,6 +452,13 @@ fn extract_streaming_routes(pool: &DescriptorPool) -> Vec { } let grpc_path = format!("/{}/{}", service.full_name(), method.name()); + let grpc_path: axum::http::uri::PathAndQuery = match grpc_path.parse() { + Ok(p) => p, + Err(e) => { + tracing::error!("skipping route with invalid gRPC path '{grpc_path}': {e}"); + continue; + } + }; for binding in extract_http_bindings(&method, &http_ext) { tracing::info!(