Skip to content

perf(evaluation): add context key filtering and index-based WASM evaluation#70

Merged
aepfli merged 4 commits into
mainfrom
feat/context-key-filtering
Feb 12, 2026
Merged

perf(evaluation): add context key filtering and index-based WASM evaluation#70
aepfli merged 4 commits into
mainfrom
feat/context-key-filtering

Conversation

@aepfli
Copy link
Copy Markdown
Contributor

@aepfli aepfli commented Feb 10, 2026

Summary

  • Extract required context keys from compiled targeting trees during update_state(), enabling host-side context filtering
  • Add evaluate_by_index(u32, ctx_ptr, ctx_len) WASM export for O(1) flag lookup by numeric index
  • Move context enrichment ($flagd.flagKey, $flagd.timestamp, targetingKey) to Java side, eliminating WASM-side clone
  • Java evaluateFlag(EvaluationContext) automatically applies filtered serialization and index-based evaluation

How it works

During update_state(), the Rust side walks each flag's compiled targeting tree (CompiledNode) to find all {"var": "X"} references. This per-flag set of required context keys is returned in the UpdateStateResponse alongside a flag-key-to-index mapping.

On the Java side, evaluateFlag(EvaluationContext) uses these to:

  1. Serialize only the context keys the targeting rule references (+ $flagd enrichment + targetingKey)
  2. Call evaluate_by_index with the numeric flag index instead of the string key

For a 1000-attribute LayeredEvaluationContext where the targeting rule checks 2 fields, context JSON shrinks from ~50KB to ~200 bytes.

Benchmark results

JMH ResolverComparisonBenchmark (1000+ attribute LayeredEvaluationContext, 1 fork, 5 measurement iterations):

WASM evaluator: before → after optimization

Benchmark main (µs) optimized (µs) Improvement
SimpleFlag (cached) 0.027 0.028 ~same
EmptyContext 0.027 0.027 ~same
TargetingMatch (1000+ attrs) 2935 17.9 164x faster
TargetingNoMatch (small ctx) 25.4 20.3 1.3x
ManyEvaluations (×1000, 1000+ attrs) 2645 21.2 125x faster

Optimized WASM evaluator vs old json-logic-java resolver

Benchmark Old Resolver (µs) WASM Optimized (µs) Speedup
SimpleFlag 0.028 0.028 ~same
TargetingMatch (1000+ attrs) 569 17.9 32x faster
TargetingNoMatch (small ctx) 6.8 20.3 0.3x (WASM overhead)
ManyEvaluations (×1000) 554 21.2 26x faster

On main without context key filtering, the WASM evaluator was 5x slower than json-logic-java for large contexts (2935 µs vs 543 µs) because it serialized all 1000+ attributes to JSON, copied them to WASM memory, parsed them in Rust, and only then evaluated. With filtering, it serializes only the 2-3 fields the rule needs, making it 32x faster than the old resolver.

Edge cases handled

  • {"var": ""} (entire context): extract_required_context_keys returns None → Java sends full context
  • evaluate_by_index export missing (older WASM): falls back to string-based evaluate_reusable
  • requiredContextKeys absent from response: falls back to full serialization
  • Thread safety: index lookup, filtering, and WASM call all inside the same synchronized block

Test plan

  • cargo test --lib --test integration_tests -- --test-threads=1 — 134 passed
  • cargo clippy -- -D warnings — clean
  • cargo fmt -- --check — clean
  • cd java && ./mvnw test — 37 passed (including 4 new FlagEvaluator tests + 3 new serializer tests)
  • JMH benchmarks run on both main and feature branch

Closes #60

🤖 Generated with Claude Code

aepfli and others added 4 commits February 10, 2026 19:41
…uation

Reduce targeting flag evaluation latency for large contexts by avoiding
unnecessary data transfer across the WASM boundary.

Rust side:
- Walk compiled targeting trees to extract referenced context keys
  (e.g. {"var": "email"} -> "email") during update_state()
- Return per-flag requiredContextKeys and flagIndices in UpdateStateResponse
- Add evaluate_by_index(u32, ctx_ptr, ctx_len) WASM export for O(1) flag
  lookup by numeric index instead of string key HashMap lookup
- Add evaluate_flag_pre_enriched() that skips context enrichment when
  $flagd is already present (host-side enrichment)

Java side:
- Cache requiredContextKeys and flagIndices from updateState() response
- EvaluationContextSerializer.serializeFiltered() serializes only the
  context keys a targeting rule references, plus $flagd enrichment
- evaluateFlag(EvaluationContext) uses filtered serialization + index-based
  eval when available, falls back to full serialization gracefully
- evaluateByIndex() calls the new WASM export with O(1) Vec lookup

JMH results (1000+ attribute LayeredEvaluationContext):
- Targeting flags: ~12.8 µs (down from ~167 µs) — 13x improvement
- vs old json-logic-java: 32-34x faster (409 µs -> 12.8 µs)
- Static/disabled flags: ~0.02 µs (pre-evaluated cache, unchanged)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The WASM tests use a thread-local singleton evaluator. Parallel test
execution causes race conditions where one test's update_state overwrites
another test's state, producing intermittent failures like
test_wasm_evaluate_by_index getting "Static" instead of "TargetingMatch".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gherkin tests (cucumber) don't support --test-threads flag. Run lib and
integration tests with --test-threads=1 for WASM singleton safety, and
gherkin tests separately without the flag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@aepfli aepfli merged commit 50d7cc0 into main Feb 12, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: Pre-evaluate flags without targeting to eliminate WASM overhead

1 participant