Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit 1c706cf

Browse files
authored
Merge pull request #338 from launchdarkly/eb/sc-158686/fewer-iterators
avoid creating List iterators during evaluations
2 parents 1b8d842 + 3cad580 commit 1c706cf

File tree

1 file changed

+90
-34
lines changed

1 file changed

+90
-34
lines changed

src/main/java/com/launchdarkly/sdk/server/Evaluator.java

Lines changed: 90 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@
55
import com.launchdarkly.sdk.LDUser;
66
import com.launchdarkly.sdk.LDValue;
77
import com.launchdarkly.sdk.LDValueType;
8+
import com.launchdarkly.sdk.server.DataModel.Clause;
89
import com.launchdarkly.sdk.server.DataModel.FeatureFlag;
10+
import com.launchdarkly.sdk.server.DataModel.Operator;
11+
import com.launchdarkly.sdk.server.DataModel.Prerequisite;
12+
import com.launchdarkly.sdk.server.DataModel.Rollout;
913
import com.launchdarkly.sdk.server.DataModel.Rule;
14+
import com.launchdarkly.sdk.server.DataModel.Segment;
15+
import com.launchdarkly.sdk.server.DataModel.SegmentRule;
16+
import com.launchdarkly.sdk.server.DataModel.Target;
1017
import com.launchdarkly.sdk.server.DataModel.VariationOrRollout;
1118
import com.launchdarkly.sdk.server.DataModel.WeightedVariation;
1219
import com.launchdarkly.sdk.server.DataModelPreprocessing.ClausePreprocessed;
@@ -22,10 +29,34 @@
2229
/**
2330
* Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment;
2431
* if it needs to retrieve flags or segments that are referenced by a flag, it does so through a read-only interface
25-
* that is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite
26-
* flags, but does not send them.
32+
* that is provided in the constructor. It also produces evaluation records (to be used in event data) as appropriate
33+
* for any referenced prerequisite flags.
2734
*/
2835
class Evaluator {
36+
//
37+
// IMPLEMENTATION NOTES ABOUT THIS FILE
38+
//
39+
// Flag evaluation is the hottest code path in the SDK; large applications may evaluate flags at a VERY high
40+
// volume, so every little bit of optimization we can achieve here could add up to quite a bit of overhead we
41+
// are not making the customer incur. Strategies that are used here for that purpose include:
42+
//
43+
// 1. Whenever possible, we are reusing precomputed instances of EvalResult; see DataModelPreprocessing and
44+
// EvaluatorHelpers.
45+
//
46+
// 2. If prerequisite evaluations happen as a side effect of an evaluation, rather than building and returning
47+
// a list of these, we deliver them one at a time via the PrerequisiteEvaluationSink callback mechanism.
48+
//
49+
// 3. If there's a piece of state that needs to be tracked across multiple methods during an evaluation, and
50+
// it's not feasible to just pass it as a method parameter, consider adding it as a field in the mutable
51+
// EvaluatorState object (which we will always have one of) rather than creating a new object to contain it.
52+
//
53+
// 4. Whenever possible, avoid using "for (variable: list)" here because it always creates an iterator object.
54+
// Instead, use the tedious old "get the size, iterate with a counter" approach.
55+
//
56+
// 5. Avoid using lambdas/closures here, because these generally cause a heap object to be allocated for
57+
// variables captured in the closure each time they are used.
58+
//
59+
2960
private final static Logger logger = Loggers.EVALUATION;
3061

3162
/**
@@ -43,8 +74,8 @@ class Evaluator {
4374
* and simplifies testing.
4475
*/
4576
static interface Getters {
46-
DataModel.FeatureFlag getFlag(String key);
47-
DataModel.Segment getSegment(String key);
77+
FeatureFlag getFlag(String key);
78+
Segment getSegment(String key);
4879
BigSegmentStoreWrapper.BigSegmentsQueryResult getBigSegments(String key);
4980
}
5081

@@ -82,7 +113,7 @@ private static class EvaluatorState {
82113
* @param eventFactory produces feature request events
83114
* @return an {@link EvalResult} - guaranteed non-null
84115
*/
85-
EvalResult evaluate(DataModel.FeatureFlag flag, LDUser user, PrerequisiteEvaluationSink prereqEvals) {
116+
EvalResult evaluate(FeatureFlag flag, LDUser user, PrerequisiteEvaluationSink prereqEvals) {
86117
if (flag.getKey() == INVALID_FLAG_KEY_THAT_THROWS_EXCEPTION) {
87118
throw EXPECTED_EXCEPTION_FROM_INVALID_FLAG;
88119
}
@@ -105,7 +136,7 @@ EvalResult evaluate(DataModel.FeatureFlag flag, LDUser user, PrerequisiteEvaluat
105136
return result;
106137
}
107138

108-
private EvalResult evaluateInternal(DataModel.FeatureFlag flag, LDUser user,
139+
private EvalResult evaluateInternal(FeatureFlag flag, LDUser user,
109140
PrerequisiteEvaluationSink prereqEvals, EvaluatorState state) {
110141
if (!flag.isOn()) {
111142
return EvaluatorHelpers.offResult(flag);
@@ -117,15 +148,20 @@ private EvalResult evaluateInternal(DataModel.FeatureFlag flag, LDUser user,
117148
}
118149

119150
// Check to see if targets match
120-
for (DataModel.Target target: flag.getTargets()) { // getTargets() and getValues() are guaranteed non-null
121-
if (target.getValues().contains(user.getKey())) {
151+
List<Target> targets = flag.getTargets(); // guaranteed non-null
152+
int nTargets = targets.size();
153+
for (int i = 0; i < nTargets; i++) {
154+
Target target = targets.get(i);
155+
if (target.getValues().contains(user.getKey())) { // getValues() is guaranteed non-null
122156
return EvaluatorHelpers.targetMatchResult(flag, target);
123157
}
124158
}
159+
125160
// Now walk through the rules and see if any match
126-
List<DataModel.Rule> rules = flag.getRules(); // guaranteed non-null
127-
for (int i = 0; i < rules.size(); i++) {
128-
DataModel.Rule rule = rules.get(i);
161+
List<Rule> rules = flag.getRules(); // guaranteed non-null
162+
int nRules = rules.size();
163+
for (int i = 0; i < nRules; i++) {
164+
Rule rule = rules.get(i);
129165
if (ruleMatchesUser(flag, rule, user, state)) {
130166
return computeRuleMatch(flag, user, rule, i);
131167
}
@@ -138,11 +174,14 @@ private EvalResult evaluateInternal(DataModel.FeatureFlag flag, LDUser user,
138174

139175
// Checks prerequisites if any; returns null if successful, or an EvalResult if we have to
140176
// short-circuit due to a prerequisite failure.
141-
private EvalResult checkPrerequisites(DataModel.FeatureFlag flag, LDUser user,
177+
private EvalResult checkPrerequisites(FeatureFlag flag, LDUser user,
142178
PrerequisiteEvaluationSink prereqEvals, EvaluatorState state) {
143-
for (DataModel.Prerequisite prereq: flag.getPrerequisites()) { // getPrerequisites() is guaranteed non-null
179+
List<Prerequisite> prerequisites = flag.getPrerequisites(); // guaranteed non-null
180+
int nPrerequisites = prerequisites.size();
181+
for (int i = 0; i < nPrerequisites; i++) {
182+
Prerequisite prereq = prerequisites.get(i);
144183
boolean prereqOk = true;
145-
DataModel.FeatureFlag prereqFeatureFlag = getters.getFlag(prereq.getKey());
184+
FeatureFlag prereqFeatureFlag = getters.getFlag(prereq.getKey());
146185
if (prereqFeatureFlag == null) {
147186
logger.error("Could not retrieve prerequisite flag \"{}\" when evaluating \"{}\"", prereq.getKey(), flag.getKey());
148187
prereqOk = false;
@@ -177,11 +216,14 @@ private static EvalResult getValueForVariationOrRollout(
177216
if (maybeVariation != null) {
178217
variation = maybeVariation.intValue();
179218
} else {
180-
DataModel.Rollout rollout = vr.getRollout();
219+
Rollout rollout = vr.getRollout();
181220
if (rollout != null && !rollout.getVariations().isEmpty()) {
182221
float bucket = bucketUser(rollout.getSeed(), user, flag.getKey(), rollout.getBucketBy(), flag.getSalt());
183222
float sum = 0F;
184-
for (DataModel.WeightedVariation wv : rollout.getVariations()) {
223+
List<WeightedVariation> variations = rollout.getVariations(); // guaranteed non-null
224+
int nVariations = variations.size();
225+
for (int i = 0; i < nVariations; i++) {
226+
WeightedVariation wv = variations.get(i);
185227
sum += (float) wv.getWeight() / 100000F;
186228
if (bucket < sum) {
187229
variation = wv.getVariation();
@@ -224,22 +266,28 @@ private static EvaluationReason experimentize(EvaluationReason reason) {
224266
return reason;
225267
}
226268

227-
private boolean ruleMatchesUser(DataModel.FeatureFlag flag, DataModel.Rule rule, LDUser user, EvaluatorState state) {
228-
for (DataModel.Clause clause: rule.getClauses()) { // getClauses() is guaranteed non-null
269+
private boolean ruleMatchesUser(FeatureFlag flag, Rule rule, LDUser user, EvaluatorState state) {
270+
List<Clause> clauses = rule.getClauses(); // guaranteed non-null
271+
int nClauses = clauses.size();
272+
for (int i = 0; i < nClauses; i++) {
273+
Clause clause = clauses.get(i);
229274
if (!clauseMatchesUser(clause, user, state)) {
230275
return false;
231276
}
232277
}
233278
return true;
234279
}
235280

236-
private boolean clauseMatchesUser(DataModel.Clause clause, LDUser user, EvaluatorState state) {
281+
private boolean clauseMatchesUser(Clause clause, LDUser user, EvaluatorState state) {
237282
// In the case of a segment match operator, we check if the user is in any of the segments,
238283
// and possibly negate
239-
if (clause.getOp() == DataModel.Operator.segmentMatch) {
240-
for (LDValue j: clause.getValues()) {
241-
if (j.isString()) {
242-
DataModel.Segment segment = getters.getSegment(j.stringValue());
284+
if (clause.getOp() == Operator.segmentMatch) {
285+
List<LDValue> values = clause.getValues(); // guaranteed non-null
286+
int nValues = values.size();
287+
for (int i = 0; i < nValues; i++) {
288+
LDValue clauseValue = values.get(i);
289+
if (clauseValue.isString()) {
290+
Segment segment = getters.getSegment(clauseValue.stringValue());
243291
if (segment != null) {
244292
if (segmentMatchesUser(segment, user, state)) {
245293
return maybeNegate(clause, true);
@@ -253,14 +301,16 @@ private boolean clauseMatchesUser(DataModel.Clause clause, LDUser user, Evaluato
253301
return clauseMatchesUserNoSegments(clause, user);
254302
}
255303

256-
private boolean clauseMatchesUserNoSegments(DataModel.Clause clause, LDUser user) {
304+
private boolean clauseMatchesUserNoSegments(Clause clause, LDUser user) {
257305
LDValue userValue = user.getAttribute(clause.getAttribute());
258306
if (userValue.isNull()) {
259307
return false;
260308
}
261309

262310
if (userValue.getType() == LDValueType.ARRAY) {
263-
for (LDValue value: userValue.values()) {
311+
int nValues = userValue.size();
312+
for (int i = 0; i < nValues; i++) {
313+
LDValue value = userValue.get(i);
264314
if (value.getType() == LDValueType.ARRAY || value.getType() == LDValueType.OBJECT) {
265315
logger.error("Invalid custom attribute value in user object for user key \"{}\": {}", user.getKey(), value);
266316
return false;
@@ -278,11 +328,11 @@ private boolean clauseMatchesUserNoSegments(DataModel.Clause clause, LDUser user
278328
return false;
279329
}
280330

281-
static boolean clauseMatchAny(DataModel.Clause clause, LDValue userValue) {
282-
DataModel.Operator op = clause.getOp();
331+
static boolean clauseMatchAny(Clause clause, LDValue userValue) {
332+
Operator op = clause.getOp();
283333
if (op != null) {
284334
ClausePreprocessed preprocessed = clause.preprocessed;
285-
if (op == DataModel.Operator.in) {
335+
if (op == Operator.in) {
286336
// see if we have precomputed a Set for fast equality matching
287337
Set<LDValue> vs = preprocessed == null ? null : preprocessed.valuesSet;
288338
if (vs != null) {
@@ -305,11 +355,11 @@ static boolean clauseMatchAny(DataModel.Clause clause, LDValue userValue) {
305355
return false;
306356
}
307357

308-
private boolean maybeNegate(DataModel.Clause clause, boolean b) {
358+
private boolean maybeNegate(Clause clause, boolean b) {
309359
return clause.isNegate() ? !b : b;
310360
}
311361

312-
private boolean segmentMatchesUser(DataModel.Segment segment, LDUser user, EvaluatorState state) {
362+
private boolean segmentMatchesUser(Segment segment, LDUser user, EvaluatorState state) {
313363
String userKey = user.getKey(); // we've already verified that the key is non-null at the top of evaluate()
314364
if (segment.isUnbounded()) {
315365
if (segment.getGeneration() == null) {
@@ -346,16 +396,22 @@ private boolean segmentMatchesUser(DataModel.Segment segment, LDUser user, Evalu
346396
return false;
347397
}
348398
}
349-
for (DataModel.SegmentRule rule: segment.getRules()) {
399+
List<SegmentRule> rules = segment.getRules(); // guaranteed non-null
400+
int nRules = rules.size();
401+
for (int i = 0; i < nRules; i++) {
402+
SegmentRule rule = rules.get(i);
350403
if (segmentRuleMatchesUser(rule, user, segment.getKey(), segment.getSalt())) {
351404
return true;
352405
}
353406
}
354407
return false;
355408
}
356409

357-
private boolean segmentRuleMatchesUser(DataModel.SegmentRule segmentRule, LDUser user, String segmentKey, String salt) {
358-
for (DataModel.Clause c: segmentRule.getClauses()) {
410+
private boolean segmentRuleMatchesUser(SegmentRule segmentRule, LDUser user, String segmentKey, String salt) {
411+
List<Clause> clauses = segmentRule.getClauses(); // guaranteed non-null
412+
int nClauses = clauses.size();
413+
for (int i = 0; i < nClauses; i++) {
414+
Clause c = clauses.get(i);
359415
if (!clauseMatchesUserNoSegments(c, user)) {
360416
return false;
361417
}
@@ -380,7 +436,7 @@ private static EvalResult computeRuleMatch(FeatureFlag flag, LDUser user, Rule r
380436
return getValueForVariationOrRollout(flag, rule, user, null, reason);
381437
}
382438

383-
static String makeBigSegmentRef(DataModel.Segment segment) {
439+
static String makeBigSegmentRef(Segment segment) {
384440
return String.format("%s.g%d", segment.getKey(), segment.getGeneration());
385441
}
386442
}

0 commit comments

Comments
 (0)