From 96ba8f259ae377abd29fff089baa47cb1f18e6f5 Mon Sep 17 00:00:00 2001 From: Gagan Trivedi Date: Fri, 24 Apr 2026 16:58:05 +0530 Subject: [PATCH 1/2] perf: avoid context deep-clone and flags serde double-pass Two independent hot-path wins on the identities and flags endpoints: 1. LocalMemEnvironmentsCache::get_context now returns Option> instead of Option. The context holds every feature, segment, rule, and condition for an environment; returning it by value caused a full deep clone on every request. Using Arc makes reads a pointer-bump and moves the one-time construction cost to the poll path (where it belongs). Callers in EnvironmentService now dereference the Arc before passing to the engine. 2. get_flags previously returned Json. Building that required serde_json::to_value(flags) which walks the whole structure once to produce an intermediate Value tree; Axum's Json response then walks the tree again to produce bytes. This eliminates the first pass: a new FlagsResponse enum wraps either a single APIFeatureState or Vec, implements IntoResponse by delegating to Json, and we serialize directly. Measured end-to-end (local Docker, 1 vCPU / 2 GB, identities endpoint, wrk with 3 trait-matching segment conditions, endpoint caches off): baseline this PR delta small 50f/15s/750ov 3,072 4,457 +45% RPS p99 @ c=200 77 ms 51 ms medium 200f/50s/8.7Kov 268 510 +90% RPS p99 @ c=200 2.14 s 412 ms -81% The medium project benefits disproportionately because the context clone cost grows with project size; Arc::clone is O(1) either way. --- src/cache/environment.rs | 18 +++++++++++------- src/routes/flags.rs | 26 ++++++++++++++++++++------ src/services/environment.rs | 4 ++-- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/cache/environment.rs b/src/cache/environment.rs index a2cbed1..5f025ad 100644 --- a/src/cache/environment.rs +++ b/src/cache/environment.rs @@ -14,8 +14,10 @@ pub trait EnvironmentsCache: Send + Sync { /// Returns Arc to avoid cloning large JSON on every request async fn get_environment(&self, environment_key: &str) -> Option>; - /// Get the pre-computed evaluation context (for flag evaluation) - async fn get_context(&self, environment_key: &str) -> Option; + /// Get the pre-computed evaluation context (for flag evaluation). + /// Returns an Arc so the caller doesn't pay a deep clone per request — + /// the context is read-heavy and only replaced on polling-interval refresh. + async fn get_context(&self, environment_key: &str) -> Option>; /// Store environment document and compute context. Returns true if changed. async fn put_environment(&self, environment_key: &str, document: Value) -> bool; @@ -29,8 +31,10 @@ pub struct LocalMemEnvironmentsCache { /// Raw environment documents (for /environment-document endpoint) /// Stored as Arc to avoid cloning large JSON on every request environments: Arc>>>, - /// Pre-computed evaluation contexts (for flag evaluation) - contexts: Arc>>, + /// Pre-computed evaluation contexts (for flag evaluation). + /// Stored as Arc so readers share the same allocation — avoids a deep + /// clone of the full context (features + segments) per request. + contexts: Arc>>>, /// Identity overrides extracted from environments identity_overrides: Arc>>>, } @@ -52,9 +56,9 @@ impl EnvironmentsCache for LocalMemEnvironmentsCache { environments.get(environment_key).cloned() // Clones Arc (cheap), not Value } - async fn get_context(&self, environment_key: &str) -> Option { + async fn get_context(&self, environment_key: &str) -> Option> { let contexts = self.contexts.read().await; - contexts.get(environment_key).cloned() + contexts.get(environment_key).cloned() // Arc clone, not deep clone } async fn put_environment(&self, environment_key: &str, document: Value) -> bool { @@ -89,7 +93,7 @@ impl EnvironmentsCache for LocalMemEnvironmentsCache { if let Ok(environment) = serde_json::from_value::(document.clone()) { let flagsmith_env: FlagsmithEnvironment = environment.to_flagsmith_environment(); let context = environment_to_context(flagsmith_env); - contexts.insert(environment_key.to_string(), context); + contexts.insert(environment_key.to_string(), Arc::new(context)); } environments.insert(environment_key.to_string(), Arc::new(document)); diff --git a/src/routes/flags.rs b/src/routes/flags.rs index 136f868..0cb9681 100644 --- a/src/routes/flags.rs +++ b/src/routes/flags.rs @@ -1,10 +1,12 @@ use crate::error::Result; +use crate::models::APIFeatureState; use crate::routes::extractors::extract_environment_key; use crate::state::AppState; use axum::{ Json, extract::{Query, State}, http::HeaderMap, + response::IntoResponse, }; use serde::Deserialize; @@ -13,22 +15,34 @@ pub struct FlagsQuery { pub feature: Option, } +pub enum FlagsResponse { + Single(APIFeatureState), + Multiple(Vec), +} + +impl IntoResponse for FlagsResponse { + fn into_response(self) -> axum::response::Response { + match self { + FlagsResponse::Single(f) => Json(f).into_response(), + FlagsResponse::Multiple(f) => Json(f).into_response(), + } + } +} + pub async fn get_flags( State(service): State, headers: HeaderMap, Query(query): Query, -) -> Result> { +) -> Result { let environment_key = extract_environment_key(&headers)?; - let flags = service + let mut flags = service .get_flags_response_data(&environment_key, query.feature.as_deref()) .await?; if query.feature.is_some() && flags.len() == 1 { - // Return single feature - Ok(Json(serde_json::to_value(&flags[0])?)) + Ok(FlagsResponse::Single(flags.swap_remove(0))) } else { - // Return all features - Ok(Json(serde_json::to_value(flags)?)) + Ok(FlagsResponse::Multiple(flags)) } } diff --git a/src/services/environment.rs b/src/services/environment.rs index f79aa86..b638602 100644 --- a/src/services/environment.rs +++ b/src/services/environment.rs @@ -255,7 +255,7 @@ impl EnvironmentService { EdgeProxyError::ServiceUnavailable("Environment not loaded".to_string()) })?; - let evaluation_result = get_evaluation_result(&context); + let evaluation_result = get_evaluation_result(&*context); let mut flag_results: Vec = evaluation_result.flags.into_values().collect(); @@ -341,7 +341,7 @@ impl EnvironmentService { identity.traits.iter().map(Into::into).collect(); let context_with_identity = - add_identity_to_context(&context, &identity.identifier, &flagsmith_traits); + add_identity_to_context(&*context, &identity.identifier, &flagsmith_traits); let evaluation_result = get_evaluation_result(&context_with_identity); From f6eea5eee5c1486612ed28fce9605322d52471e2 Mon Sep 17 00:00:00 2001 From: Gagan Trivedi Date: Fri, 24 Apr 2026 17:11:45 +0530 Subject: [PATCH 2/2] perf: rely on Arc auto-deref instead of explicit &*context Fixes clippy::explicit_auto_deref. Rust auto-derefs &Arc to &T via the Deref trait when coercing to function args, so the explicit &* was redundant. No behaviour change. --- src/services/environment.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/environment.rs b/src/services/environment.rs index b638602..f79aa86 100644 --- a/src/services/environment.rs +++ b/src/services/environment.rs @@ -255,7 +255,7 @@ impl EnvironmentService { EdgeProxyError::ServiceUnavailable("Environment not loaded".to_string()) })?; - let evaluation_result = get_evaluation_result(&*context); + let evaluation_result = get_evaluation_result(&context); let mut flag_results: Vec = evaluation_result.flags.into_values().collect(); @@ -341,7 +341,7 @@ impl EnvironmentService { identity.traits.iter().map(Into::into).collect(); let context_with_identity = - add_identity_to_context(&*context, &identity.identifier, &flagsmith_traits); + add_identity_to_context(&context, &identity.identifier, &flagsmith_traits); let evaluation_result = get_evaluation_result(&context_with_identity);