1 package evaluation
2
3 import (
4 "fmt"
5 "testing"
6
7 "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders"
8 "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
9
10 "github.com/launchdarkly/go-sdk-common/v3/ldattr"
11 "github.com/launchdarkly/go-sdk-common/v3/ldcontext"
12 "github.com/launchdarkly/go-sdk-common/v3/ldvalue"
13 )
14
15
16
17
18
19
20
21
22 var evalBenchmarkResult Result
23 var evalBenchmarkErr error
24
25 const evalBenchmarkSegmentKey = "segment-key"
26
27 func discardPrerequisiteEvents(params PrerequisiteFlagEvent) {}
28
29 type evalBenchmarkEnv struct {
30 evaluator Evaluator
31 user ldcontext.Context
32 targetFlag *ldmodel.FeatureFlag
33 otherFlags map[string]*ldmodel.FeatureFlag
34 targetSegment *ldmodel.Segment
35 targetFeatureKey string
36 targetUsers []ldcontext.Context
37 }
38
39 type evalBenchmarkCase struct {
40 numTargets int
41 numRules int
42 numClauses int
43 extraClauseValues int
44 withSegments bool
45 prereqsWidth int
46 prereqsDepth int
47 operator ldmodel.Operator
48 shouldMatchClause bool
49 }
50
51 func newEvalBenchmarkEnv() *evalBenchmarkEnv {
52 return &evalBenchmarkEnv{}
53 }
54
55 func (env *evalBenchmarkEnv) setUp(bc evalBenchmarkCase) {
56 env.evaluator = basicEvaluator()
57
58 env.user = makeEvalBenchmarkUser(bc)
59
60 env.targetFlag, env.otherFlags, env.targetSegment = makeEvalBenchmarkFlagData(bc)
61
62 dataProvider := &simpleDataProvider{
63 getFlag: func(key string) *ldmodel.FeatureFlag {
64 return env.otherFlags[key]
65 },
66 getSegment: func(key string) *ldmodel.Segment {
67 if key == evalBenchmarkSegmentKey {
68 return env.targetSegment
69 }
70 return nil
71 },
72 }
73 env.evaluator = NewEvaluator(dataProvider)
74
75 env.targetUsers = make([]ldcontext.Context, bc.numTargets)
76 for i := 0; i < bc.numTargets; i++ {
77 env.targetUsers[i] = ldcontext.New(makeEvalBenchmarkTargetUserKey(i))
78 }
79 }
80
81 func makeEvalBenchmarkUser(bc evalBenchmarkCase) ldcontext.Context {
82 if bc.shouldMatchClause {
83 builder := ldcontext.NewBuilder("user-match")
84 switch bc.operator {
85 case ldmodel.OperatorGreaterThan:
86 builder.SetInt("numAttr", 10000)
87 case ldmodel.OperatorContains:
88 builder.Name("name-0")
89 case ldmodel.OperatorMatches:
90 builder.SetString("stringAttr", "stringAttr-0")
91 case ldmodel.OperatorAfter:
92 builder.SetString("dateAttr", "2999-12-31T00:00:00.000-00:00")
93 case ldmodel.OperatorSemVerEqual:
94 builder.SetString("semVerAttr", "1.0.0")
95 case ldmodel.OperatorIn:
96 builder.SetString("stringAttr", "stringAttr-0")
97 }
98 return builder.Build()
99 }
100
101 return ldcontext.NewBuilder("user-nomatch").
102 Name("name-nomatch").
103 SetString("stringAttr", "stringAttr-nomatch").
104 SetInt("numAttr", 0).
105 SetString("dateAttr", "1980-01-01T00:00:00.000-00:00").
106 SetString("semVerAttr", "0.0.5").
107 Build()
108 }
109
110 func benchmarkEval(b *testing.B, cases []evalBenchmarkCase, action func(*evalBenchmarkEnv)) {
111 env := newEvalBenchmarkEnv()
112 for _, bc := range cases {
113 env.setUp(bc)
114
115 b.Run(fmt.Sprintf("%+v", bc), func(b *testing.B) {
116 for i := 0; i < b.N; i++ {
117 action(env)
118 }
119 })
120 }
121 }
122
123 func BenchmarkEvaluationFallthroughNoAlloc(b *testing.B) {
124 benchmarkEval(b, makeEvalBenchmarkCases(false), func(env *evalBenchmarkEnv) {
125 evalBenchmarkResult = env.evaluator.Evaluate(env.targetFlag, env.user, discardPrerequisiteEvents)
126 if evalBenchmarkResult.Detail.Value.BoolValue() {
127 b.FailNow()
128 }
129 })
130 }
131
132 func BenchmarkEvaluationRuleMatchNoAlloc(b *testing.B) {
133 benchmarkEval(b, makeEvalBenchmarkCases(true), func(env *evalBenchmarkEnv) {
134 evalBenchmarkResult = env.evaluator.Evaluate(env.targetFlag, env.user, discardPrerequisiteEvents)
135 if !evalBenchmarkResult.Detail.Value.BoolValue() {
136 b.FailNow()
137 }
138 })
139 }
140
141 func BenchmarkEvaluationUserFoundInTargetsNoAlloc(b *testing.B) {
142
143
144
145 benchmarkEval(b, makeTargetMatchBenchmarkCases(), func(env *evalBenchmarkEnv) {
146 user := env.targetUsers[len(env.targetUsers)/2]
147 evalBenchmarkResult := env.evaluator.Evaluate(env.targetFlag, user, discardPrerequisiteEvents)
148 if !evalBenchmarkResult.Detail.Value.BoolValue() {
149 b.FailNow()
150 }
151 })
152 }
153
154 func BenchmarkEvaluationUsersNotFoundInTargetsNoAlloc(b *testing.B) {
155
156
157
158 benchmarkEval(b, makeTargetMatchBenchmarkCases(), func(env *evalBenchmarkEnv) {
159 evalBenchmarkResult := env.evaluator.Evaluate(env.targetFlag, env.user, discardPrerequisiteEvents)
160 if evalBenchmarkResult.Detail.Value.BoolValue() {
161 b.FailNow()
162 }
163 })
164 }
165
166 func BenchmarkEvaluationUserIncludedInSegmentNoAlloc(b *testing.B) {
167
168
169
170 benchmarkEval(b, makeSegmentIncludeExcludeBenchmarkCases(), func(env *evalBenchmarkEnv) {
171 user := ldcontext.New(env.targetSegment.Included[len(env.targetSegment.Included)/2])
172 evalBenchmarkResult := env.evaluator.Evaluate(env.targetFlag, user, discardPrerequisiteEvents)
173 if !evalBenchmarkResult.Detail.Value.BoolValue() {
174 b.FailNow()
175 }
176 })
177 }
178
179 func BenchmarkEvaluationUserExcludedFromSegmentNoAlloc(b *testing.B) {
180
181
182
183 benchmarkEval(b, makeSegmentIncludeExcludeBenchmarkCases(), func(env *evalBenchmarkEnv) {
184 user := ldcontext.New(env.targetSegment.Excluded[len(env.targetSegment.Excluded)/2])
185 evalBenchmarkResult := env.evaluator.Evaluate(env.targetFlag, user, discardPrerequisiteEvents)
186 if evalBenchmarkResult.Detail.Value.BoolValue() {
187 b.FailNow()
188 }
189 })
190 }
191
192 func BenchmarkEvaluationUserMatchedBySegmentRuleNoAlloc(b *testing.B) {
193 benchmarkEval(b, makeSegmentRuleMatchBenchmarkCases(), func(env *evalBenchmarkEnv) {
194 evalBenchmarkResult := env.evaluator.Evaluate(env.targetFlag, env.user, discardPrerequisiteEvents)
195 if !evalBenchmarkResult.Detail.Value.BoolValue() {
196 b.FailNow()
197 }
198 })
199 }
200
201 func makeEvalBenchmarkCases(shouldMatch bool) []evalBenchmarkCase {
202 ret := []evalBenchmarkCase{}
203 for _, op := range []ldmodel.Operator{
204 ldmodel.OperatorIn,
205 ldmodel.OperatorGreaterThan,
206 ldmodel.OperatorContains,
207 ldmodel.OperatorMatches,
208 ldmodel.OperatorAfter,
209 ldmodel.OperatorSemVerEqual,
210 } {
211 ret = append(ret, evalBenchmarkCase{
212 numRules: 1,
213 numClauses: 1,
214 operator: op,
215 shouldMatchClause: shouldMatch,
216 })
217 if shouldMatch {
218
219
220 ret = append(ret, evalBenchmarkCase{
221 numRules: 1,
222 numClauses: 100,
223 operator: op,
224 shouldMatchClause: true,
225 })
226 } else {
227
228
229 ret = append(ret, evalBenchmarkCase{
230 numRules: 100,
231 numClauses: 1,
232 operator: op,
233 })
234 }
235
236 ret = append(ret, evalBenchmarkCase{
237 numRules: 1,
238 numClauses: 1,
239 extraClauseValues: 99,
240 operator: op,
241 shouldMatchClause: shouldMatch,
242 })
243
244
245 ret = append(ret, evalBenchmarkCase{
246 numRules: 1,
247 numClauses: 1,
248 operator: op,
249 shouldMatchClause: shouldMatch,
250 prereqsWidth: 5,
251 prereqsDepth: 1,
252 })
253 ret = append(ret, evalBenchmarkCase{
254 numRules: 1,
255 numClauses: 1,
256 operator: op,
257 shouldMatchClause: shouldMatch,
258 prereqsWidth: 1,
259 prereqsDepth: 5,
260 })
261 }
262 return ret
263 }
264
265 func makeEvalBenchmarkSegmentKey(i int) string {
266 return fmt.Sprintf("segment-%d", i)
267 }
268
269 func makeEvalBenchmarkTargetUserKey(i int) string {
270 return fmt.Sprintf("user-%d", i)
271 }
272
273 func makeEvalBenchmarkClauses(numClauses int, extraClauseValues int, op ldmodel.Operator) []ldmodel.Clause {
274 clauses := make([]ldmodel.Clause, 0, numClauses)
275 for i := 0; i < numClauses; i++ {
276 clause := ldmodel.Clause{Op: op}
277 var value ldvalue.Value
278 var name string
279 switch op {
280 case ldmodel.OperatorGreaterThan:
281 name = "numAttr"
282 value = ldvalue.Int(i)
283 case ldmodel.OperatorContains:
284 name = "name"
285 value = ldvalue.String("name-0")
286 case ldmodel.OperatorMatches:
287 name = "stringAttr"
288 value = ldvalue.String("stringAttr-0")
289 case ldmodel.OperatorAfter:
290 name = "dateAttr"
291 value = ldvalue.String("2000-01-01T00:00:00.000-00:00")
292 case ldmodel.OperatorSemVerEqual:
293 name = "semVerAttr"
294 value = ldvalue.String("1.0.0")
295 case ldmodel.OperatorSegmentMatch:
296 value = ldvalue.String(evalBenchmarkSegmentKey)
297 default:
298 clause.Op = ldmodel.OperatorIn
299 name = "stringAttr"
300 value = ldvalue.String("stringAttr-0")
301 }
302 if name != "" {
303 clause.Attribute = ldattr.NewLiteralRef(name)
304 }
305 if extraClauseValues == 0 {
306 clause.Values = []ldvalue.Value{value}
307 } else {
308 for i := 0; i < extraClauseValues; i++ {
309 clause.Values = append(clause.Values, ldvalue.String("not-a-match"))
310 }
311 clause.Values = append(clause.Values, value)
312 }
313 clauses = append(clauses, clause)
314 }
315 return clauses
316 }
317
318 func makeTargetMatchBenchmarkCases() []evalBenchmarkCase {
319 return []evalBenchmarkCase{
320 {numTargets: 10},
321 {numTargets: 100},
322 {numTargets: 1000},
323 }
324 }
325
326 func makeSegmentIncludeExcludeBenchmarkCases() []evalBenchmarkCase {
327
328 ret := []evalBenchmarkCase{}
329 for _, n := range []int{10, 100, 1000} {
330 ret = append(ret, evalBenchmarkCase{
331 withSegments: true,
332 numTargets: n,
333 numRules: 1,
334 numClauses: 1,
335 shouldMatchClause: false,
336 })
337 }
338 return ret
339 }
340
341 func makeSegmentRuleMatchBenchmarkCases() []evalBenchmarkCase {
342
343 ret := []evalBenchmarkCase{}
344 for _, operator := range []ldmodel.Operator{ldmodel.OperatorIn, ldmodel.OperatorMatches} {
345 ret = append(ret, evalBenchmarkCase{
346 withSegments: true,
347 numTargets: 0,
348 numRules: 1,
349 numClauses: 1,
350 operator: operator,
351 shouldMatchClause: true,
352 })
353 }
354 return ret
355 }
356
357 func buildEvalBenchmarkFlag(bc evalBenchmarkCase, key string) *ldbuilders.FlagBuilder {
358
359
360
361 builder := ldbuilders.NewFlagBuilder("flag-0").
362 Version(1).
363 On(true).
364 FallthroughVariation(0).
365 Variations(ldvalue.Bool(false), ldvalue.Bool(true))
366 if bc.numTargets > 0 {
367 values := make([]string, bc.numTargets)
368 for k := 0; k < bc.numTargets; k++ {
369 values[k] = makeEvalBenchmarkTargetUserKey(k)
370 }
371 builder.AddTarget(1, values...)
372 }
373 for j := 0; j < bc.numRules; j++ {
374 operator := bc.operator
375 if bc.withSegments {
376 operator = ldmodel.OperatorSegmentMatch
377 }
378 builder.AddRule(ldbuilders.NewRuleBuilder().
379 ID(fmt.Sprintf("%s-%d", key, j)).
380 Clauses(makeEvalBenchmarkClauses(bc.numClauses, bc.extraClauseValues, operator)...).
381 Variation(1))
382 }
383 return builder
384 }
385
386 func makeEvalBenchmarkFlagData(bc evalBenchmarkCase) (*ldmodel.FeatureFlag, map[string]*ldmodel.FeatureFlag, *ldmodel.Segment) {
387 mainFlag := buildEvalBenchmarkFlag(bc, "flag-0")
388
389 otherFlags := make(map[string]*ldmodel.FeatureFlag)
390 if bc.prereqsDepth > 0 && bc.prereqsWidth > 0 {
391 flagCounter := 1
392 makeEvalBenchmarkPrerequisites(mainFlag, &flagCounter, otherFlags, bc, bc.prereqsDepth)
393 }
394
395 var segment *ldmodel.Segment
396 if bc.withSegments {
397 sb := ldbuilders.NewSegmentBuilder(evalBenchmarkSegmentKey).Version(1)
398 included := make([]string, bc.numTargets)
399 for i := range included {
400 included[i] = makeEvalBenchmarkTargetUserKey(i)
401 }
402 sb.Included(included...)
403 excluded := make([]string, bc.numTargets)
404 for i := range excluded {
405 excluded[i] = makeEvalBenchmarkTargetUserKey(i + bc.numTargets)
406 }
407 sb.Excluded(excluded...)
408 sb.AddRule(ldbuilders.NewSegmentRuleBuilder().
409 Clauses(makeEvalBenchmarkClauses(bc.numClauses, bc.extraClauseValues, bc.operator)...))
410 s := sb.Build()
411 segment = &s
412 }
413
414 f := mainFlag.Build()
415 return &f, otherFlags, segment
416 }
417
418
419
420
421
422 func makeEvalBenchmarkPrerequisites(
423 mainFlag *ldbuilders.FlagBuilder,
424 flagCounter *int,
425 otherFlags map[string]*ldmodel.FeatureFlag,
426 bc evalBenchmarkCase,
427 remainingDepth int,
428 ) {
429 for i := 0; i < bc.prereqsWidth; i++ {
430 prereqBuilder := ldbuilders.NewFlagBuilder(fmt.Sprintf("flag-%d", *flagCounter)).
431 Version(1).
432 On(true).
433 FallthroughVariation(1).
434 Variations(ldvalue.Bool(false), ldvalue.Bool(true))
435 *flagCounter++
436 if remainingDepth > 1 {
437 makeEvalBenchmarkPrerequisites(prereqBuilder, flagCounter, otherFlags, bc, remainingDepth-1)
438 }
439 prereqFlag := prereqBuilder.Build()
440 otherFlags[prereqFlag.Key] = &prereqFlag
441 mainFlag.AddPrerequisite(prereqFlag.Key, 1)
442 }
443 }
444
View as plain text