Skip to content

Commit 9770d4f

Browse files
aepfliclaude
andauthored
perf(rust): eliminate context cloning and merge duplicated evaluation paths (#112)
## Summary - **Take owned `Value` instead of `&Value`** in all evaluate methods, avoiding a deep clone of the context on every evaluation. This is the biggest win for large contexts (1000+ attributes) - **Merge duplicated evaluation methods**: `evaluate_flag_internal` + `evaluate_flag_internal_pre_enriched` collapsed into single `evaluate_flag_core` (-151 lines) - **Add `merge_metadata_flag_set_only`** helper to avoid `HashMap::new()` allocation on flag-not-found path - **`enrich_context` now destructures owned `Value`** instead of cloning the context map Net: **-148 lines** (162 added, 310 removed) ## Motivation The evaluation hot path cloned the context `Value` on every call: - Regular path: `enrich_context` called `obj.clone()` to deep-copy the entire context map - Pre-enriched path: `context.clone()` passed to `evaluate_owned` even though nothing needed modification Since `evaluate_owned` → `Arc::new(data)` takes ownership anyway, passing owned values from the start eliminates the clone entirely. The WASM callers already own the parsed context, so this is free. The two nearly-identical `evaluate_flag_internal` / `evaluate_flag_internal_pre_enriched` methods (~130 lines each) were a maintenance hazard — any bug fix had to be applied in both. Merged into a single `evaluate_flag_core` with a `needs_enrichment: bool` parameter. ## Test plan - [x] All 134 unit tests pass - [x] All 36 integration tests pass - [x] All 39 gherkin scenarios pass - [x] All 6 metadata merging tests pass - [x] `cargo clippy -- -D warnings` clean - [x] WASM target builds successfully - [ ] Run benchmarks to measure improvement on large contexts 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 42bce60 commit 9770d4f

11 files changed

Lines changed: 165 additions & 313 deletions

File tree

benches/comparison.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ fn comparison_simple_flag_evaluator(c: &mut Criterion) {
7171
let context = json!({});
7272

7373
c.bench_function("comparison_simple_flag_evaluator", |b| {
74-
b.iter(|| evaluator.evaluate_flag(black_box("simpleFlag"), black_box(&context)))
74+
b.iter(|| evaluator.evaluate_flag(black_box("simpleFlag"), black_box(context.clone())))
7575
});
7676
}
7777

@@ -116,7 +116,7 @@ fn comparison_complex_flag_evaluator(c: &mut Criterion) {
116116
let context = json!({"tier": "standard", "score": 75});
117117

118118
c.bench_function("comparison_complex_flag_evaluator", |b| {
119-
b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(&context)))
119+
b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(context.clone())))
120120
});
121121
}
122122

benches/concurrency.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ fn concurrent_simple_1t(c: &mut Criterion) {
102102
let context = json!({});
103103

104104
c.bench_function("concurrent_simple_1t", |b| {
105-
b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(&context)))
105+
b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(context.clone())))
106106
});
107107
}
108108

@@ -122,7 +122,7 @@ fn concurrent_simple_4t(c: &mut Criterion) {
122122
thread::spawn(move || {
123123
let ctx = json!({});
124124
let guard = eval.lock().unwrap();
125-
guard.evaluate_flag(black_box("boolFlag"), black_box(&ctx))
125+
guard.evaluate_flag(black_box("boolFlag"), black_box(ctx))
126126
})
127127
})
128128
.collect();
@@ -149,7 +149,7 @@ fn concurrent_simple_8t(c: &mut Criterion) {
149149
thread::spawn(move || {
150150
let ctx = json!({});
151151
let guard = eval.lock().unwrap();
152-
guard.evaluate_flag(black_box("boolFlag"), black_box(&ctx))
152+
guard.evaluate_flag(black_box("boolFlag"), black_box(ctx))
153153
})
154154
})
155155
.collect();
@@ -177,7 +177,7 @@ fn concurrent_targeting_4t(c: &mut Criterion) {
177177
let role = if i % 2 == 0 { "admin" } else { "viewer" };
178178
let ctx = json!({"role": role});
179179
let guard = eval.lock().unwrap();
180-
guard.evaluate_flag(black_box("targetedFlag"), black_box(&ctx))
180+
guard.evaluate_flag(black_box("targetedFlag"), black_box(ctx))
181181
})
182182
})
183183
.collect();
@@ -215,7 +215,7 @@ fn concurrent_mixed_4t(c: &mut Criterion) {
215215
let context = ctx.clone();
216216
thread::spawn(move || {
217217
let guard = eval.lock().unwrap();
218-
guard.evaluate_flag(black_box(key), black_box(&context))
218+
guard.evaluate_flag(black_box(key), black_box(context))
219219
})
220220
})
221221
.collect();
@@ -251,7 +251,7 @@ fn concurrent_read_write_4t(c: &mut Criterion) {
251251
thread::spawn(move || {
252252
let ctx = json!({});
253253
let guard = eval.lock().unwrap();
254-
guard.evaluate_flag(black_box("boolFlag"), black_box(&ctx))
254+
guard.evaluate_flag(black_box("boolFlag"), black_box(ctx))
255255
})
256256
})
257257
.collect();
@@ -283,7 +283,7 @@ fn concurrent_simple_16t(c: &mut Criterion) {
283283
thread::spawn(move || {
284284
let ctx = json!({});
285285
let guard = eval.lock().unwrap();
286-
guard.evaluate_flag(black_box("boolFlag"), black_box(&ctx))
286+
guard.evaluate_flag(black_box("boolFlag"), black_box(ctx))
287287
})
288288
})
289289
.collect();
@@ -314,7 +314,7 @@ fn concurrent_targeting_16t(c: &mut Criterion) {
314314
let role = if i % 2 == 0 { "admin" } else { "viewer" };
315315
let ctx = json!({"role": role});
316316
let guard = eval.lock().unwrap();
317-
guard.evaluate_flag(black_box("targetedFlag"), black_box(&ctx))
317+
guard.evaluate_flag(black_box("targetedFlag"), black_box(ctx))
318318
})
319319
})
320320
.collect();
@@ -354,7 +354,7 @@ fn concurrent_mixed_16t(c: &mut Criterion) {
354354
let (key, ctx) = workload_defs[i % workload_defs.len()].clone();
355355
thread::spawn(move || {
356356
let guard = eval.lock().unwrap();
357-
guard.evaluate_flag(black_box(key), black_box(&ctx))
357+
guard.evaluate_flag(black_box(key), black_box(ctx))
358358
})
359359
})
360360
.collect();
@@ -391,7 +391,7 @@ fn concurrent_read_write_16t(c: &mut Criterion) {
391391
thread::spawn(move || {
392392
let ctx = json!({});
393393
let guard = eval.lock().unwrap();
394-
guard.evaluate_flag(black_box("boolFlag"), black_box(&ctx))
394+
guard.evaluate_flag(black_box("boolFlag"), black_box(ctx))
395395
})
396396
})
397397
.collect();

benches/evaluation.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ fn evaluate_flag_simple(c: &mut Criterion) {
7777
let context = json!({});
7878

7979
c.bench_function("evaluate_flag_simple", |b| {
80-
b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(&context)))
80+
b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(context.clone())))
8181
});
8282
}
8383

@@ -87,7 +87,7 @@ fn evaluate_flag_targeting_match(c: &mut Criterion) {
8787
let context = json!({"role": "admin"});
8888

8989
c.bench_function("evaluate_flag_targeting_match", |b| {
90-
b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(&context)))
90+
b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(context.clone())))
9191
});
9292
}
9393

@@ -97,7 +97,7 @@ fn evaluate_flag_targeting_no_match(c: &mut Criterion) {
9797
let context = json!({"role": "viewer"});
9898

9999
c.bench_function("evaluate_flag_targeting_no_match", |b| {
100-
b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(&context)))
100+
b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(context.clone())))
101101
});
102102
}
103103

@@ -107,7 +107,7 @@ fn evaluate_flag_complex_targeting(c: &mut Criterion) {
107107
let context = json!({"tier": "standard", "score": 75});
108108

109109
c.bench_function("evaluate_flag_complex_targeting", |b| {
110-
b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(&context)))
110+
b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(context.clone())))
111111
});
112112
}
113113

@@ -117,7 +117,7 @@ fn evaluate_flag_disabled(c: &mut Criterion) {
117117
let context = json!({});
118118

119119
c.bench_function("evaluate_flag_disabled", |b| {
120-
b.iter(|| evaluator.evaluate_flag(black_box("disabledFlag"), black_box(&context)))
120+
b.iter(|| evaluator.evaluate_flag(black_box("disabledFlag"), black_box(context.clone())))
121121
});
122122
}
123123

@@ -127,7 +127,7 @@ fn evaluate_flag_not_found(c: &mut Criterion) {
127127
let context = json!({});
128128

129129
c.bench_function("evaluate_flag_not_found", |b| {
130-
b.iter(|| evaluator.evaluate_flag(black_box("nonexistent"), black_box(&context)))
130+
b.iter(|| evaluator.evaluate_flag(black_box("nonexistent"), black_box(context.clone())))
131131
});
132132
}
133133

@@ -205,7 +205,7 @@ fn evaluate_flag_simple_small_ctx(c: &mut Criterion) {
205205
let context = small_context();
206206

207207
c.bench_function("evaluate_flag_simple_small_ctx", |b| {
208-
b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(&context)))
208+
b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(context.clone())))
209209
});
210210
}
211211

@@ -216,7 +216,7 @@ fn evaluate_flag_simple_large_ctx(c: &mut Criterion) {
216216
let context = large_context();
217217

218218
c.bench_function("evaluate_flag_simple_large_ctx", |b| {
219-
b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(&context)))
219+
b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(context.clone())))
220220
});
221221
}
222222

@@ -227,7 +227,7 @@ fn evaluate_flag_targeting_small_ctx(c: &mut Criterion) {
227227
let context = small_context(); // contains "role": "admin" which triggers the match
228228

229229
c.bench_function("evaluate_flag_targeting_small_ctx", |b| {
230-
b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(&context)))
230+
b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(context.clone())))
231231
});
232232
}
233233

@@ -238,7 +238,7 @@ fn evaluate_flag_targeting_large_ctx(c: &mut Criterion) {
238238
let context = large_context(); // contains "role": "admin" which triggers the match
239239

240240
c.bench_function("evaluate_flag_targeting_large_ctx", |b| {
241-
b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(&context)))
241+
b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(context.clone())))
242242
});
243243
}
244244

@@ -249,7 +249,7 @@ fn evaluate_flag_complex_targeting_small_ctx(c: &mut Criterion) {
249249
let context = small_context(); // contains "tier": "premium" and "score": 85
250250

251251
c.bench_function("evaluate_flag_complex_targeting_small_ctx", |b| {
252-
b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(&context)))
252+
b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(context.clone())))
253253
});
254254
}
255255

@@ -260,7 +260,7 @@ fn evaluate_flag_complex_targeting_large_ctx(c: &mut Criterion) {
260260
let context = large_context(); // contains "tier": "premium" and "score": 85
261261

262262
c.bench_function("evaluate_flag_complex_targeting_large_ctx", |b| {
263-
b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(&context)))
263+
b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(context.clone())))
264264
});
265265
}
266266

benches/scale.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ fn bench_evaluate_static_from_10k(c: &mut Criterion) {
102102
let context = json!({});
103103

104104
c.bench_function("S10_evaluate_static_from_10K_store", |b| {
105-
b.iter(|| evaluator.evaluate_flag(black_box("flag_0000"), black_box(&context)))
105+
b.iter(|| evaluator.evaluate_flag(black_box("flag_0000"), black_box(context.clone())))
106106
});
107107
}
108108

@@ -117,7 +117,7 @@ fn bench_evaluate_targeting_from_10k(c: &mut Criterion) {
117117
let context = json!({"color": "blue", "targetingKey": "user-123"});
118118

119119
c.bench_function("S11_evaluate_targeting_from_10K_store", |b| {
120-
b.iter(|| evaluator.evaluate_flag(black_box("flag_0014"), black_box(&context)))
120+
b.iter(|| evaluator.evaluate_flag(black_box("flag_0014"), black_box(context.clone())))
121121
});
122122
}
123123

examples/evaluators_demo.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ fn main() {
6767

6868
// Test 1: Admin user
6969
let context = json!({"email": "admin@company.com", "tier": "basic"});
70-
let result = evaluator.evaluate_flag("vipFeatures", &context);
70+
let result = evaluator.evaluate_flag("vipFeatures", context);
7171
println!("1️⃣ Admin user (admin@company.com, tier=basic):");
7272
println!(
7373
" → Result: {}, Variant: {}",
@@ -77,7 +77,7 @@ fn main() {
7777

7878
// Test 2: Premium user
7979
let context = json!({"email": "user@company.com", "tier": "premium"});
80-
let result = evaluator.evaluate_flag("vipFeatures", &context);
80+
let result = evaluator.evaluate_flag("vipFeatures", context);
8181
println!("\n2️⃣ Premium user (user@company.com, tier=premium):");
8282
println!(
8383
" → Result: {}, Variant: {}",
@@ -87,7 +87,7 @@ fn main() {
8787

8888
// Test 3: Regular user
8989
let context = json!({"email": "user@company.com", "tier": "basic"});
90-
let result = evaluator.evaluate_flag("vipFeatures", &context);
90+
let result = evaluator.evaluate_flag("vipFeatures", context);
9191
println!("\n3️⃣ Regular user (user@company.com, tier=basic):");
9292
println!(
9393
" → Result: {}, Variant: {}",

python/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,17 @@ impl FlagEvaluator {
108108

109109
// If we also have a flag index, use the index-based evaluation path
110110
if let Some(&index) = self.flag_indices.get(flag_key) {
111-
return self.inner.evaluate_flag_by_index(index, &filtered_context);
111+
return self.inner.evaluate_flag_by_index(index, filtered_context);
112112
}
113113

114114
// Otherwise use pre-enriched evaluation (context already has $flagd)
115115
return self
116116
.inner
117-
.evaluate_flag_pre_enriched(flag_key, &filtered_context);
117+
.evaluate_flag_pre_enriched(flag_key, filtered_context);
118118
}
119119

120120
// Full evaluation path (no optimization data available for this flag)
121-
self.inner.evaluate_flag(flag_key, context)
121+
self.inner.evaluate_flag(flag_key, context.clone())
122122
}
123123
}
124124

0 commit comments

Comments
 (0)