Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce treatment service memory usage #59

Conversation

deadlycoconuts
Copy link
Contributor

@deadlycoconuts deadlycoconuts commented Jan 19, 2023

What this PR does / why we need it:
This PR introduces some minor refactoring that reduces the memory usage of the Treatment Service by removing the redundancy in storing segmenter values twice in the ExperimentIndex struct:

  • in the various sets, i.e. ExperimentIndex.stringSets, ExperimentIndex.intSets, ExperimentIndex.realSets
  • in the field ExperimentIndex.Experiment.Segments

Given that the Treatment Service only uses the sets of an ExperimentIndex to determine a matching experiment, there is no need to store the original copy within ExperimentIndex.Experiment.Segments. While this duplication might seem minor and negligible, we have observed instances whereby experiments with a huge number of segmenter values cause a huge increase in memory usage by the Treatment Service due to the duplication of the data stored in memory.

This PR simply removes the values in ExperimentIndex.Experiment.Segments once they have been used to initialise the various sets, to reduce the overall memory footprint of the Treatment Service. Additional changes to existing functions are also made to accommodate the removal of that field.

@deadlycoconuts deadlycoconuts self-assigned this Jan 19, 2023
Comment on lines -16 to +20
segmenterValues := []*_segmenters.SegmenterValue{}
for _, val := range *values {
segmenterValues = append(segmenterValues, &_segmenters.SegmenterValue{
Value: &_segmenters.SegmenterValue_String_{String_: val},
})
segmenterValues := make([]*_segmenters.SegmenterValue, len(*values))
for i := 0; i < len(*values); i++ {
segmenterValues[i] = &_segmenters.SegmenterValue{
Value: &_segmenters.SegmenterValue_String_{String_: (*values)[i]},
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a series of minor changes to initialise a fixed-length slice instead of a dynamic length slice to store the segmenter values so as to prevent repeated allocation of memory to copy-paste the slice values when append-ing.

@@ -78,6 +78,7 @@ type ExperimentIndex struct {
stringSets map[string]*set.Set
intSets map[string]*set.Set
realSets map[string]*set.Set
boolFlags map[string]bool
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this new field to the ExperimentIndex struct to store boolean segmenter values.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be a set.Set as well. It's possible to have a bool segment as {true, false} if it applies to both values.

Copy link
Contributor Author

@deadlycoconuts deadlycoconuts Jan 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:O Really? But the original implementation of matchFlagSetSegment seems to suggest that the flag could only be either true or false , esp:

func (i *ExperimentIndex) matchFlagSetSegment(segmentName string, value bool) MatchStrength {
	// ...
	for _, val := range i.Experiment.Segments[segmentName].GetValues() {
		if val.GetBool() == value {
			return MatchStrengthExact
		}
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How? We are iterating over range i.Experiment.Segments[segmentName].GetValues(), no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh I see it! Oops my bad, I got carried away with the slightly different implementation of this function as compared to the other match___SetSegment functions and interpreted this differently 😅

Comment on lines 237 to 252
func (i *ExperimentIndex) checkSegmentHasWeakMatch(segmentName string) bool {
if set, exists := i.stringSets[segmentName]; !exists || set.Len() == 0 {
return true
}
if set, exists := i.intSets[segmentName]; !exists || set.Len() == 0 {
return true
}
if set, exists := i.realSets[segmentName]; !exists || set.Len() == 0 {
return true
}
if _, exists := i.boolFlags[segmentName]; !exists {
return true
}
return false
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new function is basically performing the same purpose as

if _, exists := i.Experiment.Segments[segmentName]; !exists ||
			len(i.Experiment.Segments[segmentName].GetValues()) == 0 {
// ...
}

but has been refactored into a separate helper function and also uses the values within the sets of ExperimentIndex instead of the values stored in ExperimentIndex.Experiment.Segments (which would have been deleted).

@@ -672,7 +675,7 @@ func TestExperimentIndexMatchSegment(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := experimentIndex.matchSegment(tt.args.segmentName, tt.args.value)
assert.Equal(t, got, tt.want)
assert.Equal(t, tt.want, got)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flipped this around because assert.Equal was used incorrectly.

@deadlycoconuts deadlycoconuts requested a review from a team January 19, 2023 05:52
@deadlycoconuts deadlycoconuts marked this pull request as ready for review January 19, 2023 05:52
@@ -78,6 +78,7 @@ type ExperimentIndex struct {
stringSets map[string]*set.Set
intSets map[string]*set.Set
realSets map[string]*set.Set
boolFlags map[string]bool
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be a set.Set as well. It's possible to have a bool segment as {true, false} if it applies to both values.

treatment-service/models/storage.go Show resolved Hide resolved
treatment-service/models/storage.go Outdated Show resolved Hide resolved
Copy link
Collaborator

@krithika369 krithika369 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks for this fix! 🚀

@deadlycoconuts
Copy link
Contributor Author

Thanks a lot for the quick review! I'll be merging this now! 🚀

@deadlycoconuts deadlycoconuts merged commit d58650f into caraml-dev:main Jan 19, 2023
@deadlycoconuts deadlycoconuts deleted the reduce_treatment_service_mem_usage branch January 19, 2023 17:45
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.

None yet

2 participants