Skip to content

Commit 5e3cfea

Browse files
perf: [Precompute rule condition numeric and semver values to avoid unnecessary object allocation during eval] (FF-2575) (#57)
* perf: [Precompute rule condition numeric and semver values to avoid unnecessary object allocation during eval] (FF-2575) * bump to 4.0.2 * fix precompute
1 parent 992e144 commit 5e3cfea

File tree

5 files changed

+158
-42
lines changed

5 files changed

+158
-42
lines changed

eppoclient/configresponse.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"encoding/json"
55
"fmt"
66
"time"
7+
8+
semver "github.com/Masterminds/semver/v3"
79
)
810

911
type configResponse struct {
@@ -20,6 +22,12 @@ type flagConfiguration struct {
2022
TotalShards int64 `json:"totalShards"`
2123
}
2224

25+
func (f *flagConfiguration) Precompute() {
26+
for i := range f.Allocations {
27+
f.Allocations[i].Precompute()
28+
}
29+
}
30+
2331
type variation struct {
2432
Key string `json:"key"`
2533
Value interface{} `json:"value"`
@@ -117,14 +125,52 @@ type allocation struct {
117125
DoLog *bool `json:"doLog"`
118126
}
119127

128+
func (a *allocation) Precompute() {
129+
for i := range a.Rules {
130+
a.Rules[i].Precompute()
131+
}
132+
}
133+
120134
type rule struct {
121135
Conditions []condition `json:"conditions"`
122136
}
123137

138+
func (r *rule) Precompute() {
139+
for i := range r.Conditions {
140+
r.Conditions[i].Precompute()
141+
}
142+
}
143+
124144
type condition struct {
125145
Operator string `json:"operator"`
126146
Attribute string `json:"attribute"`
127147
Value interface{} `json:"value"`
148+
149+
NumericValue float64
150+
NumericValueValid bool
151+
SemVerValue *semver.Version
152+
SemVerValueValid bool
153+
}
154+
155+
func (c *condition) Precompute() {
156+
// Try to convert Value to a float64
157+
if num, err := toFloat64(c.Value); err == nil {
158+
c.NumericValue = num
159+
c.NumericValueValid = true
160+
return
161+
}
162+
163+
// Try to convert Value to a string and then parse as semver
164+
if str, ok := c.Value.(string); ok {
165+
if semVer, err := semver.NewVersion(str); err == nil {
166+
c.SemVerValue = semVer
167+
c.SemVerValueValid = true
168+
return
169+
}
170+
}
171+
172+
c.NumericValueValid = false
173+
c.SemVerValueValid = false
128174
}
129175

130176
type split struct {

eppoclient/configurationrequestor.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ func (cr *configurationRequestor) fetchConfig() (configResponse, error) {
6767
return configResponse{}, err
6868
}
6969

70+
// Precompute flag values
71+
for _, flag := range response.Flags {
72+
flag.Precompute()
73+
}
74+
7075
return response, nil
7176
}
7277

eppoclient/rules.go

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,23 @@ func (condition condition) matches(subjectAttributes Attributes) bool {
4646
case "NOT_ONE_OF":
4747
return !isOneOf(subjectValue, convertToStringArray(condition.Value))
4848
case "GTE", "GT", "LTE", "LT":
49-
// Attempt to coerce both values to float64 and compare them.
49+
// Attempt to coerce the subject value to float64 and compare it
50+
// against the condition value.
5051
subjectValueNumeric, isNumericSubjectErr := toFloat64(subjectValue)
51-
conditionValueNumeric, isNumericConditionErr := toFloat64(condition.Value)
52-
if isNumericSubjectErr == nil && isNumericConditionErr == nil {
53-
return evaluateNumericCondition(subjectValueNumeric, conditionValueNumeric, condition)
52+
if isNumericSubjectErr == nil && condition.NumericValueValid {
53+
return evaluateNumericCondition(subjectValueNumeric, condition.NumericValue, condition)
5454
}
5555

56-
// Attempt to compare using semantic versioning if both values are strings.
56+
// Attempt to compare using semantic versioning if the subject value is a string.
57+
// and the condition value is a valid semantic version.
5758
subjectValueStr, isStringSubject := subjectValue.(string)
58-
conditionValueStr, isStringCondition := condition.Value.(string)
59-
if isStringSubject && isStringCondition {
60-
// Attempt to parse both values as semantic versions.
59+
if isStringSubject && condition.SemVerValueValid {
60+
// Attempt to parse the subject value as a semantic version.
6161
subjectSemVer, errSubject := semver.NewVersion(subjectValueStr)
62-
conditionSemVer, errCondition := semver.NewVersion(conditionValueStr)
6362

6463
// If parsing succeeds, evaluate the semver condition.
65-
if errSubject == nil && errCondition == nil {
66-
return evaluateSemVerCondition(subjectSemVer, conditionSemVer, condition)
64+
if errSubject == nil {
65+
return evaluateSemVerCondition(subjectSemVer, condition.SemVerValue, condition)
6766
}
6867
}
6968

@@ -196,7 +195,7 @@ func evaluateNumericCondition(subjectValue float64, conditionValue float64, cond
196195
func toFloat64(val interface{}) (float64, error) {
197196
switch v := val.(type) {
198197
case float32, float64:
199-
return promoteFloat(v), nil
198+
return promoteFloat(v), nil
200199
case int, int8, int16, int32, int64:
201200
return float64(promoteInt(v)), nil
202201
case uint, uint8, uint16, uint32, uint64:

eppoclient/rules_test.go

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ var textRule = rule{Conditions: []condition{
2323

2424
var ruleWithEmptyConditions = rule{Conditions: []condition{}}
2525

26+
func init() {
27+
numericRule.Precompute()
28+
semverRule.Precompute()
29+
textRule.Precompute()
30+
}
31+
2632
func Test_TextRule_NoMatch(t *testing.T) {
2733
subjectAttributes := make(Attributes)
2834
subjectAttributes["age"] = 99
@@ -284,42 +290,46 @@ func Test_isNotNull_attributePresent(t *testing.T) {
284290

285291
func Test_handles_all_numeric_types(t *testing.T) {
286292
condition := condition{Operator: "GT", Attribute: "powerLevel", Value: "9000"}
293+
condition.Precompute()
294+
287295
// Floats
288-
assert.True(t, condition.matches(Attributes{ "powerLevel": 9001.0}) )
289-
assert.False(t, condition.matches(Attributes{ "powerLevel": 9000.0}) )
290-
assert.True(t, condition.matches(Attributes{ "powerLevel": float64(9001)}) )
291-
assert.False(t, condition.matches(Attributes{ "powerLevel": float64(-9001.0)}) )
292-
assert.True(t, condition.matches(Attributes{ "powerLevel": float32(9001)}) )
293-
assert.False(t, condition.matches(Attributes{ "powerLevel": float32(8999)}) )
296+
assert.True(t, condition.matches(Attributes{"powerLevel": 9001.0}))
297+
assert.False(t, condition.matches(Attributes{"powerLevel": 9000.0}))
298+
assert.True(t, condition.matches(Attributes{"powerLevel": float64(9001)}))
299+
assert.False(t, condition.matches(Attributes{"powerLevel": float64(-9001.0)}))
300+
assert.True(t, condition.matches(Attributes{"powerLevel": float32(9001)}))
301+
assert.False(t, condition.matches(Attributes{"powerLevel": float32(8999)}))
294302
// Signed Integers
295-
assert.True(t, condition.matches(Attributes{ "powerLevel": 9001}) )
296-
assert.False(t, condition.matches(Attributes{ "powerLevel": 9000}) )
297-
assert.False(t, condition.matches(Attributes{ "powerLevel": int8(1)}) )
298-
assert.True(t, condition.matches(Attributes{ "powerLevel": int16(9001)}) )
299-
assert.False(t, condition.matches(Attributes{ "powerLevel": int16(-9002)}) )
300-
assert.True(t, condition.matches(Attributes{ "powerLevel": int32(10000)}) )
301-
assert.False(t, condition.matches(Attributes{ "powerLevel": int32(0)}) )
302-
assert.True(t, condition.matches(Attributes{ "powerLevel": int64(9001)}) )
303-
assert.False(t, condition.matches(Attributes{ "powerLevel": int64(8999)}) )
303+
assert.True(t, condition.matches(Attributes{"powerLevel": 9001}))
304+
assert.False(t, condition.matches(Attributes{"powerLevel": 9000}))
305+
assert.False(t, condition.matches(Attributes{"powerLevel": int8(1)}))
306+
assert.True(t, condition.matches(Attributes{"powerLevel": int16(9001)}))
307+
assert.False(t, condition.matches(Attributes{"powerLevel": int16(-9002)}))
308+
assert.True(t, condition.matches(Attributes{"powerLevel": int32(10000)}))
309+
assert.False(t, condition.matches(Attributes{"powerLevel": int32(0)}))
310+
assert.True(t, condition.matches(Attributes{"powerLevel": int64(9001)}))
311+
assert.False(t, condition.matches(Attributes{"powerLevel": int64(8999)}))
304312
// Unsigned Integers
305-
assert.False(t, condition.matches(Attributes{ "powerLevel": uint8(1)}) )
306-
assert.True(t, condition.matches(Attributes{ "powerLevel": uint16(9001)}) )
307-
assert.False(t, condition.matches(Attributes{ "powerLevel": uint16(8999)}) )
308-
assert.True(t, condition.matches(Attributes{ "powerLevel": uint32(10000)}) )
309-
assert.False(t, condition.matches(Attributes{ "powerLevel": uint32(0)}) )
310-
assert.True(t, condition.matches(Attributes{ "powerLevel": uint64(9001)}) )
311-
assert.False(t, condition.matches(Attributes{ "powerLevel": uint64(8999)}) )
313+
assert.False(t, condition.matches(Attributes{"powerLevel": uint8(1)}))
314+
assert.True(t, condition.matches(Attributes{"powerLevel": uint16(9001)}))
315+
assert.False(t, condition.matches(Attributes{"powerLevel": uint16(8999)}))
316+
assert.True(t, condition.matches(Attributes{"powerLevel": uint32(10000)}))
317+
assert.False(t, condition.matches(Attributes{"powerLevel": uint32(0)}))
318+
assert.True(t, condition.matches(Attributes{"powerLevel": uint64(9001)}))
319+
assert.False(t, condition.matches(Attributes{"powerLevel": uint64(8999)}))
312320
// Strings
313-
assert.True(t, condition.matches(Attributes{ "powerLevel": "9001"}) )
314-
assert.True(t, condition.matches(Attributes{ "powerLevel": "9000.1"}) )
315-
assert.False(t, condition.matches(Attributes{ "powerLevel": "9000"}) )
316-
assert.False(t, condition.matches(Attributes{ "powerLevel": ".2"}) )
321+
assert.True(t, condition.matches(Attributes{"powerLevel": "9001"}))
322+
assert.True(t, condition.matches(Attributes{"powerLevel": "9000.1"}))
323+
assert.False(t, condition.matches(Attributes{"powerLevel": "9000"}))
324+
assert.False(t, condition.matches(Attributes{"powerLevel": ".2"}))
317325
}
318326

319327
func Test_invalid_numeric_types(t *testing.T) {
320328
condition := condition{Operator: "GT", Attribute: "powerLevel", Value: "9000"}
321-
assert.False(t, condition.matches(Attributes{ "powerLevel": "empty"}) )
322-
assert.False(t, condition.matches(Attributes{ "powerLevel": ""}) )
323-
assert.False(t, condition.matches(Attributes{ "powerLevel": false}) )
324-
assert.False(t, condition.matches(Attributes{ "powerLevel": true}) )
329+
condition.Precompute()
330+
331+
assert.False(t, condition.matches(Attributes{"powerLevel": "empty"}))
332+
assert.False(t, condition.matches(Attributes{"powerLevel": ""}))
333+
assert.False(t, condition.matches(Attributes{"powerLevel": false}))
334+
assert.False(t, condition.matches(Attributes{"powerLevel": true}))
325335
}

eppoclient/ufc_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package eppoclient
2+
3+
import (
4+
"testing"
5+
6+
semver "github.com/Masterminds/semver/v3"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestConditionPrecompute(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
condition condition
14+
expectedNumVal float64
15+
expectedNumValValid bool
16+
expectedSemVerVal *semver.Version
17+
expectedSemVerValValid bool
18+
}{
19+
{
20+
name: "valid numeric value",
21+
condition: condition{
22+
Value: 42.0,
23+
},
24+
expectedNumVal: 42.0,
25+
expectedNumValValid: true,
26+
expectedSemVerValValid: false,
27+
},
28+
{
29+
name: "valid semver value",
30+
condition: condition{
31+
Value: "1.2.3",
32+
},
33+
expectedNumValValid: false,
34+
expectedSemVerVal: semver.MustParse("1.2.3"),
35+
expectedSemVerValValid: true,
36+
},
37+
{
38+
name: "invalid value",
39+
condition: condition{
40+
Value: "not a number or semver",
41+
},
42+
expectedNumValValid: false,
43+
expectedSemVerValValid: false,
44+
},
45+
}
46+
47+
for _, tc := range tests {
48+
t.Run(tc.name, func(t *testing.T) {
49+
tc.condition.Precompute()
50+
assert.Equal(t, tc.expectedNumVal, tc.condition.NumericValue)
51+
assert.Equal(t, tc.expectedNumValValid, tc.condition.NumericValueValid)
52+
assert.Equal(t, tc.expectedSemVerVal, tc.condition.SemVerValue)
53+
assert.Equal(t, tc.expectedSemVerValValid, tc.condition.SemVerValueValid)
54+
})
55+
}
56+
}

0 commit comments

Comments
 (0)